mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-05 16:57:32 +00:00
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:
@ -25,6 +25,20 @@ type Sequence struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Groups []Group `json:"groups"`
|
||||
|
||||
// Boot 标记该序列在**系统就绪时自动执行一次**(每次进程启动一次)。
|
||||
//
|
||||
// 这是序列最核心的用途之一(用户原话:"seq 最核心的是定时复用,
|
||||
// 比如开机执行一段序列")。单次任务里编序列往往不划算 —— 先 create
|
||||
// 再 run 比直接调工具多两步;真正省事的是**同一套流程反复跑**,
|
||||
// 而开机是最典型的周期性触发点。
|
||||
//
|
||||
// 执行时机:订阅 agent_ready 事件(内核在 kernel assembly → 插件调度
|
||||
// → agent.Start() 之后发布),因此执行时工具已全部注册、agent 已能
|
||||
// 接受输入。
|
||||
//
|
||||
// 声明方式:seq_create 传 boot:true,或序列文件里写 "boot": true。
|
||||
Boot bool `json:"boot,omitempty"`
|
||||
}
|
||||
|
||||
// Group 是一组工具:**组内并行、组间串行**,且拥有独立签名。
|
||||
@ -93,6 +107,11 @@ type rawSeq struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Groups []rawGroup `json:"groups"`
|
||||
// Boot 必须在这里声明,否则 Parse 的 DisallowUnknownFields 会把
|
||||
// "boot": true 当成**拼写错误**拒绝 —— 开机序列根本存不进去。
|
||||
// (这正是 DisallowUnknownFields 的价值:漏声明字段会立刻报错,
|
||||
// 而不是静默忽略后让人纳闷"为什么开机不执行"。)
|
||||
Boot bool `json:"boot"`
|
||||
}
|
||||
|
||||
// Parse 把序列文本解析为 AST 并完成**全部静态校验**。
|
||||
@ -117,7 +136,7 @@ func Parse(data []byte) (*Sequence, error) {
|
||||
return nil, fmt.Errorf("序列 %q 没有任何 group", raw.Name)
|
||||
}
|
||||
|
||||
seq := &Sequence{Name: raw.Name, Description: raw.Description}
|
||||
seq := &Sequence{Name: raw.Name, Description: raw.Description, Boot: raw.Boot}
|
||||
seenGroup := map[string]bool{}
|
||||
for gi, rg := range raw.Groups {
|
||||
g, err := buildGroup(rg)
|
||||
|
||||
Reference in New Issue
Block a user