mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 06:47:29 +00:00
fix(pluginmgr): 升级后恢复运行状态 —— 重装不该把正在用的插件静默停掉
## 日志实证(2026-10-20 09:54)
重装 qq 插件后,agent 的工具清单里 qq_* 全消失,它只能:
terminal_list → get_plugin_tools ×2 → plugin_start(qq) → plugin_status
→ output_send__qq_help → get_plugin_tools → 才摸到 qq_list_stickers
根因:升级路径 `installFromData` 里 StopAndUnload 停掉旧实例后,**再也没有
把它拉回来**。安装的语义应当是"换个包",不是"把正在用的插件关掉"。
## 修法
- 升级前通过 ListPluginRuntimes 记录 `wasLoaded`
- 解压成功、skill 交接完成后,若升级前在跑则 StartPlugin 拉回
- 返回体暴露 `was_running` / `running_restored`,失败不阻断安装(插件已装好)
实测:`reinstalled qq, was_running=true, running_restored=true`,装完
loaded/alive 均为 true、PID 更新。
## 测试
- TestUpgradeRestoresRunningState:升级前在跑 ⇒ StartPlugin 恰好调用一次
- TestUpgradeDoesNotStartStoppedPlugin:升级前没跑 ⇒ 不得强行拉起
## 同批日志里的另外两条(非代码问题,仅记录)
1. llmsproxy 在 09:52:21 / 09:53:14 被重启,其间 homed 收到 connection refused,
经「stream 失败→非流式 + 2s 重试→换 provider」兜底,用户侧无失败但多耗一轮。
2. 上游返回 404「The model is not registered」与 429 限流(channels-codearts),
属 llmsproxy 侧渠道配置/配额问题,建议在 llmsproxy 里核对该模型注册。
This commit is contained in:
@ -733,6 +733,19 @@ func (p *Plugin) installFromData(data []byte, overwrite bool) (interface{}, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
if existing && overwrite {
|
if existing && overwrite {
|
||||||
|
// ★ 先记住升级前是否在运行:StopAndUnload 会把它停掉,而升级不该
|
||||||
|
// 顺手改变运行状态。生产实测(2026-10-20):重装 qq 后它静默停着,
|
||||||
|
// agent 发现工具全没了,只能 get_plugin_tools×N → plugin_start 摸回来。
|
||||||
|
wasLoaded := false
|
||||||
|
if p.sdk != nil && p.sdk.PluginMgr() != nil {
|
||||||
|
for _, rt := range p.sdk.PluginMgr().ListPluginRuntimes() {
|
||||||
|
if rt.Name == pkg.Name && rt.Loaded {
|
||||||
|
wasLoaded = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 原地更新:停旧实例(保留配置表),备份旧目录,解压新包,失败回滚。
|
// 原地更新:停旧实例(保留配置表),备份旧目录,解压新包,失败回滚。
|
||||||
if p.sdk != nil && p.sdk.PluginMgr() != nil {
|
if p.sdk != nil && p.sdk.PluginMgr() != nil {
|
||||||
if err := p.sdk.PluginMgr().StopAndUnload(pkg.Name); err != nil {
|
if err := p.sdk.PluginMgr().StopAndUnload(pkg.Name); err != nil {
|
||||||
@ -771,6 +784,17 @@ func (p *Plugin) installFromData(data []byte, overwrite bool) (interface{}, erro
|
|||||||
// 失败不阻断 —— skill 登记是附带收益,插件本身已装好。
|
// 失败不阻断 —— skill 登记是附带收益,插件本身已装好。
|
||||||
skillN, skillFailed := p.handOutPluginSkills(target, pkg.Name)
|
skillN, skillFailed := p.handOutPluginSkills(target, pkg.Name)
|
||||||
|
|
||||||
|
// 升级前在跑的插件,升级后自动拉回来(运行状态不该被"换个包"改变)。
|
||||||
|
// 失败不阻断安装:返回里如实写 running_restored=false,调用方可见。
|
||||||
|
runningRestored := false
|
||||||
|
if wasLoaded && p.sdk != nil && p.sdk.PluginMgr() != nil {
|
||||||
|
if err := p.sdk.PluginMgr().StartPlugin(pkg.Name); err != nil {
|
||||||
|
log.Printf("[pluginmgr] 升级后恢复运行 %s 失败: %v(插件已安装,可手动 plugin_start)", pkg.Name, err)
|
||||||
|
} else {
|
||||||
|
runningRestored = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
checksum := fmt.Sprintf("%x", sha256.Sum256(data))
|
checksum := fmt.Sprintf("%x", sha256.Sum256(data))
|
||||||
action := "upgraded"
|
action := "upgraded"
|
||||||
if cmpVersion(pkg.Version, oldVersion) < 0 {
|
if cmpVersion(pkg.Version, oldVersion) < 0 {
|
||||||
@ -788,6 +812,8 @@ func (p *Plugin) installFromData(data []byte, overwrite bool) (interface{}, erro
|
|||||||
"action": action,
|
"action": action,
|
||||||
"reload_required": true,
|
"reload_required": true,
|
||||||
"config_kept": true,
|
"config_kept": true,
|
||||||
|
"was_running": wasLoaded,
|
||||||
|
"running_restored": runningRestored,
|
||||||
"skills_registered": skillN,
|
"skills_registered": skillN,
|
||||||
"skills_failed": skillFailed,
|
"skills_failed": skillFailed,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@ -68,6 +68,8 @@ func (m *pmSettings) IsSecret(string) bool { return false }
|
|||||||
type fakePluginMgr struct {
|
type fakePluginMgr struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
stopAndUnloads []string
|
stopAndUnloads []string
|
||||||
|
loaded map[string]bool // 升级前"在跑"的插件,供 ListPluginRuntimes 返回
|
||||||
|
starts []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakePluginMgr) ListLoadedPlugins() []string { return nil }
|
func (f *fakePluginMgr) ListLoadedPlugins() []string { return nil }
|
||||||
@ -77,7 +79,12 @@ func (f *fakePluginMgr) IsBuiltinPlugin(string) bool { return
|
|||||||
func (f *fakePluginMgr) DisablePlugin(string, string) error { return nil }
|
func (f *fakePluginMgr) DisablePlugin(string, string) error { return nil }
|
||||||
func (f *fakePluginMgr) EnablePlugin(string) error { return nil }
|
func (f *fakePluginMgr) EnablePlugin(string) error { return nil }
|
||||||
func (f *fakePluginMgr) StopPlugin(string) error { return nil }
|
func (f *fakePluginMgr) StopPlugin(string) error { return nil }
|
||||||
func (f *fakePluginMgr) StartPlugin(string) error { return nil }
|
func (f *fakePluginMgr) StartPlugin(name string) error {
|
||||||
|
f.mu.Lock()
|
||||||
|
defer f.mu.Unlock()
|
||||||
|
f.starts = append(f.starts, name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
func (f *fakePluginMgr) IsPluginRuntimeStopped(string) bool { return false }
|
func (f *fakePluginMgr) IsPluginRuntimeStopped(string) bool { return false }
|
||||||
func (f *fakePluginMgr) RemovePlugin(string) error { return nil }
|
func (f *fakePluginMgr) RemovePlugin(string) error { return nil }
|
||||||
func (f *fakePluginMgr) StopAndUnload(name string) error {
|
func (f *fakePluginMgr) StopAndUnload(name string) error {
|
||||||
@ -95,7 +102,15 @@ func (f *fakePluginMgr) PluginDir() string { return "" }
|
|||||||
func (f *fakePluginMgr) PluginRuntime(string) (sdk.PluginRuntimeInfo, bool) {
|
func (f *fakePluginMgr) PluginRuntime(string) (sdk.PluginRuntimeInfo, bool) {
|
||||||
return sdk.PluginRuntimeInfo{}, false
|
return sdk.PluginRuntimeInfo{}, false
|
||||||
}
|
}
|
||||||
func (f *fakePluginMgr) ListPluginRuntimes() []sdk.PluginRuntimeInfo { return nil }
|
func (f *fakePluginMgr) ListPluginRuntimes() []sdk.PluginRuntimeInfo {
|
||||||
|
f.mu.Lock()
|
||||||
|
defer f.mu.Unlock()
|
||||||
|
out := make([]sdk.PluginRuntimeInfo, 0, len(f.loaded))
|
||||||
|
for name, loaded := range f.loaded {
|
||||||
|
out = append(out, sdk.PluginRuntimeInfo{Name: name, Loaded: loaded})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// buildHmap 构造一个最小 .hmap 包。
|
// buildHmap 构造一个最小 .hmap 包。
|
||||||
func buildHmap(t *testing.T, name, version string) []byte {
|
func buildHmap(t *testing.T, name, version string) []byte {
|
||||||
@ -234,3 +249,65 @@ func TestExtractFailureRollsBack(t *testing.T) {
|
|||||||
t.Fatalf("old version not restored: %v / %v", mfest, err)
|
t.Fatalf("old version not restored: %v / %v", mfest, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ★ 升级不该顺手改变运行状态(2026-10-20 生产实证)。
|
||||||
|
//
|
||||||
|
// 现场:重装 qq 插件后它静默停着(StopAndUnload 只停不启),agent 发现工具
|
||||||
|
// 全没了,只能 get_plugin_tools ×N → plugin_start 摸回来。安装流程的语义应当是
|
||||||
|
// "换个包",不是"把正在用的插件关掉"。
|
||||||
|
func TestUpgradeRestoresRunningState(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
fm := &fakePluginMgr{loaded: map[string]bool{}}
|
||||||
|
bus := events.NewBus()
|
||||||
|
sdkInst := sdk.New("pluginmgr", sdk.SDKConfig{
|
||||||
|
Settings: &pmSettings{},
|
||||||
|
EventBus: bus,
|
||||||
|
PluginMgr: fm,
|
||||||
|
})
|
||||||
|
p := &Plugin{pluginDir: dir, sdk: sdkInst}
|
||||||
|
|
||||||
|
if r, _ := p.installFromData(buildHmap(t, "demo", "1.0.0"), false); r.(map[string]interface{})["status"] != "installed" {
|
||||||
|
t.Fatal("install failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 升级前它是在跑的
|
||||||
|
fm.mu.Lock()
|
||||||
|
fm.loaded["demo"] = true
|
||||||
|
fm.mu.Unlock()
|
||||||
|
|
||||||
|
r, _ := p.installFromData(buildHmap(t, "demo", "2.0.0"), true)
|
||||||
|
m := r.(map[string]interface{})
|
||||||
|
if m["was_running"] != true {
|
||||||
|
t.Fatalf("was_running 应为 true: %v", m)
|
||||||
|
}
|
||||||
|
if m["running_restored"] != true {
|
||||||
|
t.Fatalf("升级后应自动拉回运行状态: %v", m)
|
||||||
|
}
|
||||||
|
fm.mu.Lock()
|
||||||
|
starts := append([]string{}, fm.starts...)
|
||||||
|
fm.mu.Unlock()
|
||||||
|
if len(starts) != 1 || starts[0] != "demo" {
|
||||||
|
t.Fatalf("StartPlugin 应被调用一次: %v", starts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 升级前没在跑的插件,安装后也不该被强行拉起。
|
||||||
|
func TestUpgradeDoesNotStartStoppedPlugin(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
fm := &fakePluginMgr{loaded: map[string]bool{}}
|
||||||
|
bus := events.NewBus()
|
||||||
|
sdkInst := sdk.New("pluginmgr", sdk.SDKConfig{Settings: &pmSettings{}, EventBus: bus, PluginMgr: fm})
|
||||||
|
p := &Plugin{pluginDir: dir, sdk: sdkInst}
|
||||||
|
|
||||||
|
if r, _ := p.installFromData(buildHmap(t, "demo", "1.0.0"), false); r.(map[string]interface{})["status"] != "installed" {
|
||||||
|
t.Fatal("install failed")
|
||||||
|
}
|
||||||
|
r, _ := p.installFromData(buildHmap(t, "demo", "2.0.0"), true)
|
||||||
|
m := r.(map[string]interface{})
|
||||||
|
if m["was_running"] != false || m["running_restored"] != false {
|
||||||
|
t.Fatalf("未运行的插件不应被拉起: %v", m)
|
||||||
|
}
|
||||||
|
if len(fm.starts) != 0 {
|
||||||
|
t.Fatalf("StartPlugin 不应被调用: %v", fm.starts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user