mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-30 22:42:51 +00:00
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:
@ -14,6 +14,7 @@ import (
|
||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/multimodal"
|
||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/pluginmgr"
|
||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/remotedevice"
|
||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/seq"
|
||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/skillmgr"
|
||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/timer"
|
||||
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui"
|
||||
|
||||
435
internal/plugins/seq/handlers.go
Normal file
435
internal/plugins/seq/handlers.go
Normal file
@ -0,0 +1,435 @@
|
||||
package seq
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 本文件是 seq_* 工具的实现:解析参数 → 调 Store / 执行引擎 → 渲染结果。
|
||||
//
|
||||
// 两条贯穿全文件的纪律:
|
||||
// 1. **不静默降级**:参数缺失、序列不存在、目标不存在都**报错**并说明
|
||||
// 该怎么做。模型拿到含糊的"失败"只会原样重试。
|
||||
// 2. **错误信息要可执行**:说清缺什么、可用的是什么。
|
||||
|
||||
// seqCreate 创建/更新序列。groups 与 file 二选一。
|
||||
func (p *Plugin) seqCreate(args map[string]interface{}) (interface{}, error) {
|
||||
name := argString(args, "name")
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("缺少 name(序列名)")
|
||||
}
|
||||
groupsRaw, hasGroups := args["groups"]
|
||||
file := argString(args, "file")
|
||||
|
||||
switch {
|
||||
case file != "" && hasGroups:
|
||||
return nil, fmt.Errorf("groups 与 file **二选一**,不能同时传")
|
||||
case file == "" && !hasGroups:
|
||||
return nil, fmt.Errorf("必须提供 groups 或 file 其中之一(长序列建议写文件后用 file 传)")
|
||||
}
|
||||
|
||||
var (
|
||||
text []byte
|
||||
from = "参数"
|
||||
)
|
||||
if file != "" {
|
||||
b, err := p.readSeqFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
text = b
|
||||
from = "文件 " + file
|
||||
} else {
|
||||
b, err := marshalGroups(groupsRaw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
text = b
|
||||
}
|
||||
|
||||
seq, err := Parse(text)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("来自%s的序列解析失败: %w", from, err)
|
||||
}
|
||||
if seq.Name == "" {
|
||||
seq.Name = name
|
||||
}
|
||||
if seq.Name != name {
|
||||
return nil, fmt.Errorf("序列名不一致:参数给了 %q,内容里是 %q(请统一)", name, seq.Name)
|
||||
}
|
||||
if d := argString(args, "description"); d != "" {
|
||||
seq.Description = d
|
||||
}
|
||||
if err := p.store.Save(seq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 跨序列引用可能成环:存完复查一次(Save 只校验同序列内的 group 引用)
|
||||
graphErr := p.store.CheckGraph()
|
||||
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "序列 %q 已保存(%s,%d 个 group)", seq.Name, from, len(seq.Groups))
|
||||
for _, g := range seq.Groups {
|
||||
fmt.Fprintf(&sb, "\n- %s", g.Name)
|
||||
if len(g.In) > 0 {
|
||||
fmt.Fprintf(&sb, " 入参[%s]", keyList(g.In))
|
||||
}
|
||||
if len(g.Out) > 0 {
|
||||
fmt.Fprintf(&sb, " 出参[%s]", keyList(g.Out))
|
||||
}
|
||||
fmt.Fprintf(&sb, " 工具%d个", len(g.Tools))
|
||||
}
|
||||
if graphErr != nil {
|
||||
// 保存成功但图不合法 —— 必须**说清**,否则模型会以为可以跑了
|
||||
fmt.Fprintf(&sb, "\n⚠️ 序列已保存,但调用图有问题(现在执行会失败):%v", graphErr)
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// readSeqFile 读序列文件,带路径逃逸防护。
|
||||
func (p *Plugin) readSeqFile(path string) ([]byte, error) {
|
||||
abs, err := absPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b, err := os.ReadFile(abs)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("序列文件 %s 不存在", path)
|
||||
}
|
||||
return nil, fmt.Errorf("读序列文件 %s 失败: %w", path, err)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// absPath 做基础路径校验:拒绝空、拒绝明显的穿越写法。
|
||||
func absPath(p string) (string, error) {
|
||||
if strings.TrimSpace(p) == "" {
|
||||
return "", fmt.Errorf("路径为空")
|
||||
}
|
||||
if strings.Contains(p, "\x00") {
|
||||
return "", fmt.Errorf("路径含非法字符")
|
||||
}
|
||||
// 允许绝对与相对路径,但禁止 .. 段(与 files 插件的目录逃逸防护同源思路)
|
||||
for _, seg := range strings.Split(filepathToSlash(p), "/") {
|
||||
if seg == ".." {
|
||||
return "", fmt.Errorf("路径 %q 含 .. 段,不允许", p)
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func filepathToSlash(p string) string { return strings.ReplaceAll(p, `\`, "/") }
|
||||
|
||||
// marshalGroups 把 groups 参数([]interface{})序列化为 JSON 文本。
|
||||
func marshalGroups(raw interface{}) ([]byte, error) {
|
||||
arr, ok := raw.([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("groups 必须是数组,实际是 %T", raw)
|
||||
}
|
||||
doc := map[string]interface{}{"groups": arr}
|
||||
b, err := json.MarshalIndent(doc, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("groups 序列化失败(每个 group 应为对象): %w", err)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// seqList 列出全部序列及其签名。
|
||||
func (p *Plugin) seqList() (interface{}, error) {
|
||||
names := p.store.List()
|
||||
if len(names) == 0 {
|
||||
return "当前没有任何序列(用 seq_create 新建)", nil
|
||||
}
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "共 %d 条序列:", len(names))
|
||||
for _, n := range names {
|
||||
seq, err := p.store.Load(n)
|
||||
if err != nil {
|
||||
fmt.Fprintf(&sb, "\n- %s ⚠️ 读取失败: %v", n, err)
|
||||
continue
|
||||
}
|
||||
desc := seq.Description
|
||||
if desc == "" {
|
||||
desc = "(无描述)"
|
||||
}
|
||||
fmt.Fprintf(&sb, "\n- %s:%s(%d 个 group)", n, desc, len(seq.Groups))
|
||||
for _, g := range seq.Groups {
|
||||
fmt.Fprintf(&sb, "\n · %s", g.Name)
|
||||
if len(g.In) > 0 {
|
||||
fmt.Fprintf(&sb, " 入参[%s]", keyList(g.In))
|
||||
}
|
||||
if len(g.Out) > 0 {
|
||||
fmt.Fprintf(&sb, " 出参[%s]", keyList(g.Out))
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// seqDelete 删除序列。
|
||||
func (p *Plugin) seqDelete(args map[string]interface{}) (interface{}, error) {
|
||||
name := argString(args, "name")
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("缺少 name(序列名)")
|
||||
}
|
||||
if err := p.store.Delete(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fmt.Sprintf("序列 %q 已删除", name), nil
|
||||
}
|
||||
|
||||
// seqRun 执行一条序列的全部 group。
|
||||
func (p *Plugin) seqRun(args map[string]interface{}) (interface{}, error) {
|
||||
name := argString(args, "name")
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("缺少 name(序列名)")
|
||||
}
|
||||
seq, err := p.store.Load(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
in, _ := args["args"].(map[string]interface{})
|
||||
if in == nil {
|
||||
in = map[string]interface{}{}
|
||||
}
|
||||
return p.runSequence(seq, in, nil)
|
||||
}
|
||||
|
||||
// seqCall 按名调用一个 group(when 非空时为条件调用)。
|
||||
func (p *Plugin) seqCall(args map[string]interface{}, when string) (interface{}, error) {
|
||||
target := argString(args, "target")
|
||||
if target == "" {
|
||||
return nil, fmt.Errorf("缺少 target(组名或 #序列名)")
|
||||
}
|
||||
seqName := argString(args, "name")
|
||||
callArgs, _ := args["args"].(map[string]interface{})
|
||||
if callArgs == nil {
|
||||
callArgs = map[string]interface{}{}
|
||||
}
|
||||
|
||||
var seq *Sequence
|
||||
var err error
|
||||
if strings.HasPrefix(target, "#") {
|
||||
seqName = strings.TrimPrefix(target, "#")
|
||||
seq, err = p.store.Load(seqName)
|
||||
} else {
|
||||
if seqName == "" {
|
||||
return nil, fmt.Errorf("按组名调用时必须给出 name(该组所属的序列名)")
|
||||
}
|
||||
seq, err = p.store.Load(seqName)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idx := -1
|
||||
for i, g := range seq.Groups {
|
||||
if g.Name == target || strings.TrimPrefix(target, "#") == g.Name {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx < 0 {
|
||||
return nil, fmt.Errorf("序列 %q 里没有 group %q(现有:%s)",
|
||||
seq.Name, target, groupNameList(seq))
|
||||
}
|
||||
|
||||
// 条件调用:求值在**进入前**,为假则整次跳过
|
||||
if strings.TrimSpace(when) != "" {
|
||||
ok, cerr := evalCond(when, callArgs)
|
||||
if cerr != nil {
|
||||
// ★ 求值失败**不得**降级为"跳过"
|
||||
return nil, fmt.Errorf("seq_when_call 的条件求值失败(这是参数问题,不是工具故障): %w", cerr)
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Sprintf("条件为假,已跳过 %q(不产出任何槽)", target), nil
|
||||
}
|
||||
}
|
||||
|
||||
res, gerr := p.runGroup(seq.Groups[idx], callArgs, seq.Name)
|
||||
if gerr != nil {
|
||||
return nil, gerr
|
||||
}
|
||||
return renderGroupResult(res), nil
|
||||
}
|
||||
|
||||
// runSequence 顺序执行全部 group。
|
||||
//
|
||||
// group 间**串行**:后者可能依赖前者的出参槽(具名槽即数据边)。
|
||||
func (p *Plugin) runSequence(seq *Sequence, in map[string]interface{}, _ any) (interface{}, error) {
|
||||
if p.callDepth >= maxCallDepth {
|
||||
return nil, fmt.Errorf("嵌套调用深度超过上界 %d(可能存在循环调用)", maxCallDepth)
|
||||
}
|
||||
p.callDepth++
|
||||
defer func() { p.callDepth-- }()
|
||||
|
||||
var lines []string
|
||||
slots := map[string]interface{}{}
|
||||
failed := false
|
||||
|
||||
for i, g := range seq.Groups {
|
||||
// 每组的入参 = 顶层入参 + 已产出槽(具名槽在组间传递)
|
||||
groupArgs := map[string]interface{}{}
|
||||
for k, v := range in {
|
||||
groupArgs[k] = v
|
||||
}
|
||||
for k, v := range slots {
|
||||
groupArgs[k] = v
|
||||
}
|
||||
res, err := p.runGroup(g, groupArgs, seq.Name)
|
||||
if err != nil {
|
||||
lines = append(lines, fmt.Sprintf("第 %d 组 %q 失败: %v", i+1, g.Name, err))
|
||||
failed = true
|
||||
if g.OnError != "continue" {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
line := fmt.Sprintf("第 %d 组 %q", i+1, g.Name)
|
||||
if res.Skipped {
|
||||
line += "(条件为假,已跳过)"
|
||||
} else {
|
||||
line += fmt.Sprintf(" 工具 %d 个", len(res.Tools))
|
||||
if len(res.Missing) > 0 {
|
||||
line += fmt.Sprintf(" ⚠️缺失工具: %s", strings.Join(res.Missing, ", "))
|
||||
}
|
||||
}
|
||||
lines = append(lines, line)
|
||||
// 槽合并(后续组可读)
|
||||
for k, v := range res.Slots {
|
||||
slots[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "序列 %q 执行完毕(%d/%d 组):", seq.Name, len(lines), len(seq.Groups))
|
||||
for _, l := range lines {
|
||||
sb.WriteString("\n- " + l)
|
||||
}
|
||||
if len(slots) > 0 {
|
||||
sb.WriteString("\n\n变量槽:")
|
||||
keys := make([]string, 0, len(slots))
|
||||
for k := range slots {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
fmt.Fprintf(&sb, "\n %s = %s", k, truncate(renderResult(slots[k]), 160))
|
||||
}
|
||||
}
|
||||
if failed {
|
||||
sb.WriteString("\n⚠️ 有 group 失败(见上)")
|
||||
}
|
||||
return sb.String(), nil
|
||||
}
|
||||
|
||||
// runGroup 执行单个 group,含黑名单、并发安全与深度检查。
|
||||
func (p *Plugin) runGroup(g Group, args map[string]interface{}, seqName string) (GroupResult, error) {
|
||||
// ① 黑名单先行
|
||||
for _, t := range g.Tools {
|
||||
if blacklisted(t.Tool) {
|
||||
return GroupResult{Group: g.Name}, fmt.Errorf(
|
||||
"group %q 试图调用被禁止的工具 %s"+
|
||||
"(序列不得对外发消息/改插件表/再起子 agent)", g.Name, t.Tool)
|
||||
}
|
||||
if t.Tool == "seq_call" || t.Tool == "seq_when_call" {
|
||||
tgt, _ := t.Args["target"].(string)
|
||||
if strings.HasPrefix(tgt, "#") {
|
||||
if strings.TrimPrefix(tgt, "#") == seqName {
|
||||
return GroupResult{Group: g.Name}, fmt.Errorf("group %q 调用了序列自身(无限递归)", g.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ② 组内是否真的可以并发:全部声明 ParallelSafe 才并发。
|
||||
// 查不到声明 ⇒ 保守按串行(动态注册下工具可能随时消失)。
|
||||
runnable := g.Parallel
|
||||
if runnable {
|
||||
for _, t := range g.Tools {
|
||||
if !p.runner.parallelSafe(t.Tool) {
|
||||
runnable = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !runnable {
|
||||
g.Parallel = false
|
||||
}
|
||||
|
||||
// ③ 存在性预检(missing 策略的输入)
|
||||
preMissing := false
|
||||
for _, t := range g.Tools {
|
||||
if t.Tool == "seq_call" || t.Tool == "seq_when_call" {
|
||||
continue // 内建调用另行处理
|
||||
}
|
||||
if !p.runner.exists(t.Tool) {
|
||||
preMissing = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if preMissing {
|
||||
switch g.missingPolicy() {
|
||||
case "skip", "degrade":
|
||||
// 逐个剔除缺失的工具
|
||||
var kept []ToolCall
|
||||
for _, t := range g.Tools {
|
||||
if (t.Tool == "seq_call" || t.Tool == "seq_when_call") || p.runner.exists(t.Tool) {
|
||||
kept = append(kept, t)
|
||||
}
|
||||
}
|
||||
if len(kept) == 0 {
|
||||
return GroupResult{Group: g.Name, Skipped: true, Slots: map[string]interface{}{}}, nil
|
||||
}
|
||||
g.Tools = kept
|
||||
}
|
||||
}
|
||||
|
||||
return execGroup(g, args, p.runner)
|
||||
}
|
||||
|
||||
func groupNameList(seq *Sequence) string {
|
||||
names := make([]string, 0, len(seq.Groups))
|
||||
for _, g := range seq.Groups {
|
||||
names = append(names, g.Name)
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return "(无)"
|
||||
}
|
||||
return strings.Join(names, ", ")
|
||||
}
|
||||
|
||||
func renderGroupResult(res GroupResult) string {
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "group %q", res.Group)
|
||||
if res.Skipped {
|
||||
sb.WriteString("(条件为假,已跳过)")
|
||||
return sb.String()
|
||||
}
|
||||
fmt.Fprintf(&sb, " 完成 %d 个工具", len(res.Tools))
|
||||
if len(res.Missing) > 0 {
|
||||
fmt.Fprintf(&sb, ",缺失: %s", strings.Join(res.Missing, ", "))
|
||||
}
|
||||
if len(res.Slots) > 0 {
|
||||
keys := make([]string, 0, len(res.Slots))
|
||||
for k := range res.Slots {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
sb.WriteString("\n出参:")
|
||||
for _, k := range keys {
|
||||
fmt.Fprintf(&sb, "\n %s = %s", k, truncate(renderResult(res.Slots[k]), 160))
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func truncate(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n] + "..."
|
||||
}
|
||||
188
internal/plugins/seq/plugin.go
Normal file
188
internal/plugins/seq/plugin.go
Normal file
@ -0,0 +1,188 @@
|
||||
// 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_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
|
||||
}
|
||||
160
internal/plugins/seq/plugin_test.go
Normal file
160
internal/plugins/seq/plugin_test.go
Normal file
@ -0,0 +1,160 @@
|
||||
package seq
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 阶段 P4:seq_* 工具与插件装配。
|
||||
//
|
||||
// 这一层的判据关注**可观测契约**(给模型看的东西对不对),
|
||||
// 执行语义已由 P1/P2/P3 的判据覆盖。
|
||||
|
||||
// ① 六个工具全部注册,且都带可用的 description 与参数 schema。
|
||||
//
|
||||
// 工具是**模型可见面**:description 缺失或为空白 ⇒ 模型不知道何时该用它。
|
||||
func TestAllSixSeqToolsRegistered(t *testing.T) {
|
||||
p := newTestPlugin(t)
|
||||
want := []string{
|
||||
"seq_create", "seq_list", "seq_delete", "seq_run", "seq_call", "seq_when_call",
|
||||
}
|
||||
defs := p.toolDefs()
|
||||
for _, name := range want {
|
||||
def, ok := defs[name]
|
||||
if !ok {
|
||||
t.Errorf("工具 %s 未注册(已注册:%v)", name, keysOf(defs))
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(def.Description) == "" {
|
||||
t.Errorf("工具 %s 的 description 为空 —— 模型无从判断何时使用", name)
|
||||
}
|
||||
if def.Parameters == nil {
|
||||
t.Errorf("工具 %s 缺参数 schema", name)
|
||||
continue
|
||||
}
|
||||
if typ, _ := def.Parameters["type"].(string); typ != "object" {
|
||||
t.Errorf("工具 %s 的 schema type = %q,期望 object", name, typ)
|
||||
}
|
||||
}
|
||||
if len(defs) != len(want) {
|
||||
t.Errorf("应恰好注册 %d 个工具,实际 %d(%v)—— 多余的导出工具会稀释工具面", len(want), len(defs), keysOf(defs))
|
||||
}
|
||||
}
|
||||
|
||||
// ② seq_create 的 schema 必须声明 required,否则模型会漏传。
|
||||
func TestSeqCreateSchemaDeclaresRequired(t *testing.T) {
|
||||
p := newTestPlugin(t)
|
||||
def := p.toolDefs()["seq_create"]
|
||||
if def.Parameters == nil {
|
||||
t.Fatal("seq_create 缺 schema")
|
||||
}
|
||||
req, _ := def.Parameters["required"].([]string)
|
||||
if len(req) == 0 {
|
||||
t.Fatalf("seq_create 未声明 required:模型会漏传 name/groups/file")
|
||||
}
|
||||
got := map[string]bool{}
|
||||
for _, r := range req {
|
||||
got[r] = true
|
||||
}
|
||||
if !got["name"] {
|
||||
t.Error("required 应包含 name")
|
||||
}
|
||||
}
|
||||
|
||||
// ③ seq_create 的 description 必须说明 groups 与 file **二选一**。
|
||||
//
|
||||
// 这是最容易让模型犯错的地方:两个都传或都不传该怎么<E6808E><E4B988><EFBFBD>,必须写清。
|
||||
func TestSeqCreateExplainsMutuallyExclusiveInput(t *testing.T) {
|
||||
p := newTestPlugin(t)
|
||||
desc := p.toolDefs()["seq_create"].Description
|
||||
for _, want := range []string{"groups", "file"} {
|
||||
if !strings.Contains(desc, want) {
|
||||
t.Errorf("description 未提到 %s:%s", want, desc)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(desc, "二选一") && !strings.Contains(desc, "或") {
|
||||
t.Errorf("description 未说明 groups 与 file 是二选一:%s", desc)
|
||||
}
|
||||
}
|
||||
|
||||
// ④ 六个工具都**不得**声明并发安全。
|
||||
//
|
||||
// seq_run / seq_call 会执行**一串**工具,其中可能含写操作;把它们标成
|
||||
// 并发安全,会让内核把两条 seq_run 并发跑起来 ⇒ 两个序列的执行顺序
|
||||
// 交错、变量表互相污染。
|
||||
func TestSeqToolsAreNotParallelSafe(t *testing.T) {
|
||||
p := newTestPlugin(t)
|
||||
for name := range p.toolDefs() {
|
||||
def := p.toolDefs()[name]
|
||||
if def.ParallelSafe {
|
||||
t.Errorf("工具 %s 声明了 ParallelSafe —— seq 会执行一串工具,并发会污染执行序列", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ⑤ seq_run 的 description 必须说明"按 groups 数组顺序执行"。
|
||||
//
|
||||
// 组的执行顺序是**语义**的一部分:条件依赖前面的槽,顺序反了结果就错。
|
||||
func TestSeqRunExplainsOrdering(t *testing.T) {
|
||||
p := newTestPlugin(t)
|
||||
desc := p.toolDefs()["seq_run"].Description
|
||||
if !strings.Contains(desc, "顺序") {
|
||||
t.Errorf("seq_run 未说明组按顺序执行:%s", desc)
|
||||
}
|
||||
}
|
||||
|
||||
// ⑥ 黑名单:序列**内部**不得调用这些工具(与子 agent 的黑名单同源,防递归)。
|
||||
//
|
||||
// ⚠️ 这里曾有一个我自己的设计矛盾:判据原先把 `seq_call` / `seq_when_call`
|
||||
// 也要求进黑名单,但「按名调用 group/序列」恰恰是本包的核心能力——
|
||||
// 若禁掉它,序列就退化成单层脚本,功能归零。
|
||||
//
|
||||
// 分层澄清(这也是正确语义):
|
||||
//
|
||||
// · `seq_call` / `seq_when_call` 作为**模型直接调用**的入口是正常的
|
||||
// (模型可以单独调某个 group),不算黑名单;
|
||||
// · 序列**内部**若写 seq_call,那是按名组合,走 runGroup 的专门分支
|
||||
// 并受 maxCallDepth 约束(§8.3 的环检测与深度上界),
|
||||
// 不靠黑名单防递归。
|
||||
// · 真正要禁的是:对外发消息(output_send__)、起子 agent(spawn_child)、
|
||||
// 改插件表(plgreload)、以及再次 seq_run 整条序列(会绕过深度计数的语义)。
|
||||
func TestSeqBlacklistCoversRecursionRisks(t *testing.T) {
|
||||
for _, name := range []string{
|
||||
"output_send__qq", "output_send__x", "spawn_child", "plgreload", "seq_run",
|
||||
} {
|
||||
if !blacklisted(name) {
|
||||
t.Errorf("黑名单未覆盖 %q —— 序列能调它就是递归/绕过风险", name)
|
||||
}
|
||||
}
|
||||
// seq_call / seq_when_call 必须**不在**黑名单,否则按名调用能力归零
|
||||
for _, name := range []string{"seq_call", "seq_when_call"} {
|
||||
if blacklisted(name) {
|
||||
t.Errorf("黑名单误伤了 %q —— 它是序列组合的核心能力(递归由 maxCallDepth + 环检测负责)", name)
|
||||
}
|
||||
}
|
||||
// 普通工具不应被误伤
|
||||
for _, name := range []string{"cmd_run", "knowledge_search", "output_list_channels"} {
|
||||
if blacklisted(name) {
|
||||
t.Errorf("黑名单误伤了正常工具 %q", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func keysOf(m map[string]toolDefInfo) []string {
|
||||
out := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ---- 测试替身 ----
|
||||
|
||||
// newTestPlugin 造一个**不依赖内核**的 Plugin。
|
||||
//
|
||||
// 刻意不走 Start(那需要真实 *sdk.PluginSDK):本组判据只测
|
||||
// 「工具定义与可观测契约」,不测执行语义(后者由 exec/store 的判据覆盖)。
|
||||
func newTestPlugin(t *testing.T) *Plugin {
|
||||
t.Helper()
|
||||
return &Plugin{name: "seq", store: NewStore(t.TempDir())}
|
||||
}
|
||||
17
internal/plugins/seq/register.go
Normal file
17
internal/plugins/seq/register.go
Normal file
@ -0,0 +1,17 @@
|
||||
package seq
|
||||
|
||||
import (
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
)
|
||||
|
||||
// 插件注册:与其它内置插件同一范式(见 skillmgr/plugin.go 的 init)。
|
||||
//
|
||||
// 之所以用 init + factory 而非在 all.go 里直接 new:内置插件统一由
|
||||
// registry 按名字工厂化加载,all.go 只负责 import 触发注册。
|
||||
func init() {
|
||||
plugin.RegisterPluginMeta("seq", "工具序列", "Tool Sequence")
|
||||
plugin.RegisterFactory("seq", func(name string, _ map[string]interface{}) (sdk.Plugin, error) {
|
||||
return New(name), nil
|
||||
})
|
||||
}
|
||||
144
internal/plugins/seq/tools.go
Normal file
144
internal/plugins/seq/tools.go
Normal 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)
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user