From 1013aa0aa9d8d699587a8ff51d443d4db577c41e Mon Sep 17 00:00:00 2001 From: root Date: Sun, 2 Aug 2026 10:23:40 +0800 Subject: [PATCH] feat(sdk): add RegisterStopHandler/RunStopHandlers and wire stop cleanup into registry lifecycle - SDK PluginSDK gains RegisterStopHandler(fn func()) + RunStopHandlers() (LIFO, idempotent, cleared after running); synced to third_party copy - Registry keeps per-plugin SDK refs (sdkRefs); StopAll/ReloadOne/ DisablePlugin run handlers before calling Stop() - timer built-in plugin demonstrates handler-based shutdown cleanup - z_bridge (linux/windows templates) runs handlers before plugin.Stop() --- cmd/gui/renderer/style.css | 16 +++++++++++--- internal/config/registry.go | 2 -- internal/plugin/registry.go | 18 ++++++++++++++++ internal/plugins/timer/plugin.go | 3 ++- internal/plugins/webui/dashboard.html | 8 ++++--- third_party/homeagent-sdk/sdk/plugin.go | 28 +++++++++++++++++++++++++ 6 files changed, 66 insertions(+), 9 deletions(-) diff --git a/cmd/gui/renderer/style.css b/cmd/gui/renderer/style.css index fda4088..6988baa 100644 --- a/cmd/gui/renderer/style.css +++ b/cmd/gui/renderer/style.css @@ -263,9 +263,13 @@ code { font-family:monospace; font-size:12px; color:var(--pre-color) } .msg { max-width:95% } } @media(max-width:768px) { - nav { padding:0 8px; gap:2px } - nav h1 { font-size:13px; margin-right:8px } - nav a { padding:8px 6px; font-size:11px } + nav { padding:0 6px; gap:2px; overflow-x:auto; scrollbar-width:none; -ms-overflow-style:none; flex-wrap:nowrap } + nav::-webkit-scrollbar { display:none } + nav h1 { display:none } + nav a { padding:10px 8px; font-size:12px; white-space:nowrap; flex-shrink:0 } + nav > div { flex-shrink:0 } + #conn-name-display { display:none } + .conn-indicator { padding:4px 6px } .container { padding:12px } .card { padding:12px } .grid-2,.grid-3,.grid-4 { grid-template-columns:1fr } @@ -284,6 +288,12 @@ code { font-family:monospace; font-size:12px; color:var(--pre-color) } .health-item { flex-wrap:wrap; gap:4px } .health-item .check-name { flex:auto; width:100% } } +@media(max-width:480px) { + .chat-layout { gap:10px } + .chat-sidebar { min-width:0 } + .msg { max-width:98% } + .settings-sidebar a { padding:6px 10px; font-size:12px } +} /* Connection Manager */ .overlay { position:fixed; inset:0; background:rgba(0,0,0,0.6); display:none; align-items:center; justify-content:center; z-index:1000 } diff --git a/internal/config/registry.go b/internal/config/registry.go index c32c825..eba7354 100644 --- a/internal/config/registry.go +++ b/internal/config/registry.go @@ -394,7 +394,6 @@ func (r *ConfigRegistry) seedDBValues(dataDir string) { set("core.memory.text", filepath.Join(dataDir, "memory", "text")) set("core.memory.documents", filepath.Join(dataDir, "memory", "documents")) set("core.knowledge.path", filepath.Join(dataDir, "knowledge")) - set("core.skills.path", filepath.Join(dataDir, "skills")) set("core.log.path", filepath.Join(dataDir, "log")) set("core.agent.max_tool_turns", "10") @@ -488,7 +487,6 @@ func (r *ConfigRegistry) seedCoreDefs(dataDir string) { reg(ConfigDef{Key: "core.memory.text", Default: filepath.Join(dataDir, "memory", "text"), Type: "string", DisplayName: "文本记忆路径", Description: "短期文本记忆存储目录", Category: "paths"}) reg(ConfigDef{Key: "core.memory.documents", Default: filepath.Join(dataDir, "memory", "documents"), Type: "string", DisplayName: "文档记忆路径", Description: "文档记忆存储目录", Category: "paths"}) reg(ConfigDef{Key: "core.knowledge.path", Default: filepath.Join(dataDir, "knowledge"), Type: "string", DisplayName: "知识库路径", Description: "知识库存储目录", Category: "paths"}) - reg(ConfigDef{Key: "core.skills.path", Default: filepath.Join(dataDir, "skills"), Type: "string", DisplayName: "技能目录", Description: "OpenClaw 技能存储目录", Category: "paths"}) reg(ConfigDef{Key: "core.log.path", Default: filepath.Join(dataDir, "log"), Type: "string", DisplayName: "日志目录", Description: "日志文件输出目录", Category: "paths"}) reg(ConfigDef{Key: "core.agent.max_tool_turns", Default: "10", Type: "int", DisplayName: "最大工具轮次", Description: "单次请求允许的最大工具调用轮数", Category: "agent"}) diff --git a/internal/plugin/registry.go b/internal/plugin/registry.go index 633377c..729693c 100644 --- a/internal/plugin/registry.go +++ b/internal/plugin/registry.go @@ -68,6 +68,7 @@ type Registry struct { factories map[string]NativeFactory pluginAutoRestart map[string]bool + sdkRefs map[string]*sdk.PluginSDK iom *agentIO.IOManager evBus *events.Bus @@ -103,6 +104,7 @@ func NewRegistry() *Registry { plugins: make(map[string]sdk.Plugin), factories: make(map[string]NativeFactory), pluginAutoRestart: make(map[string]bool), + sdkRefs: make(map[string]*sdk.PluginSDK), knownDisabled: make(map[string]bool), } } @@ -378,16 +380,25 @@ func (r *Registry) loadOne(plgDir, name string) bool { r.mu.Lock() r.plugins[name] = plg r.pluginAutoRestart[name] = plgSDK.AutoRestart() + r.sdkRefs[name] = plgSDK r.instances = append(r.instances, plg) r.mu.Unlock() log.Printf("[plugin] loaded: %s", name) return true } +// runStopHandlers 执行插件注册的停止清理回调(SDK 层),须在调用插件 Stop() 之前执行。 +func (r *Registry) runStopHandlers(name string) { + if sdk, ok := r.sdkRefs[name]; ok { + sdk.RunStopHandlers() + } +} + func (r *Registry) StopAll() { r.mu.Lock() defer r.mu.Unlock() for _, p := range r.instances { + r.runStopHandlers(p.Name()) if err := p.Stop(); err != nil { log.Printf("[plugin] stop %s: %v", p.Name(), err) } @@ -395,6 +406,7 @@ func (r *Registry) StopAll() { r.plugins = make(map[string]sdk.Plugin) r.instances = nil r.pluginAutoRestart = make(map[string]bool) + r.sdkRefs = make(map[string]*sdk.PluginSDK) } func (r *Registry) Reload(dir string) (string, error) { @@ -410,10 +422,12 @@ func (r *Registry) ReloadOne(name string) error { r.mu.Lock() if p, ok := r.plugins[name]; ok { + r.runStopHandlers(name) if err := p.Stop(); err != nil { log.Printf("[plugin] stop %s for reload: %v", name, err) } delete(r.plugins, name) + delete(r.sdkRefs, name) for i, inst := range r.instances { if inst.Name() == name { r.instances = append(r.instances[:i], r.instances[i+1:]...) @@ -481,10 +495,12 @@ func (r *Registry) Disable(name string) error { r.mu.Lock() p, ok := r.plugins[name] if ok { + r.runStopHandlers(name) if err := p.Stop(); err != nil { log.Printf("[plugin] stop %s for disable: %v", name, err) } delete(r.plugins, name) + delete(r.sdkRefs, name) for i, inst := range r.instances { if inst.Name() == name { r.instances = append(r.instances[:i], r.instances[i+1:]...) @@ -539,10 +555,12 @@ func (r *Registry) DisablePlugin(name, by string) error { r.mu.Lock() p, ok := r.plugins[name] if ok { + r.runStopHandlers(name) if err := p.Stop(); err != nil { log.Printf("[plugin] stop %s for disable: %v", name, err) } delete(r.plugins, name) + delete(r.sdkRefs, name) for i, inst := range r.instances { if inst.Name() == name { r.instances = append(r.instances[:i], r.instances[i+1:]...) diff --git a/internal/plugins/timer/plugin.go b/internal/plugins/timer/plugin.go index 332dad6..797f1fa 100644 --- a/internal/plugins/timer/plugin.go +++ b/internal/plugins/timer/plugin.go @@ -42,6 +42,8 @@ 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() 之前执行。 + s.RegisterStopHandler(func() { close(p.stopCh) }) s.Settings().RegisterDef(sdk.ConfigDef{ Key: "max_duration", Type: "string", DisplayName: "最大定时时长", Description: "允许设置的最大定时时长,例如 24h, 7d, 1h(默认 24h)", @@ -118,7 +120,6 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { } func (p *Plugin) Stop() error { - close(p.stopCh) p.wg.Wait() return nil } diff --git a/internal/plugins/webui/dashboard.html b/internal/plugins/webui/dashboard.html index 7fdba08..c5b354c 100644 --- a/internal/plugins/webui/dashboard.html +++ b/internal/plugins/webui/dashboard.html @@ -258,9 +258,11 @@ code { font-family:monospace; font-size:12px; color:var(--pre-color) } .msg { max-width:95% } } @media(max-width:768px) { - nav { padding:0 8px; gap:2px } - nav h1 { font-size:13px; margin-right:8px } - nav a { padding:8px 6px; font-size:11px } + nav { padding:0 6px; gap:2px; overflow-x:auto; scrollbar-width:none; -ms-overflow-style:none; flex-wrap:nowrap } + nav::-webkit-scrollbar { display:none } + nav h1 { font-size:0; margin-right:6px; color:var(--accent) } + nav a { padding:10px 8px; font-size:12px; white-space:nowrap; flex-shrink:0 } + nav > div { flex-shrink:0 } .container { padding:12px } .card { padding:12px } .grid-2,.grid-3,.grid-4 { grid-template-columns:1fr } diff --git a/third_party/homeagent-sdk/sdk/plugin.go b/third_party/homeagent-sdk/sdk/plugin.go index 081285c..8285eba 100644 --- a/third_party/homeagent-sdk/sdk/plugin.go +++ b/third_party/homeagent-sdk/sdk/plugin.go @@ -199,6 +199,9 @@ type PluginSDK struct { events EventSubscriber autoRestart bool + + stopMu sync.Mutex + stopHandlers []func() } // New creates a PluginSDK with the given dependencies. @@ -367,3 +370,28 @@ func (s *PluginSDK) SetAutoRestart(enabled bool) { s.autoRestart = enabled } // AutoRestart 返回插件是否允许自动重启。 func (s *PluginSDK) AutoRestart() bool { return s.autoRestart } + +// RegisterStopHandler 注册插件停止阶段的清理回调。 +// 注册的 handler 会在插件 Stop() 之前按"后注册先执行"的顺序调用, +// 适用于释放资源、落盘状态、关闭子进程等停止时清理操作。 +// 可注册多个;执行后清空(进程停止前只执行一次)。 +func (s *PluginSDK) RegisterStopHandler(fn func()) { + if fn == nil { + return + } + s.stopMu.Lock() + s.stopHandlers = append(s.stopHandlers, fn) + s.stopMu.Unlock() +} + +// RunStopHandlers 执行全部已注册的 stop handler(后注册先执行,执行后清空,幂等)。 +// 由内核(内置插件)或插件桥接层(外部插件 z_bridge 的 StopPlugin)在调用插件 Stop() 前执行。 +func (s *PluginSDK) RunStopHandlers() { + s.stopMu.Lock() + handlers := append([]func(){}, s.stopHandlers...) + s.stopHandlers = nil + s.stopMu.Unlock() + for i := len(handlers) - 1; i >= 0; i-- { + handlers[i]() + } +}