fix(plugin): 修两处持锁自死锁(CI 双红真因)+ tools_ready 事件声明

## 死锁(这是 CI 连续两次红的真因)

`StopAll` 与 `StopPlugin` 都在持有 `r.mu` **写锁**时调用
`RemoveBuiltinSkill` → `skillDirOf` → `r.mu.RLock()` ⇒ **自死锁**。

后果不是报错而是**卡死**:
- internal/plugins 卡 300s 超时(TestCapability_RestrictedPluginStillLoads)
- internal/plugin  卡 840s 超时(TestStopPluginDoesNotTouchPersistentState)
- CI 的 `go test ./...` 与 `go test -race` 双双红

修法:拆出**不加锁**的内部版本,持锁路径只走它:
- `skillDirFrom(dataDir, name)`(纯函数)
- `removeBuiltinSkillLocked(pluginName, p, dataDir)`(显式传 dataDir,
  避免"看起来没取锁、一改就取锁"的隐患)
- `announceSkillRemovedLocked`(持锁下直读 r.evBus)

修后:internal/plugins 300s → 12s,internal/plugin 840s → 0.7s。

## 防回归判据

`TestStopAllAndStopPluginDoNotDeadlock`:死锁**不返回**,普通断言抓不到,
所以用**超时**当判据(5 秒未返回即失败),并指明应改用 Locked 版本。

## tools_ready 事件声明

新增的 tools_ready 触发 `TestEveryPublishedEventHasAnOutwardPath`
(该判据防"事件发了、客户端收不到"的静默失效)。

tools_ready 是**内核内部**就绪信号(plugin-host → seq 等订阅者),
不是对话流程事件,前端无需感知 ⇒ 在 allowed 里显式声明理由。

## 验证

go build ./... / go vet ./... / go test -p 1 ./... / -race 全通过。
This commit is contained in:
JianFeeeee
2026-10-25 18:25:37 +08:00
parent fe1c4f6984
commit d65b742bd7
28 changed files with 813 additions and 75 deletions

View File

@ -1084,8 +1084,12 @@ func (r *Registry) StopAll() {
}
// 关停时摘掉所有随包 skill:进程要退出,留下 skill 会让下次启动
// 出现"索引里有、插件却没起"的错位(尤其插件被配置禁用时)。
//
// ★ 必须用**不加锁**的路径:此处 r.mu 是写锁,而 RemoveBuiltinSkill →
// skillDirOf 要取 RLock ⇒ 自死锁(实测 300s 卡死,CI 双红)。
// 直接读 r.dataDir(已在锁内)并调纯函数 skillDirFrom。
for _, st := range stoppers {
r.RemoveBuiltinSkill(st.plugin.Name(), st.plugin)
r.removeBuiltinSkillLocked(st.plugin.Name(), st.plugin, r.dataDir)
}
r.plugins = make(map[string]sdk.Plugin)
r.instances = nil
@ -1575,7 +1579,11 @@ func (r *Registry) StopPlugin(name string) error {
}
// 先摘 skill 再停插件:停用后模型若仍看到 skill,会照做并拿到
// 一连串"工具不存在",且无从知道原因是插件被关了。
r.RemoveBuiltinSkill(name, p)
//
// ★ 用**不加锁**版:此处 r.mu 是写锁(上面刚 Lock 过),
// 而 RemoveBuiltinSkill 会取 RLock ⇒ 自死锁。
// 与 StopAll 是同一个坑(实测两处各卡 300s / 90s 超时)。
r.removeBuiltinSkillLocked(name, p, r.dataDir)
r.runStopHandlers(name)
stopErr := p.Stop()