diff --git a/internal/plugins/all.go b/internal/plugins/all.go index bcdfae35..0bacb4c5 100644 --- a/internal/plugins/all.go +++ b/internal/plugins/all.go @@ -1,5 +1,26 @@ package plugins +// 编译期内置插件清单。 +// +// ★ 顺序无关(各插件在 init() 里自注册),但**排序保持一致**: +// 这样 diff 里能一眼看出增删,而不是被 import 重排掩盖。 +// +// ## timer 已移出本清单(2026-10-19) +// +// timer 原先是这里的编译器内置插件(internal/plugins/timer + RegisterFactory), +// 后果是改一行定时器逻辑要重编整个 homed(87MB)并重启内核 —— +// 而它是**最该独立演进**的部件之一:有自己的持久化状态、自己的触发语义、 +// 自己的并发模型,却被绑死在主进程的发版节奏上。 +// +// 现在它作为**外部插件**(plugins/timer,.hmap 安装)运行: +// - 换实现只需重装插件 + 重启单个插件进程,不动主进程; +// - 插件崩了自己被拉起,不影响 agent 主循环; +// - 工具名 / 配置表(config_timer)/ 注入通道(timer)保持不变, +// 模型侧与已设的定时器都无需迁移。 +// +// ⚠️ 不要再把 timer 加回这里:父子两份同时存在会导致同名工具注册冲突, +// 而「谁赢」取决于加载顺序 —— 那是最难排查的一类故障。 + import ( _ "github.com/JianFeeeee/HomeAgent/internal/plugins/agentcli" _ "github.com/JianFeeeee/HomeAgent/internal/plugins/ai_image" @@ -17,6 +38,5 @@ import ( _ "github.com/JianFeeeee/HomeAgent/internal/plugins/seemedia" _ "github.com/JianFeeeee/HomeAgent/internal/plugins/seq" _ "github.com/JianFeeeee/HomeAgent/internal/plugins/skillmgr" - _ "github.com/JianFeeeee/HomeAgent/internal/plugins/timer" _ "github.com/JianFeeeee/HomeAgent/internal/plugins/webui" ) diff --git a/internal/plugins/integration_test.go b/internal/plugins/integration_test.go index 4d923d57..2bc7aad2 100644 --- a/internal/plugins/integration_test.go +++ b/internal/plugins/integration_test.go @@ -165,7 +165,17 @@ func TestIntegrationAllPluginsRegister(t *testing.T) { "terminal_create", "terminal_write", "terminal_read", "terminal_resize", "terminal_close", "terminal_list", "healthcheck", "healthcheck_plugins", "healthcheck_tools", "healthcheck_memory", "healthcheck_perf", - "timer_set", + // ⚠️ timer_set **不在**这里(2026-10-19 起)。 + // + // 本用例的语义是「编译期内置插件是否都注册了工具」,set 走的是本地 + // 装配(setupIntegration 只加载 internal/plugins 下的内置包)。 + // timer 已搬为**外部插件**(plugins/timer,.hmap),不参与本进程装配, + // 所以它出现在这个列表里只会造出「永远绿不了」的假失败。 + // + // 外部插件的工具是否真的注册成功,由另一层负责:装完 .hmap 后 + // 用 timer_list / timer_set 实调一次(见交付报告里的现网验证)。 + // 别为了「让它绿」把 timer 加回 all.go —— 那会让内置与外部两份 + // 同时存在,工具重名而胜者取决于加载顺序。 } defs := env.stageHost.GetToolDefs() diff --git a/internal/plugins/timer/plugin.go b/internal/plugins/timer/plugin.go deleted file mode 100644 index d5b29e24..00000000 --- a/internal/plugins/timer/plugin.go +++ /dev/null @@ -1,142 +0,0 @@ -package timer - -import ( - "fmt" - "log" - "sync" - "time" - - "github.com/JianFeeeee/HomeAgent/internal/plugin" - safeclose "github.com/JianFeeeee/HomeAgent/internal/safeclose" - sdk "github.com/JianFeeeee/HomeAgent/internal/sdk" -) - -func init() { - plugin.RegisterPluginMeta("timer", "定时任务", "Timer") - plugin.RegisterFactory("timer", func(name string, config map[string]interface{}) (sdk.Plugin, error) { - return New(name), nil - }) -} - -type Plugin struct { - name string - mu sync.Mutex - wg sync.WaitGroup - stopCh chan struct{} - maxDur time.Duration -} - -type timerTask struct { - id int - dur time.Duration - message string - doneAt time.Time - s *sdk.PluginSDK -} - -func New(name string) *Plugin { - return &Plugin{name: name, stopCh: make(chan struct{})} -} - -func (p *Plugin) Name() string { return p.name } - -func (p *Plugin) Start(s *sdk.PluginSDK) error { - s.SetAutoRestart(true) - p.maxDur = 24 * time.Hour - // 停止清理(取消倒计时)交由 stop handler:内核在调用 Stop() 之前执行。 - // 幂等(本仓 close 纪律):stop handler 由内核调用;若将来多一条 - // 停机路径(重载/热更新)就会二次 close ⇒ panic。 - s.RegisterStopHandler(safeclose.SafeFunc(p.stopCh)) - // 删除清理:移除插件自身配置(删除专用回调,重载不触发)。 - s.RegisterOnRemoveHandler(func() { - if err := s.Settings().Remove("max_duration"); err != nil { - log.Printf("[timer] onRemove cleanup: %v", err) - } - }) - s.Settings().RegisterDef(sdk.ConfigDef{ - Key: "max_duration", Type: "string", DisplayName: "最大定时时长", - Description: "允许设置的最大定时时长,例如 24h, 7d, 1h(默认 24h)", - Default: "24h", - }) - if v, _ := s.Settings().Get("max_duration"); v != nil { - if s, ok := v.(string); ok && s != "" { - if d, err := time.ParseDuration(s); err == nil && d > 0 { - p.maxDur = d - } - } - } - - // timer 通道:定时器到点经它注入 agent(见本文件 InjectInterruptTextOpts 调用)。 - _ = s.RegisterInputChannel("timer", sdk.ChannelDef{}) - s.RegisterTool("timer_set", sdk.ToolDef{ - Name: "timer_set", - Description: "设置一个定时提醒。倒计时结束后通过中断通道通知 agent。", - Parameters: map[string]interface{}{ - "type": "object", - "properties": map[string]interface{}{ - "duration": map[string]interface{}{ - "type": "string", - "description": "持续时间,例如 5s, 2m, 1h", - }, - "message": map[string]interface{}{ - "type": "string", - "description": "提醒内容", - }, - }, - "required": []string{"duration", "message"}, - }, - // 写定时器:改 p.timers(持 p.mu),按序更可预期 - Serial: true, - }, func(args map[string]interface{}) (interface{}, error) { - durStr, _ := args["duration"].(string) - message, _ := args["message"].(string) - if durStr == "" { - return map[string]interface{}{"error": "duration is required"}, nil - } - if message == "" { - return map[string]interface{}{"error": "message is required"}, nil - } - - dur, err := time.ParseDuration(durStr) - if err != nil { - return map[string]interface{}{"error": fmt.Sprintf("invalid duration %q: %v", durStr, err)}, nil - } - if dur > p.maxDur { - return map[string]interface{}{"error": fmt.Sprintf("duration %v exceeds max %v", dur, p.maxDur)}, nil - } - - p.mu.Lock() - p.wg.Add(1) - p.mu.Unlock() - - go func() { - defer p.wg.Done() - select { - case <-time.After(dur): - log.Printf("[timer] firing: %s (%s later)", message, dur) - // NoMemory:定时提醒是系统通知,不是记忆内容。 - // PriorityL3:定时器是“时钟那种实时工作”——到点就该处理, - // 比 QQ 那类可无限等待的异步消息高(L3 vs L1)。 - s.InjectInterruptTextOpts("timer", "timer", fmt.Sprintf("timer: %s", message), - sdk.InjectOptions{NoMemory: true, Priority: sdk.PriorityL3}) - case <-p.stopCh: - log.Printf("[timer] cancelled: %s", message) - } - }() - - doneAt := time.Now().Add(dur) - return map[string]interface{}{ - "status": "timer_set", - "duration": durStr, - "message": message, - "done_at": doneAt.Format(time.RFC3339), - }, nil - }) - - return nil -} - -func (p *Plugin) Stop() error { - p.wg.Wait() - return nil -}