mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
fix: cap distiller startup scan and switch to formal SDK module dependency
- replace vendored third_party/homeagent-sdk with formal module dependency - keep canonical SDK via go.mod pinned commit - optimize distiller startup loading to stream recent raw records only - parse timestamps correctly and cap startup load to 5000 records - remove quadratic string building in raw record parsing - restore fast startup while preserving memory pipeline
This commit is contained in:
73
third_party/homeagent-sdk/example/memo/README.md
vendored
73
third_party/homeagent-sdk/example/memo/README.md
vendored
@ -1,73 +0,0 @@
|
||||
# memo 插件讲解
|
||||
|
||||
备忘插件,支持创建、完成、列出备忘,自动提醒未完成事项。
|
||||
|
||||
## 工具清单
|
||||
|
||||
| 工具 | 功能 | 源码 |
|
||||
|------|------|------|
|
||||
| `memo_create` | 创建一条备忘 | `handleCreate` |
|
||||
| `memo_complete` | 标记备忘为已完成 | `handleComplete` |
|
||||
| `memo_list` | 列出所有未完成备忘 | `handleList` |
|
||||
|
||||
## 核心设计
|
||||
|
||||
### 数据持久化
|
||||
|
||||
备忘存储在 JSON 文件中,路径由核心配置 `core.daemon.data_dir` 决定:
|
||||
|
||||
```go
|
||||
// plugin.go:Start
|
||||
dataDirVal, _ := s.Settings().GetCore("core.daemon.data_dir")
|
||||
p.filePath = filepath.Join(fmt.Sprint(dataDirVal), "memos.json")
|
||||
p.load()
|
||||
```
|
||||
|
||||
`load()` 和 `save()` 实现 JSON 文件的读写,格式为 `{memos: [...], next_id: N}`。每次写操作后自动 `save()`,Stop 时也执行一次。
|
||||
|
||||
### PreAction 注入提醒
|
||||
|
||||
注册 `pre_action` 阶段钩子,在每次 LLM 调用前注入未完成备忘数量:
|
||||
|
||||
```go
|
||||
// plugin.go:stagePreAction
|
||||
n := p.pendingCount()
|
||||
if n == 0 { return nil }
|
||||
ctx.ContextMsgs = append(ctx.ContextMsgs, map[string]interface{}{
|
||||
"role": "system",
|
||||
"content": fmt.Sprintf("目前有%d条备忘未完成,调用%slist工具读取具体内容", n, p.tp),
|
||||
})
|
||||
```
|
||||
|
||||
这样每次 LLM 处理消息时都感知到未完成备忘,无需主动查询。
|
||||
|
||||
### 定时打断提醒
|
||||
|
||||
每 5 分钟检查未完成备忘,如果有则通过中断通道提醒:
|
||||
|
||||
```go
|
||||
// plugin.go:periodicCheck
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
for {
|
||||
select {
|
||||
case <-p.stopCh: return
|
||||
case <-ticker.C:
|
||||
n := p.pendingCount()
|
||||
if n == 0 { continue }
|
||||
p.sdk.InjectInterruptText(p.name, p.name,
|
||||
fmt.Sprintf("注意,你还有%d条备忘未标记完成,请检查", n))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
中断消息会打断当前 LLM 处理,在下一轮工具循环前插入 `[打断消息]`,确保 agent 不会长期忽略未完成备忘。
|
||||
|
||||
### 工具返回值
|
||||
|
||||
所有工具返回 `{content: string}` 或 `{isError: true, content: string}` 格式,LLM 通过 content 字段获取结果文本。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- `memo_create` 的 content 参数应包含事项的完整描述,方便后续回顾
|
||||
- `memo_complete` 只标记为 done,不删除数据,保留历史
|
||||
- 更早的暂停时自动 save,防丢数据
|
||||
273
third_party/homeagent-sdk/example/memo/plugin.go
vendored
273
third_party/homeagent-sdk/example/memo/plugin.go
vendored
@ -1,273 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gitcode.com/JianFeeeee/homeagent-sdk/sdk"
|
||||
)
|
||||
|
||||
type Memo struct {
|
||||
ID int64 `json:"id"`
|
||||
Content string `json:"content"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Done bool `json:"done"`
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
name string
|
||||
sdk *sdk.PluginSDK
|
||||
mu sync.RWMutex
|
||||
memos []Memo
|
||||
nextID int64
|
||||
filePath string
|
||||
stopCh chan struct{}
|
||||
tp string
|
||||
}
|
||||
|
||||
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||
return &Plugin{name: name}, nil
|
||||
}
|
||||
|
||||
func (p *Plugin) Name() string { return p.name }
|
||||
|
||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||
p.sdk = s
|
||||
p.tp = p.name + "_"
|
||||
p.stopCh = make(chan struct{})
|
||||
|
||||
dataDirVal, err := s.Settings().GetCore("core.daemon.data_dir")
|
||||
if err != nil || dataDirVal == "" {
|
||||
dataDirVal = "."
|
||||
}
|
||||
p.filePath = filepath.Join(fmt.Sprint(dataDirVal), "memos.json")
|
||||
p.load()
|
||||
|
||||
s.RegisterTool(p.tp+"create", sdk.ToolDef{
|
||||
Name: p.tp + "create",
|
||||
Description: "创建一条备忘条目。备忘内容应包含具体事项的完整描述。",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"content": map[string]interface{}{"type": "string", "description": "备忘内容"},
|
||||
},
|
||||
"required": []string{"content"},
|
||||
},
|
||||
}, p.handleCreate)
|
||||
|
||||
s.RegisterTool(p.tp+"complete", sdk.ToolDef{
|
||||
Name: p.tp + "complete",
|
||||
Description: "将指定ID的备忘标记为已完成。",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"id": map[string]interface{}{"type": "integer", "description": "备忘ID"},
|
||||
},
|
||||
"required": []string{"id"},
|
||||
},
|
||||
}, p.handleComplete)
|
||||
|
||||
s.RegisterTool(p.tp+"list", sdk.ToolDef{
|
||||
Name: p.tp + "list",
|
||||
Description: "列出所有未完成的备忘条目,包含ID、内容和创建时间。",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{},
|
||||
},
|
||||
}, p.handleList)
|
||||
|
||||
s.RegisterStage(sdk.StagePreAction, p.stagePreAction)
|
||||
|
||||
go p.periodicCheck()
|
||||
|
||||
log.Printf("[%s] started, path=%s", p.name, p.filePath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) Stop() error {
|
||||
close(p.stopCh)
|
||||
p.save()
|
||||
log.Printf("[%s] stopped", p.name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) load() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
data, err := os.ReadFile(p.filePath)
|
||||
if err != nil {
|
||||
p.memos = nil
|
||||
p.nextID = 1
|
||||
return
|
||||
}
|
||||
var store struct {
|
||||
Memos []Memo `json:"memos"`
|
||||
NextID int64 `json:"next_id"`
|
||||
}
|
||||
if json.Unmarshal(data, &store) != nil {
|
||||
p.memos = nil
|
||||
p.nextID = 1
|
||||
return
|
||||
}
|
||||
p.memos = store.Memos
|
||||
p.nextID = store.NextID
|
||||
if p.memos == nil {
|
||||
p.memos = []Memo{}
|
||||
}
|
||||
if p.nextID < 1 {
|
||||
p.nextID = 1
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) save() {
|
||||
data, _ := json.MarshalIndent(map[string]interface{}{
|
||||
"memos": p.memos,
|
||||
"next_id": p.nextID,
|
||||
}, "", " ")
|
||||
os.WriteFile(p.filePath, data, 0644)
|
||||
}
|
||||
|
||||
func (p *Plugin) pendingCount() int {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
n := 0
|
||||
for _, m := range p.memos {
|
||||
if !m.Done {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (p *Plugin) pendingMemos() []Memo {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
var out []Memo
|
||||
for _, m := range p.memos {
|
||||
if !m.Done {
|
||||
out = append(out, m)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (p *Plugin) stagePreAction(ctx *sdk.StageContext) error {
|
||||
n := p.pendingCount()
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
ctx.Lock()
|
||||
ctx.ContextMsgs = append(ctx.ContextMsgs, map[string]interface{}{
|
||||
"role": "system",
|
||||
"content": fmt.Sprintf("目前有%d条备忘未完成,调用%slist工具读取具体内容", n, p.tp),
|
||||
})
|
||||
ctx.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Plugin) periodicCheck() {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-p.stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
n := p.pendingCount()
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
if p.sdk != nil {
|
||||
p.sdk.InjectInterruptText(p.name, p.name,
|
||||
fmt.Sprintf("注意,你还有%d条备忘未标记完成,请检查", n))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plugin) handleCreate(args map[string]interface{}) (interface{}, error) {
|
||||
content, _ := args["content"].(string)
|
||||
if content == "" {
|
||||
return errorResult("content is required"), nil
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
memo := Memo{
|
||||
ID: p.nextID,
|
||||
Content: content,
|
||||
CreatedAt: time.Now().Unix(),
|
||||
Done: false,
|
||||
}
|
||||
p.nextID++
|
||||
p.memos = append(p.memos, memo)
|
||||
p.mu.Unlock()
|
||||
p.save()
|
||||
|
||||
return map[string]interface{}{
|
||||
"content": fmt.Sprintf("备忘已创建 (ID: %d)", memo.ID),
|
||||
"id": memo.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Plugin) handleComplete(args map[string]interface{}) (interface{}, error) {
|
||||
id, ok := args["id"].(float64)
|
||||
if !ok {
|
||||
return errorResult("id is required"), nil
|
||||
}
|
||||
|
||||
p.mu.Lock()
|
||||
found := false
|
||||
for i := range p.memos {
|
||||
if p.memos[i].ID == int64(id) && !p.memos[i].Done {
|
||||
p.memos[i].Done = true
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
if !found {
|
||||
return errorResult(fmt.Sprintf("未找到未完成的备忘 ID: %d", int64(id))), nil
|
||||
}
|
||||
p.save()
|
||||
|
||||
return map[string]interface{}{
|
||||
"content": fmt.Sprintf("备忘 %d 已标记为完成", int64(id)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *Plugin) handleList(args map[string]interface{}) (interface{}, error) {
|
||||
memos := p.pendingMemos()
|
||||
if len(memos) == 0 {
|
||||
return map[string]interface{}{
|
||||
"content": "暂无未完成的备忘",
|
||||
}, nil
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for i, m := range memos {
|
||||
t := time.Unix(m.CreatedAt, 0).Format("01-02 15:04")
|
||||
if i > 0 {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%d. [ID:%d] %s — %s", i+1, m.ID, m.Content, t))
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"content": sb.String(),
|
||||
"count": len(memos),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func errorResult(msg string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"isError": true,
|
||||
"content": msg,
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
{
|
||||
"name": "memo",
|
||||
"version": "1.0.0",
|
||||
"description": "备忘插件,支持创建、完成、列出备忘条目,自动提醒",
|
||||
"author": "HomeAgent",
|
||||
"entry": "plugin.so",
|
||||
"tags": ["memo", "reminder", "todo"]
|
||||
}
|
||||
Reference in New Issue
Block a user