Files
homeagent-sdk/example/memo/plugin.go
root aee63a4f98 example: 演示插件 onRemove 清理补齐(memo/rss/weather)
- memo: 卸载时删除 memos.json 数据文件
- rss: 卸载时清理 .homeagent/rss 订阅数据目录
- weather: 卸载时清理 .homeagent/weather 缓存目录
- 与 calendar(events.json)一致:仅删除触发、重载不触发;
  files(filesDir 为用户配置的访问根目录)、bili/qq(用户下载资产)、
  ocr(临时目录函数内自清理)按语义不加入删除回调
2026-08-02 13:37:59 +08:00

285 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
s.SetAutoRestart(true)
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.RegisterOnRemoveHandler(p.cleanupData)
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,
}
}
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil
}
// cleanupData 卸载时清理备忘数据文件
func (p *Plugin) cleanupData() {
if p.filePath != "" {
os.Remove(p.filePath)
}
}