From 6b64d69b106b12621962a699e6ff62c4d68ec904 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Mon, 19 Oct 2026 14:11:20 +0800 Subject: [PATCH] =?UTF-8?q?refactor(timer):=20=E4=BB=8E=E5=86=85=E6=A0=B8?= =?UTF-8?q?=E5=86=85=E7=BD=AE=E6=8F=92=E4=BB=B6=E8=BF=81=E4=B8=BA=E5=A4=96?= =?UTF-8?q?=E9=83=A8=E6=8F=92=E4=BB=B6=EF=BC=8C=E7=A7=BB=E9=99=A4=E7=BC=96?= =?UTF-8?q?=E8=AF=91=E6=9C=9F=E6=B3=A8=E5=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 老大要求「把 timer 移到外部插件,移除内核的内置 timer」。本次只做内核侧 的**摘除**,插件本体在外部工程(plugins/timer → .hmap 安装)。 timer 原先由 internal/plugins/timer 的 init() 注册进编译期工厂表。 后果是改一行定时器逻辑要重编整个 homed(87MB)并重启内核——而它恰是 最该独立演进的部件:有自己的持久化状态、触发语义、并发模型,却被绑死 在主进程的发版节奏上。 搬出来后:换实现只需重装插件 + 重启单个插件进程;插件崩了自己被拉起, 不影响 agent 主循环。 1. 删除 internal/plugins/timer(plugin.go + plugin_test.go) 2. internal/plugins/all.go 去掉该 import,并写明**不要加回去**的原因 3. internal/plugins/integration_test.go 的 expectedTools 去掉 timer_set TestIntegrationAllPluginsRegister 的语义是「编译期内置插件是否都注册了工具」, 它走的是本地装配(setupIntegration 只加载 internal/plugins 下的内置包)。 timer 搬走后不再参与这个进程的装配,留在列表里只会造出永远绿不了的假失败。 真正不该省的是验证:外部插件的工具是否真注册成功,靠装完 .hmap 后实调 timer_set / timer_list / timer_cancel 来确认(见交付报告)。 - 配置表:外部插件同用 config_ ⇒ config_timer,老值 max_duration / pending_timers 直接读回,老大已设的定时器无需迁移 - 工具名:timer_set / timer_list / timer_cancel 不变,模型侧零感知 - 注入源与通道:source/channel 仍是 "timer" - 注入级别:内置插件可声明 L4,外部插件被夹到 L3;本插件本就要 L3, 故行为等价 StageHost.RegisterTool 对同名工具返回 `already registered`,而插件注册是 批量失败(新实例的工具一个都注不上)。所以必须**先装新内核**(不含内置 timer)再装外部 timer 插件,反之会撞名。 (cherry picked from commit f584776f23c3bfd8fb2892155e6018a1ec57eac0) --- internal/plugins/all.go | 22 ++++- internal/plugins/integration_test.go | 12 ++- internal/plugins/timer/plugin.go | 142 --------------------------- 3 files changed, 32 insertions(+), 144 deletions(-) delete mode 100644 internal/plugins/timer/plugin.go 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 -}