example/memo: 待办与备忘分离(待办提醒、备忘纯记事)

- 待办(todo_add/todo_complete/todo_list):保留原有主动提醒能力——
  stagePreAction 注入未完成条数 + periodicCheck 每 5 分钟 InjectInterruptText;
  数据文件 todos.json
- 备忘(memo_create/memo_list/memo_delete):纯记事用途,不参与任何提醒
  (无 stage 注入、无周期中断);数据文件 memos.json
- cleanupData 卸载时清理两个数据文件;ID 各自独立递增
This commit is contained in:
root
2026-08-02 15:47:37 +08:00
parent 1796395668
commit 8e5610c494

View File

@ -13,20 +13,31 @@ import (
"gitcode.com/JianFeeeee/homeagent-sdk/sdk" "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
) )
type Memo struct { // Todo 待办条目:会被主动提醒
type Todo struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Content string `json:"content"` Content string `json:"content"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
Done bool `json:"done"` Done bool `json:"done"`
} }
// Memo 备忘录条目:纯记事,不主动提醒
type Memo struct {
ID int64 `json:"id"`
Content string `json:"content"`
CreatedAt int64 `json:"created_at"`
}
type Plugin struct { type Plugin struct {
name string name string
sdk *sdk.PluginSDK sdk *sdk.PluginSDK
mu sync.RWMutex mu sync.RWMutex
todos []Todo
nextTID int64
memos []Memo memos []Memo
nextID int64 nextMID int64
filePath string todoPath string
memoPath string
stopCh chan struct{} stopCh chan struct{}
tp string tp string
} }
@ -43,67 +54,134 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
if err != nil || dataDirVal == "" { if err != nil || dataDirVal == "" {
dataDirVal = "." dataDirVal = "."
} }
p.filePath = filepath.Join(fmt.Sprint(dataDirVal), "memos.json") dir := fmt.Sprint(dataDirVal)
p.load() p.todoPath = filepath.Join(dir, "todos.json")
p.memoPath = filepath.Join(dir, "memos.json")
p.loadTodos()
p.loadMemos()
// 卸载(删除)时清理备忘数据文件;重载不触发 // 卸载(删除)时清理数据文件;重载不触发
s.RegisterOnRemoveHandler(p.cleanupData) s.RegisterOnRemoveHandler(p.cleanupData)
s.RegisterTool(p.tp+"create", sdk.ToolDef{ // ── 待办(会被主动提醒)──
Name: p.tp + "create", s.RegisterTool(p.tp+"todo_add", sdk.ToolDef{
Description: "创建一条备忘条目。备忘内容应包含具体事项的完整描述。", Name: p.tp + "todo_add",
Description: "添加一条待办事项。待办会被主动提醒,完成后请及时用 todo_complete 标记。",
Parameters: map[string]interface{}{ Parameters: map[string]interface{}{
"type": "object", "type": "object",
"properties": map[string]interface{}{ "properties": map[string]interface{}{
"content": map[string]interface{}{"type": "string", "description": "备忘内容"}, "content": map[string]interface{}{"type": "string", "description": "待办内容"},
}, },
"required": []string{"content"}, "required": []string{"content"},
}, },
}, p.handleCreate) }, p.handleTodoAdd)
s.RegisterTool(p.tp+"complete", sdk.ToolDef{ s.RegisterTool(p.tp+"todo_complete", sdk.ToolDef{
Name: p.tp + "complete", Name: p.tp + "todo_complete",
Description: "将指定ID的备忘标记为已完成。", Description: "将指定ID的待办标记为已完成(不再提醒)。",
Parameters: map[string]interface{}{ Parameters: map[string]interface{}{
"type": "object", "type": "object",
"properties": map[string]interface{}{ "properties": map[string]interface{}{
"id": map[string]interface{}{"type": "integer", "description": "备忘ID"}, "id": map[string]interface{}{"type": "integer", "description": "待办ID"},
}, },
"required": []string{"id"}, "required": []string{"id"},
}, },
}, p.handleComplete) }, p.handleTodoComplete)
s.RegisterTool(p.tp+"list", sdk.ToolDef{ s.RegisterTool(p.tp+"todo_list", sdk.ToolDef{
Name: p.tp + "list", Name: p.tp + "todo_list",
Description: "列出所有未完成的备忘条目包含ID、内容和创建时间。", Description: "列出所有未完成的待办事项包含ID、内容和创建时间。",
Parameters: map[string]interface{}{ Parameters: map[string]interface{}{
"type": "object", "type": "object",
"properties": map[string]interface{}{}, "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) s.RegisterStage(sdk.StagePreAction, p.stagePreAction)
go p.periodicCheck() 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 return nil
} }
func (p *Plugin) Stop() error { func (p *Plugin) Stop() error {
close(p.stopCh) close(p.stopCh)
p.save() p.saveTodos()
p.saveMemos()
log.Printf("[%s] stopped", p.name) log.Printf("[%s] stopped", p.name)
return nil return nil
} }
func (p *Plugin) load() { func (p *Plugin) loadTodos() {
p.mu.Lock() p.mu.Lock()
defer p.mu.Unlock() defer p.mu.Unlock()
data, err := os.ReadFile(p.filePath) data, err := os.ReadFile(p.todoPath)
if err != nil { if err != nil {
p.memos = nil p.todos = []Todo{}
p.nextID = 1 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 return
} }
var store struct { var store struct {
@ -111,66 +189,78 @@ func (p *Plugin) load() {
NextID int64 `json:"next_id"` NextID int64 `json:"next_id"`
} }
if json.Unmarshal(data, &store) != nil { if json.Unmarshal(data, &store) != nil {
p.memos = nil p.memos = []Memo{}
p.nextID = 1 p.nextMID = 1
return return
} }
p.memos = store.Memos p.memos = store.Memos
p.nextID = store.NextID p.nextMID = store.NextID
if p.memos == nil { if p.memos == nil {
p.memos = []Memo{} p.memos = []Memo{}
} }
if p.nextID < 1 { if p.nextMID < 1 {
p.nextID = 1 p.nextMID = 1
} }
} }
func (p *Plugin) save() { func (p *Plugin) saveTodos() {
data, _ := json.MarshalIndent(map[string]interface{}{ data, _ := json.MarshalIndent(map[string]interface{}{
"memos": p.memos, "todos": p.todos,
"next_id": p.nextID, "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() p.mu.RLock()
defer p.mu.RUnlock() defer p.mu.RUnlock()
n := 0 n := 0
for _, m := range p.memos { for _, t := range p.todos {
if !m.Done { if !t.Done {
n++ n++
} }
} }
return n return n
} }
func (p *Plugin) pendingMemos() []Memo { func (p *Plugin) pendingTodos() []Todo {
p.mu.RLock() p.mu.RLock()
defer p.mu.RUnlock() defer p.mu.RUnlock()
var out []Memo var out []Todo
for _, m := range p.memos { for _, t := range p.todos {
if !m.Done { if !t.Done {
out = append(out, m) out = append(out, t)
} }
} }
return out return out
} }
// stagePreAction 仅在待办未完成时注入上下文提示(备忘录不提示)
func (p *Plugin) stagePreAction(ctx *sdk.StageContext) error { func (p *Plugin) stagePreAction(ctx *sdk.StageContext) error {
n := p.pendingCount() n := p.pendingTodoCount()
if n == 0 { if n == 0 {
return nil return nil
} }
ctx.Lock() ctx.Lock()
ctx.ContextMsgs = append(ctx.ContextMsgs, map[string]interface{}{ ctx.ContextMsgs = append(ctx.ContextMsgs, map[string]interface{}{
"role": "system", "role": "system",
"content": fmt.Sprintf("目前有%d条备忘未完成,调用%slist工具读取具体内容", n, p.tp), "content": fmt.Sprintf("目前有%d条待办未完成,调用%s todo_list 工具读取具体内容", n, p.tp),
}) })
ctx.Unlock() ctx.Unlock()
return nil return nil
} }
// periodicCheck 周期主动提醒未完成待办(备忘录不提醒)
func (p *Plugin) periodicCheck() { func (p *Plugin) periodicCheck() {
ticker := time.NewTicker(5 * time.Minute) ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop() defer ticker.Stop()
@ -179,19 +269,97 @@ func (p *Plugin) periodicCheck() {
case <-p.stopCh: case <-p.stopCh:
return return
case <-ticker.C: case <-ticker.C:
n := p.pendingCount() n := p.pendingTodoCount()
if n == 0 { if n == 0 {
continue continue
} }
if p.sdk != nil { if p.sdk != nil {
p.sdk.InjectInterruptText(p.name, p.name, 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) content, _ := args["content"].(string)
if content == "" { if content == "" {
return errorResult("content is required"), nil return errorResult("content is required"), nil
@ -199,23 +367,22 @@ func (p *Plugin) handleCreate(args map[string]interface{}) (interface{}, error)
p.mu.Lock() p.mu.Lock()
memo := Memo{ memo := Memo{
ID: p.nextID, ID: p.nextMID,
Content: content, Content: content,
CreatedAt: time.Now().Unix(), CreatedAt: time.Now().Unix(),
Done: false,
} }
p.nextID++ p.nextMID++
p.memos = append(p.memos, memo) p.memos = append(p.memos, memo)
p.mu.Unlock() p.mu.Unlock()
p.save() p.saveMemos()
return map[string]interface{}{ return map[string]interface{}{
"content": fmt.Sprintf("备忘已创建 (ID: %d)", memo.ID), "content": fmt.Sprintf("备忘已创建 (ID: %d)", memo.ID),
"id": memo.ID, "id": memo.ID,
}, nil }, 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) id, ok := args["id"].(float64)
if !ok { if !ok {
return errorResult("id is required"), nil return errorResult("id is required"), nil
@ -224,8 +391,8 @@ func (p *Plugin) handleComplete(args map[string]interface{}) (interface{}, error
p.mu.Lock() p.mu.Lock()
found := false found := false
for i := range p.memos { for i := range p.memos {
if p.memos[i].ID == int64(id) && !p.memos[i].Done { if p.memos[i].ID == int64(id) {
p.memos[i].Done = true p.memos = append(p.memos[:i], p.memos[i+1:]...)
found = true found = true
break break
} }
@ -233,30 +400,33 @@ func (p *Plugin) handleComplete(args map[string]interface{}) (interface{}, error
p.mu.Unlock() p.mu.Unlock()
if !found { 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{}{ return map[string]interface{}{
"content": fmt.Sprintf("备忘 %d 已标记为完成", int64(id)), "content": fmt.Sprintf("备忘 %d 已删除", int64(id)),
}, nil }, nil
} }
func (p *Plugin) handleList(args map[string]interface{}) (interface{}, error) { func (p *Plugin) handleMemoList(args map[string]interface{}) (interface{}, error) {
memos := p.pendingMemos() p.mu.RLock()
memos := append([]Memo{}, p.memos...)
p.mu.RUnlock()
if len(memos) == 0 { if len(memos) == 0 {
return map[string]interface{}{ return map[string]interface{}{
"content": "暂无未完成的备忘", "content": "暂无备忘",
}, nil }, nil
} }
var sb strings.Builder var sb strings.Builder
for i, m := range memos { 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 { if i > 0 {
sb.WriteString("\n") 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{}{ return map[string]interface{}{
@ -276,9 +446,12 @@ func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, e
return &Plugin{name: name}, nil return &Plugin{name: name}, nil
} }
// cleanupData 卸载时清理备忘数据文件 // cleanupData 卸载时清理数据文件(待办 + 备忘)
func (p *Plugin) cleanupData() { func (p *Plugin) cleanupData() {
if p.filePath != "" { if p.todoPath != "" {
os.Remove(p.filePath) os.Remove(p.todoPath)
}
if p.memoPath != "" {
os.Remove(p.memoPath)
} }
} }