From 77f596fb0794192811a41483bfac00080d76deb5 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sun, 25 Oct 2026 15:55:26 +0800 Subject: [PATCH] =?UTF-8?q?chore(log):=20=E5=85=A8=E4=BB=93=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=9C=9F=E6=97=A5=E5=BF=97=E4=B8=8E=20Fatalf=20?= =?UTF-8?q?=E8=8B=B1=E6=96=87=E5=8C=96=EF=BC=88320=20=E5=A4=84=20/=2083=20?= =?UTF-8?q?=E6=96=87=E4=BB=B6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户要求:"历史以来的全部写入日志的区域"改为英文,因为中文日志 在没有中文字形的终端/容器里会乱码,且跨机器、跨工具 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 --- cmd/homed/bootstrap.go | 4 +- cmd/homed/guard.go | 12 +- cmd/homed/heartbeat_cmd.go | 10 +- cmd/homed/kmod_runtime.go | 8 +- cmd/homed/main.go | 44 ++--- cmd/homed/plugins_scheduled.go | 2 +- cmd/mock-server/main.go | 8 +- cmd/mock-server/ws.go | 44 ++--- internal/agent/api/codec.go | 4 +- internal/agent/api/provider_security.go | 2 +- internal/agent/core/agent.go | 4 +- internal/agent/core/crossmodal.go | 4 +- internal/agent/core/distill.go | 20 +- internal/agent/core/eventloop.go | 6 +- internal/agent/core/graphmedia.go | 22 +-- internal/agent/core/inputroute.go | 2 +- internal/agent/core/log_scrub.go | 2 +- internal/agent/core/medialoop.go | 14 +- internal/agent/core/medialoop_test.go | 14 +- internal/agent/core/mediaref.go | 2 +- internal/agent/core/memory_scrub.go | 2 +- internal/agent/core/memorypass.go | 4 +- internal/agent/core/offload.go | 6 +- internal/agent/core/output.go | 2 +- internal/agent/core/prompt_memory_test.go | 13 +- internal/agent/core/prompt_seq_test.go | 29 +++ internal/agent/core/resident.go | 2 +- internal/agent/core/resident_tools.go | 2 +- internal/agent/core/scheduler.go | 6 +- internal/agent/core/spawn.go | 6 +- internal/agent/core/stages.go | 2 +- internal/agent/core/task.go | 2 +- internal/agent/core/toolcall.go | 24 +-- internal/agent/core/tooldefs.go | 13 ++ internal/agent/io/channel.go | 4 +- internal/config/registry.go | 4 +- internal/config/secret.go | 16 +- internal/devicebridge/client/bridge.go | 4 +- internal/kernelboot/kernelstate.go | 2 +- internal/kernelboot/lifecycle.go | 8 +- internal/kernelboot/migrate.go | 20 +- internal/kernelmod/agent/mod.go | 10 +- internal/kernelmod/knowledge/migrate_impl.go | 12 +- internal/kernelmod/memory/mod.go | 4 +- internal/kernelmod/multimodal/mod.go | 4 +- internal/kernelmod/onnx/mod.go | 2 +- internal/kernelmod/pluginhost/mod.go | 22 +-- internal/memory/block_recall.go | 6 +- internal/memory/centroid_store.go | 6 +- internal/memory/cut.go | 4 +- internal/memory/edge_entity.go | 16 +- internal/memory/graph.go | 4 +- internal/memory/indexer.go | 4 +- internal/memory/migrate_entities.go | 6 +- internal/memory/scene_migrate.go | 8 +- internal/plugin/builtin_skill.go | 184 ++++++++++++++++++ internal/plugin/builtin_skill_test.go | 192 +++++++++++++++++++ internal/plugin/dynamic_proc.go | 26 +-- internal/plugin/proc/corehandler.go | 2 +- internal/plugin/proc/host.go | 4 +- internal/plugin/proc/plugin.go | 14 +- internal/plugin/proc/process.go | 50 ++--- internal/plugin/proc/shm_security.go | 2 +- internal/plugin/proc/stage.go | 6 +- internal/plugin/proc/supervisor.go | 8 +- internal/plugin/registry.go | 31 ++- internal/plugins/clawhubadapter/plugin.go | 4 +- internal/plugins/kbtree/plugin.go | 8 +- internal/plugins/pluginmgr/plugin.go | 6 +- internal/plugins/pluginmgr/skill_handout.go | 6 +- internal/plugins/remotedevice/device.go | 8 +- internal/plugins/remotedevice/plugin.go | 12 +- internal/plugins/seq/plugin.go | 2 +- internal/plugins/seq/skill.go | 94 +++++++++ internal/plugins/seq/skill_test.go | 26 +++ internal/plugins/webui/handler.go | 2 +- internal/plugins/webui/handler_chat.go | 2 +- internal/plugins/webui/handler_device.go | 2 +- internal/plugins/webui/handler_upload.go | 4 +- internal/plugins/webui/history.go | 14 +- internal/plugins/webui/login_limiter.go | 4 +- internal/plugins/webui/proxy.go | 6 +- internal/sdk/memory_impl.go | 14 +- internal/security/embedded.go | 12 +- internal/security/global.go | 4 +- internal/security/harden.go | 2 +- internal/security/key.go | 4 +- internal/security/keyring.go | 4 +- 88 files changed, 912 insertions(+), 354 deletions(-) create mode 100644 internal/agent/core/prompt_seq_test.go create mode 100644 internal/plugin/builtin_skill.go create mode 100644 internal/plugin/builtin_skill_test.go create mode 100644 internal/plugins/seq/skill.go create mode 100644 internal/plugins/seq/skill_test.go diff --git a/cmd/homed/bootstrap.go b/cmd/homed/bootstrap.go index 6c435d67..83668389 100644 --- a/cmd/homed/bootstrap.go +++ b/cmd/homed/bootstrap.go @@ -418,7 +418,7 @@ func compactConfigDB(cfgReg *internalConfig.ConfigRegistry) { } done, err := cfgReg.MaybeCompact(1<<20, 0.25) if err != nil { - log.Printf("[homed] warning: 配置库压缩失败: %v", err) + log.Printf("[homed] warning: config DB compaction failed: %v", err) return } if !done { @@ -428,5 +428,5 @@ func compactConfigDB(cfgReg *internalConfig.ConfigRegistry) { if st, err := os.Stat(cfgReg.DBPath()); err == nil { after = st.Size() } - log.Printf("[homed] 配置库已压缩: %d -> %d 字节", before, after) + log.Printf("[homed] config DB compacted: %d -> %d bytes", before, after) } diff --git a/cmd/homed/guard.go b/cmd/homed/guard.go index dfcc2764..a71bda89 100644 --- a/cmd/homed/guard.go +++ b/cmd/homed/guard.go @@ -267,7 +267,7 @@ func runGuard(dataDir string) { // · 只有 guard 知道防护上下文(unit 名、当前是否 failback 中) // · 决策集中在一处,不散在两个进程里 if c := consumeHeartbeatCmd(hbPath); c.OK && c.Cmd == hbCmdShutdown { - log.Printf("[guard] worker 已退出且留下关机指令(%s)—— 执行 systemctl stop,不再拉起", + log.Printf("[guard] worker exited and left a shutdown directive (%s) -- running systemctl stop, not restarting", c.Reason) stopOwnUnit(ownUnit) return @@ -587,11 +587,11 @@ func doLastResort(cfg *guardConfig) { // 故不删除,只在「确实是自指」时改走退出路径,并留下明确日志。 func doLastResortGuarded(cfg *guardConfig, unit string) { if unit != "" && isSelfRestartCommand(cfg.RestartCommand, unit) { - log.Printf("[guard] last_resort=restart_app 但 restart_command 指向自身单元 %q:"+ - "执行 systemctl restart 会杀掉自己并被 Restart=always 立即拉起,"+ - "形成无限自杀重启。改为**直接退出** —— "+ - "在 Restart=always 下这与 restart_app 等价且无自杀窗口。"+ - "(若希望 guard 不被 systemd 管,请改用其它 last_resort 或外部守护器)", + log.Printf("[guard] last_resort=restart_app but restart_command points at own unit %q: "+ + "systemctl restart would kill itself and Restart=always would immediately start it again, "+ + "creating an infinite suicide-restart loop. Switching to **direct exit** -- "+ + "under Restart=always this is equivalent to restart_app with no suicide window. "+ + "(If guard should not be managed by systemd, use another last_resort or an external supervisor)", unit) // 交给 systemd 重新拉起(等价于 restart_app)。 // 退出码沿用 workerCleanExit 语义:正常路径下 guard 本就在 worker diff --git a/cmd/homed/heartbeat_cmd.go b/cmd/homed/heartbeat_cmd.go index a38efbb9..e53cfd6d 100644 --- a/cmd/homed/heartbeat_cmd.go +++ b/cmd/homed/heartbeat_cmd.go @@ -209,7 +209,7 @@ func nextHeartbeatWrite(hbPath string, now int64) string { if pendingWritten { // 检查 guard 是否已消费(guard 消费 = 把文件重写成纯 t= 行) if b, err := os.ReadFile(hbPath); err == nil && !strings.Contains(string(b), "cmd=") { - log.Printf("[homed] 控制指令 %s 已被 guard 消费,清除本地待发状态", pendingCmd.Cmd) + log.Printf("[homed] control command %s consumed by guard, clearing local pending state", pendingCmd.Cmd) pendingCmd = hbCommand{} pendingWritten = false return "" @@ -236,10 +236,10 @@ func nextHeartbeatWrite(hbPath string, now int64) string { // 那种情况下 guard 退出后进程就真的没了,无需额外动作。 func stopOwnUnit(unit string) { if unit == "" { - log.Printf("[guard] 非 systemd 环境(unit 未知):guard 退出即可,systemd 不会拉起") + log.Printf("[guard] non-systemd environment (unit unknown): guard can just exit, systemd will not restart it") return } - log.Printf("[guard] 执行 systemctl stop %s", unit) + log.Printf("[guard] running systemctl stop %s", unit) // 同步执行:guard 此时已经没有别的事要做,且必须在**自己退出之前** // 让 systemd 收到停止请求。异步会与 return 竞争 —— // guard 退了、systemctl 还没跑,systemd 可能已经判定 unit 需要重启。 @@ -247,7 +247,7 @@ func stopOwnUnit(unit string) { cmd.Stdout = os.Stderr cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { - log.Printf("[guard] systemctl stop %s 失败: %v(guard 仍退出;"+ - "若 unit 是 Restart=always 可能被重新拉起)", unit, err) + log.Printf("[guard] systemctl stop %s failed: %v (guard still exits; "+ + "if the unit is Restart=always it may be started again)", unit, err) } } diff --git a/cmd/homed/kmod_runtime.go b/cmd/homed/kmod_runtime.go index 3875bea1..bf284308 100644 --- a/cmd/homed/kmod_runtime.go +++ b/cmd/homed/kmod_runtime.go @@ -128,7 +128,7 @@ func RunKernelModules(ks *kernelboot.KernelState, hooks kernelAssembleHooks) (*k for _, m := range layer { names = append(names, m.Name()) } - log.Printf("[homed] 内核装配 第 %d 层完成(%d 个): %s", + log.Printf("[homed] kernel assembly layer %d complete (%d modules): %s", i, len(layer), strings.Join(names, ", ")) // ★ 把"某层完成"事件交给 main(用于在 stage-host 就绪时 // 立刻接上插件拉起动作,而不是等 agent 层的 35 秒)。 @@ -141,7 +141,7 @@ func RunKernelModules(ks *kernelboot.KernelState, hooks kernelAssembleHooks) (*k return nil, err } - log.Printf("[homed] 内核装配完成: %d 层 / %d 模块(峰值并发 %d,原为 17 步串行)", + log.Printf("[homed] kernel assembly complete: %d layers / %d modules (peak concurrency %d, previously 17 serial steps)", res.LayerCount, len(res.Order), res.MaxConcurrent) return bus, nil } @@ -161,7 +161,7 @@ func loadStartupPlan(mods []kernelboot.Module) (*kernelboot.Plan, error) { if gf == nil { // dev:没有图(也没注入 hash)⇒ 运行期现算。 // 这条路径**不是**生产路径;它存在只为让 go test / go run 能用。 - log.Printf("[homed] 未找到编译期依赖图(dev 构建),回退运行期分层") + log.Printf("[homed] compile-time dependency graph not found (dev build), falling back to runtime layering") return kernelboot.Build(mods) } @@ -169,7 +169,7 @@ func loadStartupPlan(mods []kernelboot.Module) (*kernelboot.Plan, error) { if err != nil { return nil, err } - log.Printf("[homed] 启动顺序来自编译期依赖图 %s(digest %s)", + log.Printf("[homed] startup order from compile-time dependency graph %s (digest %s)", filepath.Base(graphPath), gf.Digest[:16]) return plan, nil } diff --git a/cmd/homed/main.go b/cmd/homed/main.go index 9a9cd63c..86f4ca9f 100644 --- a/cmd/homed/main.go +++ b/cmd/homed/main.go @@ -113,19 +113,19 @@ func main() { // // 失败不 Fatal(权限问题不该让服务不可用),但会逐条响亮记录。 if hr := security.HardenDataDir(opt.dataDir); len(hr.Changed) > 0 { - log.Printf("[homed] 已收紧 %d 项权限(内嵌密钥场景的必要缓解): %v", + log.Printf("[homed] hardened %d file permissions (necessary mitigation for embedded-key scenario): %v", len(hr.Changed), hr.Changed) } key, keySrc, kerr := security.LoadOrCreateKey(opt.dataDir) if kerr != nil { - log.Fatalf("[homed] 安全模块密钥初始化失败(拒绝以降级模式启动): %v", kerr) + log.Fatalf("[homed] security key init failed (refusing to start in degraded mode): %v", kerr) } // E-1:若存在密钥环,装入历史钥(只读旧密文;当前钥仍由 // systemd credential/env/security.key 提供)。密钥环损坏或 current 不一致 // 必须响亮失败,不能猜一把钥继续启动。 oldKeys, rerr := security.LoadKeyring(opt.dataDir, key) if rerr != nil { - log.Fatalf("[homed] 安全模块密钥环加载失败(拒绝静默选错钥): %v", rerr) + log.Fatalf("[homed] security keyring load failed (refusing to silently pick wrong key): %v", rerr) } // ★ 内嵌钥上台时,磁盘钥必须继续参与**解密**。 // @@ -138,7 +138,7 @@ func main() { oldKeys = append(oldKeys, extra...) } if len(oldKeys) > 0 { - log.Printf("[homed] 安全模块已装入 %d 把历史/额外密钥(E-1 平滑轮换 + 内嵌迁移)", len(oldKeys)) + log.Printf("[homed] security module loaded %d historical/extra key(s) (E-1 smooth rotation + embedded migration)", len(oldKeys)) } if _, serr := security.InitGlobal(security.Options{ Path: filepath.Join(opt.dataDir, "security", "variables.json"), @@ -146,22 +146,22 @@ func main() { OldKeys: oldKeys, Enabled: true, }); serr != nil { - log.Fatalf("[homed] 安全模块变量库加载失败(不静默重建): %v", serr) + log.Fatalf("[homed] security variable store load failed (will not silently rebuild): %v", serr) } // 配置落盘用的子密钥(域隔离:与别名密钥不共用)。 // 必须早于 EncryptExistingSecrets,后者依赖它。 if _, cerr := security.InitConfigCodecWithOldKeys(key, oldKeys); cerr != nil { - log.Fatalf("[homed] 配置加密子密钥派生失败: %v", cerr) + log.Fatalf("[homed] config-encryption subkey derivation failed: %v", cerr) } // 把库里仍是明文的敏感项就地加密(幂等;升级路径必须跑)。 if n, merr := cfgReg.EncryptExistingSecrets(); merr != nil { // 不 Fatal:库可能是只读/被锁,而“部分为明文”不等于服务不可用。 // 但要**响亮告警**:这是用户必须知道的安全事实。 - log.Printf("[homed] 存量敏感配置加密未完成(部分项仍为明文): %v", merr) + log.Printf("[homed] existing sensitive config encryption incomplete (some entries still plaintext): %v", merr) } else if n > 0 { - log.Printf("[homed] 已加密 %d 条存量敏感配置", n) + log.Printf("[homed] encrypted %d existing sensitive config entries", n) } - log.Printf("[homed] 安全模块已启用(密钥来源 %s,变量 %d 条,配置落盘加密已开启)", + log.Printf("[homed] security module enabled (key source %s, %d variables, config-at-rest encryption on)", keySrc, security.Global().Count()) // ★ E-2:备份跨机器恢复的诊断。 @@ -171,14 +171,14 @@ func main() { // 此前这种状态只在“读某一条配置”时才暴露成“某个功能坏了”, // 用户看到的是症状而不是原因。这里一次性说清事实与修复动作。 if okN, badN, aerr := cfgReg.AuditSecrets(); aerr == nil && badN > 0 { - log.Printf("[homed] ⚠ 检测到 %d 条密文无法用当前主密钥解开(可解 %d 条)—— "+ - "最常见原因是:这份 config.db 来自**另一台机器/另一个密钥**。"+ - "修复:把原机器的密钥一起带过来(systemd credential / %s / "+ - "%s),或用旧密钥启动一次并执行重加密迁移。", + log.Printf("[homed] WARNING: %d ciphertext entries cannot be decrypted with current master key (%d decryptable) — " + + "most common cause: this config.db came from **another machine / another key**. " + + "Fix: bring the original machine's key over (systemd credential / %s / " + + "%s), or start once with the old key and run the re-encryption migration.", badN, okN, security.EnvKeyName, security.KeyringFileName) } } else { - log.Printf("[homed] 安全模块被显式关闭(core.security.enabled=false)—— 明文落盘且中转无防护") + log.Printf("[homed] security module explicitly disabled (core.security.enabled=false) — data at rest is plaintext and relay is unprotected") } // ======================================================================== @@ -222,10 +222,10 @@ func main() { // 是谁改的)。修复由人执行 —— 命令里已内联 sed 替换。 if cdir := ks.Cfg.Daemon.DataDir; cdir != "" && cdir != opt.dataDir { st := systemd.State{RuntimeDataDir: opt.dataDir, ConfigDataDir: cdir} - log.Printf("[homed] ⚠️ 数据目录不一致(数据会分裂):") - log.Printf("[homed] -data = %s(图记忆、插件 Settings.DataDir)", opt.dataDir) - log.Printf("[homed] DB data_dir = %s(文本/知识/文档)", cdir) - log.Printf("[homed] 修复命令(勿手改单元,生产用 drop-in 覆盖 ExecStart):") + log.Printf("[homed] WARNING: data directories mismatch (data will split):") + log.Printf("[homed] -data = %s (graph memory, plugin Settings.DataDir)", opt.dataDir) + log.Printf("[homed] DB data_dir = %s (text/knowledge/doc)", cdir) + log.Printf("[homed] fix command (do not hand-edit the unit; use a drop-in to override ExecStart in production):") log.Printf("[homed] %s", st.SyncCommand(cdir)) } @@ -260,7 +260,7 @@ func main() { } }, }); err != nil { - log.Fatalf("[homed] 内核装配失败: %v", err) + log.Fatalf("[homed] kernel assembly failed: %v", err) } defer ks.CloseAll() @@ -289,10 +289,10 @@ func main() { // ③ 拉起"声明设施已就绪"的插件;挂起"等更晚设施(如 agent)"的 // (挂起项经 syncbus 在 agent 就绪时自动拉起,见 pluginhost/waiter.go) if cat, ok := ks.PluginCatalog.(*pluginhost.PluginCatalog); ok { - log.Printf("[homed] 插件调度完成:元数据缓存 %d 个,已加载 %d 个", + log.Printf("[homed] plugin scheduling done: %d metadata cached, %d loaded", cat.Size(), len(pluginReg.ListLoadedPlugins())) } else { - log.Printf("[homed] 插件调度完成:已加载 %d 个", len(pluginReg.ListLoadedPlugins())) + log.Printf("[homed] plugin scheduling done: %d loaded", len(pluginReg.ListLoadedPlugins())) } // 插件加载完成后再回收空闲页:大值(如老版聊天记录)可能在这一步被搬走/删除, @@ -327,7 +327,7 @@ func main() { waitForShutdown(ctx, opt.dataDir, restartCh, stopHeartbeatEarly, pluginReg, trk, cfgReg, sup, func() { if err := ks.CloseAll(); err != nil { - log.Printf("[homed] 模块关闭: %v", err) + log.Printf("[homed] module shutdown: %v", err) } }) } diff --git a/cmd/homed/plugins_scheduled.go b/cmd/homed/plugins_scheduled.go index 0b4c884b..7d998dd2 100644 --- a/cmd/homed/plugins_scheduled.go +++ b/cmd/homed/plugins_scheduled.go @@ -44,7 +44,7 @@ func resolveFailbackAllowlist(cfgReg *internalConfig.ConfigRegistry, names = append(names, s) } } - log.Printf("[homed] failback boot: plugin allowlist = %v(装配前解析)", names) + log.Printf("[homed] failback boot: plugin allowlist = %v (parsed before assembly)", names) return names } diff --git a/cmd/mock-server/main.go b/cmd/mock-server/main.go index 11b12a26..038d6847 100644 --- a/cmd/mock-server/main.go +++ b/cmd/mock-server/main.go @@ -512,10 +512,10 @@ func main() { addr := ":9099" log.Printf("=== HologramAgent Mock Server ====") - log.Printf("监听地址: http://0.0.0.0%s", addr) - log.Printf("API 基础路径: http://0.0.0.0%s/api/v1/", addr) - log.Printf("SSE 端点: http://0.0.0.0%s/api/v1/chat/events", addr) - log.Printf("设备桥 WS: ws://0.0.0.0%s/api/v1/device/ws", addr) + log.Printf("listening on: http://0.0.0.0%s", addr) + log.Printf("API base path: http://0.0.0.0%s/api/v1/", addr) + log.Printf("SSE endpoint: http://0.0.0.0%s/api/v1/chat/events", addr) + log.Printf("device bridge WS: ws://0.0.0.0%s/api/v1/device/ws", addr) log.Printf("==============================") log.Fatal(http.ListenAndServe(addr, server.middleware(mux))) } diff --git a/cmd/mock-server/ws.go b/cmd/mock-server/ws.go index 739af950..32036935 100644 --- a/cmd/mock-server/ws.go +++ b/cmd/mock-server/ws.go @@ -235,7 +235,7 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) { } defer ws.Close() - log.Printf("[ws] 新设备连接") + log.Printf("[ws] new device connected") // 处理 hello/bind/cmd 协议 var device *MockDevice @@ -303,7 +303,7 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) { "op": "hello_ack", "code": 0, }) - log.Printf("[ws] 设备登记: %s (%s) caps=%v", devMeta.DeviceID, devMeta.Name, devMeta.Caps) + log.Printf("[ws] device registered: %s (%s) caps=%v", devMeta.DeviceID, devMeta.Name, devMeta.Caps) case "bind": if msg.DeviceID == "" || msg.Token == "" { @@ -327,14 +327,14 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) { "op": "bind_ack", "code": 0, }) - log.Printf("[ws] 设备授权: %s", msg.DeviceID) + log.Printf("[ws] device authorized: %s", msg.DeviceID) // 绑定成功后,发送全量能力测试命令序列 go func() { time.Sleep(500 * time.Millisecond) // 测试 1: screensee 截图 - log.Printf("[ws] 发命令 1/7: homeagent-screensee") + log.Printf("[ws] send command 1/7: homeagent-screensee") ws.WriteJSON(client.CmdMsg{ Op: "cmd", ReqID: "test_1_screensee_" + fmt.Sprintf("%d", time.Now().UnixMilli()), @@ -345,7 +345,7 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) { time.Sleep(800 * time.Millisecond) // 测试 2: clipboardsue 写入剪贴板 - log.Printf("[ws] 发命令 2/7: homeagent-clipboardsue") + log.Printf("[ws] send command 2/7: homeagent-clipboardsue") ws.WriteJSON(client.CmdMsg{ Op: "cmd", ReqID: "test_2_clipboardsue_" + fmt.Sprintf("%d", time.Now().UnixMilli()), @@ -356,7 +356,7 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) { time.Sleep(800 * time.Millisecond) // 测试 3: clipboardsee 读取剪贴板 - log.Printf("[ws] 发命令 3/7: homeagent-clipboardsee") + log.Printf("[ws] send command 3/7: homeagent-clipboardsee") ws.WriteJSON(client.CmdMsg{ Op: "cmd", ReqID: "test_3_clipboardsee_" + fmt.Sprintf("%d", time.Now().UnixMilli()), @@ -367,7 +367,7 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) { time.Sleep(800 * time.Millisecond) // 测试 4: computeruse 鼠标移动(使用非标准 JSON 格式测试兼容性) - log.Printf("[ws] 发命令 4/7: homeagent-computeruse move") + log.Printf("[ws] send command 4/7: homeagent-computeruse move") ws.WriteJSON(client.CmdMsg{ Op: "cmd", ReqID: "test_4_computeruse_" + fmt.Sprintf("%d", time.Now().UnixMilli()), @@ -378,7 +378,7 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) { time.Sleep(800 * time.Millisecond) // 测试 4b: computeruse 鼠标点击(标准 JSON 格式) - log.Printf("[ws] 发命令 4b/7: homeagent-computeruse click") + log.Printf("[ws] send command 4b/7: homeagent-computeruse click") ws.WriteJSON(client.CmdMsg{ Op: "cmd", ReqID: "test_4b_click_" + fmt.Sprintf("%d", time.Now().UnixMilli()), @@ -389,7 +389,7 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) { time.Sleep(800 * time.Millisecond) // 测试 5: speakeruse TTS 播报 - log.Printf("[ws] 发命令 5/7: homeagent-speakeruse") + log.Printf("[ws] send command 5/7: homeagent-speakeruse") ws.WriteJSON(client.CmdMsg{ Op: "cmd", ReqID: "test_5_speakeruse_" + fmt.Sprintf("%d", time.Now().UnixMilli()), @@ -400,7 +400,7 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) { time.Sleep(800 * time.Millisecond) // 测试 6: screensue 弹窗显示 - log.Printf("[ws] 发命令 6/7: homeagent-screensue(HTML)") + log.Printf("[ws] send command 6/7: homeagent-screensue (HTML)") ws.WriteJSON(client.CmdMsg{ Op: "cmd", ReqID: "test_6_screensue_" + fmt.Sprintf("%d", time.Now().UnixMilli()), @@ -411,7 +411,7 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) { time.Sleep(800 * time.Millisecond) // 测试 7: omniparse 解析当前窗口 UI 元素 - log.Printf("[ws] 发命令 7/7: homeagent-omniparse") + log.Printf("[ws] send command 7/7: homeagent-omniparse") ws.WriteJSON(client.CmdMsg{ Op: "cmd", ReqID: "test_7_omniparse_" + fmt.Sprintf("%d", time.Now().UnixMilli()), @@ -421,40 +421,40 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) { }() case "cmd_result": - log.Printf("[ws] 命令结果: req=%s status=%s", msg.ReqID, msg.Status) + log.Printf("[ws] command result: req=%s status=%s", msg.ReqID, msg.Status) if msg.Output != "" { output := msg.Output if len(output) > 100 { output = output[:100] + "..." } - log.Printf("[ws] 输出: %s", output) + log.Printf("[ws] output: %s", output) } if msg.Error != "" { - log.Printf("[ws] 错误: %s", msg.Error) + log.Printf("[ws] error: %s", msg.Error) } case "data_start": var ds client.DataStart json.Unmarshal(msg.Payload, &ds) - log.Printf("[ws] 二进制数据开始: req=%s kind=%s mime=%s total=%d", ds.ReqID, ds.Kind, ds.MIME, ds.Total) + log.Printf("[ws] binary data start: req=%s kind=%s mime=%s total=%d", ds.ReqID, ds.Kind, ds.MIME, ds.Total) case "data_end": - log.Printf("[ws] 二进制数据结束: req=%s status=%s", msg.ReqID, msg.Status) + log.Printf("[ws] binary data end: req=%s status=%s", msg.ReqID, msg.Status) case "speech_start": - log.Printf("[ws] TTS 音频开始: req=%s", msg.ReqID) + log.Printf("[ws] TTS audio start: req=%s", msg.ReqID) case "speech_end": - log.Printf("[ws] TTS 音频结束: req=%s", msg.ReqID) + log.Printf("[ws] TTS audio end: req=%s", msg.ReqID) case "status": - log.Printf("[ws] 状态上报: %s -> %s", msg.DeviceID, msg.Status) + log.Printf("[ws] status report: %s -> %s", msg.DeviceID, msg.Status) case "event": - log.Printf("[ws] 事件上报: %s type=%s", msg.DeviceID, string(msg.Payload)) + log.Printf("[ws] event report: %s type=%s", msg.DeviceID, string(msg.Payload)) default: - log.Printf("[ws] 未知消息类型: %s", msg.Op) + log.Printf("[ws] unknown message type: %s", msg.Op) } } @@ -468,5 +468,5 @@ func (s *MockServer) handleDeviceWS(w http.ResponseWriter, r *http.Request) { } s.mu.Unlock() } - log.Printf("[ws] 设备断开") + log.Printf("[ws] device disconnected") } diff --git a/internal/agent/api/codec.go b/internal/agent/api/codec.go index 7d88c9e0..9e749dd4 100644 --- a/internal/agent/api/codec.go +++ b/internal/agent/api/codec.go @@ -29,8 +29,8 @@ func ModelContextWindow(model string) int { if w := modelContextWindowC(model); w != contextWindowUnknown { return w } - log.Printf("[provider] 模型 %q 无法推断上下文窗口,回退 %d;"+ - "若真实窗口更大,请设置 core.llm.sources..context_window", + log.Printf("[provider] cannot infer context window for model %q, falling back to %d; "+ + "if the real window is larger, set core.llm.sources..context_window", strings.ToLower(model), defaultInferredContextWindow) return defaultInferredContextWindow } diff --git a/internal/agent/api/provider_security.go b/internal/agent/api/provider_security.go index b9894264..e37aa69a 100644 --- a/internal/agent/api/provider_security.go +++ b/internal/agent/api/provider_security.go @@ -122,7 +122,7 @@ func (d *secureProviderDecorator) scrubRequest(req *CompletionRequest) { // logEvts 记录拦截(**只记别名,绝不记明文**)。 func (d *secureProviderDecorator) logEvts(evts []security.Evt) { for _, e := range evts { - log.Printf("[security] 出向拦截 %s ← 模式 %s,%d 字节,合并=%v,位置 %s", + log.Printf("[security] outbound block %s <- pattern %s, %d bytes, merged=%v, at %s", e.Alias, e.Kind, e.Len, e.Merged, e.Where) } } diff --git a/internal/agent/core/agent.go b/internal/agent/core/agent.go index 309c9a9b..30f2d23c 100644 --- a/internal/agent/core/agent.go +++ b/internal/agent/core/agent.go @@ -383,7 +383,7 @@ func New(cfg AgentConfig) *Agent { // 媒体文件,无需 mediaGet/mediaPut 接线。 built, skipped := cfg.Knowledge.ReindexDense() if built > 0 || skipped > 0 { - log.Printf("[knowledge] 多模态稠密索引: 新建 %d 跳过 %d(其余命中缓存)", built, skipped) + log.Printf("[knowledge] multimodal dense index: built %d, skipped %d (rest hit cache)", built, skipped) } } } @@ -492,7 +492,7 @@ func (a *Agent) drainPendingInterrupts(reason string) { for _, evt := range pending { a.emitSkippedReply(evt, reason) } - log.Printf("[agent] %s: 停机,%d 条待办任务已补 skipped 终态", a.id, len(pending)) + log.Printf("[agent] %s: shutting down, filled skipped terminal state for %d pending task(s)", a.id, len(pending)) } // graphMemoryOf 决定本 agent 的图记忆共同面实现。 diff --git a/internal/agent/core/crossmodal.go b/internal/agent/core/crossmodal.go index bdedea28..63eae47f 100644 --- a/internal/agent/core/crossmodal.go +++ b/internal/agent/core/crossmodal.go @@ -107,7 +107,7 @@ func (a *Agent) retrieveCrossModal(query string, topK int, cfg CrossModalFusionC if a.multimodalSpace != nil && a.multimodalSpace.Loaded() && a.memory != nil { qv, err := a.multimodalSpace.VectorizeDense(query) if err != nil { - log.Printf("[crossmodal] 多模态文本编码失败: %v", err) + log.Printf("[crossmodal] multimodal text encoding failed: %v", err) } else if hits, err := a.memory.RecallBlocks(memory.BlockRecallQuery{ Vector: qv, Fingerprint: a.multimodalSpace.Fingerprint(), @@ -123,7 +123,7 @@ func (a *Agent) retrieveCrossModal(query string, topK int, cfg CrossModalFusionC // 截断**之前**,否则名额已被文本块占满。 ExcludeText: true, }); err != nil { - log.Printf("[crossmodal] 媒体块检索失败: %v", err) + log.Printf("[crossmodal] media block retrieval failed: %v", err) } else { for _, h := range hits { if h.Block.Modality == memory.BlockText { diff --git a/internal/agent/core/distill.go b/internal/agent/core/distill.go index 951dae99..d43b36e8 100644 --- a/internal/agent/core/distill.go +++ b/internal/agent/core/distill.go @@ -182,7 +182,7 @@ func (a *Agent) archiveColdDocs() { if n, err := a.memory.DecaySceneRefs(30*24*time.Hour, 0.05); err != nil { log.Printf("[agent] scene decay error: %v", err) } else if n > 0 { - log.Printf("[agent] 场景关联衰减:清理 %d 条长期未重现的引用", n) + log.Printf("[agent] scene-association decay: pruned %d long-unseen references", n) } } @@ -203,7 +203,7 @@ func (a *Agent) archiveColdDocs() { // 「有没有真的写进图库」只剩块数这一个信号。 blocksBefore, err := a.memory.MemoryBlockCount() if err != nil { - log.Printf("[agent] doc→graph: %s 读块数失败,保留文档: %v", doc.ID, err) + log.Printf("[agent] doc->graph: %s failed to read block count, keeping doc: %v", doc.ID, err) continue } @@ -278,23 +278,23 @@ func (a *Agent) archiveColdDocs() { // blocksAfter, err := a.memory.MemoryBlockCount() if err != nil { - log.Printf("[agent] doc→graph: %s 读块数失败,保留文档: %v", doc.ID, err) + log.Printf("[agent] doc->graph: %s failed to read block count, keeping doc: %v", doc.ID, err) continue } if blocksAfter == blocksBefore { - log.Printf("[agent] doc→graph: %s 未写入任何块,保留文档待下轮重试"+ - "(三元组 %d 条全被实体名校验拒绝)", doc.ID, len(triples)) + log.Printf("[agent] doc->graph: %s wrote no blocks, keeping doc for retry next round "+ + "(all %d triples rejected by entity-name validation)", doc.ID, len(triples)) continue } - log.Printf("[agent] doc→graph: %s → 新增 %d 块(三元组 %d 条,"+ - "媒体挂接 %d,旧表计数已停用 ec=%d rc=%d)", + log.Printf("[agent] doc->graph: %s -> added %d blocks (%d triples, "+ + "%d media attachments, legacy table counters disabled ec=%d rc=%d)", doc.ID, blocksAfter-blocksBefore, len(triples), blocks, ec, rc) // 文档持有的一等块写入 L3,并以 document --contains--> block 边关联; // 块 ID 原样保留(迁移而非重建)。块迁走后删除文档即完成迁移。 if len(doc.Blocks) > 0 { if bound := a.linkBlocksToDocument(doc.ID, doc.Blocks, memory.ChannelScene(doc.Source)); bound != len(doc.Blocks) { - log.Printf("[agent] doc→graph: %s 块迁移不完整 (%d/%d),保留文档待下轮重试", + log.Printf("[agent] doc->graph: %s block migration incomplete (%d/%d), keeping doc for retry next round", doc.ID, bound, len(doc.Blocks)) continue } @@ -402,11 +402,11 @@ func (a *Agent) dedupeScenes() { } merged, err := a.memory.DedupeScenes() if err != nil { - log.Printf("[agent] 场景去重失败(下轮重试): %v", err) + log.Printf("[agent] scene dedup failed (will retry next round): %v", err) return } if merged > 0 { - log.Printf("[agent] 场景去重:合并 %d 组同键场景(同一场面的重复键)", merged) + log.Printf("[agent] scene dedup: merged %d groups of same-key scenes (duplicate keys for one scene)", merged) } } diff --git a/internal/agent/core/eventloop.go b/internal/agent/core/eventloop.go index 50f6b0fd..baaa81e1 100644 --- a/internal/agent/core/eventloop.go +++ b/internal/agent/core/eventloop.go @@ -342,7 +342,7 @@ func (a *Agent) emitSkippedReply(evt *agentIO.InputEvent, reason string) { if evt.ResponseCh == nil { // 无可回执的通道:不静默。异步来源本就靠 agent 主动 output_send, // 丢弃后没有任何东西会告诉用户,因此这条日志是唯一的线索。 - log.Printf("[agent] %s: 丢弃一条无回执通道的输入(source=%s channel=%s request=%s reason=%s)", + log.Printf("[agent] %s: dropping input on channel without receipt support (source=%s channel=%s request=%s reason=%s)", a.id, evt.Source, evt.OutputChannel, evt.RequestID, reason) return } @@ -458,7 +458,7 @@ func (a *Agent) emitResponse(evt *agentIO.InputEvent, response string, turnUsage OutputChannel: ch, }: default: - log.Printf("[agent] ResponseCh 已满,终态回执被丢弃(request=%s,可能违反不变量 I5)", evt.RequestID) + log.Printf("[agent] ResponseCh full, terminal receipt dropped (request=%s, possible invariant I5 violation)", evt.RequestID) } } @@ -551,7 +551,7 @@ func (a *Agent) cleanInputFor(evt *agentIO.InputEvent, input string) string { if chDef, ok := a.io.GetInputChannelDef(name); ok && chDef.Cleaner != nil { return chDef.Cleaner(input) } - log.Printf("[agent] 注入声明了 cleaner_name=%q 但没有注册过该通道的 Cleaner,已回退", name) + log.Printf("[agent] injection declared cleaner_name=%q but no Cleaner was registered for that channel, falling back", name) } if chDef, ok := a.io.GetInputChannelDef(evt.Source); ok && chDef.Cleaner != nil { return chDef.Cleaner(input) diff --git a/internal/agent/core/graphmedia.go b/internal/agent/core/graphmedia.go index b29363d3..a982d97f 100644 --- a/internal/agent/core/graphmedia.go +++ b/internal/agent/core/graphmedia.go @@ -43,11 +43,11 @@ func (a *Agent) migrateLegacyGraphMedia() { return memory.MemoryBlock{}, false }) if err != nil { - log.Printf("[media] 旧媒体实体迁移失败(下轮重试): %v", err) + log.Printf("[media] legacy media entity migration failed (will retry next round): %v", err) return } if blocks > 0 || entities > 0 { - log.Printf("[media] 旧媒体实体迁移完成: 新建 %d 个原生块,删除 %d 个描述式实体", blocks, entities) + log.Printf("[media] legacy media entity migration done: created %d native blocks, deleted %d descriptive entities", blocks, entities) } } @@ -68,11 +68,11 @@ func (a *Agent) attachBlocksToSentenceBlock(sentenceBlockID string, refs []strin var err error b, ok, err = a.memory.BlockByIDPrefix(ref) if err != nil { - log.Printf("[media] 查块 %s 失败: %v", ref, err) + log.Printf("[media] lookup block %s failed: %v", ref, err) continue } if !ok { - log.Printf("[media] 块引用 %s 无唯一对应块,跳过", ref) + log.Printf("[media] block ref %s has no unique match, skipping", ref) continue } } @@ -82,11 +82,11 @@ func (a *Agent) attachBlocksToSentenceBlock(sentenceBlockID string, refs []strin b.Scene = scene } if err := a.memory.PutMemoryBlocks([]memory.MemoryBlock{b}); err != nil { - log.Printf("[media] L3 块写入失败 (%s): %v", shortID(b.ID), err) + log.Printf("[media] L3 block write failed (%s): %v", shortID(b.ID), err) continue } if err := a.memory.AddMemoryBlockEdge("block", sentenceBlockID, "block", b.ID, "contains"); err != nil { - log.Printf("[media] 句子→块边建立失败 (%s): %v", shortID(b.ID), err) + log.Printf("[media] sentence->block edge creation failed (%s): %v", shortID(b.ID), err) continue } bound++ @@ -101,14 +101,14 @@ func (a *Agent) linkBlocksToDocument(docID string, blocks []memory.MemoryBlock, return 0 } if err := a.memory.PutDocumentNode(docID, ""); err != nil { - log.Printf("[media] 写入 L3 文档节点失败 (%s): %v", docID, err) + log.Printf("[media] L3 doc node write failed (%s): %v", docID, err) return 0 } // 文档层与场景模型兼容:文档节点也进场景,好让「这个场面有哪些文档」 // 可枚举、可统计(场景贯穿流水线的 doc 层落地)。 if scene != "" { if err := a.memory.TagSceneDocument(scene, docID); err != nil { - log.Printf("[media] 文档挂场景失败 (%s): %v", docID, err) + log.Printf("[media] attaching doc to scene failed (%s): %v", docID, err) } } // 文档层把场景传给块:归档进图库的块属于该文档的来源场面(QQ 归档的图 @@ -119,13 +119,13 @@ func (a *Agent) linkBlocksToDocument(docID string, blocks []memory.MemoryBlock, } } if err := a.memory.PutMemoryBlocks(blocks); err != nil { - log.Printf("[media] 写入 L3 记忆块失败 (doc %s): %v", docID, err) + log.Printf("[media] L3 memory block write failed (doc %s): %v", docID, err) return 0 } bound := 0 for _, b := range blocks { if err := a.memory.AddMemoryBlockEdge("document", docID, "block", b.ID, "contains"); err != nil { - log.Printf("[media] 文档→块边建立失败 (%s): %v", shortID(b.ID), err) + log.Printf("[media] doc->block edge creation failed (%s): %v", shortID(b.ID), err) continue } bound++ @@ -196,7 +196,7 @@ func (a *Agent) resolveMediaRefs(refs []string) []string { for _, r := range refs { b, ok, err := a.memory.BlockByIDPrefix(r) if err != nil || !ok { - log.Printf("[media] 模型给的块引用 %s 无法唯一解析", r) + log.Printf("[media] model-provided block ref %s could not be resolved uniquely", r) continue } if seen[b.ID] { diff --git a/internal/agent/core/inputroute.go b/internal/agent/core/inputroute.go index f02b790f..511412ae 100644 --- a/internal/agent/core/inputroute.go +++ b/internal/agent/core/inputroute.go @@ -36,7 +36,7 @@ func (a *Agent) routeInputByOwner(evt *agentIO.InputEvent, isInterrupt bool) boo // 归属到一个不存在(或已销毁、登记表尚未归还)的 agent: // **不吞输入** —— 由本内核兜底处理并留痕。吞掉一条输入比多处理一条更糟: // 用户会看到"消息发出去了却没人理",而日志里什么都没有。 - log.Printf("[route] inputch %s 归属 %s 无对应 agent,输入由 %s 兜底", evt.OutputChannel, entry.Owner, a.id) + log.Printf("[route] inputch %s owner %s has no matching agent, input handled by fallback %s", evt.OutputChannel, entry.Owner, a.id) return false } rc.agent.io.DeliverRouted(evt, isInterrupt) diff --git a/internal/agent/core/log_scrub.go b/internal/agent/core/log_scrub.go index cadb7d26..ef2ec9c9 100644 --- a/internal/agent/core/log_scrub.go +++ b/internal/agent/core/log_scrub.go @@ -53,7 +53,7 @@ func scrubForLog(text, where string) string { // 拦截本身要记一笔(不打印原文,只打印模式与长度)—— // 否则运维不知道"这个工具经常吐凭据",也就不会去修插件。 for _, e := range evts { - log.Printf("[security] 日志拦截 %s ← %s(%d 字节),原文未落盘", + log.Printf("[security] log block %s <- %s (%d bytes), raw content not written to disk", where, e.Kind, e.Len) } return out diff --git a/internal/agent/core/medialoop.go b/internal/agent/core/medialoop.go index 07d63119..f67ff615 100644 --- a/internal/agent/core/medialoop.go +++ b/internal/agent/core/medialoop.go @@ -43,7 +43,7 @@ func (a *Agent) reembedStaleMedia() { fp := a.multimodalSpace.Fingerprint() stale, err := a.memory.StaleBlockVectors(fp) if err != nil { - log.Printf("[media] 查询需重算向量的块失败: %v", err) + log.Printf("[media] querying blocks needing re-embedding failed: %v", err) return } // 只迁图像:多模态空间当前覆盖 text+image,音频/视频不在其内 @@ -56,7 +56,7 @@ func (a *Agent) reembedStaleMedia() { } } if len(targets) == 0 { - log.Printf("[media] 无需迁移向量(所有图像块已与当前空间对齐 fp=%s)", shortFP(fp)) + log.Printf("[media] no vector migration needed (all image blocks aligned with current space fp=%s)", shortFP(fp)) return } @@ -64,7 +64,7 @@ func (a *Agent) reembedStaleMedia() { if len(fp) >= 5 && fp[:5] == "http:" { workers = 8 } - log.Printf("[media] 启动向量迁移: %d 个图像块 → fp=%s dim=%d workers=%d", + log.Printf("[media] starting vector migration: %d image blocks -> fp=%s dim=%d workers=%d", len(targets), shortFP(fp), a.multimodalSpace.Dim(), workers) var jobsOnce sync.Once @@ -111,21 +111,21 @@ func (a *Agent) reembedStaleMedia() { for i, b := range targets { jobs <- b if (i+1)%20 == 0 { - log.Printf("[media] 向量迁移进度: %d/%d (done=%d failed=%d)", + log.Printf("[media] vector migration progress: %d/%d (done=%d failed=%d)", i+1, len(targets), atomic.LoadInt64(&done), failed) } } safeclose.Safe(&jobsOnce, jobs) wg.Wait() - log.Printf("[media] 向量迁移完成: 成功=%d 失败=%d 不支持=%d 总计=%d fp=%s", + log.Printf("[media] vector migration complete: ok=%d failed=%d unsupported=%d total=%d fp=%s", done, failed, unsupported, len(targets), shortFP(fp)) // 逐类打印失败原因(聚合后通常只有 1~2 类)。 // 没有这段,运维只能看到"失败=N"而不知为何 —— 本次 webp 问题的排查成本 // 全花在这里。 if len(failReasons) > 0 { - log.Printf("[media] 失败原因分类(按 mime | error 聚合):") + log.Printf("[media] failure reasons grouped by mime | error:") for reason, n := range failReasons { - log.Printf("[media] %d 个: %s", n, reason) + log.Printf("[media] %d x: %s", n, reason) } } } diff --git a/internal/agent/core/medialoop_test.go b/internal/agent/core/medialoop_test.go index 87a581b5..8e75580c 100644 --- a/internal/agent/core/medialoop_test.go +++ b/internal/agent/core/medialoop_test.go @@ -150,19 +150,19 @@ func TestReembedStaleMedia_LogsFailureReasons(t *testing.T) { a.reembedStaleMedia() }) - if !strings.Contains(logs, "失败原因分类") { - t.Fatalf("迁移失败时必须打印失败原因分类,实际日志:\n%s", logs) + if !strings.Contains(logs, "failure reasons grouped by") { + t.Fatalf("migration failure must log grouped failure reasons, actual log:\n%s", logs) } if !strings.Contains(logs, "image/webp") { - t.Fatalf("失败原因里必须出现具体 mime(否则无从定位是哪种格式),实际日志:\n%s", logs) + t.Fatalf("failure reasons must contain the concrete mime (otherwise the format cannot be identified), actual log:\n%s", logs) } if !strings.Contains(logs, "模拟解码失败") { t.Fatalf("失败原因里必须带上 provider 返回的错误原文,实际日志:\n%s", logs) } // 计数必须出现(格式 "N 个: ...")—— 具体数字取决于去重后的块数, // 这里只要求"有计数",不绑死数字(绑死会让判据随去重策略变化而脆)。 - if !regexp.MustCompile(`\d+ 个: image/webp`).MatchString(logs) { - t.Fatalf("失败原因应带聚合计数(形如「N 个: image/webp | ...」),实际日志:\n%s", logs) + if !regexp.MustCompile(`\d+ x: image/webp`).MatchString(logs) { + t.Fatalf("failure reasons must include an aggregate count (like \"N x: image/webp | ...\"), actual log:\n%s", logs) } } @@ -182,8 +182,8 @@ func TestReembedStaleMedia_SeparatesUnsupportedFromFailed(t *testing.T) { a.reembedStaleMedia() }) - if !strings.Contains(logs, "不支持=1") { - t.Fatalf("不受支持的模态应单独计数(不支持=1),实际日志:\n%s", logs) + if !strings.Contains(logs, "unsupported=1") { + t.Fatalf("unsupported modality must be counted separately (unsupported=1), actual log:\n%s", logs) } } diff --git a/internal/agent/core/mediaref.go b/internal/agent/core/mediaref.go index bf0037ef..97ecee24 100644 --- a/internal/agent/core/mediaref.go +++ b/internal/agent/core/mediaref.go @@ -98,7 +98,7 @@ func (a *Agent) embedBlockOnIngest(b *memory.MemoryBlock) { } vec, err := a.multimodalSpace.EmbedImageDense(b.Data, b.MIME) if err != nil { - log.Printf("[media] 入库嵌入失败 %s: %v", shortID(b.ID), err) + log.Printf("[media] ingest embedding failed %s: %v", shortID(b.ID), err) return } b.Vector = vec diff --git a/internal/agent/core/memory_scrub.go b/internal/agent/core/memory_scrub.go index 9f63df6c..8cbcb041 100644 --- a/internal/agent/core/memory_scrub.go +++ b/internal/agent/core/memory_scrub.go @@ -129,7 +129,7 @@ func scrubMemoryCandidate(payload map[string]interface{}) { func logEvtHits(where string, evts []security.Evt) { for _, e := range evts { - log.Printf("[security] %s 拦截 %s(模式 %s,%d 字节,合并=%v)", + log.Printf("[security] %s blocked %s (pattern %s, %d bytes, merged=%v)", where, e.Alias, e.Kind, e.Len, e.Merged) } } diff --git a/internal/agent/core/memorypass.go b/internal/agent/core/memorypass.go index c6cd32c4..b692dea3 100644 --- a/internal/agent/core/memorypass.go +++ b/internal/agent/core/memorypass.go @@ -450,9 +450,9 @@ func (a *Agent) resolveTurnScenes(f *TaskFrame, tool string) memory.TurnScene { } } if turn.Emergent { - log.Printf("[agent] 场景涌现/命中: %q(指纹 %v)", turn.Primary, sig.Keys()) + log.Printf("[agent] scene emerged/hit: %q (fingerprint %v)", turn.Primary, sig.Keys()) } else if len(turn.DeclaredCreated) > 0 { - log.Printf("[agent] 声明场景成立: %v(指纹 %v)", turn.DeclaredCreated, sig.Keys()) + log.Printf("[agent] declared scene confirmed: %v (fingerprint %v)", turn.DeclaredCreated, sig.Keys()) } if f != nil { f.turnScene = turn diff --git a/internal/agent/core/offload.go b/internal/agent/core/offload.go index 681d4479..b582eb95 100644 --- a/internal/agent/core/offload.go +++ b/internal/agent/core/offload.go @@ -180,7 +180,7 @@ func (a *Agent) offloadPendingTasks(opts OffloadOptions) int { // 拉不起来就把任务**放回队列**,绝不能丢:丢一条输入比多处理一条更糟 // (与 routeInputByOwner 的兜底同一条理由)。 a.sched.requeueFront(cands) - log.Printf("[offload] 无法为 %d 条积压任务准备驻留子,已放回队列: %v", len(cands), err) + log.Printf("[offload] cannot prepare resident child for %d backlogged task(s), returned to queue: %v", len(cands), err) return 0 } @@ -196,7 +196,7 @@ func (a *Agent) offloadPendingTasks(opts OffloadOptions) int { if err := a.forwardInputToResident(residentID, c.Event); err != nil { // 某条投不进去:放回原队列,其余继续(部分成功好过全部回滚)。 a.sched.requeueFront([]offloadCandidate{c}) - log.Printf("[offload] 转发任务给驻留子 %s 失败,已放回队列: %v", residentID, err) + log.Printf("[offload] forwarding task to resident child %s failed, returned to queue: %v", residentID, err) continue } moved++ @@ -213,7 +213,7 @@ func (a *Agent) offloadPendingTasks(opts OffloadOptions) int { {Event: a.syntheticEvent(offloadNotice(moved, residentID))}, }) - log.Printf("[offload] 主 agent 已忙 %s,把 %d 条积压任务转投给驻留子 %s(队列留 1 条说明)", + log.Printf("[offload] primary agent busy for %s, offloading %d backlogged task(s) to resident child %s (leaving 1 note in queue)", busyFor.Truncate(time.Second), moved, residentID) return moved } diff --git a/internal/agent/core/output.go b/internal/agent/core/output.go index 3dcdf629..17071a5c 100644 --- a/internal/agent/core/output.go +++ b/internal/agent/core/output.go @@ -274,7 +274,7 @@ func (a *Agent) sendOneOutput(channel string, item outputItem) (string, bool) { if sc := security.Global(); sc != nil { if filled, n, used := sc.Fill(args["payload"].(string)); n > 0 { args["payload"] = filled - log.Printf("[security] 通道 %s 载荷回填 %d 处(别名 %v)", channel, n, used) + log.Printf("[security] channel %s payload backfilled %d place(s) (aliases %v)", channel, n, used) } } result, err := dev.Execute("output", args) diff --git a/internal/agent/core/prompt_memory_test.go b/internal/agent/core/prompt_memory_test.go index cf3bb7e8..c3a35f5c 100644 --- a/internal/agent/core/prompt_memory_test.go +++ b/internal/agent/core/prompt_memory_test.go @@ -227,13 +227,20 @@ func TestSystemPromptHasNoImplementationJargon(t *testing.T) { // ⑦ 提示词长度上限(防止技术说明再次淤积)。 // -// 当前实测 1723 字(收敛前 2309)。上限取 2000:留有增删余地, -// 但堵住"又加一大段实现说明"这种回潮。 +// 历史:收敛前 2309 → 收敛后 1723 → 上限取 2000(留 277 字增删余地)。 +// +// 2026-10-25 上调到 2100:新增【加速序列】115 字(让模型知道何时该用 seq)。 +// 该块属于判据认可的**行为判据**——"模型会据此做什么不同的事"答得上来: +// 决定用序列还是逐个调工具。因此占用增删余地是正当的。 +// +// ⚠️ 调整门槛没有放松:**技术说明仍然不许占用这份额度**。 +// 下次若因"又加一大段实现说明"而想再调上限,应该改的是那段说明(下沉到 +// skill 或工具描述),不是这个数字。 func TestSystemPromptStaysCompact(t *testing.T) { a := newToolTestAgent(t) prompt := a.buildSystemPrompt("", "测试输入") - const maxRunes = 2000 + const maxRunes = 2100 if n := len([]rune(prompt)); n > maxRunes { t.Errorf("系统提示词 %d 字,超过上限 %d —— \n"+ "新加的内容先问:模型会据此做什么不同的事?答不上来就该进 skill 或工具描述", diff --git a/internal/agent/core/prompt_seq_test.go b/internal/agent/core/prompt_seq_test.go new file mode 100644 index 00000000..7ac8ded2 --- /dev/null +++ b/internal/agent/core/prompt_seq_test.go @@ -0,0 +1,29 @@ +package core + +import ( + "strings" + "testing" +) + +// 【加速序列】块必须在系统提示词里,且给出可判定的触发条件。 +// +// 为什么需要判据:这一块缺失时**没有任何报错**——模型只是不会用 seq, +// 表现为"工具在、但几乎没人调用",与 doc_query 当年的症状完全一样。 +func TestSystemPrompt_HasAccelerationSequenceBlock(t *testing.T) { + a := newPreemptAgent(t, newPreemptProvider()) + p := a.buildSystemPrompt("", "测试输入") + + if !strings.Contains(p, "【加速序列】") { + t.Fatal("系统提示词缺【加速序列】块 —— 模型不会知道何时该用 seq") + } + // 触发条件必须是**可判定的**,不能只说"有用时用"。 + for _, want := range []string{"多个对象", "顺序已知", "seq_list", "组内并行", "组间串行"} { + if !strings.Contains(p, want) { + t.Fatalf("【加速序列】块缺 %q(模型据此判断该不该用序列)", want) + } + } + // 必须指路到 skill,避免提示词里重复格式细节(两处真相)。 + if !strings.Contains(p, "seq-tools") { + t.Fatal("应指向 seq-tools skill,而不是在提示词里重复格式细节") + } +} diff --git a/internal/agent/core/resident.go b/internal/agent/core/resident.go index 675f2a3d..57266e94 100644 --- a/internal/agent/core/resident.go +++ b/internal/agent/core/resident.go @@ -406,7 +406,7 @@ func (a *Agent) applyResidualEvents(id string, events []*agentIO.InputEvent, pol for _, evt := range events { // 丢弃必须留痕:异步通道(qq/wechat)没有 ResponseCh, // 不记日志的话“这条消息为什么没人回”永远查不出来。 - log.Printf("[resident] %s 残余任务按父的决定丢弃:source=%s channel=%s request=%s content=%s", + log.Printf("[resident] %s residual task discarded per parent decision: source=%s channel=%s request=%s content=%s", id, evt.Source, evt.OutputChannel, evt.RequestID, truncateStr(inputTextOf(evt), 80)) a.emitSkippedReply(evt, "residual_dropped_by_parent") } diff --git a/internal/agent/core/resident_tools.go b/internal/agent/core/resident_tools.go index 1aadb4af..ddc5a78f 100644 --- a/internal/agent/core/resident_tools.go +++ b/internal/agent/core/resident_tools.go @@ -65,7 +65,7 @@ func (a *Agent) executeResidentAgents(tc agentAPI.ToolCall) string { // 静默报错会让线上表现为"工具能调但永远建不出来"(实测就是这样)。 if a.memory != nil && a.memory.Path() != "" { anchor = filepath.Dir(filepath.Dir(a.memory.Path())) - log.Printf("[resident] data_dir 未接线,回退到主图库目录: %s", anchor) + log.Printf("[resident] data_dir not wired, falling back to main graph dir: %s", anchor) } } if anchor == "" { diff --git a/internal/agent/core/scheduler.go b/internal/agent/core/scheduler.go index cb2b7467..73008817 100644 --- a/internal/agent/core/scheduler.go +++ b/internal/agent/core/scheduler.go @@ -836,6 +836,7 @@ func (s *scheduler) queueSnapshot() []*Task { // 2. 中断队列 L4→L1(同级 FIFO) // 3. 中断栈顶(与 2 比级别取高者;栈顶是排队任务时视为最低) // 4. 排队队列(FIFO) +// // nextRef 选出下一个要执行的任务。 // // ★ 本函数只做**编排**,不实现优先级规则: @@ -947,7 +948,6 @@ func removeTask(list []*Task, target *Task) []*Task { // preemptGrantedFor 报告运行任务是否应在当前安全点让位。 - // rearmPending 在**安全点重新求值**中断队列(设计 §4.3 / §5.2)。 // // 为什么必须有这一步:中断只在 registerInterrupt 里被武装一次,而那一刻运行任务 @@ -1313,8 +1313,8 @@ func (s *scheduler) waitSchedulerStopped(timeout time.Duration, name string) { } time.Sleep(10 * time.Millisecond) } - log.Printf("[resident] %s 调度器 %s 内未停止(仍在执行任务),"+ - "残余任务可能少回收一条(那条会由 Stop 补终态)", name, timeout) + log.Printf("[resident] %s did not stop within scheduler timeout %s (still executing tasks), "+ + "one residual task may be under-reclaimed (Stop will fill in its terminal state)", name, timeout) } // pumpInbox 把 channel 里**已就绪**的输入搬进就绪队列(非阻塞)。 diff --git a/internal/agent/core/spawn.go b/internal/agent/core/spawn.go index 321b10f2..d0e3c421 100644 --- a/internal/agent/core/spawn.go +++ b/internal/agent/core/spawn.go @@ -211,7 +211,7 @@ func (a *Agent) runChildTask(taskID, task string, parentChannel string, maxTurns // 子 Agent 的工具调用必须自记一行。此前子侧全程静默:父内核日志里 // 只有「started / done」两行,子任务失败时无法判断倒在哪一步(是搜不到、 // 解析失败,还是被轮上限截断)——只能看到一句「超时」。 - log.Printf("[child] %s turn %d/%d: %s → %d 字节", taskID, turn+1, maxTurns, ct.Name, len(result)) + log.Printf("[child] %s turn %d/%d: %s -> %d bytes", taskID, turn+1, maxTurns, ct.Name, len(result)) msgs = append(msgs, agentAPI.Message{Role: "assistant", Content: resp.Content, ToolCalls: []agentAPI.ToolCall{ct}}) msgs = append(msgs, agentAPI.Message{Role: "tool", ToolCallID: ct.ID, Content: result}) } @@ -228,7 +228,7 @@ func (a *Agent) runChildTask(taskID, task string, parentChannel string, maxTurns // 只能重 spawn(日志里 child_5→6→7 就是同一任务连试三次,每次都 // 重烧一整个上下文)。所以先做一次「不带工具的收尾调用」,把已有 // 成果落成文本。仅在失败路径上多花一次 LLM 调用。 - log.Printf("[child] %s 跑满预算 maxTurns=%d,尝试收尾提炼已有成果", taskID, maxTurns) + log.Printf("[child] %s exhausted budget maxTurns=%d, attempting wrap-up distillation of results so far", taskID, maxTurns) if summary := a.salvageChildSummary(msgs, maxTurns); summary != "" { finalResult = summary } else { @@ -325,7 +325,7 @@ func (a *Agent) salvageChildSummary(msgs []agentAPI.Message, maxTurns int) strin DisableThinking: !a.thinkingEnabled, }) if err != nil { - log.Printf("[child] 收尾提炼失败(回退为事实陈述): %v", err) + log.Printf("[child] wrap-up distillation failed (falling back to factual summary): %v", err) return "" } content := strings.TrimSpace(resp.Content) diff --git a/internal/agent/core/stages.go b/internal/agent/core/stages.go index c0720b69..7d5cb193 100644 --- a/internal/agent/core/stages.go +++ b/internal/agent/core/stages.go @@ -253,7 +253,7 @@ func (h *StageHost) ExecuteToolCtx(ctx context.Context, name string, args map[st // 而插件需要真凭据。回填是二者之间唯一的通道。 if s := security.Global(); s != nil && args != nil { if n, used := s.FillMap(args); n > 0 { - log.Printf("[security] 工具 %s 参数回填 %d 处(别名 %v)", name, n, used) + log.Printf("[security] tool %s backfilled %d argument(s) (aliases %v)", name, n, used) } } // 把 ctx 放进**执行栈**而不是 handler 签名:proc 桥在自己的 CallContext diff --git a/internal/agent/core/task.go b/internal/agent/core/task.go index 54fcfc42..54203f93 100644 --- a/internal/agent/core/task.go +++ b/internal/agent/core/task.go @@ -262,7 +262,7 @@ func (a *Agent) runTaskSteps(f *TaskFrame) stepOutcome { // f.Turn 只在 stepTurnEnd 递增,所以它等于「已完成的工具批数」; // 因此这里允许 maxToolTurns 批,而不会多跑第 maxToolTurns+1 轮。 if f.Step == StepLLM && a.maxToolTurns > 0 && f.Turn >= a.maxToolTurns { - log.Printf("[agent] 已达最大工具轮次 %d(turn=%d),强制收尾", a.maxToolTurns, f.Turn) + log.Printf("[agent] reached max tool turns %d (turn=%d), forcing wrap-up", a.maxToolTurns, f.Turn) if f.Resp != nil && strings.TrimSpace(f.Resp.Content) != "" { f.Response = f.Resp.Content } else { diff --git a/internal/agent/core/toolcall.go b/internal/agent/core/toolcall.go index f0ea37b8..281dd82f 100644 --- a/internal/agent/core/toolcall.go +++ b/internal/agent/core/toolcall.go @@ -67,7 +67,7 @@ func (a *Agent) resolveToolTimeout(toolName string, args map[string]interface{}) if d, err := time.ParseDuration(declared); err == nil && d > 0 { base = d } else if err != nil { - log.Printf("[agent] tool %s 声明的 Timeout=%q 解析失败(%v),用默认 %v", + log.Printf("[agent] tool %s declares Timeout=%q which failed to parse (%v), using default %v", toolName, declared, err, defaultToolTimeout) } } @@ -77,7 +77,7 @@ func (a *Agent) resolveToolTimeout(toolName string, args map[string]interface{}) } } if base > maxToolTimeout { - log.Printf("[agent] tool %s 超时 %v 超过硬上限,夹到 %v", toolName, base, maxToolTimeout) + log.Printf("[agent] tool %s timeout %v exceeds hard cap, clamped to %v", toolName, base, maxToolTimeout) return maxToolTimeout } return base @@ -94,17 +94,17 @@ func (a *Agent) resolveToolTimeoutDeclared(toolName string) time.Duration { } d, err := time.ParseDuration(strings.TrimSpace(def.Timeout)) if err != nil { - log.Printf("[agent] tool %s 声明的 Timeout=%q 解析失败(%v),回退默认 %v", + log.Printf("[agent] tool %s declares Timeout=%q which failed to parse (%v), falling back to default %v", toolName, def.Timeout, err, defaultToolTimeout) return defaultToolTimeout } if d <= 0 { - log.Printf("[agent] tool %s 声明的 Timeout=%q 非正数,回退默认 %v", + log.Printf("[agent] tool %s declares Timeout=%q which is not positive, falling back to default %v", toolName, def.Timeout, defaultToolTimeout) return defaultToolTimeout } if d > maxToolTimeout { - log.Printf("[agent] tool %s 声明的 Timeout=%v 超过硬上限,夹到 %v", + log.Printf("[agent] tool %s declares Timeout=%v exceeding hard cap, clamped to %v", toolName, d, maxToolTimeout) return maxToolTimeout } @@ -187,7 +187,7 @@ func (a *Agent) executeToolCallOutcome(tc agentAPI.ToolCall, channel string, tur if clean, evts := st.Replace(out.Text, "tool_out:"+tc.Name, security.Patterns); len(evts) > 0 { out.Text = clean for _, e := range evts { - log.Printf("[security] 工具输出 %s 拦截 %s(模式 %s,%d 字节)", + log.Printf("[security] tool output %s blocked %s (pattern %s, %d bytes)", tc.Name, e.Alias, e.Kind, e.Len) } } @@ -234,7 +234,7 @@ func (a *Agent) executeToolCallOutcome(tc agentAPI.ToolCall, channel string, tur case <-time.After(toolTimeout): // 先 cancel 再返回:这一行就是"让插件停下来"的触发点。 cancel() - log.Printf("[agent] tool %s timed out after %v(已向执行方发送取消)", tc.Name, toolTimeout) + log.Printf("[agent] tool %s timed out after %v (cancel sent to executor)", tc.Name, toolTimeout) te := newToolError(ErrReasonTimeout, "", fmt.Sprintf("工具 %s 执行超时(%s)", tc.Name, fmtDuration(toolTimeout)), "该工具本次未在时限内返回。可改用更小的任务,或换用其他工具。") return toolOutcome{Text: te.Error(), Raw: te} @@ -898,7 +898,7 @@ func (a *Agent) knowledgeMediaRefs(refs []string) []knowledge.KnowledgeMediaRef for _, r := range refs { b, ok, err := a.memory.BlockByIDPrefix(r) if err != nil || !ok { - log.Printf("[knowledge] 媒体块引用 %s 无法唯一解析,跳过", r) + log.Printf("[knowledge] media block ref %s could not be resolved uniquely, skipping", r) continue } out = append(out, knowledge.KnowledgeMediaRef{ @@ -1213,7 +1213,7 @@ func (a *Agent) recallByBlocks(query string) string { MinScore: blockRecallMinScore, }, query) if abstain != nil { - log.Printf("[memory] abstain: 符号零命中,拒答「%s」", query) + log.Printf("[memory] abstain: zero symbol hits, refusing to answer %q", query) return abstain.Notice } if err != nil || len(hits) == 0 { @@ -1223,7 +1223,7 @@ func (a *Agent) recallByBlocks(query string) string { // 被取代的块不返回给模型,但**记一笔**:旧值被显式作废这件事 // 本身有信息(「旧号 4379 停用」解释了为什么现在打不通), // 而完全静默会让「记忆里为什么没有旧号」变成无解之谜。 - log.Printf("[memory] recall blocks: %d 条被时序仲裁剔除(被更新的值取代)", n) + log.Printf("[memory] recall blocks: %d removed by temporal arbitration (superseded by newer values)", n) } // 归因统计:融合到底救回了多少条 —— 这是判断它在起作用的关键读数 var bySymbol, byExact int @@ -1235,7 +1235,7 @@ func (a *Agent) recallByBlocks(query string) string { } } if byExact+bySymbol > 0 { - log.Printf("[memory] recall blocks: 符号路补位 %d 条(精确串 %d,词法 %d)", + log.Printf("[memory] recall blocks: symbol path backfilled %d (exact string %d, lexical %d)", byExact+bySymbol, byExact, bySymbol) } @@ -1357,7 +1357,7 @@ func (a *Agent) restoreDocEvents(d *document.Doc) int { blocks += len(held) } - log.Printf("[agent] doc→context: %s 逐条回填 %d 条原始事件(%s → %s),块 %d 个迁回 L1", + log.Printf("[agent] doc->context: %s backfilled %d raw events one by one (%s -> %s), %d blocks migrated back to L1", d.ID, n, d.Events[0].Timestamp.Format("01-02 15:04:05"), d.Events[len(d.Events)-1].Timestamp.Format("01-02 15:04:05"), blocks) diff --git a/internal/agent/core/tooldefs.go b/internal/agent/core/tooldefs.go index 6886db3e..65d92907 100644 --- a/internal/agent/core/tooldefs.go +++ b/internal/agent/core/tooldefs.go @@ -261,6 +261,19 @@ func (a *Agent) buildSystemPrompt(memContext string, userInput string) string { prompt += "\n\n" + a.indexer.BuildToolPrompt() } + // 【加速序列】—— 与 doc_query 同一类问题:能力存在但模型不知道何时该用。 + // + // seq 有 7 个工具、也有 skill(seq-tools),但 skill 索引只给一行摘要。 + // 生产观察:agent 极少使用序列加速,因为它不会主动想"这件事我做过、 + // 该编成序列"或"这套操作要在多个对象上重复,该并行"。 + // + // 这里只留**模型必须据此作决定**的判据(与其余内核块同一风格): + // 什么时候该想起它。格式细节、组语义、常见错误全在 skill 与工具描述里, + // 不在这里重复——重复就是两处真相。 + prompt = appendPromptBlock(prompt, "【加速序列】同一套操作在**多个对象**上重复、或几件**顺序已知**的事连做时,"+ + "先想编成序列用 `seq_run`(组内并行、组间串行);以前做过的先用 `seq_list` 找。"+ + "格式见 `seq-tools` skill。") + // 技能索引(方案B):轻量注入已加载技能列表,LLM 匹配到场景时 // 主动 skill_info 拉取全文按文档执行 if a.skillIndex != nil { diff --git a/internal/agent/io/channel.go b/internal/agent/io/channel.go index 18c3ceeb..9fbda994 100644 --- a/internal/agent/io/channel.go +++ b/internal/agent/io/channel.go @@ -415,8 +415,8 @@ func applyInjectOpts(payload map[string]interface{}, opts InjectOptions) { // 属于应当被看见的事件(见下方 log)。 for k, v := range opts.Payload { if IsReservedPayloadKey(k) { - log.Printf("[io] 注入 payload 试图覆盖内核保留键 %q(已忽略)——"+ - "该键由内核语义决定,插件不可改写", k) + log.Printf("[io] injected payload tried to override kernel-reserved key %q (ignored) -- "+ + "this key is decided by kernel semantics and cannot be rewritten by plugins", k) continue } payload[k] = v diff --git a/internal/config/registry.go b/internal/config/registry.go index d9772e6f..cd5f525d 100644 --- a/internal/config/registry.go +++ b/internal/config/registry.go @@ -538,7 +538,7 @@ func (r *ConfigRegistry) listPrefixLocked(prefix string) map[string]string { out[k] = plain } else { // 解不开就不给值:宁可缺失(可诊断)也不把密文当明文交出去。 - log.Printf("[config] listPrefix 解密 %s 失败,已跳过: %v", k, oerr) + log.Printf("[config] listPrefix decrypt %s failed, skipped: %v", k, oerr) } } } @@ -563,7 +563,7 @@ func (r *ConfigRegistry) Dump() map[string]interface{} { if plain, oerr := openFromStorage(k, v); oerr == nil { result[k] = plain } else { - log.Printf("[config] Dump 解密 %s 失败: %v", k, oerr) + log.Printf("[config] Dump decrypt %s failed: %v", k, oerr) } } } diff --git a/internal/config/secret.go b/internal/config/secret.go index 6a7f2b0c..c8e65f47 100644 --- a/internal/config/secret.go +++ b/internal/config/secret.go @@ -122,7 +122,7 @@ func sealForStorage(key, value string) (string, bool) { if sealed, err := c.Seal(value); err == nil { return sealed, true } else { - log.Printf("[config] 加密 %s 失败(将按明文写入,此项暴露风险): %v", key, err) + log.Printf("[config] encrypting %s failed (will be written as plaintext, this entry is exposed): %v", key, err) } } return value, false @@ -143,7 +143,7 @@ func openFromStorage(key, stored string) (string, error) { } plain, err := c.Open(stored) if err != nil { - log.Printf("[config] 解密 %s 失败: %v", key, err) + log.Printf("[config] decrypting %s failed: %v", key, err) return "", err } return plain, nil @@ -216,18 +216,18 @@ func (r *ConfigRegistry) EncryptExistingSecrets() (int, error) { for _, t := range todo { sealed, err := c.Seal(t.val) if err != nil { - log.Printf("[config] 迁移加密 %s.%s 失败: %v", t.table, t.key, err) + log.Printf("[config] migration-encrypt %s.%s failed: %v", t.table, t.key, err) continue } if _, err := r.db.Exec( "UPDATE "+t.table+" SET value = ? WHERE key = ?", sealed, t.key); err != nil { - log.Printf("[config] 迁移加密 %s.%s 写回失败: %v", t.table, t.key, err) + log.Printf("[config] migration-encrypt %s.%s write-back failed: %v", t.table, t.key, err) continue } n++ } if n > 0 { - log.Printf("[config] 已加密 %d 条存量敏感配置(落盘不再有明文)", n) + log.Printf("[config] encrypted %d existing sensitive config entries (no plaintext at rest)", n) } return n, nil } @@ -279,19 +279,19 @@ func (r *ConfigRegistry) RekeySecrets() (int, int, error) { if err != nil { // ★ 解不开不能静默跳:那是“密钥环缺一代”的信号,必须可见。 failed++ - log.Printf("[config] 重加密 %s.%s 失败(旧密钥不在环里?): %v", table, p.k, err) + log.Printf("[config] re-encrypt %s.%s failed (old key not in keyring?): %v", table, p.k, err) continue } if _, err := r.db.Exec("UPDATE "+table+" SET value = ? WHERE key = ?", fresh, p.k); err != nil { failed++ - log.Printf("[config] 重加密 %s.%s 写回失败: %v", table, p.k, err) + log.Printf("[config] re-encrypt %s.%s write-back failed: %v", table, p.k, err) continue } rekeyed++ } } if rekeyed > 0 || failed > 0 { - log.Printf("[config] 密钥轮换重加密:完成 %d 条,失败 %d 条", rekeyed, failed) + log.Printf("[config] key-rotation re-encrypt: %d succeeded, %d failed", rekeyed, failed) } return rekeyed, failed, nil } diff --git a/internal/devicebridge/client/bridge.go b/internal/devicebridge/client/bridge.go index eacc85e3..1caee4b5 100644 --- a/internal/devicebridge/client/bridge.go +++ b/internal/devicebridge/client/bridge.go @@ -540,11 +540,11 @@ func (b *Bridge) handleMessage(msg map[string]interface{}) { if reason == "" { reason = "bind rejected" } - log.Printf("[devicebridge] bind 被拒:%s(令牌不匹配或设备未授权)", reason) + log.Printf("[devicebridge] bind rejected: %s (token mismatch or device unauthorized)", reason) b.markUnbound(reason) return } - log.Printf("[devicebridge] bind 成功,设备已登记") + log.Printf("[devicebridge] bind succeeded, device registered") b.markBound() return } diff --git a/internal/kernelboot/kernelstate.go b/internal/kernelboot/kernelstate.go index 0fe435ce..5b3dc0e4 100644 --- a/internal/kernelboot/kernelstate.go +++ b/internal/kernelboot/kernelstate.go @@ -320,7 +320,7 @@ func (s *KernelState) CloseAll() error { func() { defer func() { if r := recover(); r != nil { - log.Printf("[kernelboot] 关闭函数 panic(已隔离): %v", r) + log.Printf("[kernelboot] shutdown function panicked (isolated): %v", r) } }() fn() diff --git a/internal/kernelboot/lifecycle.go b/internal/kernelboot/lifecycle.go index 5093e26a..ff87568d 100644 --- a/internal/kernelboot/lifecycle.go +++ b/internal/kernelboot/lifecycle.go @@ -78,13 +78,13 @@ func (s *KernelState) Quiesce() ([]string, error) { // 已暂停的按**反序**回滚(把它唤醒回去),别留半关状态 for j := len(paused) - 1; j >= 0; j-- { if r := s.resumeOne(qs, paused[j]); r != nil { - log.Printf("[quiesce] 回滚唤醒 %s 失败: %v", paused[j], r) + log.Printf("[quiesce] rollback wake %s failed: %v", paused[j], r) } } return nil, fmt.Errorf("暂停 %s 失败(已回滚 %d 个): %w", q.Name, len(paused), err) } paused = append(paused, q.Name) - log.Printf("[quiesce] 已暂停: %s", q.Name) + log.Printf("[quiesce] paused: %s", q.Name) } return paused, nil } @@ -99,12 +99,12 @@ func (s *KernelState) Resume(paused []string) error { var firstErr error for i := len(paused) - 1; i >= 0; i-- { if err := s.resumeOne(qs, paused[i]); err != nil { - log.Printf("[quiesce] 唤醒 %s 失败: %v", paused[i], err) + log.Printf("[quiesce] wake %s failed: %v", paused[i], err) if firstErr == nil { firstErr = fmt.Errorf("唤醒 %s: %w", paused[i], err) } } else { - log.Printf("[quiesce] 已唤醒: %s", paused[i]) + log.Printf("[quiesce] woken: %s", paused[i]) } } return firstErr diff --git a/internal/kernelboot/migrate.go b/internal/kernelboot/migrate.go index 5beff3e9..cb2995cc 100644 --- a/internal/kernelboot/migrate.go +++ b/internal/kernelboot/migrate.go @@ -117,7 +117,7 @@ func (s *KernelState) BeginMigration(pq PluginQuiescer) (*MigrateSession, error) // 停不下来 ⇒ 回滚已停的,别留半关 for j := len(sess.stopped) - 1; j >= 0; j-- { if r := pq.StartPluginRuntime(sess.stopped[j]); r != nil { - log.Printf("[migrate] 回滚启动插件 %s 失败: %v", sess.stopped[j], r) + log.Printf("[migrate] rollback start plugin %s failed: %v", sess.stopped[j], r) } } return nil, fmt.Errorf("运行态停止插件 %s 失败(已回滚 %d 个): %w", @@ -125,7 +125,7 @@ func (s *KernelState) BeginMigration(pq PluginQuiescer) (*MigrateSession, error) } sess.stopped = append(sess.stopped, name) } - log.Printf("[migrate] 已运行态停止 %d 个插件(不改持久状态,唤醒即恢复)", len(sess.stopped)) + log.Printf("[migrate] stopped %d plugin(s) at runtime (persistent state unchanged, restored on wake)", len(sess.stopped)) } // ② 逆序暂停内核写者(distiller/textmem 等后台定时写) @@ -134,10 +134,10 @@ func (s *KernelState) BeginMigration(pq PluginQuiescer) (*MigrateSession, error) // 失败 ⇒ 把插件拉回去,内核回到正常态 for j := len(sess.stopped) - 1; j >= 0; j-- { if r := pq.StartPluginRuntime(sess.stopped[j]); r != nil { - log.Printf("[migrate] 回滚启动插件 %s 失败: %v", sess.stopped[j], r) + log.Printf("[migrate] rollback start plugin %s failed: %v", sess.stopped[j], r) } } - log.Printf("[migrate] 伪关机失败,已回滚: %v", err) + log.Printf("[migrate] pseudo-shutdown failed, rolled back: %v", err) return nil, fmt.Errorf("伪关机失败: %w", err) } sess.paused = paused @@ -151,10 +151,10 @@ func (s *KernelState) BeginMigration(pq PluginQuiescer) (*MigrateSession, error) time.Sleep(50 * time.Millisecond) } if hasInflightTools() { - log.Printf("[migrate] warning: 仍有在途工具调用未完成(继续;迁移脚本 stop 服务后天然安全)") + log.Printf("[migrate] warning: tool calls still in flight (continuing; safe after migration script stops the service)") } - log.Printf("[migrate] 伪关机完成(插件 %d 个、内核写者 %d 个)", len(sess.stopped), len(paused)) + log.Printf("[migrate] pseudo-shutdown complete (%d plugins, %d kernel writers)", len(sess.stopped), len(paused)) return sess, nil } @@ -165,18 +165,18 @@ func (m *MigrateSession) End() { } // ① 先恢复内核写者(它们是被插件依赖的下层) if err := m.ks.Resume(m.paused); err != nil { - log.Printf("[migrate] ★ 唤醒内核写者失败(可能半醒): %v", err) + log.Printf("[migrate] * waking kernel writers failed (may be half-awake): %v", err) } // ② 再拉起插件(按**停止的逆序** = 原启动顺序) if m.plugins != nil { for i := len(m.stopped) - 1; i >= 0; i-- { name := m.stopped[i] if err := m.plugins.StartPluginRuntime(name); err != nil { - log.Printf("[migrate] ★ 拉起插件 %s 失败: %v", name, err) + log.Printf("[migrate] * starting plugin %s failed: %v", name, err) } else { - log.Printf("[migrate] 已拉起插件: %s", name) + log.Printf("[migrate] plugin started: %s", name) } } } - log.Printf("[migrate] 唤醒完成") + log.Printf("[migrate] wake complete") } diff --git a/internal/kernelmod/agent/mod.go b/internal/kernelmod/agent/mod.go index b2a8984b..b0385cbf 100644 --- a/internal/kernelmod/agent/mod.go +++ b/internal/kernelmod/agent/mod.go @@ -280,16 +280,16 @@ func loadPersonality(cfg *types.Config, cfgReg *config.ConfigRegistry) *agentPkg log.Printf("[homed] warning: load personality: %v", err) } if personality != nil && personality.Content != "" { - log.Printf("[homed] 人格来源=文件 %s(优先于配置项),%d 字节", personalPath, len(personality.Content)) + log.Printf("[homed] persona source=file %s (takes precedence over config), %d bytes", personalPath, len(personality.Content)) if hints := agentPkg.PersonaStaleHints(personality.Content); len(hints) > 0 { - log.Printf("[homed] warning: 人格文件含会腐坏的内容 %v — 建议迁到配置项 core.agent.personal_prompt"+ - "(默认模板不含版本号,被问版本时以运行时快照为准)", hints) + log.Printf("[homed] warning: persona file contains content that will rot %v -- migrate to config entry core.agent.personal_prompt "+ + "(the default template has no version number; when asked for the version, use the runtime snapshot)", hints) } } else if pv := cfgReg.GetString("core.agent.personal_prompt", config.DefaultPersonaPrompt); strings.TrimSpace(pv) != "" { personality = &agentPkg.Personality{Content: pv, Path: "(core.agent.personal_prompt)"} - log.Printf("[homed] 人格来源=配置项 core.agent.personal_prompt,%d 字节", len(pv)) + log.Printf("[homed] persona source=config entry core.agent.personal_prompt, %d bytes", len(pv)) } else { - log.Printf("[homed] 人格来源=无(配置项为空且无人格文件)") + log.Printf("[homed] persona source=none (config entry empty and no persona file)") } return personality } diff --git a/internal/kernelmod/knowledge/migrate_impl.go b/internal/kernelmod/knowledge/migrate_impl.go index 778bb87c..a87fc24c 100644 --- a/internal/kernelmod/knowledge/migrate_impl.go +++ b/internal/kernelmod/knowledge/migrate_impl.go @@ -18,7 +18,7 @@ func runKnowledgeMigration(root string) { ) items, err := knowledge.PlanMigration(root) if err != nil { - log.Printf("[homed] 知识库迁移扫描失败(跳过): %v", err) + log.Printf("[homed] knowledge base migration scan failed (skipping): %v", err) return } need, illegal := 0, 0 @@ -33,21 +33,21 @@ func runKnowledgeMigration(root string) { return } if illegal > 0 { - log.Printf("[homed] 知识库迁移:%d 条名称非法(含 .. / 点段 / 隐藏段),写入与删除均已拒绝,需人工处理", illegal) + log.Printf("[homed] knowledge base migration: %d illegal names (contain .. / dot segments / hidden segments), writes and deletes refused, manual handling required", illegal) } if need == 0 { return } - log.Printf("[homed] 知识库迁移:%d/%d 条目录名待规范化(例:%s → %s)", need, len(items), + log.Printf("[homed] knowledge base migration: %d/%d directory names need normalization (e.g. %s -> %s)", need, len(items), items[0].OldName, items[0].NewName) if !defaultApply { - log.Printf("[homed] 知识库迁移:当前为只报告模式。确认清单后执行:homed-kb-migrate -root %s -apply", root) + log.Printf("[homed] knowledge base migration: report-only mode. After reviewing the list run: homed-kb-migrate -root %s -apply", root) return } if need > maxRenamePerRun { - log.Printf("[homed] 知识库迁移:需改名 %d 条超过单次上限 %d,本次只处理前 %d 条", + log.Printf("[homed] knowledge base migration: %d renames exceed per-run limit %d, processing only the first %d", need, maxRenamePerRun, maxRenamePerRun) } applied, failed := knowledge.ApplyMigration(root, items, maxRenamePerRun) - log.Printf("[homed] 知识库迁移完成:成功 %d,失败 %d", applied, failed) + log.Printf("[homed] knowledge base migration complete: %d succeeded, %d failed", applied, failed) } diff --git a/internal/kernelmod/memory/mod.go b/internal/kernelmod/memory/mod.go index caac8357..2165f296 100644 --- a/internal/kernelmod/memory/mod.go +++ b/internal/kernelmod/memory/mod.go @@ -129,14 +129,14 @@ func initDistillSplitter(cfgReg *config.ConfigRegistry) pipeline.RecordSplitter } provider, err := generation.Open(providerName, generation.Config{Options: opts}) if err != nil { - log.Printf("[homed] warning: 蒸馏生成 provider %q 打开失败: %v(蒸馏回退 jieba 路;已注册: %s)", + log.Printf("[homed] warning: opening distillation provider %q failed: %v (distillation falls back to jieba; registered: %s)", providerName, err, strings.Join(generation.Names(), ", ")) return nil } if !provider.Info().SupportsJSONSchema { // 拆分器的零幻觉完全依赖 schema 约束。不支持 schema 的 provider // 会拿到自由文本,解析必炸 —— 与其让它运行时反复失败,不如现在就拒。 - log.Printf("[homed] warning: 蒸馏生成 provider %q 不支持 JSON Schema,拆分不可靠(蒸馏回退 jieba 路)", + log.Printf("[homed] warning: distillation provider %q does not support JSON Schema, splitting is unreliable (falling back to jieba)", providerName) provider.Close() return nil diff --git a/internal/kernelmod/multimodal/mod.go b/internal/kernelmod/multimodal/mod.go index c90b43ab..639700d8 100644 --- a/internal/kernelmod/multimodal/mod.go +++ b/internal/kernelmod/multimodal/mod.go @@ -38,12 +38,12 @@ func (Module) Init(ks *kernelboot.KernelState) error { provider, err := embedding.Open(mmProvider, embedding.Config{Options: opts}) if err != nil { mmErr = err.Error() - log.Printf("[homed] warning: 多模态向量 provider %q 打开失败: %v(多模态向量检索已禁用;已注册: %s)", + log.Printf("[homed] warning: opening multimodal embedding provider %q failed: %v (multimodal vector retrieval disabled; registered: %s)", mmProvider, err, strings.Join(embedding.Names(), ", ")) } else if adapted, err := vector.AdaptProvider(provider); err != nil { provider.Close() mmErr = err.Error() - log.Printf("[homed] warning: 多模态向量 provider %q 元数据不合法: %v(多模态向量检索已禁用)", mmProvider, err) + log.Printf("[homed] warning: multimodal embedding provider %q has invalid metadata: %v (multimodal vector retrieval disabled)", mmProvider, err) } else { multimodalSpace = adapted closeAdapted = provider.Close diff --git a/internal/kernelmod/onnx/mod.go b/internal/kernelmod/onnx/mod.go index 52b80ac6..ecf2a704 100644 --- a/internal/kernelmod/onnx/mod.go +++ b/internal/kernelmod/onnx/mod.go @@ -44,7 +44,7 @@ func (Module) Init(ks *kernelboot.KernelState) error { // core.agent.onnx_model_path 留空时内核会用**二进制内嵌**的 dep_parser.onnx // (解到 /nlp 后加载)。此前只打配置值,于是出现 // "initialized (model: )" —— 看着像 ONNX 没启用,实际内嵌模型一直在跑。 - log.Printf("[homed] dep parser ready (ONNX,模型: %s)", onnxParser.ModelPath()) + log.Printf("[homed] dep parser ready (ONNX, model: %s)", onnxParser.ModelPath()) return nil } diff --git a/internal/kernelmod/pluginhost/mod.go b/internal/kernelmod/pluginhost/mod.go index 80fe229e..f9a38c1e 100644 --- a/internal/kernelmod/pluginhost/mod.go +++ b/internal/kernelmod/pluginhost/mod.go @@ -72,16 +72,16 @@ func (Module) Init(ks *kernelboot.KernelState) error { // 这条日志的价值在于:新增外部插件若忘写 plugin.json,会出现在这里 // (那时"无 manifest ⇒ 只能走工厂 ⇒ 找不到 ⇒ 不加载",可据此发现)。 if len(noManifest) > 0 { - log.Printf("[pluginhost] %d 个目录无 plugin.json(内置插件属正常:它们由编译期工厂加载): %v", + log.Printf("[pluginhost] %d directories have no plugin.json (normal for built-in plugins, loaded by compile-time factories): %v", len(noManifest), noManifest) } - log.Printf("[pluginhost] 外部插件元数据缓存就绪:%d 个(目录 %s)", + log.Printf("[pluginhost] external plugin metadata cache ready: %d (dir %s)", catalog.Size(), dir) // ── ③ 按**三形态**分流(用户设计要求)── groups := classify(catalog) - log.Printf("[pluginhost] 形态分流:自动启动 %d / 声明了依赖 %d / "+ - "启用但不自动启动 %d / 持久禁用 %d", + log.Printf("[pluginhost] form routing: auto-start %d / declared deps %d / "+ + "enabled-but-not-auto-start %d / persistently disabled %d", len(groups.autoStart), len(groups.declared), len(groups.manual), len(groups.disabled)) @@ -119,7 +119,7 @@ func (Module) Init(ks *kernelboot.KernelState) error { // ★★ typed-nil 陷阱:`var nb *SyncBus; var i SyncState = nb` // ⇒ i **非 nil** ⇒ 方法里解引用空指针 panic(实测复现)。 // 所以这里必须显式判 `!= nil` 再赋给接口变量。 - log.Printf("[pluginhost] warning: SyncBus 为空 —— 非生产装配路径," + + log.Printf("[pluginhost] warning: SyncBus is nil -- non-production assembly path, " + "退化为立即加载(依赖未就绪的插件在 Start 时自行处理)") fallback := kernelboot.NewSyncBus() fallback.MarkKernelComplete() @@ -131,7 +131,7 @@ func (Module) Init(ks *kernelboot.KernelState) error { return ks.PluginRegistry.LoadOneDir(filepath.Join(dir, spec.Name), spec.Name) }) sched.SetLaunchErrorHandler(func(spec kernelboot.PluginSpec, err error) { - log.Printf("[pluginhost] warning: 拉起 %s 失败: %v", spec.Name, err) + log.Printf("[pluginhost] warning: failed to start %s: %v", spec.Name, err) }) ks.PluginScheduler = sched @@ -142,7 +142,7 @@ func (Module) Init(ks *kernelboot.KernelState) error { // · !AutoStart ⇒ 不进(§2.6.6) // 同时 Submit 会做拼错校验(声明的设施名内核从未注册 ⇒ 立即报错)。 for _, n := range sched.Submit(specs) { - log.Printf("[pluginhost] 插件已拉起(依赖已就绪,早于装配完成): %s", n) + log.Printf("[pluginhost] plugin started (dependencies ready, before assembly finished): %s", n) } // ── ⑤ 加载**内置插件**(编译期工厂,**没有 plugin.json**)── @@ -177,10 +177,10 @@ func (Module) Init(ks *kernelboot.KernelState) error { } loadOnce.Do(func() { if err := ks.PluginRegistry.LoadSkipping(dir, skip); err != nil { - log.Printf("[pluginhost] warning: 加载内置/未声明插件: %v", err) + log.Printf("[pluginhost] warning: loading built-in/undeclared plugin: %v", err) } ks.PluginsReady = true - log.Printf("[pluginhost] 内核完全初始化 → 已加载其余插件(含 %d 个内置)", + log.Printf("[pluginhost] kernel fully initialized -> loaded remaining plugins (%d built-in)", len(noManifest)) }) } @@ -193,8 +193,8 @@ func (Module) Init(ks *kernelboot.KernelState) error { } loadRest(bus) - log.Printf("[pluginhost] 本阶段:声明了依赖的 %d 个由调度器处理"+ - "(已就绪的已拉起),未声明的 %d 个 + 内置 %d 个等内核完全初始化", + log.Printf("[pluginhost] this stage: %d with declared deps handled by scheduler "+ + "(those already ready were started), %d undeclared + %d built-in wait for kernel full init", len(groups.declared), len(groups.autoStart)-len(groups.declared), len(noManifest)) return nil } diff --git a/internal/memory/block_recall.go b/internal/memory/block_recall.go index b1b9102c..a3eaff31 100644 --- a/internal/memory/block_recall.go +++ b/internal/memory/block_recall.go @@ -91,7 +91,7 @@ func (g *GraphDB) RecallBlocks(q BlockRecallQuery) ([]BlockHit, error) { if err != nil { // 中心读取失败不该让召回失败 —— 退回原始向量即可, // 只是少了个校正。记日志以便发现。 - log.Printf("[graph] recall blocks: 中心向量读取失败,退回原始向量: %v", err) + log.Printf("[graph] recall blocks: reading centroid failed, falling back to raw vectors: %v", err) } else if use { centroid = c } @@ -162,11 +162,11 @@ func (g *GraphDB) RecallBlocks(q BlockRecallQuery) ([]BlockHit, error) { } if skippedDim > 0 { - log.Printf("[graph] recall blocks: 跳过 %d 个维度不匹配的块(查询 %d 维,共扫 %d)", + log.Printf("[graph] recall blocks: skipped %d blocks with mismatched dimension (query %d dims, scanned %d)", skippedDim, dim, scanned) } if skippedFP > 0 { - log.Printf("[graph] recall blocks: 跳过 %d 个指纹不匹配的块(查询空间 %s,共扫 %d)", + log.Printf("[graph] recall blocks: skipped %d blocks with mismatched fingerprint (query space %s, scanned %d)", skippedFP, shortFP(q.Fingerprint), scanned) } diff --git a/internal/memory/centroid_store.go b/internal/memory/centroid_store.go index 521e49ea..e378b1ed 100644 --- a/internal/memory/centroid_store.go +++ b/internal/memory/centroid_store.go @@ -131,7 +131,7 @@ func (g *GraphDB) SaveCentroid(c *Centroid, fingerprint string) error { string(rawStats), meta.BuiltAt); err != nil { return fmt.Errorf("graph: save centroid: %w", err) } - log.Printf("[graph] 中心向量已存: fp=%s dim=%d 样本=%d 均值范数=%.4f anomalous=%v", + log.Printf("[graph] centroid stored: fp=%s dim=%d samples=%d mean_norm=%.4f anomalous=%v", shortFP(fingerprint), c.Dim, c.Count, stats.MeanLength, stats.Anomalous) return nil } @@ -178,7 +178,7 @@ func (g *GraphDB) LoadCentroid(fingerprint string, queryDim int) (*Centroid, boo if vecCount <= 0 { // 没有统计过任何向量:这不是一个可用的中心。 if curVectored > 0 { - log.Printf("[graph] 中心向量不可用: 建中心时样本数为 0,现库内有 %d 个带向量块", curVectored) + log.Printf("[graph] centroid unavailable: built from 0 samples, store now has %d vectored blocks", curVectored) } return nil, false, nil } @@ -187,7 +187,7 @@ func (g *GraphDB) LoadCentroid(fingerprint string, queryDim int) (*Centroid, boo delta = -delta } if delta > centroidRebuildRatio { - log.Printf("[graph] 中心向量已失效: 样本 %d → 库内 %d(变化 %.0f%% > %.0f%%)", + log.Printf("[graph] centroid invalidated: samples %d -> store %d (change %.0f%% > %.0f%%)", vecCount, curVectored, delta*100, centroidRebuildRatio*100) return nil, false, nil } diff --git a/internal/memory/cut.go b/internal/memory/cut.go index a183f8b1..2123a0a8 100644 --- a/internal/memory/cut.go +++ b/internal/memory/cut.go @@ -53,7 +53,7 @@ func GetJieba() *gojieba.Jieba { }() d := jiebaDictDir() if d == "" { - log.Printf("[jieba] 未找到词库目录(内嵌落盘失败且模块缓存也不存在),jieba disabled") + log.Printf("[jieba] dictionary directory not found (embedded extraction failed and no module cache), jieba disabled") return } jiebaInst = gojieba.NewJieba( @@ -75,7 +75,7 @@ func jiebaDictDir() string { if dir, err := materializeJiebaDict(); err == nil && dir != "" { return dir } - log.Printf("[jieba] 内嵌词库落盘失败,回退到模块缓存查找(内嵌失败通常意味着缓存目录不可写)") + log.Printf("[jieba] writing embedded dictionary failed, falling back to module cache lookup (embedded failure usually means cache dir is not writable)") // 回退:开发机上存在的模块缓存(仅作为兵底,不应依赖它)。 // diff --git a/internal/memory/edge_entity.go b/internal/memory/edge_entity.go index d6f29dbb..4ea59f30 100644 --- a/internal/memory/edge_entity.go +++ b/internal/memory/edge_entity.go @@ -212,7 +212,7 @@ func ensureRelationEdgeUniqueness(tx *sql.Tx) { return // 新表或已升格 } - log.Printf("[graph] 边表升格:重建 memory_block_edges(移除 UNIQUE 以支持同类边并存)") + log.Printf("[graph] edge table upgrade: rebuilding memory_block_edges (dropping UNIQUE to allow same-kind edges to coexist)") if _, err := tx.Exec(`CREATE TABLE memory_block_edges_new ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_kind TEXT NOT NULL, @@ -227,7 +227,7 @@ func ensureRelationEdgeUniqueness(tx *sql.Tx) { status TEXT DEFAULT '', merged_into TEXT DEFAULT '' )`); err != nil { - log.Printf("[graph] 边表升格失败(保留原约束): %v", err) + log.Printf("[graph] edge table upgrade failed (keeping original constraint): %v", err) return } // 搬数据(只搬旧表已有的 6 列,新列留默认值) @@ -235,21 +235,21 @@ func ensureRelationEdgeUniqueness(tx *sql.Tx) { (id, source_kind, source_id, target_kind, target_id, edge_type, created_at) SELECT id, source_kind, source_id, target_kind, target_id, edge_type, created_at FROM memory_block_edges`); err != nil { - log.Printf("[graph] 边表数据迁移失败,回滚: %v", err) + log.Printf("[graph] edge table data migration failed, rolling back: %v", err) if _, derr := tx.Exec(`DROP TABLE memory_block_edges_new`); derr != nil { - log.Printf("[graph] 边表升格:回滚临时表失败: %v", derr) + log.Printf("[graph] edge table upgrade: rollback of temp table failed: %v", derr) } return } if _, err := tx.Exec(`DROP TABLE memory_block_edges`); err != nil { - log.Printf("[graph] 边表删除失败,回滚: %v", err) + log.Printf("[graph] edge table drop failed, rolling back: %v", err) if _, derr := tx.Exec(`DROP TABLE memory_block_edges_new`); derr != nil { - log.Printf("[graph] 边表升格:回滚临时表失败: %v", derr) + log.Printf("[graph] edge table upgrade: rollback of temp table failed: %v", derr) } return } if _, err := tx.Exec(`ALTER TABLE memory_block_edges_new RENAME TO memory_block_edges`); err != nil { - log.Printf("[graph] 边表改名失败: %v", err) + log.Printf("[graph] edge table rename failed: %v", err) return } // 索引要重建(DROP TABLE 会带走) @@ -261,7 +261,7 @@ func ensureRelationEdgeUniqueness(tx *sql.Tx) { } { _, _ = tx.Exec(idx) } - log.Printf("[graph] 边表升格完成") + log.Printf("[graph] edge table upgrade complete") } // BFSBlocks 从 startID 出发,沿**关系边**展开 depth 层,返回全部可达节点。 diff --git a/internal/memory/graph.go b/internal/memory/graph.go index df5d5a0f..72b53126 100644 --- a/internal/memory/graph.go +++ b/internal/memory/graph.go @@ -1004,7 +1004,7 @@ func (g *GraphDB) recallSortedLocked(keywords []string, seedEntities []string, d }) } if len(result.Entities) >= maxFullRecallEntities { - log.Printf("[graph] full recall 命中实体上限 %d,可能有实体未纳入", maxFullRecallEntities) + log.Printf("[graph] full recall hit the entity cap %d, some entities may be excluded", maxFullRecallEntities) } relRows, err := g.db.Query( @@ -1354,7 +1354,7 @@ func (g *GraphDB) recallSortedLocked(keywords []string, seedEntities []string, d if len(result.Entities) > maxRecallEntities { dropped := len(result.Entities) - maxRecallEntities result.Entities = result.Entities[:maxRecallEntities] - log.Printf("[graph] recall: 截断 %d 个实体(全局上限 %d)", dropped, maxRecallEntities) + log.Printf("[graph] recall: truncated %d entities (global cap %d)", dropped, maxRecallEntities) } sortRecallRelations(result.Relations, mode) diff --git a/internal/memory/indexer.go b/internal/memory/indexer.go index 22eb7491..aed8b535 100644 --- a/internal/memory/indexer.go +++ b/internal/memory/indexer.go @@ -160,10 +160,10 @@ func (idx *Indexer) syncIfStale() bool { return false } if err := idx.Sync(); err != nil { - log.Printf("[indexer] 增量同步失败(沿用旧索引): %v", err) + log.Printf("[indexer] incremental sync failed (keeping old index): %v", err) return false } - log.Printf("[indexer] 实体数 %d→%d,已增量重建实体名向量索引", known, count) + log.Printf("[indexer] entity count %d -> %d, incrementally rebuilt entity-name vector index", known, count) return true } diff --git a/internal/memory/migrate_entities.go b/internal/memory/migrate_entities.go index bd592751..2badf35b 100644 --- a/internal/memory/migrate_entities.go +++ b/internal/memory/migrate_entities.go @@ -259,8 +259,8 @@ func (g *GraphDB) MigrateLegacyTextEntities(embed EntityEmbedder) (MigrateResult if err := tx.Commit(); err != nil { return res, err } - log.Printf("[graph] 存量实体迁移完成: 句子 %d,块 %d,边 %d"+ - "(跳过孤儿关系 %d,去重边 %d,无向量 %d)", + log.Printf("[graph] legacy entity migration complete: %d sentences, %d blocks, %d edges "+ + "(skipped orphan relations %d, deduped edges %d, no vector %d)", res.Sentences, res.Blocks, res.Edges, res.SkippedOrphan, res.DedupedEdges, res.SkippedNoVec) return res, nil @@ -389,7 +389,7 @@ func parseLegacyTime(s string) time.Time { return t.UTC() } } - log.Printf("[graph] 旧库时间 %q 格式无法识别,该块将落为当前时间(时序信息丢失)", s) + log.Printf("[graph] legacy timestamp %q format unrecognized, block will use current time (temporal ordering lost)", s) return time.Time{} } diff --git a/internal/memory/scene_migrate.go b/internal/memory/scene_migrate.go index 74f70364..13475002 100644 --- a/internal/memory/scene_migrate.go +++ b/internal/memory/scene_migrate.go @@ -242,12 +242,12 @@ func (g *GraphDB) MigrateSceneRefsToBlocks() (int, error) { // ★ 三类异常都要显式报出:「完整迁移」是判据的硬要求, // 静默处理会让 scene 在旧表退场后指向虚无,且没人知道。 if entSkipped > 0 || relSkipped > 0 || entDedup > 0 || relDedup > 0 { - log.Printf("[scene] 引用迁移:%d 条完成"+ - "|★ 无对应对象 entity %d / relation %d"+ - "|去重 entity %d / relation %d", + log.Printf("[scene] reference migration: %d done"+ + " | no matching object entity %d / relation %d"+ + " | deduped entity %d / relation %d", moved, entSkipped, relSkipped, entDedup, relDedup) } else { - log.Printf("[scene] 引用迁移:%d 条完成,无跳过无去重", moved) + log.Printf("[scene] reference migration: %d done, nothing skipped or deduped", moved) } return moved, nil } diff --git a/internal/plugin/builtin_skill.go b/internal/plugin/builtin_skill.go new file mode 100644 index 00000000..ae91c8b7 --- /dev/null +++ b/internal/plugin/builtin_skill.go @@ -0,0 +1,184 @@ +package plugin + +// 内置插件随包 SKILL 的**统一生命周期管理**。 +// +// ## 为什么放在内核(而不是让每个插件自己写) +// +// 第一版我让 seq 自己在 Start() 里写盘 + 发事件。那是错的,有两个硬伤: +// +// ① 每个内置插件都要重复实现一遍「释放 / 清理 / 幂等」, +// 而且**禁用插件后 skill 还留着** —— 模型看得到 skill,点进去工具却不存在 +// ② 内置插件是按需加载的:未 `plugin_start` 时它的工具本就不在, +// skill 却已经出现在索引里,等于对模型说谎 +// +// 正确的语义:**skill 是插件能力的一部分,生命周期必须与插件同生共死**。 +// 所以由内核在插件启停处统一切换,插件作者一行都不用写。 +// +// ## 挂载点 +// +// StartPlugin / loadOne 成功 → InstallBuiltinSkill +// StopPlugin / StopAll → RemoveBuiltinSkill +// 持久禁用(plugin_disable) → RemoveBuiltinSkill +// +// ## 来源 +// +// 源码树里约定 `<插件源码目录>/skill//SKILL.md`(与 SDK 示例 +// third_party/homeagent-sdk/example/editdoc/skill/office-tools 一致)。 +// 但内置插件是**编译期内置**,运行时没有源码树,所以 SKILL 正文必须 +// 编进二进制(见各插件包里的 embed),由插件通过 SkillProvider 暴露。 +// +// 现阶段用一个最小接口从插件实例上取:**插件只需实现一个方法**, +// 不需要自己碰文件系统、不需要发事件、不需要管清理。 + +import ( + "log" + "os" + "path/filepath" + "strings" + + "github.com/JianFeeeee/HologramAgent/internal/events" + sdk "github.com/JianFeeeee/HologramAgent/internal/sdk" +) + +// SkillProvider 是**可选**接口:内置插件实现它即可让自己的 SKILL 随启停出入。 +// +// 只需返回 SKILL 文档集合(名字 → SKILL.md 全文),其余全由内核处理: +// 写盘位置、幂等、事件通知、停用时清理。 +// +// 不实现该接口的插件不受任何影响(绝大多数插件没有 SKILL)。 +type SkillProvider interface { + // BuiltinSkills 返回本插件随包的 SKILL 文档。 + // + // map 的键是 skill 名(**必须**与 SKILL.md frontmatter 的 name 一致, + // 也用作目录名);值是 SKILL.md 全文(含 frontmatter)。 + BuiltinSkills() map[string]string +} + +// skillDirOf 返回某个内置 skill 在磁盘上的约定位置。 +// +// 与 skillmgr 的扫描口径对齐:它扫 /skills/*,每个目录里要有 SKILL.md。 +func (r *Registry) skillDirOf(skillName string) string { + r.mu.RLock() + dataDir := r.dataDir + r.mu.RUnlock() + if dataDir == "" { + return "" + } + return filepath.Join(dataDir, "skills", skillName) +} + +// InstallBuiltinSkill 把某个插件的随包 SKILL 释放到 /skills/ 并通知 skillmgr。 +// +// 幂等: +// - 目录已存在且 SKILL.md 非空 ⇒ 视为已安装,只补发一次事件(让 skillmgr 接管) +// - 内容为空/读取失败 ⇒ 重写 +// +// 失败只记日志、不返回错误:skill 装不上不该阻止插件启动(工具本身仍可用)。 +// 但必须**可见** —— 没有 skill 的插件模型基本不会去用,这是静默失效。 +func (r *Registry) InstallBuiltinSkill(pluginName string, p sdk.Plugin) { + provider, ok := p.(SkillProvider) + if !ok { + return // 绝大多数插件没有 SKILL,这是常态,不是错误 + } + skills := provider.BuiltinSkills() + if len(skills) == 0 { + return + } + + for name, body := range skills { + if strings.TrimSpace(body) == "" { + log.Printf("[plugin] %s: skill %q has empty body, skipping (an empty SKILL gives the model nothing to read)", pluginName, name) + continue + } + dir := r.skillDirOf(name) + if dir == "" { + log.Printf("[plugin] %s: data_dir not set, cannot install skill %q", pluginName, name) + continue + } + file := filepath.Join(dir, "SKILL.md") + + // 已存在且非空:保留(用户可能改过措辞)。但仍要发事件, + // 否则重启后 skillmgr 若没扫到,这条 skill 就永久消失。 + cur, readErr := os.ReadFile(file) + if readErr != nil || len(cur) == 0 { + if err := os.MkdirAll(dir, 0755); err != nil { + log.Printf("[plugin] %s: creating skill dir failed %s: %v", pluginName, dir, err) + continue + } + if err := os.WriteFile(file, []byte(body), 0644); err != nil { + log.Printf("[plugin] %s: writing skill failed %s: %v", pluginName, file, err) + continue + } + log.Printf("[plugin] %s: installed skill %q -> %s", pluginName, name, dir) + } + + r.announceSkill(pluginName, dir) + } +} + +// RemoveBuiltinSkill 在插件停用/禁用时移除其随包 SKILL。 +// +// 为什么必须移除:否则模型在 skill 索引里仍看得到它,点进去照做, +// 而工具已被停用 —— 模型会拿到一连串"工具不存在"的错误, +// 且它无从知道原因是"插件被关了"。 +func (r *Registry) RemoveBuiltinSkill(pluginName string, p sdk.Plugin) { + provider, ok := p.(SkillProvider) + if !ok { + return + } + for name := range provider.BuiltinSkills() { + dir := r.skillDirOf(name) + if dir == "" { + continue + } + if err := os.RemoveAll(dir); err != nil { + log.Printf("[plugin] %s: removing skill %q failed %s: %v", pluginName, name, dir, err) + continue + } + log.Printf("[plugin] %s: removed skill %q (plugin is stopped)", pluginName, name) + // 通知 skillmgr 从索引里摘掉(复用同一事件,负载带 removed 标记)。 + r.announceSkillRemoved(pluginName, dir) + } +} + +// announceSkill 发布 skill_detected,让 skillmgr **当场**接管。 +// +// 不能只依赖 skillmgr 的启动全扫:它启动早于 plugin-host 层, +// 首次部署/停止后重启插件时都扫不到(实测:skillmgr 在内核第 4 层装配后 +// 10s 完成扫描,此时插件才刚被加载)。 +func (r *Registry) announceSkill(pluginName, dir string) { + r.mu.RLock() + bus := r.evBus + r.mu.RUnlock() + if bus == nil { + return + } + bus.Publish(&events.Event{ + Type: events.EventSkillDetected, + Source: pluginName, + Payload: map[string]interface{}{ + "path": dir, + "from_plugin": pluginName, + "builtin": true, + }, + }) +} + +func (r *Registry) announceSkillRemoved(pluginName, dir string) { + r.mu.RLock() + bus := r.evBus + r.mu.RUnlock() + if bus == nil { + return + } + bus.Publish(&events.Event{ + Type: events.EventSkillDetected, + Source: pluginName, + Payload: map[string]interface{}{ + "path": dir, + "from_plugin": pluginName, + "builtin": true, + "removed": true, + }, + }) +} diff --git a/internal/plugin/builtin_skill_test.go b/internal/plugin/builtin_skill_test.go new file mode 100644 index 00000000..58b6a84f --- /dev/null +++ b/internal/plugin/builtin_skill_test.go @@ -0,0 +1,192 @@ +package plugin + +// 内置插件随包 SKILL 生命周期的判据。 +// +// ## 为什么这些判据必须存在 +// +// skill 没装上/没摘掉,**插件本身照常工作**(工具都在),只是模型的行为变了: +// · 缺 skill ⇒ 模型看不到能力摘要,几乎不会用("seq 很少被使用"的直接原因) +// · 停用后 skill 还在 ⇒ 模型看得到却调不到,拿到一串"工具不存在"且不知原因 +// +// 两种都是**静默失效**:没有任何报错,只能靠判据钉住。 + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/JianFeeeee/HologramAgent/internal/events" + sdk "github.com/JianFeeeee/HologramAgent/internal/sdk" +) + +// fakeSkillPlugin 是一个实现 SkillProvider 的最小插件替身。 +type fakeSkillPlugin struct { + name string + skills map[string]string +} + +func (f *fakeSkillPlugin) Name() string { return f.name } +func (f *fakeSkillPlugin) Start(*sdk.PluginSDK) error { return nil } +func (f *fakeSkillPlugin) Stop() error { return nil } +func (f *fakeSkillPlugin) BuiltinSkills() map[string]string { return f.skills } + +// plainPlugin 不实现 SkillProvider —— 绝大多数插件是这样,必须不受影响。 +type plainPlugin struct{ name string } + +func (p *plainPlugin) Name() string { return p.name } +func (p *plainPlugin) Start(*sdk.PluginSDK) error { return nil } +func (p *plainPlugin) Stop() error { return nil } + +// testDataDir 读回注入的 data_dir(同包测试可直接读私有字段)。 +func (r *Registry) testDataDir() string { + r.mu.RLock() + defer r.mu.RUnlock() + return r.dataDir +} + +func newTestRegistryForSkills(t *testing.T) *Registry { + t.Helper() + r := NewRegistry() + r.SetDataDir(t.TempDir()) + r.SetEventBus(events.NewBus()) + return r +} + +const sampleSkill = `--- +name: demo-tools +description: 演示用 skill。 +--- + +演示用 skill 的正文段落,模型会读到这一行作为摘要。 +` + +// ── 安装:写到约定位置(skillmgr 的扫描口径)── + +func TestBuiltinSkill_InstallsToConventionPath(t *testing.T) { + r := newTestRegistryForSkills(t) + dataDir := r.testDataDir() + + p := &fakeSkillPlugin{name: "demo", skills: map[string]string{"demo-tools": sampleSkill}} + r.InstallBuiltinSkill("demo", p) + + file := filepath.Join(dataDir, "skills", "demo-tools", "SKILL.md") + data, err := os.ReadFile(file) + if err != nil { + t.Fatalf("SKILL.md 未落到约定位置 %s: %v(skillmgr 扫不到它)", file, err) + } + if string(data) != sampleSkill { + t.Fatal("落盘内容与插件提供的不一致") + } +} + +// 不实现 SkillProvider 的插件必须被安静跳过(不能报错、不能创建目录)。 +func TestBuiltinSkill_NonProviderIsNoop(t *testing.T) { + r := newTestRegistryForSkills(t) + r.InstallBuiltinSkill("plain", &plainPlugin{name: "plain"}) + + skillsDir := filepath.Join(r.testDataDir(), "skills") + if entries, err := os.ReadDir(skillsDir); err == nil && len(entries) > 0 { + t.Fatalf("无 skill 的插件不应产生任何目录,实际 %d 项", len(entries)) + } +} + +// 幂等:重复安装不报错,且不覆盖用户已改过的内容。 +func TestBuiltinSkill_IdempotentNoOverwrite(t *testing.T) { + r := newTestRegistryForSkills(t) + dataDir := r.testDataDir() + p := &fakeSkillPlugin{name: "demo", skills: map[string]string{"demo-tools": sampleSkill}} + + r.InstallBuiltinSkill("demo", p) + file := filepath.Join(dataDir, "skills", "demo-tools", "SKILL.md") + const edited = "# 用户改过的内容\n" + if err := os.WriteFile(file, []byte(edited), 0644); err != nil { + t.Fatal(err) + } + + r.InstallBuiltinSkill("demo", p) + got, _ := os.ReadFile(file) + if string(got) != edited { + t.Fatalf("重复安装覆盖了用户内容:期望 %q,实际 %q", edited, string(got)) + } +} + +// 空正文必须跳过(空的 SKILL 会让模型读到无内容,等于装了个假 skill)。 +func TestBuiltinSkill_EmptyBodySkipped(t *testing.T) { + r := newTestRegistryForSkills(t) + r.InstallBuiltinSkill("demo", &fakeSkillPlugin{ + name: "demo", + skills: map[string]string{"empty": " \n "}, + }) + dir := filepath.Join(r.testDataDir(), "skills", "empty") + if _, err := os.Stat(dir); err == nil { + t.Fatal("空正文不应创建 skill 目录") + } +} + +// ── 卸载:停用时必须摘掉 ── + +func TestBuiltinSkill_RemoveOnStop(t *testing.T) { + r := newTestRegistryForSkills(t) + dataDir := r.testDataDir() + p := &fakeSkillPlugin{name: "demo", skills: map[string]string{"demo-tools": sampleSkill}} + + r.InstallBuiltinSkill("demo", p) + dir := filepath.Join(dataDir, "skills", "demo-tools") + if _, err := os.Stat(dir); err != nil { + t.Fatalf("前置:skill 应已安装: %v", err) + } + + r.RemoveBuiltinSkill("demo", p) + if _, err := os.Stat(dir); !os.IsNotExist(err) { + t.Fatalf("停用后 skill 目录必须移除(否则模型看得到却调不到工具): err=%v", err) + } +} + +// ── 事件:必须主动通告(启动全扫有时序空窗)── + +func TestBuiltinSkill_AnnouncesDetected(t *testing.T) { + r := newTestRegistryForSkills(t) + bus := events.NewBus() + r.SetEventBus(bus) + + var got []*events.Event + bus.Subscribe(events.EventSkillDetected, func(e *events.Event) { got = append(got, e) }) + + r.InstallBuiltinSkill("demo", &fakeSkillPlugin{ + name: "demo", + skills: map[string]string{"demo-tools": sampleSkill}, + }) + + if len(got) != 1 { + t.Fatalf("安装必须通告一次 skill_detected,实际 %d 次", len(got)) + } + if p, _ := got[0].Payload["path"].(string); !strings.Contains(p, "demo-tools") { + t.Fatalf("payload.path 应指向 skill 目录,实际 %q", p) + } + if got[0].Source != "demo" { + t.Fatalf("Source 应为插件名,实际 %q", got[0].Source) + } +} + +// 卸载时通告带 removed 标记:让 skillmgr 能从索引里摘掉。 +func TestBuiltinSkill_AnnouncesRemoved(t *testing.T) { + r := newTestRegistryForSkills(t) + bus := events.NewBus() + r.SetEventBus(bus) + + var got []*events.Event + bus.Subscribe(events.EventSkillDetected, func(e *events.Event) { got = append(got, e) }) + + p := &fakeSkillPlugin{name: "demo", skills: map[string]string{"demo-tools": sampleSkill}} + r.InstallBuiltinSkill("demo", p) + r.RemoveBuiltinSkill("demo", p) + + if len(got) != 2 { + t.Fatalf("安装+卸载应各通告一次,实际 %d 次", len(got)) + } + removed, _ := got[1].Payload["removed"].(bool) + if !removed { + t.Fatal("卸载通告必须带 removed=true(否则 skillmgr 分不清是新增还是移除)") + } +} diff --git a/internal/plugin/dynamic_proc.go b/internal/plugin/dynamic_proc.go index 986a02f0..9743d425 100644 --- a/internal/plugin/dynamic_proc.go +++ b/internal/plugin/dynamic_proc.go @@ -97,11 +97,11 @@ func (r *Registry) loadProc(dir, name string, config map[string]interface{}) (sd if mft := readManifest(dir); mft != nil { caps = mft.Capabilities if len(caps) > 0 { - log.Printf("[plugin] %s 声明能力: %v", name, caps) + log.Printf("[plugin] %s declares capabilities: %v", name, caps) } hsTimeout = mft.HandshakeTimeout(proc.DefaultHandshakeTimeout) if mft.HandshakeTimeoutSec > 0 { - log.Printf("[plugin] %s 声明建链超时 %s(内核默认 %s)", + log.Printf("[plugin] %s declares link timeout %s (kernel default %s)", name, hsTimeout, proc.DefaultHandshakeTimeout) } } @@ -128,10 +128,10 @@ func (r *Registry) ensureProcHost() (*proc.Host, error) { if bus := r.eventBusOrNil(); bus != nil { er := NewEventRing(host.EvtRing(), int(host.Evtfd().Fd()), bus) host.SetEvtSubscriber(er) - log.Printf("[plugin] 事件环已创建(Bus → EvtRing → eventfd)") + log.Printf("[plugin] event ring created (Bus -> EvtRing -> eventfd)") } - log.Printf("[plugin] 共享段已创建(全部子进程插件共用一块,%d KB)", host.ShmSize()/1024) + log.Printf("[plugin] shared segment created (shared by all child-process plugins, %d KB)", host.ShmSize()/1024) return host, nil } @@ -143,7 +143,7 @@ func (r *Registry) closeProcHost() { return } if err := r.procHost.Close(); err != nil { - log.Printf("[plugin] 关闭共享段: %v", err) + log.Printf("[plugin] closing shared segment: %v", err) } r.procHost = nil } @@ -162,7 +162,7 @@ func (r *Registry) closeProcHost() { // // 所以崩溃回调必须做三件事:摘注册面、喂健康计数、排一次重启。 func (r *Registry) onProcCrash(name string, err error) { - log.Printf("[plugin] 子进程插件 %s 异常退出: %v(homed 未受影响)", name, err) + log.Printf("[plugin] child-process plugin %s exited abnormally: %v (homed unaffected)", name, err) // 1) 摘掉工具/stage/通道。**必须先做**:从这一刻起模型就不该再看到这些工具, // 否则在重启完成前的窗口里每次调用都是确定的失败。 @@ -212,13 +212,13 @@ func (r *Registry) scheduleProcRestart(name string, cause error) { return // 内核正在关停,不再拉起 } if !r.AutoRestartEnabled(name) { - log.Printf("[plugin] %s 声明了不自动重启,保持缺席状态", name) + log.Printf("[plugin] %s declared no-auto-restart, staying absent", name) return } n := r.noteCrash(name) if n > procMaxRestarts { - log.Printf("[plugin] %s 在 %v 内崩溃 %d 次,停止自动重启(需人工介入)", + log.Printf("[plugin] %s crashed %d times within %v, stopping auto-restart (manual intervention required)", name, procCrashWindow, n) return } @@ -235,23 +235,23 @@ func (r *Registry) scheduleProcRestart(name string, cause error) { return } if r.isDisabled(name) { - log.Printf("[plugin] %s 已被禁用,取消自动重启", name) + log.Printf("[plugin] %s is disabled, cancelling auto-restart", name) return } r.mu.RLock() already := r.plugins[name] != nil r.mu.RUnlock() if already { - log.Printf("[plugin] %s 已被其他路径重新加载,取消自动重启", name) + log.Printf("[plugin] %s was reloaded via another path, cancelling auto-restart", name) return } - log.Printf("[plugin] 自动重启 %s(第 %d 次,退避 %v,起因: %v)", name, n, delay, cause) + log.Printf("[plugin] auto-restarting %s (attempt %d, backoff %v, cause: %v)", name, n, delay, cause) if err := r.ReloadOne(name); err != nil { - log.Printf("[plugin] %s 自动重启失败: %v", name, err) + log.Printf("[plugin] %s auto-restart failed: %v", name, err) return } - log.Printf("[plugin] %s 自动重启成功", name) + log.Printf("[plugin] %s auto-restart succeeded", name) } // noteCrash 记录一次崩溃并返回窗口内的累计次数。 diff --git a/internal/plugin/proc/corehandler.go b/internal/plugin/proc/corehandler.go index 6f6314a9..867b3f24 100644 --- a/internal/plugin/proc/corehandler.go +++ b/internal/plugin/proc/corehandler.go @@ -419,7 +419,7 @@ func (h *coreHandler) invokeCleanerText(scope, name, text string) (string, error if res.TextRef.Flags&sharedRefFlagExpand != 0 { defer func() { if err := arena.Free(h.owner, res.TextRef); err != nil { - log.Printf("[proc] %s %s Cleaner 扩容块归还失败(arena 将缓慢泄漏): %v", scope, name, err) + log.Printf("[proc] %s %s Cleaner expansion block return failed (arena will leak slowly): %v", scope, name, err) } }() } diff --git a/internal/plugin/proc/host.go b/internal/plugin/proc/host.go index a4aa80c0..e156504b 100644 --- a/internal/plugin/proc/host.go +++ b/internal/plugin/proc/host.go @@ -307,7 +307,7 @@ func (h *Host) endStage(coord *stageCoordinator) error { // · releaseIfLeaked —— 插件异常路径未 unlock 时,锁不会毒化下一轮 // · unbind —— 下一轮的 stage.lock 不得再打到本轮的旧锁上 if h.locks.releaseIfLeaked(coord.lock) { - log.Printf("[proc] stage 协调器结束时清理了泄漏的 stage 锁") + log.Printf("[proc] stage coordinator cleaned up a leaked stage lock at exit") } h.locks.unbind(coord.lock) return err @@ -384,7 +384,7 @@ func (c *stageCoordinator) finish(sc *pubsdk.StageContext, written bool) error { return fmt.Errorf("回读共享段: %w", rErr) } if reclaimed := c.seg.Compact(); reclaimed > 0 { - log.Printf("[proc] stage 结束,arena 压实回收 %d 字节", reclaimed) + log.Printf("[proc] stage ended, arena compaction reclaimed %d bytes", reclaimed) } return nil } diff --git a/internal/plugin/proc/plugin.go b/internal/plugin/proc/plugin.go index e83ba76b..b524374a 100644 --- a/internal/plugin/proc/plugin.go +++ b/internal/plugin/proc/plugin.go @@ -265,12 +265,12 @@ func (p *Plugin) Close() error { func (p *Plugin) handleExit(name string, err error) { if p.host != nil { if p.host.ForceReleaseLock(name) { - log.Printf("[proc] %s 退出,内核已释放其持有的 stage 锁", name) + log.Printf("[proc] %s exited, kernel released its stage locks", name) } // 回收该插件未归还的共享槽:崩溃的插件无法自己归还, // 不回收会让槽池慢慢耗尽,最终所有共享内存调用退化成内联 RPC。 if n := p.host.ReclaimOwner(p.ownerID); n > 0 { - log.Printf("[proc] %s 退出,回收 %d 个残留共享槽", name, n) + log.Printf("[proc] %s exited, reclaimed %d leftover shared slot(s)", name, n) } } // 内核主动停止(Stop/Close,含宽限期超时后的 Kill)不算崩溃: @@ -405,17 +405,17 @@ func (p *Plugin) noteCancelIgnored(callID uint64, grace time.Duration) { p.cancelIgnoredMu.Unlock() if n < cancelIgnoredLimit { - log.Printf("[proc] %s 取消后未停手(第 %d/%d 次,调用 %d):暂不重启,继续观察", + log.Printf("[proc] %s did not stop after cancel (%d/%d, call %d): not restarting yet, still observing", p.name, n, cancelIgnoredLimit, callID) return } - log.Printf("[proc] %s 连续 %d 次在取消后 %v 内未停手,判定处于崩溃/不稳定态 —— 主动重启", + log.Printf("[proc] %s failed to stop within %v after cancel %d times in a row, judged crashed/unstable -- restarting proactively", p.name, n, grace) // 走既有的崩溃重启链(含退避、限流、禁用手工介入检查): // 不在这里自己 Kill+Reload,否则会绕开那些保护。 if p.onCrash != nil { - p.onCrash(p.name, fmt.Errorf("取消后连续 %d 次无响应(最后调用 %d,宽限 %v)", + p.onCrash(p.name, fmt.Errorf("unresponsive %d times in a row after cancel (last call %d, grace %v)", n, callID, grace)) } } @@ -426,7 +426,7 @@ func (p *Plugin) noteCancelIgnored(callID uint64, grace time.Duration) { // 之前是 `_ = arena.Free(...)`,把原因直接丢弃,线上排查代价极高。 func (p *Plugin) freeExpand(what, name string, ref SharedRef) { if err := p.host.Arena().Free(p.ownerID, ref); err != nil { - log.Printf("[proc] %s %s %s 扩容块归还失败(arena 将缓慢泄漏): %v", + log.Printf("[proc] %s %s %s expansion block return failed (arena will leak slowly): %v", p.name, what, name, err) } } @@ -449,7 +449,7 @@ func (p *Plugin) invokeStage(ctx context.Context, stage string, seq uint64) erro } } if res.DirtyFields > 0 { - log.Printf("[proc] %s stage %s 改写了 %d 个字段", p.name, stage, res.DirtyFields) + log.Printf("[proc] %s stage %s modified %d field(s)", p.name, stage, res.DirtyFields) } return nil } diff --git a/internal/plugin/proc/process.go b/internal/plugin/proc/process.go index 8f354cb0..c917459a 100644 --- a/internal/plugin/proc/process.go +++ b/internal/plugin/proc/process.go @@ -307,7 +307,7 @@ func (p *Process) handshake(timeout time.Duration) error { return fmt.Errorf("proc: %s 主版本不匹配(插件 SDK %s,内核 %s)——请用配套 hmapdev 重编", p.name, res.SDKVersion, meta.Version) } - log.Printf("[proc] %s 已建链(pid=%d sdk=%s core=%s)", + log.Printf("[proc] %s link established (pid=%d sdk=%s core=%s)", p.name, p.PID(), res.SDKVersion, meta.Version) return nil } @@ -383,12 +383,12 @@ func (p *Process) readLoop() { // ⇒ 现在把**内容摘要**与**可能原因 + 修法**一起打出来。 // 限长是因为坏帧可能很大(插件打了长文本/二进制), // 截断到 160 字节足以认出"这是哪句日志"。 - log.Printf("[proc] %s 收到非法 JSON 帧(%d 字节): %v\n"+ - " ↑ 内容摘要: %s\n"+ - " 可能原因:插件往 **stdout** 打印了日志,而 stdout 是"+ - "JSON-RPC **协议通道**(stderr 才是日志通道)。\n"+ - " 修法:把插件里的 fmt.Printf/fmt.Println 改成 log.Printf"+ - "(脚手架模板 1.4.0 起已修正)。", + log.Printf("[proc] %s received malformed JSON frame (%d bytes): %v\n"+ + " ^ content excerpt: %s\n"+ + " likely cause: the plugin printed logs to **stdout**, but stdout is the "+ + "JSON-RPC **protocol channel** (stderr is the log channel).\n"+ + " fix: change fmt.Printf/fmt.Println in the plugin to log.Printf "+ + "(fixed in scaffolding template since 1.4.0).", p.name, len(line), err, summarizeFrame(line, 160)) continue } @@ -403,14 +403,14 @@ func (p *Process) readLoop() { var resp Response if err := json.Unmarshal(line, &resp); err != nil { - log.Printf("[proc] %s 应答解析失败: %v", p.name, err) + log.Printf("[proc] %s response parse failed: %v", p.name, err) continue } p.deliverResponse(&resp) } if err := scanner.Err(); err != nil { - log.Printf("[proc] %s 读取 stdout 出错: %v", p.name, err) + log.Printf("[proc] %s stdout read error: %v", p.name, err) } // stdout 关闭(EOF)通常意味着进程结束——2.5ms 内即可感知(实验 6)。 @@ -460,9 +460,9 @@ func (p *Process) markExited() { if p.waitErr != nil { e := fmt.Errorf("插件进程 %s 异常退出: %w", p.name, p.waitErr) p.exitErr.Store(&e) - log.Printf("[proc] %s 退出: %v", p.name, p.waitErr) + log.Printf("[proc] %s exited: %v", p.name, p.waitErr) } else { - log.Printf("[proc] %s 正常退出", p.name) + log.Printf("[proc] %s exited normally", p.name) } // 进程没了:所有在途调用都无法完成。按状态机收敛—— @@ -515,7 +515,7 @@ func (p *Process) markExited() { func (p *Process) serveRequest(line []byte) { var req Request if err := json.Unmarshal(line, &req); err != nil { - log.Printf("[proc] %s 请求解析失败: %v", p.name, err) + log.Printf("[proc] %s request parse failed: %v", p.name, err) return } @@ -538,7 +538,7 @@ func (p *Process) serveRequest(line []byte) { // ID==0 是通知,不回应答(§2.4 约束 B:post-and-forget) if req.ID == 0 { if err != nil { - log.Printf("[proc] %s 通知 %s 处理失败: %v", p.name, req.Method, err) + log.Printf("[proc] %s notification %s handling failed: %v", p.name, req.Method, err) } return } @@ -554,7 +554,7 @@ func (p *Process) serveRequest(line []byte) { } } if wErr := p.writeFrame(&resp); wErr != nil { - log.Printf("[proc] %s 回写应答失败: %v", p.name, wErr) + log.Printf("[proc] %s writing response failed: %v", p.name, wErr) } } @@ -661,7 +661,7 @@ func (p *Process) deliverResponse(resp *Response) { c, ok := p.inflight[resp.ID] if ok && c.State() != callPending && c.State() != callCanceled { p.mu.Unlock() - log.Printf("[proc] %s 丢弃重复应答 id=%d(该调用已处于 %s)", + log.Printf("[proc] %s dropping duplicate response id=%d (call already in state %s)", p.name, resp.ID, c.State()) return } @@ -671,7 +671,7 @@ func (p *Process) deliverResponse(resp *Response) { p.mu.Unlock() if !ok { - log.Printf("[proc] %s 丢弃未知 id=%d 的应答(不属于任何在途调用)", p.name, resp.ID) + log.Printf("[proc] %s dropping response with unknown id=%d (not owned by any in-flight call)", p.name, resp.ID) return } @@ -686,7 +686,7 @@ func (p *Process) deliverResponse(resp *Response) { } // 通道容量 1 且此处无读者,不写入(写了会阻塞 readLoop)。 default: - log.Printf("[proc] %s 丢弃无法归类的应答 id=%d(状态 %s)", + log.Printf("[proc] %s dropping unclassifiable response id=%d (state %s)", p.name, resp.ID, c.State()) } } @@ -802,11 +802,11 @@ func (p *Process) expireCanceled(c *call) { } // ① 回收块:refs 的 owner 是插件,但回收决策来自内核(见 ReclaimRefs)。 if n := p.reclaimRefs(c.takeBlocks()); n > 0 { - log.Printf("[proc] %s 调用 %d 取消后无响应:已强制回收 %d 个扩容块", p.name, c.id, n) + log.Printf("[proc] %s call %d unresponsive after cancel: forcibly reclaimed %d expansion block(s)", p.name, c.id, n) } // ② 上报:由上层判定稳定性并决定是否重启。 // 这里只负责"如实汇报事实",不做重启决策——proc 不知道插件生命周期策略。 - log.Printf("[proc] %s 调用 %d 在取消后 %v 内未停手(插件失去响应)", p.name, c.id, cancelGrace) + log.Printf("[proc] %s call %d did not stop within %v after cancel (plugin unresponsive)", p.name, c.id, cancelGrace) if p.onCancelIgnored != nil { p.onCancelIgnored(c.id, cancelGrace) } @@ -852,7 +852,7 @@ func (p *Process) cancelCall(id uint64, cause error) { reason = cause.Error() } if err := p.Notify(MethodCallCancel, CallCancelParams{CallID: id, Reason: reason}); err != nil { - log.Printf("[proc] %s 发送调用取消失败(id=%d,可能已退出): %v", p.name, id, err) + log.Printf("[proc] %s failed to send call cancel (id=%d, likely already exited): %v", p.name, id, err) } } @@ -872,7 +872,7 @@ func (p *Process) Stop() error { if _, err := p.CallContext(ctx, MethodPluginStop, nil); err != nil { // 停止调用失败不影响后续 Kill——插件可能已经崩了 if !errors.Is(err, ErrProcessExited) { - log.Printf("[proc] %s plugin.stop 失败(将强制结束): %v", p.name, err) + log.Printf("[proc] %s plugin.stop failed (will force terminate): %v", p.name, err) } } @@ -880,7 +880,7 @@ func (p *Process) Stop() error { case <-p.exited: return nil case <-time.After(stopGracePeriod): - log.Printf("[proc] %s 宽限期内未退出,强制结束", p.name) + log.Printf("[proc] %s did not exit within grace period, force terminating", p.name) return p.Kill() } } @@ -919,7 +919,7 @@ func (p *Process) Kill() error { case <-time.After(killReapTimeout): // SIGKILL 后仍未收割:进程卡在不可中断的内核态(D 状态,如 NFS I/O)。 // 不能无限等,否则重载路径整体挂死;留日志供定位。 - log.Printf("[proc] %s SIGKILL 后 %v 仍未被收割(进程可能卡在内核态)", p.name, killReapTimeout) + log.Printf("[proc] %s still not reaped %v after SIGKILL (process may be stuck in kernel mode)", p.name, killReapTimeout) } // readerWG.Wait 必须有界:孙进程持有 stdout 写端时 readLoop 永不返回, // 无超时就是"任何插件泄漏一个孙进程都能拖死整个关停"。 @@ -934,7 +934,7 @@ func (p *Process) Kill() error { select { case <-readerDone: case <-time.After(killReapTimeout): - log.Printf("[proc] %s 的 stdout 读取未在 %v 内结束(孙进程可能仍持有写端),强制关闭读端", + log.Printf("[proc] %s stdout read did not finish within %v (grandchild process may still hold the write end), force-closing read end", p.name, killReapTimeout) if p.stdoutFile != nil { _ = p.stdoutFile.Close() @@ -944,7 +944,7 @@ func (p *Process) Kill() error { case <-time.After(killReapTimeout): // 极端情况:关管道也没能让它退出。不再等 —— 宁可让这次 // Stop 少等 2 秒,也不能把关停无限期挂住。 - log.Printf("[proc] %s 读端关闭后 readLoop 仍未退出,放弃等待", p.name) + log.Printf("[proc] %s readLoop still did not exit after read end closed, giving up", p.name) } } if killErr != nil && !errors.Is(killErr, os.ErrProcessDone) { diff --git a/internal/plugin/proc/shm_security.go b/internal/plugin/proc/shm_security.go index 9a9b2dc8..e4500ed3 100644 --- a/internal/plugin/proc/shm_security.go +++ b/internal/plugin/proc/shm_security.go @@ -74,7 +74,7 @@ func (h *Host) AuditShmMappings(expectedPlugins int) (ShmAudit, bool) { a.ActualMappings, ok = h.countOurShmMappings() a.Delta = a.ActualMappings - a.ExpectedPlugins if ok && a.Delta > 0 { - log.Printf("[proc] 共享内存映射审计:期望 %d 实测 %d(+delta=%d)—— 可能有泄漏句柄或未授权挂载", + log.Printf("[proc] shared memory mapping audit: expected %d actual %d (+delta=%d) -- possible leaked handles or unauthorized mappings", a.ExpectedPlugins, a.ActualMappings, a.Delta) } return a, ok diff --git a/internal/plugin/proc/stage.go b/internal/plugin/proc/stage.go index 56d51627..4d55892a 100644 --- a/internal/plugin/proc/stage.go +++ b/internal/plugin/proc/stage.go @@ -73,7 +73,7 @@ func (r *lockRegistry) releaseIfLeaked(l *stageLock) bool { return false } if l.ForceRelease(owner) { - log.Printf("[proc] stage 协调器结束时发现 %s 仍持有 stage 锁(异常路径未 unlock),已代为释放", owner) + log.Printf("[proc] stage coordinator found %s still holding stage lock at exit (abnormal path missed unlock), released on its behalf", owner) return true } return false @@ -130,7 +130,7 @@ func (h *coreHandler) runStage(stage string, sc *pubsdk.StageContext) error { } defer func() { if endErr := h.host.endStage(coord); endErr != nil { - log.Printf("[proc] %s stage %s 收尾失败: %v", h.name, stage, endErr) + log.Printf("[proc] %s stage %s finalization failed: %v", h.name, stage, endErr) } }() @@ -141,7 +141,7 @@ func (h *coreHandler) runStage(stage string, sc *pubsdk.StageContext) error { // 插件可能在持锁时失败(崩溃/超时)——强制释放,避免后续插件死锁。 // 这正是"锁仲裁回内核"的自愈价值(实验 9):无需 robust mutex。 if h.host.ForceReleaseLock(h.name) { - log.Printf("[proc] %s stage %s 失败后强制释放其持有的 stage 锁", h.name, stage) + log.Printf("[proc] %s stage %s failed, force-releasing its stage lock", h.name, stage) } return err } diff --git a/internal/plugin/proc/supervisor.go b/internal/plugin/proc/supervisor.go index baec52a1..2f2b501c 100644 --- a/internal/plugin/proc/supervisor.go +++ b/internal/plugin/proc/supervisor.go @@ -51,7 +51,7 @@ func (s *Supervisor) track(p *Process) { return } if old, ok := s.procs[p.name]; ok && old != p { - log.Printf("[proc] 台账中 %s 已有 pid=%d,被 pid=%d 覆盖", p.name, old.PID(), p.PID()) + log.Printf("[proc] ledger already has %s with pid=%d, overwritten by pid=%d", p.name, old.PID(), p.PID()) } s.procs[p.name] = p } @@ -124,7 +124,7 @@ func (s *Supervisor) StopAll(timeout time.Duration) { if len(procs) == 0 { return } - log.Printf("[proc] 关停 %d 个子进程插件", len(procs)) + log.Printf("[proc] shutting down %d child-process plugin(s)", len(procs)) var wg sync.WaitGroup for _, p := range procs { @@ -132,7 +132,7 @@ func (s *Supervisor) StopAll(timeout time.Duration) { go func(pr *Process) { defer wg.Done() if err := pr.Stop(); err != nil { - log.Printf("[proc] 停止 %s: %v", pr.Name(), err) + log.Printf("[proc] stopping %s: %v", pr.Name(), err) } }(p) } @@ -169,7 +169,7 @@ func (s *Supervisor) StopAll(timeout time.Duration) { } } if len(stuck) > 0 { - log.Printf("[proc] %v 内未优雅退出,强制结束: %v", timeout, stuck) + log.Printf("[proc] did not exit gracefully within %v, force terminating: %v", timeout, stuck) } killers.Wait() } diff --git a/internal/plugin/registry.go b/internal/plugin/registry.go index 3b612800..ce2a1471 100644 --- a/internal/plugin/registry.go +++ b/internal/plugin/registry.go @@ -817,7 +817,7 @@ func (r *Registry) loadOne(plgDir, name string) bool { // 注意这一条也挡住 loadPlugins 的启动扫描 —— 若插件在启动前就被 // 运行态关闭(理论上不会,但保持语义一致),它不该被拉起。 if r.isRuntimeStopped(name) { - log.Printf("[plugin] %s 处于运行态关闭(runtime stop),跳过加载", name) + log.Printf("[plugin] %s is runtime-stopped, skipping load", name) return false } @@ -885,6 +885,10 @@ func (r *Registry) loadOne(plgDir, name string) bool { } r.mu.Unlock() log.Printf("[plugin] loaded: %s", name) + + // 随包 SKILL 与插件同生:加载成功才让它出现在模型可见的 skill 索引里。 + // 放在 Start() 之后(工具已注册)—— 否则模型会看到 skill 却调不到工具。 + r.InstallBuiltinSkill(name, plg) return true } @@ -940,8 +944,8 @@ func (r *Registry) warnOutputOnlyChannels(plugin string) { r.channelsMu.Unlock() sort.Strings(only) for _, ch := range only { - log.Printf("[plugin] %s 只声明了出站通道 %q(未 RegisterInputChannel);"+ - "内核已兜底登记 inputch,若这是有意为之可忽略", plugin, ch) + log.Printf("[plugin] %s declared only outbound channel %q (no RegisterInputChannel); "+ + "kernel registered inputch as fallback, ignore if intentional", plugin, ch) } } @@ -1027,12 +1031,12 @@ func (r *Registry) detachPlugin(name string) { tc.UnregisterPluginTools(name) if sc, ok := tc.(PluginStageCleaner); ok { if n := sc.UnregisterPluginStages(name); n > 0 { - log.Printf("[plugin] %s: 摘除 %d 个 stage handler", name, n) + log.Printf("[plugin] %s: removed %d stage handler(s)", name, n) } } } if chans := r.releasePluginChannels(name); len(chans) > 0 { - log.Printf("[plugin] %s: 摘除 IO 通道 %v", name, chans) + log.Printf("[plugin] %s: removed IO channels %v", name, chans) } } @@ -1078,6 +1082,11 @@ func (r *Registry) StopAll() { } stoppers = append(stoppers, stopTarget{plugin: p, sdkRef: r.sdkRefs[p.Name()]}) } + // 关停时摘掉所有随包 skill:进程要退出,留下 skill 会让下次启动 + // 出现"索引里有、插件却没起"的错位(尤其插件被配置禁用时)。 + for _, st := range stoppers { + r.RemoveBuiltinSkill(st.plugin.Name(), st.plugin) + } r.plugins = make(map[string]sdk.Plugin) r.instances = nil r.pluginAutoRestart = make(map[string]bool) @@ -1564,6 +1573,10 @@ func (r *Registry) StopPlugin(name string) error { r.mu.Unlock() return fmt.Errorf("plugin %s 未在运行(已标记为运行态关闭)", name) } + // 先摘 skill 再停插件:停用后模型若仍看到 skill,会照做并拿到 + // 一连串"工具不存在",且无从知道原因是插件被关了。 + r.RemoveBuiltinSkill(name, p) + r.runStopHandlers(name) stopErr := p.Stop() delete(r.plugins, name) @@ -1583,9 +1596,9 @@ func (r *Registry) StopPlugin(name string) error { r.detachPlugin(name) if stopErr != nil { - log.Printf("[plugin] runtime stop %s(Stop 报错): %v", name, stopErr) + log.Printf("[plugin] runtime stop %s (Stop returned error): %v", name, stopErr) } - log.Printf("[plugin] runtime stopped: %s(不改持久状态)", name) + log.Printf("[plugin] runtime stopped: %s (persistent state unchanged)", name) return nil } @@ -1633,7 +1646,7 @@ func (r *Registry) StartPlugin(name string) error { plgDir := filepath.Join(r.pluginDirOrEmpty(), name) if r.loadOne(plgDir, name) { if wasStopped { - log.Printf("[plugin] runtime started: %s(已清运行态关闭)", name) + log.Printf("[plugin] runtime started: %s (runtime-stop flag cleared)", name) } else { log.Printf("[plugin] runtime started: %s", name) } @@ -1897,7 +1910,7 @@ func (r *Registry) tryDynamic(plgDir, name string, config map[string]interface{} return nil, err } if plg != nil { - log.Printf("[plugin] %s: 经 proc 通道加载(子进程)", name) + log.Printf("[plugin] %s: loaded via proc channel (child process)", name) return plg, nil } return nil, fmt.Errorf("plugin %s: entry 声明 %s 但未找到可用二进制", name, binEntry) diff --git a/internal/plugins/clawhubadapter/plugin.go b/internal/plugins/clawhubadapter/plugin.go index 47628cee..e5974ae1 100644 --- a/internal/plugins/clawhubadapter/plugin.go +++ b/internal/plugins/clawhubadapter/plugin.go @@ -168,8 +168,8 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { // 变成工具、也没交给 skillmgr,等于半死不活。 // 现在:失败即降级为「纯指令技能」移交 skillmgr,让模型仍可 // 按 SKILL 文档用命令跑它;日志改成说明而不是错误。 - log.Printf("[clawhubadapter] pysidecar %s 未就绪(%v);"+ - "按 CLI 型技能处理,移交 skillmgr", entry.Name(), err) + log.Printf("[clawhubadapter] pysidecar %s not ready (%v); "+ + "treating as CLI-type skill, handing off to skillmgr", entry.Name(), err) p.publishSkillDetected(s, skillPath) } case hasOCManifest || hasOCPackage: diff --git a/internal/plugins/kbtree/plugin.go b/internal/plugins/kbtree/plugin.go index cfa7272e..8c529984 100644 --- a/internal/plugins/kbtree/plugin.go +++ b/internal/plugins/kbtree/plugin.go @@ -131,16 +131,16 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error { } go func() { if err := p.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Printf("[kbtree] 服务错误: %v", err) + log.Printf("[kbtree] server error: %v", err) } }() if p.generatedToken { - log.Printf("[kbtree] 已启动 http://%s(token 未配置,本次随机生成;重启后失效)", p.addr) + log.Printf("[kbtree] started http://%s (no token configured, randomly generated for this run; invalidated on restart)", p.addr) } else { - log.Printf("[kbtree] 已启动 http://%s", p.addr) + log.Printf("[kbtree] started http://%s", p.addr) } - log.Printf("[kbtree] 外部 agent 可用:GET /tree、/categories、/counts、/search?q=&category=") + log.Printf("[kbtree] available to external agents: GET /tree, /categories, /counts, /search?q=&category=") return nil } diff --git a/internal/plugins/pluginmgr/plugin.go b/internal/plugins/pluginmgr/plugin.go index 6b93c569..c90170e1 100644 --- a/internal/plugins/pluginmgr/plugin.go +++ b/internal/plugins/pluginmgr/plugin.go @@ -789,7 +789,7 @@ func (p *Plugin) installFromData(data []byte, overwrite bool) (interface{}, erro runningRestored := false if wasLoaded && p.sdk != nil && p.sdk.PluginMgr() != nil { if err := p.sdk.PluginMgr().StartPlugin(pkg.Name); err != nil { - log.Printf("[pluginmgr] 升级后恢复运行 %s 失败: %v(插件已安装,可手动 plugin_start)", pkg.Name, err) + log.Printf("[pluginmgr] post-upgrade restart of %s failed: %v (plugin is installed, run plugin_start manually)", pkg.Name, err) } else { runningRestored = true } @@ -1136,7 +1136,7 @@ func parseLimitMB(v interface{}) int64 { } parsed, err := strconv.ParseInt(s, 10, 64) if err != nil { - log.Printf("[pluginmgr] max_package_mb=%q 不是整数,回退默认 %dMB", s, defaultMaxPackageMB) + log.Printf("[pluginmgr] max_package_mb=%q is not an integer, falling back to default %dMB", s, defaultMaxPackageMB) return int64(defaultMaxPackageMB) << 20 } mb = parsed @@ -1147,7 +1147,7 @@ func parseLimitMB(v interface{}) int64 { case float64: mb = int64(n) default: - log.Printf("[pluginmgr] max_package_mb 类型意外(%T),回退默认 %dMB", v, defaultMaxPackageMB) + log.Printf("[pluginmgr] max_package_mb has unexpected type (%T), falling back to default %dMB", v, defaultMaxPackageMB) return int64(defaultMaxPackageMB) << 20 } if mb < 0 { diff --git a/internal/plugins/pluginmgr/skill_handout.go b/internal/plugins/pluginmgr/skill_handout.go index 8bb9a36a..ff002a09 100644 --- a/internal/plugins/pluginmgr/skill_handout.go +++ b/internal/plugins/pluginmgr/skill_handout.go @@ -84,7 +84,7 @@ func (p *Plugin) handOutPluginSkills(pluginDir, pluginName string) (int, []strin // 会被 skillmgr 拒收(它的 loadOne 要求 SKILL.md/skill.json), // 于是每次安装都报一条无意义的失败。在源头过滤更干净。 if !looksLikeNativeSkill(path) { - log.Printf("[pluginmgr] %s: %s/%s 缺 SKILL.md 或 skill.json,跳过登记", + log.Printf("[pluginmgr] %s: %s/%s missing SKILL.md or skill.json, skipping registration", pluginName, SkillDirName, name) failed = append(failed, name+"(缺 SKILL.md 或 skill.json)") continue @@ -102,7 +102,7 @@ func (p *Plugin) handOutPluginSkills(pluginDir, pluginName string) (int, []strin }, Timestamp: time.Now().Unix(), }) - log.Printf("[pluginmgr] %s: 已移交 skill %s 给 skillmgr", pluginName, name) + log.Printf("[pluginmgr] %s: handed off skill %s to skillmgr", pluginName, name) n++ } return n, failed @@ -166,7 +166,7 @@ func (p *Plugin) reHandOutAllPluginSkills() { n += cnt } if n > 0 { - log.Printf("[pluginmgr] 启动重扫:已重新移交 %d 个随包 skill(否则重启后会静默丢失)", n) + log.Printf("[pluginmgr] startup rescan: re-handed off %d bundled skill(s) (otherwise they would be silently lost after restart)", n) } } diff --git a/internal/plugins/remotedevice/device.go b/internal/plugins/remotedevice/device.go index db65a8e9..ab02494b 100644 --- a/internal/plugins/remotedevice/device.go +++ b/internal/plugins/remotedevice/device.go @@ -792,8 +792,8 @@ const clipboardsueMaxBytes = 2 * 1024 * 1024 // 能回答:「有没有回传」「是不是合法图片」「多长」「两次是不是同一张」「会不会被截断」。 func logScreenseePayload(deviceID, dataURL string) { if dataURL == "" { - log.Printf("[remotedevice] screensee 回传 device=%s len=0 ★ 空内容:设备端没截到图"+ - "(锁屏/虚拟桌面/缩略图未就绪?)", deviceID) + log.Printf("[remotedevice] screensee returned device=%s len=0 * empty content: device captured no image "+ + "(lock screen / virtual desktop / thumbnail not ready?)", deviceID) return } raw := dataURL @@ -803,7 +803,7 @@ func logScreenseePayload(deviceID, dataURL string) { b, err := base64.StdEncoding.DecodeString(raw) if err != nil { // ★ 解不出来本身就是关键信号:说明回传的不是合法 base64(可能被截断)。 - log.Printf("[remotedevice] screensee 回传 device=%s len=%d ★ base64 解码失败: %v", + log.Printf("[remotedevice] screensee returned device=%s len=%d * base64 decode failed: %v", deviceID, len(raw), err) return } @@ -813,7 +813,7 @@ func logScreenseePayload(deviceID, dataURL string) { head = head[:24] } w, h := jpegSize(b) - log.Printf("[remotedevice] screensee 回传 device=%s bytes=%d magic=%s %dx%d b64head=%q sha=%x", + log.Printf("[remotedevice] screensee returned device=%s bytes=%d magic=%s %dx%d b64head=%q sha=%x", deviceID, len(b), magicName(b), w, h, head, sum[:8]) } diff --git a/internal/plugins/remotedevice/plugin.go b/internal/plugins/remotedevice/plugin.go index e80ca437..f4520d6d 100644 --- a/internal/plugins/remotedevice/plugin.go +++ b/internal/plugins/remotedevice/plugin.go @@ -390,7 +390,7 @@ func (p *Plugin) describeScreen(dataURL string, provider string) (string, error) defer cancel() resp, err := llm.Chat(ctx, req) if err != nil { - log.Printf("[remotedevice] screensee 视觉描述失败 source=%s err=%v", usedSource, err) + log.Printf("[remotedevice] screensee visual description failed source=%s err=%v", usedSource, err) // ★ 2026-10-05:模型名不被接受时,**就地降级到 AUTO 重试一次**。 // // 生产实测(13:51):agent 传 provider=visionllm,而那个源的 @@ -405,19 +405,19 @@ func (p *Plugin) describeScreen(dataURL string, provider string) (string, error) // ⇒ 显式模型名不可用时回 AUTO,比把错误抛给 agent 让它反复重试好。 // 注意**不覆盖原错误**:两者都返回,由上层看到完整信息。 if isModelNotAllowed(err) { - log.Printf("[remotedevice] screensee: 源 %s 的模型名不被接受,回退 AUTO 重试", usedSource) + log.Printf("[remotedevice] screensee: model name for source %s not accepted, retrying with AUTO", usedSource) if e2 := llm.SetSource("AUTO"); e2 == nil { rctx, rcancel := context.WithTimeout(context.Background(), 120*time.Second) defer rcancel() if rresp, e3 := llm.Chat(rctx, req); e3 == nil && rresp != nil && strings.TrimSpace(rresp.Content) != "" { - log.Printf("[remotedevice] screensee AUTO 回退成功 len=%d", len(rresp.Content)) + log.Printf("[remotedevice] screensee AUTO fallback succeeded len=%d", len(rresp.Content)) return rresp.Content, nil } else { - log.Printf("[remotedevice] screensee AUTO 回退也失败: %v", e3) + log.Printf("[remotedevice] screensee AUTO fallback also failed: %v", e3) } } else { - log.Printf("[remotedevice] screensee AUTO 回退无法切源: %v", e2) + log.Printf("[remotedevice] screensee AUTO fallback cannot switch source: %v", e2) } } // 保留原始错误文本(那是唯一诊断线索),但以 error 形态上抛。 @@ -427,7 +427,7 @@ func (p *Plugin) describeScreen(dataURL string, provider string) (string, error) // 空内容与「失败」对模型同样难以区分,不能静默当成结果。 return "", fmt.Errorf("屏幕截图视觉描述失败(视觉源 %s): 模型返回空内容", usedSource) } - log.Printf("[remotedevice] screensee 视觉描述完成 source=%s len=%d", usedSource, len(resp.Content)) + log.Printf("[remotedevice] screensee visual description done source=%s len=%d", usedSource, len(resp.Content)) return resp.Content, nil } diff --git a/internal/plugins/seq/plugin.go b/internal/plugins/seq/plugin.go index 86390b40..7f1753bd 100644 --- a/internal/plugins/seq/plugin.go +++ b/internal/plugins/seq/plugin.go @@ -155,7 +155,7 @@ func (p *Plugin) registerTools() { // ⚠️ 内置插件在 main() 的装配期加载,panic 会直接拖垮内核启动 // —— 而"某个工具没注册上"只该让该工具不可用,不该让整个 agent 起不来。 // (与 clawhubadapter / mcp 的处理一致:log 后继续。) - log.Printf("[seq] 注册工具 %s 失败: %v", def.Name, err) + log.Printf("[seq] registering tool %s failed: %v", def.Name, err) } } } diff --git a/internal/plugins/seq/skill.go b/internal/plugins/seq/skill.go new file mode 100644 index 00000000..d0497afb --- /dev/null +++ b/internal/plugins/seq/skill.go @@ -0,0 +1,94 @@ +package seq + +// seq 的随包 SKILL 文档。 +// +// ## 插件这边只做一件事:交出文档正文 +// +// 写盘、幂等、事件通知、停用时清理——全部由内核统一处理 +// (internal/plugin/builtin_skill.go,挂载在 loadOne / StopPlugin / StopAll)。 +// +// 第一版我让插件自己写盘 + 发事件,那是错的: +// · 每个内置插件都要重复实现一遍「释放/清理/幂等」 +// · 更要命的是**禁用插件后 skill 还留着** —— 模型看得到 skill, +// 点进去工具却不存在,只会拿到一连串"工具不存在"且无从知道原因 +// +// 正确语义:skill 是插件能力的一部分,生命周期必须与插件同生共死。 +// +// ## 为什么要有 skill(而不只靠 ToolDef.Description) +// +// skill 会被 skillmgr 汇总成索引注入系统提示词(buildSystemPrompt 的 skillIndex 段): +// 模型**每轮**都能看到"有这么个能力"的一行摘要。只写在 ToolDef.Description 里, +// 模型不主动翻工具列表就看不到——这正是此前 seq 几乎不被使用的原因 +// (与 doc_query 的历史教训同构:内核从没解释过 L1/L2/L3,agent 就只能靠猜)。 + +const builtinSkillName = "seq-tools" + +// BuiltinSkills 实现 plugin.SkillProvider —— 内核据此让 SKILL 随插件启停出入。 +func (p *Plugin) BuiltinSkills() map[string]string { + return map[string]string{ + builtinSkillName: seqSkillMarkdown, + } +} + +// seqSkillMarkdown 是 SKILL 正文。 +// +// frontmatter 的 name 必须与 map 的键(也就是目录名)一致。 +// +// ⚠️ 注意 description 的取值:内核的 extractDescription **跳过 frontmatter**, +// 取正文第一个非空非标题段落。所以正文首段必须能独立看懂—— +// 它才是注入系统提示词的那一行。 +const seqSkillMarkdown = `--- +name: seq-tools +description: 把反复出现的同类工具操作编成序列存起来,以后一次执行。 +--- + +把反复出现的同类工具操作编成「序列」存起来,以后一次执行:组内并行、组间串行,可带条件判断与变量槽引用。 + +## 什么时候用 + +**看到下面任一情形,先想一下要不要用序列:** + +- 你要连续做几件**顺序已知**的事(例:拉日志 → 过滤 → 汇总 → 发通知)。 +- 同样一套操作要在**多个对象**上重复(例:对 5 个仓库各跑一次同样的三步检查)。 +- 某套流程**以前做过**,这次还要再做一遍 —— 先用 ` + "`seq_list`" + ` 看有没有现成的,直接 ` + "`seq_run`" + `。 +- 一轮里要调的工具很多、参数很长 —— 编成序列可以写成文件(` + "`seq_create`" + ` 的 file 参数),避免参数被长度上限截断。 + +**不适合**:只有一两个工具、或后一步依赖前一步的**实际返回值**(那种要分多轮)。 + +## 核心概念 + +一条序列由若干 **group(组)** 组成: + +- **组内并行** —— 同一个 group 的 tools 同时执行 +- **组间串行** —— 按 groups 数组顺序,一组跑完再下一组 +- 每组有自己的签名:` + "`in`" + `(入参)/ ` + "`out`" + `(出参槽名) +- 组可以被 ` + "`seq_call`" + ` 按名单独调用 + +理解这两条决定了你怎么分组:**能并行的放同一组,要前一步结果的放后一组**。 + +## 工具 + +| 工具 | 用途 | +|---|---| +| ` + "`seq_help`" + ` | **写序列前先查这个** —— tools 字段的分隔符、in/out 写法、as 与 out 的关系都在这里 | +| ` + "`seq_create`" + ` | 创建/更新序列。groups 与 file 二选一 | +| ` + "`seq_list`" + ` | 列出全部序列及各组签名 | +| ` + "`seq_run`" + ` | 执行整条序列(按组顺序,每组先判 when) | +| ` + "`seq_call`" + ` | 按名调用单个 group。target 写组名,或 #序列名 跨序列调用 | +| ` + "`seq_when_call`" + ` | 条件按名调用:when 为真才执行,否则整次跳过(不产出槽) | +| ` + "`seq_delete`" + ` | 删除序列 | + +## 正确用法 + +1. **第一次编序列前先 ` + "`seq_help`" + `** —— 不要凭猜写 tools 字段,它的格式有坑 + (每个 tool 后都要有 ` + "`;`" + `,含最后一个;in 必须是对象,无入参写 ` + "`{}`" + `)。 +2. 用 ` + "`seq_list`" + ` 看有没有现成的复用,别重复造。 +3. 长序列用 ` + "`file`" + ` 参数从文件加载,比在参数里写一大坨 JSON 稳。 +4. 执行后看返回的**变量槽快照**,后续步骤用 ` + "`$槽名`" + ` 引用。 + +## 常见错误 + +- 把「后一步要用前一步返回值」的流程硬塞进一个 group —— 组内并行,拿不到彼此的槽。 + 跨组可以(组间串行),组内不行。 +- 不查 ` + "`seq_help`" + ` 就写 tools,分隔符或 ` + "`as`" + `/` + "`out`" + ` 对不上,静态校验会直接报错。 +` diff --git a/internal/plugins/seq/skill_test.go b/internal/plugins/seq/skill_test.go new file mode 100644 index 00000000..31b26a98 --- /dev/null +++ b/internal/plugins/seq/skill_test.go @@ -0,0 +1,26 @@ +package seq + +import ( + "testing" + + "github.com/JianFeeeee/HologramAgent/internal/plugin" +) + +// 编译期断言:seq 必须实现 plugin.SkillProvider, +// 否则内核的 InstallBuiltinSkill 会因类型断言失败而静默跳过。 +var _ plugin.SkillProvider = (*Plugin)(nil) + +func TestSkillProviderContract(t *testing.T) { + p := New("seq") + skills := p.BuiltinSkills() + if len(skills) == 0 { + t.Fatal("BuiltinSkills 返回空 —— model 永远看不到 seq") + } + body, ok := skills[builtinSkillName] + if !ok { + t.Fatalf("缺 %q 条目", builtinSkillName) + } + if len(body) < 200 { + t.Fatalf("SKILL 正文过短(%d 字节),不足以让模型知道怎么用", len(body)) + } +} diff --git a/internal/plugins/webui/handler.go b/internal/plugins/webui/handler.go index fe3d5e35..8f178abe 100644 --- a/internal/plugins/webui/handler.go +++ b/internal/plugins/webui/handler.go @@ -97,7 +97,7 @@ func init() { } { body, err := dashboardFS.ReadFile(a.file) if err != nil { - log.Printf("[webui] 读取前端资产 %s 失败: %v", a.file, err) + log.Printf("[webui] reading frontend asset %s failed: %v", a.file, err) continue } // 资产文件末尾的换行由占位符所在行自己的换行承担,避免多出空行。 diff --git a/internal/plugins/webui/handler_chat.go b/internal/plugins/webui/handler_chat.go index c120c481..fc6fcf93 100644 --- a/internal/plugins/webui/handler_chat.go +++ b/internal/plugins/webui/handler_chat.go @@ -376,7 +376,7 @@ func (h *Handler) flushChat() { h.chatMu.Unlock() if err := h.history.Save(msgs); err != nil { - log.Printf("[webui] 写聊天记录 %s 失败(稍后重试): %v", h.history.Path(), err) + log.Printf("[webui] writing chat history %s failed (will retry later): %v", h.history.Path(), err) h.chatMu.Lock() h.chatDirty = true if h.chatDirtySince.IsZero() { diff --git a/internal/plugins/webui/handler_device.go b/internal/plugins/webui/handler_device.go index 67516f00..6515ac18 100644 --- a/internal/plugins/webui/handler_device.go +++ b/internal/plugins/webui/handler_device.go @@ -125,7 +125,7 @@ func (h *Handler) reverseToUpstream(w http.ResponseWriter, r *http.Request, url, // (实测泄露 "http://127.0.0.1:9890/...: connect: connection refused")。 // 那对用户无用(内网地址他访问不了),却把内部拓扑说给了外人。 // 完整原因写日志,对外只给一句话。 - log.Printf("[webui] 反代上游不可达 url=%s err=%v", url, err) + log.Printf("[webui] reverse-proxy upstream unreachable url=%s err=%v", url, err) writeJSON(w, http.StatusBadGateway, map[string]string{ "error": "上游服务不可达", }) diff --git a/internal/plugins/webui/handler_upload.go b/internal/plugins/webui/handler_upload.go index 1c111bcd..bb15b354 100644 --- a/internal/plugins/webui/handler_upload.go +++ b/internal/plugins/webui/handler_upload.go @@ -125,10 +125,10 @@ func (h *Handler) handleChatFile(w http.ResponseWriter, r *http.Request) { var mediaBlocks []sdk.ContentBlock if attType == "image" || attType == "audio" { if sz > maxInlineMediaBytes { - log.Printf("[webui] %s %s 有 %s,超过 %s 内联上限,退回按路径处理", + log.Printf("[webui] %s %s has %s exceeding inline limit %s, falling back to path handling", attType, base, formatBytesGo(sz), formatBytesGo(maxInlineMediaBytes)) } else if raw, err := os.ReadFile(savePath); err != nil { - log.Printf("[webui] 读回上传的%s失败,退回按路径处理: %v", attType, err) + log.Printf("[webui] reading back uploaded %s failed, falling back to path handling: %v", attType, err) } else { dataURL := "data:" + ct + ";base64," + base64.StdEncoding.EncodeToString(raw) if attType == "image" { diff --git a/internal/plugins/webui/history.go b/internal/plugins/webui/history.go index ff036518..59efc549 100644 --- a/internal/plugins/webui/history.go +++ b/internal/plugins/webui/history.go @@ -46,13 +46,13 @@ func (hs *historyStore) Load() []ChatMsg { b, err := os.ReadFile(hs.path) if err != nil { if !os.IsNotExist(err) { - log.Printf("[webui] 读取聊天记录 %s 失败: %v", hs.path, err) + log.Printf("[webui] reading chat history %s failed: %v", hs.path, err) } return nil } var msgs []ChatMsg if err := json.Unmarshal(b, &msgs); err != nil { - log.Printf("[webui] 聊天记录 %s 解析失败(按空历史处理): %v", hs.path, err) + log.Printf("[webui] chat history %s parse failed (treating as empty): %v", hs.path, err) return nil } return msgs @@ -88,7 +88,7 @@ func (hs *historyStore) LoadWithMigration(settings sdk.SettingsAPI) []ChatMsg { if settings != nil { if v, err := settings.Get("chathistory"); err == nil && v != nil { if err := settings.Remove("chathistory"); err == nil { - log.Printf("[webui] 已清理历史遗留的配置项 chathistory(记录现存放于 %s)", hs.path) + log.Printf("[webui] removed legacy config entry chathistory (records now stored at %s)", hs.path) } } } @@ -114,17 +114,17 @@ func migrateLegacyHistory(settings sdk.SettingsAPI, hs *historyStore) []ChatMsg } var msgs []ChatMsg if err := json.Unmarshal([]byte(raw), &msgs); err != nil { - log.Printf("[webui] 历史配置项无法解析,保留原值不迁移: %v", err) + log.Printf("[webui] legacy config entry unparseable, keeping original and skipping migration: %v", err) return nil } if err := hs.Save(msgs); err != nil { - log.Printf("[webui] 聊天记录迁移到 %s 失败(保留原配置项): %v", hs.path, err) + log.Printf("[webui] migrating chat history to %s failed (keeping legacy config entry): %v", hs.path, err) return nil } if err := settings.Remove("chathistory"); err != nil { - log.Printf("[webui] 聊天记录已迁移到 %s,但旧配置项删除失败: %v", hs.path, err) + log.Printf("[webui] chat history migrated to %s, but deleting legacy config entry failed: %v", hs.path, err) } else { - log.Printf("[webui] 聊天记录已迁移到独立文件 %s(%d 条),并从插件配置表移除", hs.path, len(msgs)) + log.Printf("[webui] chat history migrated to standalone file %s (%d messages) and removed from plugin config", hs.path, len(msgs)) } return msgs } diff --git a/internal/plugins/webui/login_limiter.go b/internal/plugins/webui/login_limiter.go index eba59f27..bea403e8 100644 --- a/internal/plugins/webui/login_limiter.go +++ b/internal/plugins/webui/login_limiter.go @@ -283,11 +283,11 @@ func parseTrustedProxies(raw string) []string { } if strings.Contains(part, "/") { if _, _, err := net.ParseCIDR(part); err != nil { - log.Printf("[webui] trusted_proxies: 非法 CIDR %q 已忽略(%v)", part, err) + log.Printf("[webui] trusted_proxies: invalid CIDR %q ignored (%v)", part, err) continue } } else if net.ParseIP(part) == nil { - log.Printf("[webui] trusted_proxies: 非法 IP %q 已忽略", part) + log.Printf("[webui] trusted_proxies: invalid IP %q ignored", part) continue } out = append(out, part) diff --git a/internal/plugins/webui/proxy.go b/internal/plugins/webui/proxy.go index 71ee0b50..2a506c24 100644 --- a/internal/plugins/webui/proxy.go +++ b/internal/plugins/webui/proxy.go @@ -251,7 +251,7 @@ func buildProxyTable(decls []proxyDecl, manualText string, settings sdk.Settings prefix = r.Path } r.reverse = newReverseProxy(u, r.Auth, prefix) - log.Printf("[webui] 反代: %s.%s → %s (plugin=%s ws=%v auth=%s)", + log.Printf("[webui] reverse proxy: %s.%s -> %s (plugin=%s ws=%v auth=%s)", r.Host, base, r.Target, r.Plugin, r.WS, r.Auth) } @@ -342,7 +342,7 @@ func newReverseProxy(u *url.URL, auth, stripPrefix string) *httputil.ReverseProx }, FlushInterval: -1, // 立即 flush:SSE/长轮询逐帧下发 ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { - log.Printf("[webui] 反代 %s 失败: %v", r.Host, err) + log.Printf("[webui] reverse proxy %s failed: %v", r.Host, err) w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusBadGateway) fmt.Fprintf(w, `{"error":"上游不可达: %s"}`, strings.ReplaceAll(err.Error(), `"`, `'`)) @@ -782,7 +782,7 @@ func readPluginProxyDecls(pluginDir string) []proxyDecl { } entries, err := readDirNames(pluginDir) if err != nil { - log.Printf("[webui] 读取插件目录失败(反代自动发现跳过): %v", err) + log.Printf("[webui] reading plugin dir failed (reverse-proxy auto-discovery skipped): %v", err) return nil } var out []proxyDecl diff --git a/internal/sdk/memory_impl.go b/internal/sdk/memory_impl.go index d4670abb..218187b4 100644 --- a/internal/sdk/memory_impl.go +++ b/internal/sdk/memory_impl.go @@ -179,26 +179,26 @@ func (m *graphMemory) bindSentences(sentenceIDs map[string]string, triples []mem // 媒体块 ID 是内容派生的 blk_media_,故前缀足以唯一定位。 b, ok, err := m.db.BlockByIDPrefix(ref) if err != nil { - log.Printf("[sdk media] 插件 %s 查块 %s 失败: %v", m.plugin, ref, err) + log.Printf("[sdk media] plugin %s lookup block %s failed: %v", m.plugin, ref, err) continue } if !ok { - log.Printf("[sdk media] 插件 %s 的媒体引用 %s 无唯一对应块,跳过", m.plugin, ref) + log.Printf("[sdk media] plugin %s media ref %s has no unique match, skipping", m.plugin, ref) continue } if err := m.db.PutMemoryBlocks([]memory.MemoryBlock{b}); err != nil { - log.Printf("[sdk media] 插件 %s 写入 L3 记忆块失败: %v", m.plugin, err) + log.Printf("[sdk media] plugin %s L3 memory block write failed: %v", m.plugin, err) continue } if err := m.db.AddMemoryBlockEdge("block", sid, "block", b.ID, "contains"); err != nil { - log.Printf("[sdk media] 插件 %s 建立句子→块边失败: %v", m.plugin, err) + log.Printf("[sdk media] plugin %s sentence->block edge creation failed: %v", m.plugin, err) continue } bound++ } } if bound > 0 { - log.Printf("[sdk media] 插件 %s 的三元组写入 %d 个 L3 记忆块", m.plugin, bound) + log.Printf("[sdk media] plugin %s triples wrote %d L3 memory block(s)", m.plugin, bound) } } @@ -307,8 +307,8 @@ func (m *textMemoryImpl) Append(evt TextEvent) error { return nil } if len(evt.Attachments) > 0 { - log.Printf("[sdk media] 插件 %s 向文本记忆追加了 %d 份附件,已忽略:"+ - "文本层是字符串日志,不具备块存储;请改用文档/图记忆保存媒体", m.plugin, len(evt.Attachments)) + log.Printf("[sdk media] plugin %s appended %d attachment(s) to text memory, ignored: "+ + "the text layer is a string log with no block storage; use doc/graph memory for media", m.plugin, len(evt.Attachments)) } return m.tm.Append(text.Event{ Timestamp: evt.Timestamp, Source: evt.Role, Input: evt.Content, AgentID: evt.Channel, diff --git a/internal/security/embedded.go b/internal/security/embedded.go index 4c621e22..daf2ec31 100644 --- a/internal/security/embedded.go +++ b/internal/security/embedded.go @@ -75,11 +75,11 @@ func embeddedKey() []byte { } k, err := parseKey(embeddedKeyHex) if err != nil { - log.Printf("[security] ★ 内嵌主密钥格式非法(构建时 -ldflags 注入有误),已忽略: %v", err) + log.Printf("[security] * embedded master key has invalid format (bad -ldflags injection at build time), ignored: %v", err) return nil } if len(k) != 32 { - log.Printf("[security] ★ 内嵌主密钥长度 %d 非法(需 32 字节),已忽略", len(k)) + log.Printf("[security] * embedded master key has invalid length %d (need 32 bytes), ignored", len(k)) return nil } return k @@ -98,10 +98,10 @@ func loadEmbeddedKey(dataDir string) ([]byte, bool) { if b, err := os.ReadFile(diskPath); err == nil { if dk, err := parseKey(string(trimSpaceBytes(b))); err == nil { if !sameKey(dk, k) { - log.Printf("[security] ★★ 内嵌主密钥与 %s **不一致**:"+ - "该机器上存在两套互不可解的密文(内嵌钥能解一部分、磁盘钥能解另一部分)。"+ - "本次以**内嵌钥为当前钥**,磁盘钥请通过 security.keyring 的 history "+ - "或 HOMED_SECURITY_KEY 保留,否则旧密文会读不出来", diskPath) + log.Printf("[security] ** embedded master key does NOT match %s: "+ + "this machine holds two mutually undecryptable ciphertext sets (embedded key decrypts some, disk key the rest). "+ + "Using the **embedded key as current**, keep the disk key via security.keyring history "+ + "or HOMED_SECURITY_KEY, otherwise old ciphertext will become unreadable", diskPath) } } } diff --git a/internal/security/global.go b/internal/security/global.go index 4aa0dbee..49bc7874 100644 --- a/internal/security/global.go +++ b/internal/security/global.go @@ -79,7 +79,7 @@ func FillToolArgs(tool string, args map[string]interface{}) (int, []string) { } n, used := s.FillMap(args) if n > 0 { - log.Printf("[security] 工具 %s 参数回填 %d 处(别名 %v)", tool, n, used) + log.Printf("[security] tool %s backfilled %d argument(s) (aliases %v)", tool, n, used) } return n, used } @@ -95,7 +95,7 @@ func FillPayload(where, text string) string { } out, n, used := s.Fill(text) if n > 0 { - log.Printf("[security] %s 载荷回填 %d 处(别名 %v)", where, n, used) + log.Printf("[security] %s payload backfilled %d place(s) (aliases %v)", where, n, used) } return out } diff --git a/internal/security/harden.go b/internal/security/harden.go index b8ed5822..49d8320f 100644 --- a/internal/security/harden.go +++ b/internal/security/harden.go @@ -130,7 +130,7 @@ func HardenDataDir(dataDir string) HardenResult { if len(res.Errors) > 0 { // 响亮:权限没收紧意味着"以为受保护其实没有"。 for _, e := range res.Errors { - log.Printf("[security] ★★ 权限收紧失败(该项仍可能对同机其他用户可读): %s", e) + log.Printf("[security] ** permission hardening failed (this entry may still be readable by other local users): %s", e) } } return res diff --git a/internal/security/key.go b/internal/security/key.go index c06f6b21..2f109b71 100644 --- a/internal/security/key.go +++ b/internal/security/key.go @@ -177,8 +177,8 @@ func AdditionalDecryptKeys(dataDir string, current []byte) [][]byte { } if !sameKey(diskKey, current) { out = append(out, diskKey) - log.Printf("[security] 已把 %s 作为**额外解密钥**装入(内嵌钥为当前钥)——"+ - "旧密文仍可读;请勿删除该文件,直到确认全部密文已重加密", path) + log.Printf("[security] loaded %s as an **additional decrypt key** (embedded key is current) -- "+ + "old ciphertext still readable; do not delete this file until all ciphertext is confirmed re-encrypted", path) } return out } diff --git a/internal/security/keyring.go b/internal/security/keyring.go index ccf02ae2..a1a4c84a 100644 --- a/internal/security/keyring.go +++ b/internal/security/keyring.go @@ -52,8 +52,8 @@ func LoadKeyring(dataDir string, current []byte) ([][]byte, error) { // 为何不能按旧逻辑 Fatal:内嵌的目的就是防丢;一个"有 keyring 的老机器 // 升级到内嵌二进制就再也起不来"的行为,恰好造成它要防的那个后果。 if !sameKey(k, current) && EmbeddedKeyPresent() { - log.Printf("[security] 密钥环 current 与当前(内嵌)钥不同——"+ - "按**额外解密钥**保留,旧密文仍可读: %s", filepath.Join(dataDir, KeyringFileName)) + log.Printf("[security] keyring current differs from the current (embedded) key -- "+ + "kept as an **additional decrypt key**, old ciphertext still readable: %s", filepath.Join(dataDir, KeyringFileName)) return appendHistoryKeys(f.History, current, k) } if len(current) != 32 || !sameKey(k, current) {