Files
HomeAgent/internal/plugins/seq/plugin.go
JianFeeeee 116dc413f0 feat(seq): 新增 seq_help —— 格式说明 + 可照抄示例
动机来自真机实跑:模型写序列时踩了三个坑,各试 1~3 次才改对
  ① tools 漏末尾的 ';'       → 「末尾缺少 ';'」
  ② group 的 in 传成字符串     → 重试 3 次
  ③ as 指向未声明的 out 槽      → 静态校验拦下
这三处都是**格式细节**,塞不进工具描述(有长度限制),却恰是模型最易错处。
散落在七个描述里等于没有集中入口。

实现(help.go + tools.go + plugin.go):
· seq_help 无参数、纯文本返回(与仓内 output_send__*_help 同范式)
· 「格式要点」逐条写明:in/out 必须是**对象**、tools 必须是**字符串**、
  每个 tool 后(含最后一个)都要 ';'、as 必须在 out 声明、
  groups 与 file 二选一、组内并行组间串行
· 「条件 when」列出支持的表达式形态
· 「可照抄的完整示例」给一行**单行紧凑**的合法序列

★ 判据(plugin_test.go,3 条):
· seq_help 已注册、有 description、不声明并发安全
· 内容覆盖真机踩过的**每一个**坑(判据从"坑"出发而非从"打算写什么")
· ★ 示例**自己能被本包解析器接受**:validateHelpExample 从帮助文本里
  抽出示例喂给 Parse —— 模型是照抄的,示例自己解析不过就是给模型挖坑。
  而"从文本里有没有某个词"是看不出这类 bug 的。

过程中判据自己错了两次(都被这条示例判据照出来):
1. 抽取用 strings.Index(help, `{"name"`) ⇒ 先命中「格式要点」里**有意写的**
   示意片段,截到非示例的内容,报出莫名其妙的 invalid character '…'。
2. 修完又混用两套偏移基准(base 的下标拿去切 help)⇒ invalid character '\xaf'。
   ⇒ 重写为全程在同一 base 上定位。
★ 两次都说明:**判据的抽取逻辑本身就是需要验证的代码**,
它出错时报出的信息极具误导性(看起来像实现有 bug)。

示例形态也改过一次:原为多行缩进 JSON,改为**单行紧凑** —— 模型照抄时
免去缩进/换行带来的额外风险。

变异验证:去掉示例里的末尾 ';' ⇒ 示例判据 FAIL。

另一处:加 seq_help 后「恰好注册 6 个工具」判据 FAIL(实际 7)——
这正是那条判据的用意(防止悄悄多加工具稀释工具面),已更新并注明原因。

回归:go build ./... 通过;internal/plugins/... core sdk 全绿。
2026-09-27 13:48:15 +08:00

192 lines
5.7 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 的插件层:把序列能力暴露为模型可调用的 seq_* 工具。
//
// 本文件只做「接线」:工具定义、参数校验、调用 Store/执行引擎。
// 语义全在 parse.go / exec.go / store.go,三者各自有判据。
package seq
import (
"fmt"
"log"
"strings"
"sync"
"gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
// toolDefInfo 是本包内部用的工具声明视图(判据也用它)。
type toolDefInfo = sdk.ToolDef
// blacklisted 判断某工具名是否**禁止**被序列调用。
//
// 与子 agent 的黑名单同源(core/spawn.go:117):防递归与绕过。
//
// ⚠️ seq_call / seq_when_call **不在**黑名单里:按名调用 group/序列正是
// 本包的核心能力,禁掉它序列就退化成单层脚本。它们作为**模型直接调用**的
// 入口是正常的;序列内部若写 seq_call,走 runGroup 的专门分支并受
// maxCallDepth + 环检测约束(见设计文档 §8.3),不靠黑名单防递归。
func blacklisted(name string) bool {
switch {
case strings.HasPrefix(name, "output_send__"):
return true // 序列不负责对外发消息
case name == "spawn_child":
return true // 防子 agent 递归
case name == "plgreload":
return true // 改插件注册表会与当前执行交错
case name == "seq_run":
return true // 序列内再跑整条序列:语义上是递归,且绕过 depth 计数的可读性
}
return false
}
// seqRunner 是执行引擎对内核工具的依赖。
//
// 刻意**不**直接依赖 sdk.ToolAPI,而是收窄成两个方法——这样判据能用
// 假实现驱动,而不必构造整个内核。
type seqRunner interface {
call(name string, args map[string]interface{}) (string, error)
// exists 报告工具是否存在(动态注册下"不存在"是常态)。
exists(name string) bool
// parallelSafe 报告工具是否声明可并发。
parallelSafe(name string) bool
}
// Plugin 是本插件。
type Plugin struct {
name string
mu sync.RWMutex
store *Store
sdk *sdk.PluginSDK
// runner 指向内核(由 Start 注入)
runner seqRunner
// 调用栈深度:跨序列/跨 group 嵌套的**结构上界**(maxCallDepth)
callDepth int
}
// New 构造插件实例。
func New(name string) *Plugin {
return &Plugin{name: name}
}
// Name 实现 plugin.Plugin。
func (p *Plugin) Name() string { return p.name }
// Start 注入内核能力并注册工具。
func (p *Plugin) Start(s *sdk.PluginSDK) error {
p.sdk = s
dir := "."
if v, _ := s.Settings().GetCore("daemon.data_dir"); v != nil {
if d, ok := v.(string); ok && d != "" {
dir = d + "/sequences"
}
}
p.store = NewStore(dir)
p.runner = &kernelRunner{tool: s.Tool()}
p.registerTools()
return nil
}
// Stop 清理(无订阅需注销)。
func (p *Plugin) Stop() error { return nil }
// kernelRunner 把 sdk.ToolAPI 适配成 seqRunner。
type kernelRunner struct{ tool sdk.ToolAPI }
func (k *kernelRunner) call(name string, args map[string]interface{}) (string, error) {
if k.tool == nil {
return "", fmt.Errorf("工具执行器不可用")
}
res, err := k.tool.ExecuteTool(name, args)
if err != nil {
return "", err
}
return renderResult(res), nil
}
func (k *kernelRunner) exists(name string) bool {
if k.tool == nil {
return false
}
return k.tool.ToolDefByName(name) != nil
}
func (k *kernelRunner) parallelSafe(name string) bool {
if k.tool == nil {
return false
}
def := k.tool.ToolDefByName(name)
return def != nil && def.ParallelSafe
}
// renderResult 渲染工具返回值。
//
// ⚠️ 绝不用 fmt.Sprintf("%v"):对象会变成 `map[k:v]` 这种模型读不懂的
// Go 语法(与 core.renderToolResult 同一约定)。非字符串用紧凑 JSON。
func renderResult(v interface{}) string {
if v == nil {
return ""
}
if s, ok := v.(string); ok {
return s
}
return compactJSON(v)
}
// toolDefs 返回已注册的工具定义(判据与内部都读它,保证同一真相)。
func (p *Plugin) toolDefs() map[string]toolDefInfo {
out := map[string]toolDefInfo{}
for _, d := range seqToolDefs() {
out[d.Name] = d
}
return out
}
// registerTools 把六个工具注册到内核。
//
// ⚠️ 六个工具都**不声明** ParallelSafe:seq_run / seq_call 会执行一串
// 工具,其中可能含写操作;标成并发安全会让内核把两条 seq_run 并发跑,
// 两个序列的执行顺序交错、变量表互相污染。
func (p *Plugin) registerTools() {
for _, d := range seqToolDefs() {
def := d
if err := p.sdk.RegisterTool(def.Name, def, func(args map[string]interface{}) (interface{}, error) {
return p.dispatch(def.Name, args)
}); err != nil {
// 注册失败**记日志并继续**,不 panic。
// ⚠️ 内置插件在 main() 的装配期加载,panic 会直接拖垮内核启动
// —— 而"某个工具没注册上"只该让该工具不可用,不该让整个 agent 起不来。
// (与 clawhubadapter / mcp 的处理一致:log 后继续。)
log.Printf("[seq] 注册工具 %s 失败: %v", def.Name, err)
}
}
}
// dispatch 按工具名分派。
func (p *Plugin) dispatch(name string, args map[string]interface{}) (interface{}, error) {
switch name {
case "seq_create":
return p.seqCreate(args)
case "seq_help":
// 纯查询:返回格式说明 + 可照抄示例(示例由判据校验其**自己解析得过**)
return seqHelpText(), nil
case "seq_list":
return p.seqList()
case "seq_delete":
return p.seqDelete(args)
case "seq_run":
return p.seqRun(args)
case "seq_call":
return p.seqCall(args, "")
case "seq_when_call":
return p.seqCall(args, getString(args, "when"))
}
return nil, fmt.Errorf("未知工具 %s", name)
}
func getString(m map[string]interface{}, k string) string {
if m == nil {
return ""
}
s, _ := m[k].(string)
return s
}