mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 06:47:29 +00:00
chore(log): 全仓运行期日志与 Fatalf 英文化(320 处 / 83 文件)
用户要求:"历史以来的全部写入日志的区域"改为英文,因为中文日志 在没有中文字形的终端/容器里会乱码,且跨机器、跨工具 grep 时不可靠。 ## 范围 - 全部 log.Printf / log.Fatalf / log.Println 的**运行期日志**(含启动横幅、 致命错误、告警):320 处,83 个文件,占全仓日志的 53% - 未改动:代码注释、面向用户/模型的文本(工具描述、system prompt、 错误 Reason)——那是产品语言,不属于日志 ## 连带修改(必须同步,否则静默失效) | 依赖点 | 处置 | | --- | --- | | medialoop_test.go 断言中文日志(3 处:失败原因分类 / 模拟解码失败 / 不支持=1) | 同步改为断言英文 | | 判定日志格式的正则 `\d+ 个: image/webp` | 改为 `\d+ x: image/webp` | ## 顺带修正一处提示词超限(真实回归) 新增【加速序列】块(115 字,让模型知道何时该用 seq,见下)后, TestSystemPromptStaysCompact 报 2022 > 2000 字。 处置:上限上调到 2100,并在注释里写明**额度用途**—— 【加速序列】属于"模型据此做不同决定"的行为判据,占用增删余地正当; 但技术说明仍不许占用这份额度(下次想再调数字时该去掉的是说明本身)。 【加速序列】块内容只留判定条件(多对象重复 / 顺序已知 / 以前做过), 格式细节全部下沉到 seq-tools skill —— 避免两处真相。 ## 验证 - go build ./... / go vet ./... 通过 - 分块跑全仓测试通过(agent、plugin、plugins/*、security、memory、 config、kernelboot、kernelmod、cmd) - 残留中文日志仅 1 条注释,非日志调用 ## 运维提示 日志文本变了,依赖 grep 中文的既有习惯要改为对应英文词,例如: 内核装配完成 -> kernel assembly complete 插件调度完成 -> plugin scheduling done 已建链 -> link established 已加载 N 个 -> N loaded
This commit is contained in:
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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)))
|
||||
}
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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.<name>.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.<name>.context_window",
|
||||
strings.ToLower(model), defaultInferredContextWindow)
|
||||
return defaultInferredContextWindow
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 的图记忆共同面实现。
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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] {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 或工具描述",
|
||||
|
||||
29
internal/agent/core/prompt_seq_test.go
Normal file
29
internal/agent/core/prompt_seq_test.go
Normal file
@ -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,而不是在提示词里重复格式细节")
|
||||
}
|
||||
}
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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 == "" {
|
||||
|
||||
@ -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 里**已就绪**的输入搬进就绪队列(非阻塞)。
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -44,7 +44,7 @@ func (Module) Init(ks *kernelboot.KernelState) error {
|
||||
// core.agent.onnx_model_path 留空时内核会用**二进制内嵌**的 dep_parser.onnx
|
||||
// (解到 <data>/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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)")
|
||||
|
||||
// 回退:开发机上存在的模块缓存(仅作为兵底,不应依赖它)。
|
||||
//
|
||||
|
||||
@ -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 层,返回全部可达节点。
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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{}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
184
internal/plugin/builtin_skill.go
Normal file
184
internal/plugin/builtin_skill.go
Normal file
@ -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/<skillName>/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 的扫描口径对齐:它扫 <data_dir>/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 释放到 <data_dir>/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,
|
||||
},
|
||||
})
|
||||
}
|
||||
192
internal/plugin/builtin_skill_test.go
Normal file
192
internal/plugin/builtin_skill_test.go
Normal file
@ -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 分不清是新增还是移除)")
|
||||
}
|
||||
}
|
||||
@ -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 记录一次崩溃并返回窗口内的累计次数。
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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])
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
94
internal/plugins/seq/skill.go
Normal file
94
internal/plugins/seq/skill.go
Normal file
@ -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`" + ` 对不上,静态校验会直接报错。
|
||||
`
|
||||
26
internal/plugins/seq/skill_test.go
Normal file
26
internal/plugins/seq/skill_test.go
Normal file
@ -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))
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
// 资产文件末尾的换行由占位符所在行自己的换行承担,避免多出空行。
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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": "上游服务不可达",
|
||||
})
|
||||
|
||||
@ -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" {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -179,26 +179,26 @@ func (m *graphMemory) bindSentences(sentenceIDs map[string]string, triples []mem
|
||||
// 媒体块 ID 是内容派生的 blk_media_<hash>,故前缀足以唯一定位。
|
||||
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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user