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 fd518ef7bc
commit 699eb33f72
8 changed files with 986 additions and 1 deletions

View File

@ -0,0 +1,144 @@
package seq
import "strings"
// seqToolDefs 返回本插件导出的六个工具定义。
//
// ⚠️ 全部**不声明** ParallelSafe:seq_run / seq_call 会执行**一串**工具,
// 其中可能含写操作。标成并发安全会让内核把两条 seq_run 并发跑起来,
// 两个序列的执行顺序交错、变量表互相污染。
//
// 为独立真相源:注册、判据、文档都从这里取,避免三处各写一份。
func seqToolDefs() []toolDefInfo {
return []toolDefInfo{
{
Name: "seq_create",
Description: "创建/更新一条工具序列。**groups 与 file 二选一**:传 groups 直接给结构," +
"或用 file 加载你已写好的序列文件(适合长序列——长参数会被 max_tokens 截断,写文件更稳)。\n" +
"序列由若干 group 组成:**组内并行、组间串行**;group 拥有独立签名(in 入参 / out 出参)," +
"可被 seq_call 按名调用。\n" +
"保存时会做静态校验:as 必须已在 out 声明、$args.x 必须已在 in 声明、组名不重复、" +
"组内 ; 分隔符必须完整。任一项不满足都会报错并说明原因。\n" +
"格式:JSON;每个 group 的 tools 是一个字符串,内含若干以 ' ; ' 分隔的 JSON 对象," +
"形如 {\"tool\":\"cmd_run\",\"args\":{...},\"as\":\"槽名\"}。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"type": "string", "description": "序列名(唯一)",
},
"description": map[string]interface{}{
"type": "string", "description": "一句话说明这条序列做什么",
},
"groups": map[string]interface{}{
"type": "array",
"description": "组数组,按数组顺序执行;与 file 二选一",
"items": map[string]interface{}{"type": "object"},
},
"file": map[string]interface{}{
"type": "string",
"description": "序列文件路径(与 groups 二选一)。目录内文件优先用 files 工具查看",
},
},
"required": []string{"name"},
},
},
{
Name: "seq_list",
Description: "列出全部可用序列:名称、描述、组数与各组的签名(in/out)。" +
"按名调用前先用它确认名称与签名。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
},
},
{
Name: "seq_delete",
Description: "删除一条序列。序列不存在时会报错(不会静默成功)。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"type": "string", "description": "要删除的序列名",
},
},
"required": []string{"name"},
},
},
{
Name: "seq_run",
Description: "执行一条序列:按 groups 数组的顺序逐组执行,组内并行、组间串行。" +
"每组先求值 when 条件,为真才执行。返回逐组摘要与最终变量槽快照。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"type": "string", "description": "要执行的序列名",
},
"args": map[string]interface{}{
"type": "object",
"description": "传给各组的入参(组内用 $args.<键> 读取)",
},
},
"required": []string{"name"},
},
},
{
Name: "seq_call",
Description: "按名调用一条序列里的 group:target 写组名(限本序列内)或 " +
"#序列名(跨序列)。返回该组的 out 槽。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"type": "string", "description": "所属序列名(组名调用时必填)",
},
"target": map[string]interface{}{
"type": "string",
"description": "组名,或 #序列名 表示跨序列调用",
},
"args": map[string]interface{}{
"type": "object",
"description": "传给该组的入参",
},
},
"required": []string{"target"},
},
},
{
Name: "seq_when_call",
Description: "条件按名调用:when 表达式为真才执行,否则整次调用跳过(不产出任何槽)。" +
"when 只可读传入的 args(如 $args.flag == true)。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"name": map[string]interface{}{
"type": "string", "description": "所属序列名(组名调用时必填)",
},
"target": map[string]interface{}{
"type": "string",
"description": "组名,或 #序列名 表示跨序列调用",
},
"args": map[string]interface{}{
"type": "object",
"description": "传给该组的入参",
},
"when": map[string]interface{}{
"type": "string",
"description": "条件表达式,如 $args.flag == true",
},
},
"required": []string{"target", "when"},
},
},
}
}
// 内部小工具:把 args 里的字符串字段取出来并 trim。
func argString(args map[string]interface{}, key string) string {
if args == nil {
return ""
}
s, _ := args[key].(string)
return strings.TrimSpace(s)
}