From 8e5610c494c5b2e18b0faa3e75ef4c72da5cc7c6 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 2 Aug 2026 15:47:37 +0800 Subject: [PATCH] =?UTF-8?q?example/memo:=20=E5=BE=85=E5=8A=9E=E4=B8=8E?= =?UTF-8?q?=E5=A4=87=E5=BF=98=E5=88=86=E7=A6=BB=EF=BC=88=E5=BE=85=E5=8A=9E?= =?UTF-8?q?=E6=8F=90=E9=86=92=E3=80=81=E5=A4=87=E5=BF=98=E7=BA=AF=E8=AE=B0?= =?UTF-8?q?=E4=BA=8B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 待办(todo_add/todo_complete/todo_list):保留原有主动提醒能力—— stagePreAction 注入未完成条数 + periodicCheck 每 5 分钟 InjectInterruptText; 数据文件 todos.json - 备忘(memo_create/memo_list/memo_delete):纯记事用途,不参与任何提醒 (无 stage 注入、无周期中断);数据文件 memos.json - cleanupData 卸载时清理两个数据文件;ID 各自独立递增 --- example/memo/plugin.go | 309 ++++++++++++++++++++++++++++++++--------- 1 file changed, 241 insertions(+), 68 deletions(-) diff --git a/example/memo/plugin.go b/example/memo/plugin.go index 6f325af..70f7323 100644 --- a/example/memo/plugin.go +++ b/example/memo/plugin.go @@ -13,20 +13,31 @@ import ( "gitcode.com/JianFeeeee/homeagent-sdk/sdk" ) -type Memo struct { +// Todo 待办条目:会被主动提醒 +type Todo struct { ID int64 `json:"id"` Content string `json:"content"` CreatedAt int64 `json:"created_at"` Done bool `json:"done"` } +// Memo 备忘录条目:纯记事,不主动提醒 +type Memo struct { + ID int64 `json:"id"` + Content string `json:"content"` + CreatedAt int64 `json:"created_at"` +} + type Plugin struct { name string sdk *sdk.PluginSDK mu sync.RWMutex + todos []Todo + nextTID int64 memos []Memo - nextID int64 - filePath string + nextMID int64 + todoPath string + memoPath string stopCh chan struct{} tp string } @@ -43,67 +54,134 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { if err != nil || dataDirVal == "" { dataDirVal = "." } - p.filePath = filepath.Join(fmt.Sprint(dataDirVal), "memos.json") - p.load() + dir := fmt.Sprint(dataDirVal) + p.todoPath = filepath.Join(dir, "todos.json") + p.memoPath = filepath.Join(dir, "memos.json") + p.loadTodos() + p.loadMemos() - // 卸载(删除)时清理备忘数据文件;重载不触发 + // 卸载(删除)时清理数据文件;重载不触发 s.RegisterOnRemoveHandler(p.cleanupData) - s.RegisterTool(p.tp+"create", sdk.ToolDef{ - Name: p.tp + "create", - Description: "创建一条备忘条目。备忘内容应包含具体事项的完整描述。", + // ── 待办(会被主动提醒)── + s.RegisterTool(p.tp+"todo_add", sdk.ToolDef{ + Name: p.tp + "todo_add", + Description: "添加一条待办事项。待办会被主动提醒,完成后请及时用 todo_complete 标记。", Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ - "content": map[string]interface{}{"type": "string", "description": "备忘内容"}, + "content": map[string]interface{}{"type": "string", "description": "待办内容"}, }, "required": []string{"content"}, }, - }, p.handleCreate) + }, p.handleTodoAdd) - s.RegisterTool(p.tp+"complete", sdk.ToolDef{ - Name: p.tp + "complete", - Description: "将指定ID的备忘标记为已完成。", + s.RegisterTool(p.tp+"todo_complete", sdk.ToolDef{ + Name: p.tp + "todo_complete", + Description: "将指定ID的待办标记为已完成(不再提醒)。", Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ - "id": map[string]interface{}{"type": "integer", "description": "备忘ID"}, + "id": map[string]interface{}{"type": "integer", "description": "待办ID"}, }, "required": []string{"id"}, }, - }, p.handleComplete) + }, p.handleTodoComplete) - s.RegisterTool(p.tp+"list", sdk.ToolDef{ - Name: p.tp + "list", - Description: "列出所有未完成的备忘条目,包含ID、内容和创建时间。", + s.RegisterTool(p.tp+"todo_list", sdk.ToolDef{ + Name: p.tp + "todo_list", + Description: "列出所有未完成的待办事项,包含ID、内容和创建时间。", Parameters: map[string]interface{}{ "type": "object", "properties": map[string]interface{}{}, }, - }, p.handleList) + }, p.handleTodoList) + // ── 备忘(纯记事,不提醒)── + s.RegisterTool(p.tp+"memo_create", sdk.ToolDef{ + Name: p.tp + "memo_create", + Description: "创建一条备忘录。备忘录是纯记事(备注)用途,不会主动提醒,内容应包含完整信息供后续查阅。", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "content": map[string]interface{}{"type": "string", "description": "备忘录内容"}, + }, + "required": []string{"content"}, + }, + }, p.handleMemoCreate) + + s.RegisterTool(p.tp+"memo_list", sdk.ToolDef{ + Name: p.tp + "memo_list", + Description: "列出所有备忘录,包含ID、内容和创建时间。", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{}, + }, + }, p.handleMemoList) + + s.RegisterTool(p.tp+"memo_delete", sdk.ToolDef{ + Name: p.tp + "memo_delete", + Description: "删除指定ID的备忘录。", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "id": map[string]interface{}{"type": "integer", "description": "备忘录ID"}, + }, + "required": []string{"id"}, + }, + }, p.handleMemoDelete) + + // 待办提醒:预动作注入未完成条数 + 周期主动提醒(备忘录不参与) s.RegisterStage(sdk.StagePreAction, p.stagePreAction) - go p.periodicCheck() - log.Printf("[%s] started, path=%s", p.name, p.filePath) + log.Printf("[%s] started, todos=%s memos=%s", p.name, p.todoPath, p.memoPath) return nil } func (p *Plugin) Stop() error { close(p.stopCh) - p.save() + p.saveTodos() + p.saveMemos() log.Printf("[%s] stopped", p.name) return nil } -func (p *Plugin) load() { +func (p *Plugin) loadTodos() { p.mu.Lock() defer p.mu.Unlock() - data, err := os.ReadFile(p.filePath) + data, err := os.ReadFile(p.todoPath) if err != nil { - p.memos = nil - p.nextID = 1 + p.todos = []Todo{} + p.nextTID = 1 + return + } + var store struct { + Todos []Todo `json:"todos"` + NextID int64 `json:"next_id"` + } + if json.Unmarshal(data, &store) != nil { + p.todos = []Todo{} + p.nextTID = 1 + return + } + p.todos = store.Todos + p.nextTID = store.NextID + if p.todos == nil { + p.todos = []Todo{} + } + if p.nextTID < 1 { + p.nextTID = 1 + } +} + +func (p *Plugin) loadMemos() { + p.mu.Lock() + defer p.mu.Unlock() + data, err := os.ReadFile(p.memoPath) + if err != nil { + p.memos = []Memo{} + p.nextMID = 1 return } var store struct { @@ -111,66 +189,78 @@ func (p *Plugin) load() { NextID int64 `json:"next_id"` } if json.Unmarshal(data, &store) != nil { - p.memos = nil - p.nextID = 1 + p.memos = []Memo{} + p.nextMID = 1 return } p.memos = store.Memos - p.nextID = store.NextID + p.nextMID = store.NextID if p.memos == nil { p.memos = []Memo{} } - if p.nextID < 1 { - p.nextID = 1 + if p.nextMID < 1 { + p.nextMID = 1 } } -func (p *Plugin) save() { +func (p *Plugin) saveTodos() { data, _ := json.MarshalIndent(map[string]interface{}{ - "memos": p.memos, - "next_id": p.nextID, + "todos": p.todos, + "next_id": p.nextTID, }, "", " ") - os.WriteFile(p.filePath, data, 0644) + os.WriteFile(p.todoPath, data, 0644) } -func (p *Plugin) pendingCount() int { +func (p *Plugin) saveMemos() { + data, _ := json.MarshalIndent(map[string]interface{}{ + "memos": p.memos, + "next_id": p.nextMID, + }, "", " ") + os.WriteFile(p.memoPath, data, 0644) +} + +// ── 待办:未完成计数与提醒 ── + +func (p *Plugin) pendingTodoCount() int { p.mu.RLock() defer p.mu.RUnlock() n := 0 - for _, m := range p.memos { - if !m.Done { + for _, t := range p.todos { + if !t.Done { n++ } } return n } -func (p *Plugin) pendingMemos() []Memo { +func (p *Plugin) pendingTodos() []Todo { p.mu.RLock() defer p.mu.RUnlock() - var out []Memo - for _, m := range p.memos { - if !m.Done { - out = append(out, m) + var out []Todo + for _, t := range p.todos { + if !t.Done { + out = append(out, t) } } return out } +// stagePreAction 仅在待办未完成时注入上下文提示(备忘录不提示) func (p *Plugin) stagePreAction(ctx *sdk.StageContext) error { - n := p.pendingCount() + n := p.pendingTodoCount() 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), + "content": fmt.Sprintf("目前有%d条待办未完成,调用%s todo_list 工具读取具体内容", n, p.tp), }) ctx.Unlock() return nil } +// periodicCheck 周期主动提醒未完成待办(备忘录不提醒) func (p *Plugin) periodicCheck() { ticker := time.NewTicker(5 * time.Minute) defer ticker.Stop() @@ -179,19 +269,97 @@ func (p *Plugin) periodicCheck() { case <-p.stopCh: return case <-ticker.C: - n := p.pendingCount() + n := p.pendingTodoCount() if n == 0 { continue } if p.sdk != nil { p.sdk.InjectInterruptText(p.name, p.name, - fmt.Sprintf("注意,你还有%d条备忘未标记完成,请检查", n)) + fmt.Sprintf("注意,你还有%d条待办未完成,请检查", n)) } } } } -func (p *Plugin) handleCreate(args map[string]interface{}) (interface{}, error) { +// ── 待办工具 ── + +func (p *Plugin) handleTodoAdd(args map[string]interface{}) (interface{}, error) { + content, _ := args["content"].(string) + if content == "" { + return errorResult("content is required"), nil + } + + p.mu.Lock() + todo := Todo{ + ID: p.nextTID, + Content: content, + CreatedAt: time.Now().Unix(), + Done: false, + } + p.nextTID++ + p.todos = append(p.todos, todo) + p.mu.Unlock() + p.saveTodos() + + return map[string]interface{}{ + "content": fmt.Sprintf("待办已添加 (ID: %d)", todo.ID), + "id": todo.ID, + }, nil +} + +func (p *Plugin) handleTodoComplete(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.todos { + if p.todos[i].ID == int64(id) && !p.todos[i].Done { + p.todos[i].Done = true + found = true + break + } + } + p.mu.Unlock() + + if !found { + return errorResult(fmt.Sprintf("未找到未完成的待办 ID: %d", int64(id))), nil + } + p.saveTodos() + + return map[string]interface{}{ + "content": fmt.Sprintf("待办 %d 已标记为完成", int64(id)), + }, nil +} + +func (p *Plugin) handleTodoList(args map[string]interface{}) (interface{}, error) { + todos := p.pendingTodos() + if len(todos) == 0 { + return map[string]interface{}{ + "content": "暂无未完成的待办", + }, nil + } + + var sb strings.Builder + for i, t := range todos { + ts := time.Unix(t.CreatedAt, 0).Format("01-02 15:04") + if i > 0 { + sb.WriteString("\n") + } + sb.WriteString(fmt.Sprintf("%d. [ID:%d] %s — %s", i+1, t.ID, t.Content, ts)) + } + + return map[string]interface{}{ + "content": sb.String(), + "count": len(todos), + }, nil +} + +// ── 备忘工具 ── + +func (p *Plugin) handleMemoCreate(args map[string]interface{}) (interface{}, error) { content, _ := args["content"].(string) if content == "" { return errorResult("content is required"), nil @@ -199,23 +367,22 @@ func (p *Plugin) handleCreate(args map[string]interface{}) (interface{}, error) p.mu.Lock() memo := Memo{ - ID: p.nextID, + ID: p.nextMID, Content: content, CreatedAt: time.Now().Unix(), - Done: false, } - p.nextID++ + p.nextMID++ p.memos = append(p.memos, memo) p.mu.Unlock() - p.save() + p.saveMemos() return map[string]interface{}{ - "content": fmt.Sprintf("备忘已创建 (ID: %d)", memo.ID), + "content": fmt.Sprintf("备忘录已创建 (ID: %d)", memo.ID), "id": memo.ID, }, nil } -func (p *Plugin) handleComplete(args map[string]interface{}) (interface{}, error) { +func (p *Plugin) handleMemoDelete(args map[string]interface{}) (interface{}, error) { id, ok := args["id"].(float64) if !ok { return errorResult("id is required"), nil @@ -224,8 +391,8 @@ func (p *Plugin) handleComplete(args map[string]interface{}) (interface{}, error 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 + if p.memos[i].ID == int64(id) { + p.memos = append(p.memos[:i], p.memos[i+1:]...) found = true break } @@ -233,30 +400,33 @@ func (p *Plugin) handleComplete(args map[string]interface{}) (interface{}, error p.mu.Unlock() if !found { - return errorResult(fmt.Sprintf("未找到未完成的备忘 ID: %d", int64(id))), nil + return errorResult(fmt.Sprintf("未找到备忘录 ID: %d", int64(id))), nil } - p.save() + p.saveMemos() return map[string]interface{}{ - "content": fmt.Sprintf("备忘 %d 已标记为完成", int64(id)), + "content": fmt.Sprintf("备忘录 %d 已删除", int64(id)), }, nil } -func (p *Plugin) handleList(args map[string]interface{}) (interface{}, error) { - memos := p.pendingMemos() +func (p *Plugin) handleMemoList(args map[string]interface{}) (interface{}, error) { + p.mu.RLock() + memos := append([]Memo{}, p.memos...) + p.mu.RUnlock() + if len(memos) == 0 { return map[string]interface{}{ - "content": "暂无未完成的备忘", + "content": "暂无备忘录", }, nil } var sb strings.Builder for i, m := range memos { - t := time.Unix(m.CreatedAt, 0).Format("01-02 15:04") + ts := 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)) + sb.WriteString(fmt.Sprintf("%d. [ID:%d] %s — %s", i+1, m.ID, m.Content, ts)) } return map[string]interface{}{ @@ -276,9 +446,12 @@ func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, e return &Plugin{name: name}, nil } -// cleanupData 卸载时清理备忘数据文件 +// cleanupData 卸载时清理数据文件(待办 + 备忘) func (p *Plugin) cleanupData() { - if p.filePath != "" { - os.Remove(p.filePath) + if p.todoPath != "" { + os.Remove(p.todoPath) + } + if p.memoPath != "" { + os.Remove(p.memoPath) } }