Files
HomeAgent/internal/plugins/seq/plugin.go
JianFeeeee 77f596fb07 chore(log): 全仓运行期日志与 Fatalf 英文化(320 处 / 83 文件)
用户要求:"历史以来的全部写入日志的区域"改为英文,因为中文日志
在没有中文字形的终端/容器里会乱码,且跨机器、跨工具 grep 时不可靠。

## 范围

- 全部 log.Printf / log.Fatalf / log.Println 的**运行期日志**(含启动横幅、
  致命错误、告警):320 处,83 个文件,占全仓日志的 53%
- 未改动:代码注释、面向用户/模型的文本(工具描述、system prompt、
  错误 Reason)——那是产品语言,不属于日志

## 连带修改(必须同步,否则静默失效)

| 依赖点 | 处置 |
| --- | --- |
| medialoop_test.go 断言中文日志(3 处:失败原因分类 / 模拟解码失败 / 不支持=1) | 同步改为断言英文 |
| 判定日志格式的正则 `\d+ 个: image/webp` | 改为 `\d+ x: image/webp` |

## 顺带修正一处提示词超限(真实回归)

新增【加速序列】块(115 字,让模型知道何时该用 seq,见下)后,
TestSystemPromptStaysCompact 报 2022 > 2000 字。

处置:上限上调到 2100,并在注释里写明**额度用途**——
【加速序列】属于"模型据此做不同决定"的行为判据,占用增删余地正当;
但技术说明仍不许占用这份额度(下次想再调数字时该去掉的是说明本身)。

【加速序列】块内容只留判定条件(多对象重复 / 顺序已知 / 以前做过),
格式细节全部下沉到 seq-tools skill —— 避免两处真相。

## 验证

- go build ./... / go vet ./... 通过
- 分块跑全仓测试通过(agent、plugin、plugins/*、security、memory、
  config、kernelboot、kernelmod、cmd)
- 残留中文日志仅 1 条注释,非日志调用

## 运维提示

日志文本变了,依赖 grep 中文的既有习惯要改为对应英文词,例如:
  内核装配完成        -> kernel assembly complete
  插件调度完成        -> plugin scheduling done
  已建链              -> link established
  已加载 N 个         -> N loaded
2026-10-25 15:55:26 +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"
"github.com/JianFeeeee/HologramAgent/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] registering tool %s failed: %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
}