Files
HomeAgent/internal/plugins/seq/tools.go
JianFeeeee 71c894c182 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 包全绿。
2026-09-27 13:05:28 +08:00

145 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}