feat(seq): 六个 seq_* 工具、插件装配,并补内核两处缺口(插件线 P4)

内核缺口(都是 P3 落地时暴露的真实缺陷):
· **GetAllTools 丢 ParallelSafe**:它只带出 Name/Description/Parameters,
  插件看到的设备工具一律"不可并发" ⇒ 设备工具的并发声明**对插件不可见**。
· **ToolAPI 缺按名查**:新增 ToolDefByName。插件需要在**运行前**判断目标
  是否存在/是否并发安全(工具动态注册,"不存在"是常态),
  而 GetAllTools 只能拿到全量列表。查不到返回 nil,不 panic。

seq 插件:
· plugin.go:插件骨架 + kernelRunner(把 sdk.ToolAPI 收窄成三个方法,
  判据因此能用假实现驱动,不必构造整个内核)
· tools.go:六个工具定义(独立真相源,注册/判据/文档都从它取)
· handlers.go:seq_create/list/delete/run/call/when_call 的实现
· register.go + all.go:按 skillmgr 同一范式 init 注册

★ 过程中解决一个**我自己的设计矛盾**:
判据原先要求 `seq_call` / `seq_when_call` 进黑名单,但"按名调用
group/序列"恰恰是本包的核心能力——禁掉它,序列就退化成单层脚本。
分层澄清后:黑名单只管**对外发消息 / 起子 agent / 改插件表 / 再跑整条
序列**;seq_call 系列留给序列内部组合,其递归由 maxCallDepth + 环检测
负责(设计文档 §8.3 本来就是这么定的,我把两层混了)。
`seq_run` 留在黑名单:序列内再跑整条序列语义上是递归。

**六个工具一律不声明 ParallelSafe**:seq_run/seq_call 会执行一串工具,
其中可能含写操作;标成并发安全会让内核把两条 seq_run 并发跑,
两个序列的执行顺序交错、变量表互相污染。

安全性:序列名与文件路径都做穿越防护(`..` 段、分隔符、空名)。

过程中四次自伤:
1. 臆造 `jsonMarshalIndent`(不存在)→ 改 encoding/json.MarshalIndent;
   并把 execGroup 的 runner 传错成 p(应 p.runner)。
2. seq 判据里写了 `black(name)`,而 blacklisted 是**谓词**不是函数。
3. 一次 python 替换删漏,把「跨序列目标存在性检查」那段从 CheckNew
   里整段摘掉又贴回原处——靠编译错误发现。
4. ★ 注册失败我写了 panic:内置插件在 main() 装配期加载,panic 会
   **直接拖垮内核启动**,而"某个工具没注册上"只该让该工具不可用。
   已改为 log.Printf + 继续(与 clawhubadapter / mcp 一致)。

变异验证:把 seq_run 移出黑名单 ⇒ 黑名单判据 FAIL。

判据(plugin_test.go,6 条):六个工具全部注册且 description/参数 schema
非空;seq_create 声明 required 并说明 groups/file 二选一;
seq_run 说明"按数组顺序";六个工具均未声明 ParallelSafe;
黑名单含递归风险项且**不误伤** seq_call 与普通工具。

回归:seq -race 全绿;internal/sdk/... internal/plugins/... 12 包全绿。
This commit is contained in:
JianFeeeee
2026-09-27 13:05:28 +08:00
parent e329231c35
commit da8841e52d
8 changed files with 986 additions and 1 deletions

View File

@ -15,6 +15,11 @@ type ToolAPI interface {
GetToolDefs() []ToolDef
// GetAllTools returns all tools exposed by IO devices/channels.
GetAllTools() []ToolDef
// ToolDefByName 按名字查任一来源(StageHost 插件工具 / IO 设备工具)的声明。
// 插件需要它来在**运行前**判断目标是否存在、是否并发安全 ——
// 而工具是动态注册的,"不存在"是常态(见 seq 包的 missing 策略)。
// 查不到返回 nil(调用方按"不存在"处理,不得 panic)。
ToolDefByName(name string) *ToolDef
// ExecuteTool executes a tool by name, resolving across the stage host first.
ExecuteTool(name string, args map[string]interface{}) (interface{}, error)
}

View File

@ -30,7 +30,15 @@ func (t *toolImpl) GetAllTools() []ToolDef {
defs := t.iom.GetAllTools()
out := make([]ToolDef, 0, len(defs))
for _, d := range defs {
out = append(out, ToolDef{Name: d.Name, Description: d.Description, Parameters: d.Parameters})
// ⚠️ ParallelSafe 必须一并带出:它决定该工具能否被并发执行。
// 此前这里漏了它 ⇒ 插件看到的设备工具一律"不可并发",
// 设备工具的并发声明等于对插件不可见。
out = append(out, ToolDef{
Name: d.Name,
Description: d.Description,
Parameters: d.Parameters,
ParallelSafe: d.ParallelSafe,
})
}
return out
}
@ -48,3 +56,30 @@ func (t *toolImpl) ExecuteTool(name string, args map[string]interface{}) (interf
}
var _ ToolAPI = (*toolImpl)(nil)
// ToolDefByName 按名字查任一来源的工具声明(StageHost 优先,再查 IO 设备)。
//
// 用途:插件在**运行前**判断目标工具是否存在、是否并发安全。工具是动态
// 注册的,"不存在"是常态(插件未加载/已卸载/崩溃),因此查不到一律返回
// nil 交由调用方按"不存在"处理——不得 panic。
func (t *toolImpl) ToolDefByName(name string) *ToolDef {
if t == nil || name == "" {
return nil
}
if t.stageHost != nil {
if def := t.stageHost.ToolDef(name); def != nil {
return def
}
}
if t.iom != nil {
if def, ok := t.iom.ToolDefOf(name); ok {
return &ToolDef{
Name: def.Name,
Description: def.Description,
Parameters: def.Parameters,
ParallelSafe: def.ParallelSafe,
}
}
}
return nil
}