|
|
4e70b8d5d8
|
refactor(module): 核心仓 Go module 路径改为 github.com/JianFeeeee/HologramAgent
批 1,不含 SDK 仓与目录改名。
- go.mod 第 1 行 module 路径
- 366 个 .go 文件的完整前缀 github.com/JianFeeeee/HomeAgent 机械替换
(857 处,仅完整前缀;展示名/注释里的 HomeAgent 字样留给批 5)
- 未改:third_party 目录名、SDK 自身 go.mod module、go.mod replace 目标、
homed/waiter 可执行名、.hmap 后缀
|
2026-10-21 12:17:19 +08:00 |
|
|
|
4e66e644da
|
test(security): 修 -race 下 TempDir 清理假红 —— 断言已过却被清理判失败
## CI 实证(run #86 / #87,Race detector 连续两次红)
失败信息自相矛盾:
--- FAIL: TestE2E_ResponseSecretNeverReachesContext (0.00s)
persist_leak_test.go:94: ✅ 回复侧明文未进 context;变量库 1 条
testing.go:1369: TempDir RemoveAll cleanup: unlinkat /tmp/TestE2E_.../001: directory not empty
断言全过、变量库也拦住了,唯一失败点是 `t.TempDir()` 的严格清理:RemoveAll
要 rmdir 那一刻,变量库还有一次落盘(saveLocked 的 tmp→rename)在途/刚落完,
于是判 "directory not empty"。本地不复现是因为机器快、时序不同;CI 在 -race
下慢一拍就撞上。
## 修法
`newScrubStore` 改为自建 MkdirTemp + 重试清理(50×20ms),最后尽力删除且
**不让清理失败污染判据**。安全判据本身没有放宽——只修测试基础设施。
## 验证
本地 `go test -race ./internal/agent/core/ ./cmd/waiter/ -count=1`:
两个包全绿(16.2s / 1.0s)。
|
2026-10-20 10:24:31 +08:00 |
|
|
|
8a2503796e
|
feat(persona): 说话方式增加「主动发表情包」——情绪到位别只干说话
配套 QQ 表情能力(qq_list_stickers / qq_send_sticker / qq_add_sticker):
人格里补一条表达习惯,让模型在有表情能力时**主动**用,而不是等用户点名要。
分寸也写进人格(一轮最多一两张、要真对得上情绪、没合适的不发),避免刷屏。
实例 personal.md 同步。
注:人格只写"表达习惯",不写工具名/调用方式——那些在 qq-tools 技能里。
|
2026-10-20 10:06:49 +08:00 |
|
|
|
679536692f
|
fix(pluginmgr): 升级后恢复运行状态 —— 重装不该把正在用的插件静默停掉
## 日志实证(2026-10-20 09:54)
重装 qq 插件后,agent 的工具清单里 qq_* 全消失,它只能:
terminal_list → get_plugin_tools ×2 → plugin_start(qq) → plugin_status
→ output_send__qq_help → get_plugin_tools → 才摸到 qq_list_stickers
根因:升级路径 `installFromData` 里 StopAndUnload 停掉旧实例后,**再也没有
把它拉回来**。安装的语义应当是"换个包",不是"把正在用的插件关掉"。
## 修法
- 升级前通过 ListPluginRuntimes 记录 `wasLoaded`
- 解压成功、skill 交接完成后,若升级前在跑则 StartPlugin 拉回
- 返回体暴露 `was_running` / `running_restored`,失败不阻断安装(插件已装好)
实测:`reinstalled qq, was_running=true, running_restored=true`,装完
loaded/alive 均为 true、PID 更新。
## 测试
- TestUpgradeRestoresRunningState:升级前在跑 ⇒ StartPlugin 恰好调用一次
- TestUpgradeDoesNotStartStoppedPlugin:升级前没跑 ⇒ 不得强行拉起
## 同批日志里的另外两条(非代码问题,仅记录)
1. llmsproxy 在 09:52:21 / 09:53:14 被重启,其间 homed 收到 connection refused,
经「stream 失败→非流式 + 2s 重试→换 provider」兜底,用户侧无失败但多耗一轮。
2. 上游返回 404「The model is not registered」与 429 限流(channels-codearts),
属 llmsproxy 侧渠道配置/配额问题,建议在 llmsproxy 里核对该模型注册。
|
2026-10-20 10:01:59 +08:00 |
|
|
|
276553727f
|
fix(persona): 默认人格补「一条消息内不空行分段」
与 SDK 仓的 QQ 通道收敛配套:提示词层先要求不空行(模型大概率照做),
通道层再确定性收敛(模型没照做时兜底)。实例 personal.md 已同步。
|
2026-10-20 09:44:29 +08:00 |
|
|
|
f33ee3e396
|
docs: 把「业务语义归插件、联动走事件总线」补进 OVERVIEW 与 PLUGIN_DEV
上一提交只在 ARCHITECTURE 里记了终端归属。本次把这条**通用原则**补到另外两份
文档,避免只读 OVERVIEW / 插件开发指南的人仍旧按旧直觉写代码。
## OVERVIEW(中英)
设计要点新增一条「业务语义留在插件层」:内核不定义业务对象(终端、命令历史、
设备状态、媒体库都是插件域)。以终端为例说明:PTY/生命周期/输出缓冲归
agentcli,WebUI/CLI 订阅总线自攒视图,内核只提供事件总线与注册面 —— 新增一类
能力不需要改内核,也不会在核心里堆出第二套业务真相。
## PLUGIN_DEV(中英)
PluginSDK 章节新增「与其它插件联动 — 走事件总线,不要依赖内核内部组件」:
- 代码示例:订阅 terminal_output + tool_call 自己攒视图
- 两条稳定事件表(谁发布、用途)
- ★ 外部插件只能订阅、不能发布(Publish 属 withheld 能力),所以「能力所有者
需要广播状态」的插件必须内置(agentcli 即此例)
- 指向 internal/pluginview 作为参考实现,并写明「视图不是权威、晚启动会缺早期
事件」
一个准确性修正:公开 SDK **没有** EventTerminalOutput 常量(属内核内部事件
类型),所以示例里外部插件用 sdk.EventType("terminal_output") 字符串订阅,
并注明内置插件可用 internal sdk 常量 —— 避免文档给出编不过的示例。
验证:四份文档代码围栏配对(74/74、0/0);仅文档改动,无代码影响。
|
2026-10-20 09:41:07 +08:00 |
|
|
|
3119d48625
|
refactor(terminal): 终端逻辑回归 agentcli,内核不再编码终端语义
按用户指正调整架构:终端是 agentcli 的职责,内核不该有终端逻辑;插件与
agentcli 的联动应走事件总线,而不是内核内部组件。
## 删掉内核里的终端语义
`internal/agent/core/terminal_registry.go`(280 行)整体删除。它做了两件不该
内核做的事:定义「什么是终端会话」、从 EventToolCall 的 Go map 文本里解析
terminal id / command 并归并会话与命令历史。连带移除:
- Agent 的 terminalReg 字段、构造、subscribeTerminalRegistry 订阅
- StatusAPI 上的 ListTerminals / CmdHistory
- sdk.TerminalAPI + PluginSDK.Terminal() + Registry.SetTerminalAPI 及接线
- 对应测试(迁到插件侧视图包)
保留 `sdk.TerminalStatus` / `CmdExecStatus`:它们只是线上 JSON 格式,WebUI/CLI
返回给前端时口径一致。
## 插件侧订阅事件总线自攒视图
新增 `internal/pluginview`(TerminalView):把原先内核那份归并逻辑原样搬过来,
由插件自己持有。数据源仍是两条总线事件:
EventTerminalOutput(agentcli 发)→ 会话输出 + 生命周期
EventToolCall (内核发) → cmd_run 历史、terminal_create/close 归并
WebUI 在 NewHandler 订阅、Close 退订;CLI 在 Start 订阅、Stop 退订。
两边共用同一实现,避免历史上「两份推导各踩一次 Go map 文本」的坑。
视图不是权威:真值在 agentcli,插件晚于会话启动会缺早期会话(注释写明)。
## agentcli 保持内置
它现在仍需要 `Publish`(进程内事件总线写入)——外部插件没有上行事件通道
(`EventPublish` 在 withheldCapabilities 里,proc 只有 events.subscribe)。
按判定标准留内置;将来要外置,须先开一条受控的插件→内核上行事件通道,
本次刻意不开(用户决定:先保持内置)。
## 文档
中英 ARCHITECTURE 新增「终端归属」小节:说明职责边界、两条事件、视图非权威、
以及 agentcli 为何仍内置;内置判定表里 cli/webui 去掉 TerminalAPI 一项。
## 验收(本机实例实测)
- 内核编译通过,删除后 `go build ./... && go vet ./...` 干净,`go test ./internal/...` 全绿
- 实例重启后:让 agent 开终端跑 `echo term-view-test` →
WebUI `GET /api/v1/terminals` 返回 term_1(output 正确、running=false)
CLI `/terminals`(unix socket)返回同一份;
两处都是从事件总线自攒,内核侧无终端代码参与
- cmd 历史分支由迁过来的单测覆盖(Go map 文本解析、生命周期、上限裁剪)
|
2026-10-20 09:33:08 +08:00 |
|
|
|
b9097986c8
|
fix(prompt): 拼装时空行翻倍 + 人格支持版本占位符 + 配置项说明对齐分层原则
## 1. 提示词里的空行翻倍(用户实测发现)
抓包看实际 payload,system 里出现 3 处连续 3~4 个换行:
…别包装成"稍等我看一下"。\n\n\n【人格设定】
…/mascot.webp 直接能看。\n\n\n\n【场景记忆】
…能否同轮发出。\n\n\n【输出规则】
根因:拼装统一用 `prompt += "\n\n" + block`,而块自己常以换行结尾
(人格块是 "【人格设定】\n%s\n",自定义 system_prompt 也常带尾部换行)。
空行不致命但推高 token,也让「提示词到底长什么样」难以肉眼核对。
修法:新增 appendPromptBlock(trim 块两端 + 只拼一个 "\n\n"),所有拼装点改用它。
重新抓包:连续 3+ 换行 0 处。
## 2. 人格块支持版本占位符
buildSystemPrompt 只对 systemPrompt 调 expandPromptVars,人格块没展开 ——
人格里写死版本号仍会随发版说谎(实测线上人格卡写 v1.0.3)。现在人格同样展开
{{kernel_version}}/{{kernel_commit}}/{{sdk_version}}。
## 3. 人格/系统提示词两个配置项的说明重写
- core.agent.personal_prompt:DisplayName 改为「人格设定(称呼/语气/说话方式/形象)」,
说明只写人设;工具用法写工具描述、多步流程写技能;支持占位符;文件优先。
- core.agent.system_prompt:DisplayName 改为「系统提示词(身份与行为规则)」,
说明只写跨工具/跨通道成立的内容;运行时通道投递/并行/时序语义由内核注入无需重复;
留在提示词里的工具用法会随插件外置而失真。
## 4. 【记忆清理指令】→【记忆维护】
具体选哪个 memory_* 工具、各自副作用早已写在工具描述里(【记忆清理】前缀),
提示词里那份逐条列举属于重复。只保留「必须真做、不能只描述计划」这条
不看工具描述也会犯的错,并要求用一句人话说结果。
## 5. 新增 opt-in 提示词抓包(排查用)
HOMED_PROMPT_DUMP=<path> 时把每轮实际发给 LLM 的 messages 落 jsonl(0600)。
环境变量而非常规配置项:内容是用户隐私,只该在诊断时临时开。
本次排查即由它拿到证据;诊断后已移除临时 drop-in。
验证:go test(agent core / config / sdk / healthcheck)全绿;
实例重启后抓包复核空行归零;对话口吻仍为「短、口语、不列清单」。
|
2026-10-20 09:16:07 +08:00 |
|
|
|
d35b7b7275
|
feat(status): ONNX 状态补 dep_parser 字段;人格改成「说人话」的说话方式
## 1. ONNX 状态补依存句法分析器(onnx.dep_parser)
此前 onnx.enabled 只反映**多模态向量空间**,解析器状态完全不可见 —— 于是
只能靠翻启动日志猜,而日志又曾把「内嵌模型已启用」错报成「规则引擎」
(见上一个提交)。现在健康快照直接给:
"dep_parser": { "enabled": true,
"model_path": "/home/newqqagent/nlp/dep_parser.onnx",
"embedded": true }
- sdk.ONNXStatus 增加 DepParser / DepParserStatus(enabled/model_path/embedded)
- agent 侧由 nlp 包级默认解析器反查实际状态(判据是「真能 Parse 且报得出路径」,
不是「配置里写了 onnx_model_path」)
- healthcheck_kernel 的描述同步说明该字段
## 2. 人格:从「交报告」改成「聊天」
用户反馈实例说话一股人机味。实测原文:
第一件·文件读写:
- 写:files_write 往 /tmp/... 写入 hello-files,回执 11 字节
- 读:files_read 读回来,内容就是 hello-files,对上了
问题不在语气词,而在**结构**:清单体 + 小标题 + 加粗 + 把工具名/字节数/耗时
当成绩单报出来。旧人格只禁了「客服腔四件套」,没禁这些。
DefaultPersonaPrompt 的说话方式一节重写,明确:
- 短句为主;先给结果/情绪,再补解释
- 不用清单体(“-”/“1.”/“•”)、不用小标题/加粗/表格
- 不报工单式细节(不罗列工具名、参数、字节数、耗时)——说人话
- 不用客服腔,拿不准/失败直说
- 只有用户明确要清单/步骤/数据时才结构化
## 3. 本机实例人格同步优化(personal.md,优先级高于配置项)
按同一套规则重写 /home/newqqagent/personal/personal.md,并保留「照大肥鱼那股
活人味来」与形象特征。重启后实测(问「在吗?状态咋样」):
在呢在呢 (`・ω・´)
刚拉了个运行时快照看了眼,状态挺好——内核 1.4.0dev 跑着,29 个插件全部 loaded……
刚忙完就赶紧来找我聊天,老大你这节奏也太充实了呗~
无清单、无小标题、无工具名报账。
验证:go test(sdk / agent core / healthcheck / config)全绿;
本机实例重启后 onnx.dep_parser 三字段正确返回。
|
2026-10-20 09:08:20 +08:00 |
|
|
|
48ca95f964
|
fix(onnx): 启动日志错报「走规则引擎」—— 实际一直用内嵌 ONNX 模型
用户质疑「编译的内核没启用 onnx」。核查结论:**ONNX 是启用的**,是我上一版
日志改错了,误导了判断。
## 事实
- 编译:`make build` 默认 `HOMED_TAGS=onnxruntime`(Makefile 有明确注释,
产物里 `go version -m` 可验 `-tags=onnxruntime`、CGO_ENABLED=1)。
- 多模态向量空间:`provider=chineseclip dim=512` 已加载,健康快照
`onnx.enabled=true`。
- 依存句法分析器:`core.agent.onnx_model_path` 留空时走**二进制内嵌模型**
(`//go:embed models/*` → 解到 `<data>/nlp/dep_parser.onnx` 后加载),
同样是 ONNX 推理,不是规则引擎。
## 上一版错在哪
我只回显了**配置值**(空串),于是打出
`dep parser ready (内置规则引擎;未配置 ...)` —— 把「内嵌模型已启用」
说成了「降级到规则引擎」。原始日志 `initialized (model: )` 的空括号
本来就是同一个信息缺失,我把它改成了**更确定的错误结论**。
## 修法
- `nlp.ONNXParser` 记录实际加载的 `modelPath` 并暴露 `ModelPath()`
(stub 构建下恒为空,两套构建接口一致)。
- 内核启动日志改为报实际路径:
`dep parser ready (ONNX,模型: /home/newqqagent/nlp/dep_parser.onnx)`
部署后实录:该行正确输出实际模型路径;`onnx.enabled=true`;
启动告警仅剩两条 CLI 型技能降级说明(已在上一个提交解释)。
|
2026-10-20 09:04:11 +08:00 |
|
|
|
16c9d4f69e
|
fix(startup): 启动阶段两类噪声归零 —— 插件 stdout 日志与 CLI 型技能误判
用户反馈启动日志里有报错。实测定位到两处,都是真实问题:
## 1. huawei_smarthome 往 stdout 打日志(8 条「收到非法 JSON 帧」)
stdout 是 JSON-RPC **协议通道**,它在 plugin.go/handlers.go/service.go 等处
用了 fmt.Printf(31 处),内核每收到一行就把协议解析失败一次。
这不是内核的错,是插件写法错 —— 已在其源码(/home/newqqagent/plugindev/
huawei_smarthome)把 fmt.Printf/Println 全部改成 log.Printf(stderr),
重新 hmapdev 打包并安装。重启后非法帧 0 条、10 个工具正常注册。
## 2. clawhubadapter 把 CLI 型 main.py 当 JSON-RPC sidecar
判定规则原为「有 main.py 就按 pysidecar 拉起」,但 ClawHub 上大量技能的
main.py 是 **CLI 入口**(`python3 main.py <tool> '<json>'`),对它发 ping
必然超时(bilibili、gaode 各等 5 秒)。旧代码只打一行错误就完事 ——
启动日志一片红,而技能既没变成工具、也没交给 skillmgr,等于半死不活。
改为:sidecar 就绪失败即降级为「CLI 型技能」,移交 skillmgr 并给出说明,
日志措辞从「错误」改为「未就绪 → 按 CLI 型处理」。两个技能现已可经
skill_info 读到文档、再用命令执行。
## 3. 顺带修一条看着像配置泄漏的日志
`dep parser initialized (model: )` —— 空括号是「未配置 ONNX 模型、走内置规则
引擎」的正常回退。改为显式说明,避免误读成缺配置。
## 验收(本机实例重启后实录)
- 「非法 JSON 帧」0 条(此前 8 条)
- dep parser 日志:`ready (内置规则引擎;未配置 core.agent.onnx_model_path)`
- 剩余 3 条均为说明性:mail-bridge 出站通道兜底、两个 CLI 型技能降级移交
- 13 个内置 + 13 个自启外部插件全部 loaded,agent main started
|
2026-10-20 09:00:58 +08:00 |
|
|
|
b5f024b054
|
refactor(prompt): 提示词去工具化 + WebUI 插件状态按运行期真相展示
## 1. 系统提示词/人格瘦身(工具用法搬去 tooldef 与 skill)
用户指出:默认系统提示词与 personal 里有大量本该属于 skill / 工具描述的内容。
核对属实,且已造成**真实故障**:提示词教模型用 cmd_run,而 cmd 外置按需后
工具根本不在清单里 ⇒ 模型第一轮调用不存在的工具,拿到「工具 cmd_run 不存在」。
改动:
- kernelmod/agent.defaultSystemPrompt:删掉 output_* 用法、工具分类清单、
cmd_run/terminal/files 策略、并行策略、媒体工具清单、emoji 风格要求。
只留:身份 + 「工具描述与技能索引会注入,先读再用」+「只依据工具返回值作答」。
- config.DefaultPersonaPrompt:只留身份/称呼/语气 + 版本以运行时快照为准
(后者由 TestDefaultPersonaPromptHasNoVersionLiterals 钉住)。
- 命令与文件策略下沉到工具描述:cmd_run 补「完整 bash,支持管道/heredoc/重定向」,
files_write 补「原子替换 + 覆盖受保护路径前留档」。
- 对话时序块自带语义说明(原先靠提示词解释,提示词一精简就丢语义)。
- 文档新增「提示词分层」章节(中英),写明归属判据与这次的反面教材。
## 2. WebUI:插件「启动状态」按运行期真相展示
旧实现只看内核 status 的 loaded 一个布尔,三种形态被混成同一个「未加载」:
auto_start=false 的按需待命(正常)、该起没起(异常)、plugin_stop 后的运行态停止。
用户看到一屏「未加载」以为插件全坏了。
- sdk.PluginRuntimeInfo 新增 AutoStart / RuntimeStopped;registry 从清单填充。
- pluginmgr /plugins 列表暴露 auto_start / runtime_stopped / builtin / tool_count。
- webui 新增 POST /plugins/<name>/start|stop(运行态,不动持久禁用清单)。
- dashboard 状态列改为五态:已禁用 / 进程丢失 / 已运行 / 按需待命 / 未运行,
并额外标注「自启 / 按需」;按钮改为 启动/停止(运行态)与 禁用/启用(持久态)分列。
- 新增两条回归判据(引用 auto_start/runtime_stopped/alive 字段;startPlugin 走 /start)。
## 验收(本机实例实测)
- files 保持 auto_start=false(按需);问 agent 写文件 → 它先 plugin_start cmd+files,
再 files_write/files_read,内容校验一致;未用 shell 代替文件操作。
- WebUI 插件页截图确认:a2a/acp/agentcli 显示「已运行 自启」,
ai_image 显示「按需待命 按需」+「启动」按钮。
- go test(config/agent core/plugin/pluginmgr/webui/sdk)全绿。
|
2026-10-20 08:57:36 +08:00 |
|
|
|
2aab90c508
|
fix(plugins): 内置清单终审 15 → 13 —— kbtree/multimodal 翻案加回
## 翻案依据(逐方法对照公开 SDK,非方法名 grep)
初判把 kbtree/multimodal 归入"只用公开 SDK",错了。两边的
s.Knowledge() / s.LLM() **同名但接口面不同**:
- kbtree 需要 KnowledgeAPI.Subtree/CategoryCounts/SearchIn/Categories
与 KnowledgeTreeView/KnowledgeCategoryCount 类型 —— 公开 KnowledgeAPI
只有 Search/Add/List,树视图类型公开面根本没有
- multimodal 需要 LLMAPI.Chat + LLMMessage/LLMContentBlock ——
公开 LLMAPI 只有 ListSources/SetSource/CurrentSource,没有 Chat
外置它们 = OCR/看图/转写、知识树浏览静默失效。
## 最终内置 13 个
cli webui agentcli healthcheck pluginmgr seq remotedevice
clawhubadapter kbtree multimodal cfgmgr skillmgr seemedia
外置 5 个(SDK 仓 example/ 已提供工程并已部署验证):
cmd localuse mcp(本轮)+ ai_image files(此前)+ timer(更早)
## 文档同步(中英 6 份)
数量 11 → 13;判定标准小节补「终审翻案」与教训:
**判外置与否必须逐方法对照公开接口,同名方法不等于同面。**
验证:go build/vet/test 全绿;生产实例已部署新内核 + 新插件包,
四个按需插件经 plugin_start 拉起、工具全部注册(cmd_run /
local_screensee / mcp_* / files_edit_line 实核)。
|
2026-10-19 22:09:09 +08:00 |
|
|
|
835688c94a
|
refactor(plugins): 按「内置插件判定标准」收敛内置清单 16 → 11
## 判定标准(写进 all.go 与 ARCHITECTURE)
**只有必须使用内部 SDK 能力(公开 SDK 没有的接口面)的插件,才留在
编译期内置清单**;其余一律外置为 .hmap 插件,独立编译、独立发版。
依据是 internal/plugin/proc/capability.go 的 withheldCapabilities:
内部 SDK 的 TerminalAPI/TrackerAPI/StatusAPI/SupervisorAPI/AdapterAPI/
SelftestAPI/ToolAPI/Publish 等能力面,外部插件在类型层就不存在。
需要它们的插件外置后功能会**静默消失**(不报错),必须内置;
只要公开能力的插件,留在内核里是白付重编成本。
## 内置 11 个(all.go 单一权威清单)
cli webui TerminalAPI/Tracker/Status/Supervisor/Adapter
agentcli EventPublish(终端输出走 Publish,公开 SDK 无此方法)
healthcheck SelftestAPI
pluginmgr failback 恢复链必需
seq ToolAPI
remotedevice RegisterChannel
clawhubadapter InjectInputSync
cfgmgr skillmgr seemedia(保留)
## 外置 8 个(源码保留,.hmap 由 SDK 仓 example/ 提供)
timer(此前)ai_image files cmd kbtree localuse mcp multimodal
## 同步修正
- kbtree 归位:它一直是编译期内置插件,却长期被 main.go 单独导入,
游离在 all.go 清单之外(文档盘点因此数错)。现移入 all.go,
main.go 不再单独导入任何插件,并加注释禁止该做法。
- integration_test:外置插件的 7 个用例(cmd_run/terminal_*)加
requireExternalTool 跳过保护——用例保留(行为判据仍有价值),
在纯内置装配下 Skip,不造假失败。
- 全部文档同步 11 个的清单与判定标准(中英四份 + 两份 README)。
## 已核实不做的事
- 保留的 11 个内置插件无一在代码里硬依赖被外置的插件:
cli 对 terminal_* 是 ExecuteTool 按名动态调用(运行时依赖);
healthcheck/seq 只在白名单与示例文本里出现;webui 只订阅事件。
- kernelmod/(内核模块)与 plugins/(插件)严格区分:
kernelmod/multimodal 是内核装配模块,与外置的 multimodal 插件无关。
验证:go build ./... 全绿;go test ./internal/plugins/... 全绿;
webui 新增 5 条命令历史渲染回归判据(TestCmdHistory*)全绿。
|
2026-10-19 21:13:23 +08:00 |
|
|
|
515cf2deb1
|
docs: 按源码全面校正(插件数/内置 timer/RPC 数/工具数/失效行号)
逐条对着源码核过,修掉 5 类与实现不符的陈述(中英四份文档同步):
1. **内置插件数 20 → 17**
`internal/plugins/all.go` 的空白导入实际是 17 个。旧文档还列了 `timer`
(已外置)与 `data`/`kbtree`(根本不是内置插件,前者只有 skills 目录,
后者未进 all.go)。清单按 all.go 重写。
涉及:README、README_EN(含「许可」章节的 AGPL 覆盖范围)、OVERVIEW 中英、
ARCHITECTURE 中英的加载方式表。
2. **删除所有把 timer 当内置插件的表述**
ARCHITECTURE 中英目录树里的 `internal/plugins/timer/` 行、
PLUGIN_DEV 内置插件示例表的 Timer 行(该行还与同页下方「timer 已移出内置插件」
的说明自相矛盾)。示例表改用 healthcheck(真实存在、30m 定时自检)。
3. **子进程 RPC 方法数 55 → 82**
权威清单是 `internal/plugin/proc/protocol.go` 的 Method 常量,
实际 82 个(settings 16 / plugin 9 / io 8 / secrets 7 …)。
v1.1.1「51 → 55」作为历史叙述保留,但补注说明 82 才是当前值,
避免读者把 55 当现值。
4. **qq 示例工具数 20 → 24**
按 `example/qq/plugin.go` 的 `Name: tp + "..."` 实数。
5. **修掉整张失效的「文件:行号」引用表**
原文写 `context.go:155` / `document.go:206` / `indexer.go:96+111`,
但 memory 重组后 `context.go` 已迁到 `agent/core/`,行号全部错位。
改为**函数名锚点**(`RelevanceContext.Prune`、`Store.Query`、
`Indexer.vectorSearchEntities`),并改正 `distill.go` 的算法描述:
文档写 Bigram Jaccard,源码是**加权** Jaccard(`scene_emerge.go` 有明确注释),
阈值 `sim > 0.75` 在 `distill.go`。
同时补 Provider 接口漏项:实际有 4 个方法(Name / Chat / ChatStream /
MaxContextTokens),文档只写了前 3 个。
验证:`go build ./...` 通过;文档内相对链接 0 坏链;
引用的 22 个 .go 路径逐个 stat 存在(`internal/memory/media/` 两处是
「已删除机制」的历史叙述,指向不存在路径是正确语义,保持不变)。
|
2026-10-19 20:49:43 +08:00 |
|
|
|
852d6a2891
|
chore(gitignore): 忽略本地 NLP 模型权重 /nlp/
nlp/dep_parser.onnx(48MB ONNX 依赖句法分析器)是**有意不入库**的本机资产:
二进制大文件一旦进 git 就永久留在历史里,且需要与本机加载路径配套。
此前它只是碰巧没被 git add(untracked),没有忽略规则保护 —— 任何一次
`git add -A` 都会把它带进来。补规则把这个意图固化下来。
|
2026-10-19 20:38:03 +08:00 |
|
|
|
d9e1a44884
|
chore(sdk): 同步 SDK 仓 sdk/autostart.go 到 vendor 副本
sdk/autostart.go 在 SDK 仓已随 74e2c65(22 个示例插件配齐 skill +
三态启动声明 + 打包期校验)提交,但外层仓锁定的 vendor 子集没跟上,
导致本仓 checkout 出来的 SDK 源码缺这个文件 —— 直接依赖 auto_start
契约的代码在本仓内无法编译/阅读。
同步方式:逐字节取自 SDK 仓 HEAD(sha256 = 2869619… 两边一致),
不做任何本地修改,保持 vendor 是远端内容的镜像。
|
2026-10-19 20:37:44 +08:00 |
|
|
|
875c75445b
|
chore(gitignore): 忽略内置 pi 的会话日志 .pi-sessions/
每条工程对话会在工作目录留下 .pi-sessions/*.jsonl(本次 592K 单文件),
属 agent 工作产物而非源码。内部按本机路径组织、且随对话无限增长,
故按 / 锚定忽略,不影响仓库其它位置同名目录。
(cherry picked from commit 223752b622f637040ab3d3c6e348bfe26fd925b1)
|
2026-10-19 16:09:03 +08:00 |
|
|
|
7e371c1304
|
docs(plugin-dev): 补记「装上≠生效」——旧内核上装 timer 仍走内置工厂
loadOne 先查 factories[name],命中就不再读 plugin.bin;旧内核里内置 timer 工厂还在,所以落盘安装后 plugin_status 仍是 builtin=true / tools=[timer_set],外部版本要等内核升级重启(工厂消失)才真正接管。plgreload 在这中间是空动作。
这样「先装插件再升级内核」的顺序约束才有可执行的含义:先落盘,再换二进制重启。
(cherry picked from commit 9e72898170386526425f8abe474d0f9622242422)
|
2026-10-19 16:09:03 +08:00 |
|
|
|
47272adffe
|
docs(plugin-dev): 注明 timer 已外置,并写明「先装插件再升级内核」的顺序约束
timer 移出内置插件后,升级路径上有个静默故障点:若先升级内核、后装 timer 插件,
就会有一段窗口期三个 timer 工具全部不存在 —— 不报错、日志无线索,只是 LLM
视野里工具没了,表现为「定时提醒功能不见了」,极难归因。
在 zh/en 两份 PLUGIN_DEV.md 的「内置插件」表下补注记(原 Timer 示例行就删在这里,
正是读者会产生疑问的位置):
- timer 已改为外部 hmap 插件提供,三个工具与 timer 输入通道均由它注册
- 升级顺序:先装 timer 插件,再升级内核
- 外部版是严格超集:原内置版只有 timer_set 且重启丢在途倒计时,
外部版增加 timer_list / timer_cancel 并跨重启保留待触发列表
assets/docs 是纯文档,无 go:embed、无代码引用,改动不影响构建。
(cherry picked from commit 62332562a80165b6e86af77315c07d18e743e7fa)
|
2026-10-19 16:09:03 +08:00 |
|
|
|
99914a9e9b
|
test(plugins): 加负向断言守护 timer 不再被注册进内核
timer 已移出内置插件(717054a),但原有 TestIntegrationAllPluginsRegister
只是正向清单 —— 它断言「该有的工具都在」,对「多出来」是瞎的:
把 internal/plugins/timer 与 all.go 的 blank import 加回去,那条测试依然全绿。
于是「已移除」这件事只能靠人记住。
补一条负向断言,在三个互相独立的注册面上各断一次(任一处漏掉都能单独复活 timer):
- 工具面:timer_set / timer_list / timer_cancel 不应存在
- 输入面:inputch "timer" 不应存在(定时器注入中断的通道)
- 注册表:插件元信息与编译期工厂都不应存在(IsBuiltinPlugin 为假)
已做变异验证:把 timer 插件与 blank import 恢复后,四条断言全部触发、测试 FAIL;
移除后 PASS。故该判据非空转。
(cherry picked from commit d5882ee44a4810fe9e5ee02fb329ad423b065d6e)
|
2026-10-19 16:09:03 +08:00 |
|
|
|
6b64d69b10
|
refactor(timer): 从内核内置插件迁为外部插件,移除编译期注册
老大要求「把 timer 移到外部插件,移除内核的内置 timer」。本次只做内核侧
的**摘除**,插件本体在外部工程(plugins/timer → .hmap 安装)。
timer 原先由 internal/plugins/timer 的 init() 注册进编译期工厂表。
后果是改一行定时器逻辑要重编整个 homed(87MB)并重启内核——而它恰是
最该独立演进的部件:有自己的持久化状态、触发语义、并发模型,却被绑死
在主进程的发版节奏上。
搬出来后:换实现只需重装插件 + 重启单个插件进程;插件崩了自己被拉起,
不影响 agent 主循环。
1. 删除 internal/plugins/timer(plugin.go + plugin_test.go)
2. internal/plugins/all.go 去掉该 import,并写明**不要加回去**的原因
3. internal/plugins/integration_test.go 的 expectedTools 去掉 timer_set
TestIntegrationAllPluginsRegister 的语义是「编译期内置插件是否都注册了工具」,
它走的是本地装配(setupIntegration 只加载 internal/plugins 下的内置包)。
timer 搬走后不再参与这个进程的装配,留在列表里只会造出永远绿不了的假失败。
真正不该省的是验证:外部插件的工具是否真注册成功,靠装完 .hmap 后实调
timer_set / timer_list / timer_cancel 来确认(见交付报告)。
- 配置表:外部插件同用 config_<name> ⇒ config_timer,老值 max_duration /
pending_timers 直接读回,老大已设的定时器无需迁移
- 工具名:timer_set / timer_list / timer_cancel 不变,模型侧零感知
- 注入源与通道:source/channel 仍是 "timer"
- 注入级别:内置插件可声明 L4,外部插件被夹到 L3;本插件本就要 L3,
故行为等价
StageHost.RegisterTool 对同名工具返回 `already registered`,而插件注册是
批量失败(新实例的工具一个都注不上)。所以必须**先装新内核**(不含内置
timer)再装外部 timer 插件,反之会撞名。
(cherry picked from commit f584776f23c3bfd8fb2892155e6018a1ec57eac0)
|
2026-10-19 16:09:03 +08:00 |
|
|
|
c53d1da2a6
|
docs: 内核性能实测基线 + payload 透传/保留键设计
## docs/zh/kernel-performance.md(新增)
全部是实测数据,不是估算,每条标出测量方法与复现命令。
冷启动 15.4s(5 层 17 模块 + 22 插件),耗时分布:
第 0→1 层 3.1s 20% 内存/ONNX/文档存储初始化
第 1→4 层 2.4s 16% LLM/知识库/agent 装配
第 4→完成 9.9s 64% 22 个插件子进程建链
★ 与早期记录(1538–1709ms)的差异要说明:那是**纯内核**装配时间,
不含插件。两者口径不同,混用会得出"启动变慢 10 倍"的错误结论。
其余指标:RSS 26MB / 插件进程 12 个(13 个按需加载)/ 三层工具超时
(30s ctx → max(声明,参数) → 10m 硬上限)/ 上下文预算实测值 /
`offload_busy_after` 1m。
含三个实测踩过的性能陷阱:长工具执行期间 stop 无法生效、
同阶段重复注册静默覆盖、before_output 双触发路径。
## kernel-modularization-design.md 新增 2.7 节
注入 payload 的透传机制与保留键硬约束。
保留键必须覆盖两类:注入路径自己写入的(no_memory/context_policy/…)
与内核读取的语义键(interrupt/stop/command/…)。插件写保留键会被
跳过并记日志 —— 允许覆盖等于允许伪造"这条不记记忆"或"这条立即打断",
那是权限提升。
同时记录 output_send__* 的 stage 语义:before_output/after_output
有两条触发路径,output.go 那条才是"消息真发出去"的时刻,而它历史上
连 Extra 都没初始化(实测 extra_keys=0)。
## 设计过程中的一次返工
曾加 InjectOptions.Meta,等于把同一件事做两遍且只服务注入路径。
用户指出后改为复用 InputEvent.Payload 的扩展口,所有注入路径共用。
|
2026-10-18 18:36:26 +08:00 |
|
|
|
ecffb8a7de
|
test(plugin): InjectOptions 含 map 后不可比较,改逐字段验零值
Payload 字段(map[string]interface{})加进 InjectOptions 后,
结构体不再可比 ⇒ `z != sdk.InjectOptions{}` 直接编译失败
(invalid operation: struct containing map cannot be compared)。
|
2026-10-18 18:30:04 +08:00 |
|
|
|
aad8ddbf1e
|
fix(core): output_send 的 stage 补 Extra 与 after_output
## 缺陷(2026-10-18 实测定位)
`before_output` 被两个语义共用:
emitResponse(eventloop.go) = 一轮的回复,turn 边界
output.go = 单次 output_send__* 的工具输出
**真正把消息发出去的是后者**,而它构造 stageCtx 时连 Extra 都没初始化:
stageCtx := &sdk.StageContext{
FinalText: payload,
Phase: sdk.StageBeforeOutput,
}
诊断日志实证:`[qq-dbg] before_output: input_source="" qq_uid=<nil> extra_keys=0`
后果:通道插件在"消息即将发出"这个最需要上下文的时点,什么都读不到。
QQ 的「对方正在输入中」因此一次都没能触发。
## 修法
Extra 补三个键:
output_channel 目标通道(插件据此判断"是不是发给我的")
output_meta 本次发送的 meta 原文(user_id / group_id 在里面)
output_type text / image / file / voice
## 同时补上 after_output
这是"消息真的送达"的时刻,但此前只有 before_output。没有配对收尾,
插件只能等自己的超时(QQ 那边是 30 秒)—— 用户看到指示器挂很久。
阶段顺序:before_output(可改写 payload/拦截)→ Execute → after_output。
## 为什么 after_output 放在 Execute 成功之后
失败时不发 after_output:消息没送达,"发出后收尾"的语义不成立,
提前触发会让插件误以为已经发出去(进而在状态跟踪上记错)。
|
2026-10-18 18:01:15 +08:00 |
|
|
|
70f7bea4b9
|
feat(io): InjectOptions.Payload 业务字段扩展,透传到本轮 stage
## 为什么需要
QQ 的「对方正在输入中」必须只在**自己引起的** before_output 里发,且要
知道发给谁。但两条路都断了:
on_input 的 stageCtx 是局部变量(stageCtxFromInput),runStage 完就丢
before_output 的 stageCtx 是内核**新建**的(emitResponse)
⇒ 两者**不共享 Extra**
## 第一版是错的(用户指出)
我先加了 `InjectOptions.Meta` —— 那是把同一件事做了两遍。用户直接问:
「为什么又额外定义了 injectMeta,而不是包含在 inputch 的请求体中?」
查证后的真实边界:`InputEvent.Payload` **本就是通道送入内核的请求体**,
身份信息本该在那里。但 `IOInjector` 接口全是
`(source, channel, text, opts)`,插件无法给 payload 加字段 —— 所以
payload 需要一个**扩展口**,而不是一个平行的新概念。
改名为 `Payload` 还有一个实际好处:所有注入路径(HTTP/CLI/ACP)共用
同一套机制,不是注入专属。
## 内核怎么处理
注入点 opts.Payload
→ 与真 payload 合并(**内核保留键不可覆盖**)
→ emitResponse 把非保留键原样透传到 stageCtx.Extra
保留键清单 `reservedPayloadKeys` 覆盖注入路径自身写入的键
(no_memory/context_policy/recall_policy/scene_policy/cleaner_name/
priority/type/content)与内核读取的语义键。允许覆盖意味着插件能伪造
"这条不记记忆"或"这条要立即打断"——那是权限提升,不是便利。
拦截方式是**跳过**而非拒绝整次注入:插件可能只想带一个业务字段,
因顺手多写一个保留键就让整条注入失败,代价太大。
## 判据(4 条,变异 2/2 咬)
· 插件字段原样进 payload
· 保留键不可覆盖(逐个验证 11 个键)
· 空 Payload 不改变旧行为
· 保留字清单必须覆盖注入路径自己写入的键(否则新键=新提权面)
## 附带修掉的编译破坏
`internal/plugin/lua_surface_test.go` 用 `z != sdk.InjectOptions{}` 比较
零值 —— 结构体含 map 就**不可比较**,加了 Payload 后直接编译失败。
改为逐字段验证。
|
2026-10-18 17:40:07 +08:00 |
|
|
|
40de84facc
|
feat(systemd,webui,security): 数据目录一致性接线 + 内嵌主密钥注入
## 1. 数据目录一致性(原为死代码)
`internal/systemd/sync.go` 能编译但**从未接线** —— 没有任何地方调用它。
内核有**两处**数据目录来源且不等价:
-data <dir> ← systemd 单元 → memory/graph.db、插件数据根
core.daemon.data_dir ← 数据库 → memory/text、documents、knowledge
不一致时数据**分裂**:图记忆在 A、文本/知识在 B,检索跨目录、备份会漏。
生产当前两者一致所以没暴露,但 WebUI 允许改 DB 里的值而**无任何提示**。
接线三处:
· cmd/homed/main.go 启动时检查,不一致则打告警 + 内联修复命令
· GET /api/v1/system/data-dir 返回诊断(只在真不一致时给 sync_command)
· 判据 6 条(InSync/Clean/SyncCommand 内联 sed/备份/daemon-reload/drop-in)
★ 只告警**不改**:静默改 systemd 单元比不提示更危险 —— 改错了服务
起不来,而用户不知道是谁改的。命令里已内联 sed 替换,人复制执行即可。
## 2. 内嵌主密钥(方案 C)
build.sh 支持 `HA_EMBEDDED_KEY_HEX`(64 位 hex)经 -ldflags 注入。
从**环境变量**读而不是写进文件:本仓有两个公开远端,64 位 hex 一旦
写进被跟踪的文件就永久留在 git 历史里。
未设时不注入、不报错:开发机与 CI 本来就不该带生产密钥。
设了但格式不对则**中断构建**(`return 1`)—— 静默产出一个没有内嵌钥的
二进制,会让"防丢失"这个目的悄悄失效。
实测(真机):磁盘上**没有** security.key 时,仅靠 embedded 密钥即可
Submit + 二次加载解密成功;日志确认
`密钥来源 embedded (构建期注入,指纹 0db8e9c5)`。
## 判据第一版的两个错误(记下来)
· 没给 UnitPath 就断言"必须含 sed -i" —— 实际 sed 行只在 UnitPath
非空时生成,判据误报。
· 断言 `/` 必须转义 —— 但 sed 分隔符是 `|` 不是 `/`。`/` 无需转义;
真正必须转义的是 `&`(sed 里表示整个匹配)与 `|`(分隔符)。
|
2026-10-18 13:51:00 +08:00 |
|
|
|
6a15b80ee9
|
revert: 撤掉多余的 auto_start 门(调度器早已处理)
## 为什么要撤
c426921 我以为"外部插件的 auto_start 完全不生效"。**判断错了。**
实机验证:13 个声明 auto_start=false 的插件在重启后确实没加载
(建链 22 → 11)。靠的是**既有机制**,不是我加的门:
internal/kernelboot/scheduler.go:277 canLaunch: if !p.AutoStart { return false }
internal/kernelmod/pluginhost/mod.go buildSkipSet: catalog 里的插件全交给调度器
internal/kernelmod/pluginhost/catalog_test.go:198 判据已覆盖
我当时只看到 `Load` 遍历目录时不查 auto_start,就断定该字段不生效 ——
那只是**没看到另一条路径**(LoadSkipping + 调度器分层)。
## 为什么必须撤而不是留作"兜底"
· 两处逻辑重叠:调度器按 catalog 决定跳过,我的门在 loadOne 里再判一次。
两处各说各话,将来改一处漏另一处就是新的静默不一致。
· 我那条判据测的是**我自己加的门**,不是真实行为 —— 它变绿不证明任何
产品行为正确(撤掉这道门,判据红,但产品行为不变)。
· **没验证过的防御就是噪音。**
## 撤回后行为不变
按需加载链路实测(weather):
plugin_start → loaded=true / alive=true / pid=838963
plugin_stop → loaded=false / alive=false,pid 消失
两者都 persistent="未改动"(不动持久禁用清单)
这些都发生在**没有**我这个门的旧二进制上。
|
2026-10-18 13:36:16 +08:00 |
|
|
|
59f7001932
|
fix(security): 工具输出与参数会原样写进日志 —— 落盘前脱敏
## 缺陷
两处日志把工具内容原样打印,没有经过 security.Replace:
task.go:1038 log.Printf("[agent] tool %s result: %s", ..., truncateStr(result, 100))
task.go:967 log.Printf("[agent] tool %s raw_arguments: %s", ..., truncateStr(tc.RawArguments, 300))
实测场景:enc_shell 跑 curl 拿到 200,响应体里带着上游 key 的完整值
(`{"key":"sk-..."}`),这一行直接写进 /home/newqqagent/log/homed_*.log。
## 为什么日志泄漏比上下文泄漏更严重
模型上下文 短期——一轮结束就不再发送
日志 长期——每天一个文件、从不自动清理,systemd journal 另存一份,
排查问题时还会被贴进 issue
一次落盘就是永久泄漏。凭据轮换前,任何拿到日志的人都能直接用。
## 改动
· 新增 scrubForLog(text, where):唯一入口,空安全(Global()==nil 时原样返回)
· 两处日志调用点都改为先 scrubForLog 再落盘
· 拦截本身记一笔(只记模式与长度,不记原文),便于事后发现"哪个工具常吐凭据"
## 判据(4 条)
· 明文被换成别名(真 Store,不用 mock)
· 普通文本不被改动(不误伤)
· Global() 为 nil 时不 panic(安全模块未启用是常态)
· **接线**:两处调用点确实走了 scrubForLog(只测函数不测接线会漏掉原缺陷)
变异:scrubForLog 直接返回原文 → 咬;工具结果日志去掉 scrubForLog → 咬。
|
2026-10-18 13:20:54 +08:00 |
|
|
|
d9c2e51f61
|
fix(agent): cancel 停不下调度循环 —— 队列非空时永远不看 ctx.Done
## 缺陷(真缺陷,不是测试问题)
`schedulerLoop` 只在"队列空"的分支里 select `ctx.Done()`:
if kind == nextNone {
select { ... case <-a.ctx.Done(): return }
}
于是**只要队列里有任务,循环就永远不看 ctx.Done** —— 一直取、一直执行,
直到队列被取空才可能退出。
生产后果(`reclaim` / `destroy`):销毁流程先回收残余任务、再销毁子。
而回收到销毁之间子仍在运行,继续把队列里的任务取走执行 —— 那些任务的
结果**永远无人认领**(子随即被销毁),且日志无痕迹:既不在"残余清单"里,
也没跑到出结果。带 ResponseCh 的同步调用方(cli/a2a)更糟:Stop 的
drainPendingInterrupts 给它补一个 "skipped",用户看到"已跳过",
而任务其实执行过。
## 修法
1. `schedulerLoop` 循环顶部(**取任务之前**)检查 ctx.Done。
2. 新增 `waitSchedulerStopped`:cancel 之后等 running 归零(上限 5s,
超时如实记日志,不让控制面永久挂住)。
3. 新增 `FinishResident`:停止调度 → 回收残余 → 处置 → 收割记忆 → 销毁,
**一步完成**,消除"取完到销毁"之间的窗口。`reclaim` / `destroy` 改走它。
4. 记忆收割抽成 `harvestResidentMemory`,供 FinishResident 的 onDestroy 复用
(必须在 Destroy 之前 —— Destroy 会关掉 temp)。
## 判据(2 条,都做了"撤销修复必须变红"的验证)
· TestSchedulerStopsTakingTasksAfterCancelWithNonEmptyQueue
用阻塞 provider 把循环钉在 executeNewTask 里,此时塞 5 条进队列、
cancel、放行。修复在位 ⇒ 5 条一条都不执行;撤销修复 ⇒ 取走 5 条。
**实测撤销后报「仍取走了 5 条(剩 0/5)」,恢复后 10/10 绿。**
· TestFinishResidentIsAtomic
断言返回后子已从登记表移除(销毁确实在同一事务内完成)。
原 flaky 的 TestResidualKeepReturnsTasksToParent 与
TestResidualDropNotifiesSyncCaller 也修了:先 cancel + 等静止再入队,
得到确定的初始状态(它们验的是转交逻辑,不是子的生命周期)。30/30 绿。
## 我踩过的两条弯路(都写进判据注释了)
1. 加 `scheduler.freeze` 绕过。错在:调度器本来就有停止语义(ctx),
再引入一个状态位会让"谁负责停"含糊,且忘了解除就永久僵住。已删。
2. 判据第一版写成「先 Start、再 cancel、然后入队」—— 那时队列是**空的**,
循环正好走 nextNone 看到 ctx.Done 退出,于是**撤掉产品修复判据依然
30/30 全绿**。是用户追问"凭什么说修好了"才发现这是假绿。
必须造出"队列非空时 cancel"这个确切场景才测得到。
|
2026-10-18 13:07:40 +08:00 |
|
|
|
c426921847
|
fix(plugin): 外部插件的 auto_start 从未生效 —— 补三态启动门
## 缺陷
`plugin.json` 的 `auto_start` 一路写得出来(hmapdev 打包会写),
但 **Load 的目录扫描从不读它**:`loadOne` 里只有持久禁用门与运行态门。
也就是说这个字段在**外部插件上完全不生效** —— 声明 `auto_start: false`
的插件照样开机自启。
(内置模块那条走 `kernelboot/scheduler.go` 的 `p.AutoStart`,与外部插件
是两条路,所以缺陷只暴露在外部插件上。)
## 三态语义(现在三条都成立)
自动启动 auto_start 缺省/true + 启动扫描 ⇒ 拉起
启用但不自启 auto_start=false + 启动扫描 ⇒ 跳过
同上 + plugin_start ⇒ 拉起
禁用 持久禁用清单 + 任何路径 ⇒ 拒绝(plugin_start 也拒)
★ `explicitStart` 是这次的关键设计点:`auto_start: false` 的含义是
"**别在开机时拉起我**",不是"永远不许启动"。少了这个区分,
按需加载的插件就再也起不来了 —— 比不声明更糟。
## 判据(4 条)
· auto_start=false 在启动扫描时被拦(且不调用 Start)
· 同一插件经显式启动**真的加载并 Start**(plugin_start 可用)
· 未声明 auto_start ⇒ 自启(不误伤存量插件)
· plugin.json 读不到 ⇒ 默认自启(清单损坏不该让插件静默消失)
## ★ 判据第一版是假绿(变异不咬)
第一版直接断言 `loadOneOpt` 返回 true/false。但 **false 有两个来源**:
① 被 auto_start 门拦住(正确)② 没有 plugin.bin 加载失败(噪音)。
两者无法区分 ⇒ 删掉 auto_start 门后判据照样通过。
改成用 `RegisterNative` 注册一个**真能 Start 的**假插件:
返回 true 必然意味着门没拦它,返回 false 必然意味着门拦了它,语义唯一。
修完两个变异都准确咬住(删门 / 忽略 explicitStart)。
|
2026-10-18 11:24:14 +08:00 |
|
|
|
53950e0388
|
test(pluginmgr): 钉住「插件必须走 plugin_install,不能手工替换二进制」
## 我犯的错(2026-10-18)
改完 commondev 后我是这么"部署"的:
python3 -c "zipfile...extractall('/tmp/x')"
install -m 0755 /tmp/x/plugin.bin <插件目录>/plugin.bin
install -m 0644 /tmp/x/rawfile/runtime/pi/sidecar.mjs <插件目录>/...
systemctl restart homeagent
用户一句「为什么不打包为 hmap 安装,而是手动替换」点破了问题。
## 手动替换丢了什么
换成标准 plugin_install 后,返回:
action: "reinstalled", checksum: "13a46f87161d37c8…",
config_kept: true, skills_registered: 1, reload_required: true
这个 checksum 正是 .hmap 的 sha256(实测一致)。而手工 cp 拿不到的东西:
· **整包校验和** —— 包与落盘结果之间没有任何可核对的凭据
· **备份 + 失败回滚** —— 手工 install 半途失败会留下半新半旧的目录,
比彻底失败更难查(插件能加载、某个功能诡异失效)
· **全量路径校验** —— validatePackage 遍历**所有**条目拒 `..`/绝对路径
(该循环曾因 break 漏检 rawfile,已修)
· **rawfile 全量解包** —— 我只覆盖了 2 个 sidecar,其余(node/pi 运行时)
仍是旧的。证据:替换后 `rawfile/` 目录 mtime 停在 10-16,
标准安装后整个目录 mtime 才变成 10-49
· **配置保留与 skill 注册** —— config_kept / skills_registered 在返回值里
## 判据(2 条)
代码无法禁止人手工 cp,但可以把**唯一正确入口**钉住:
· TestInstallKeepsBackupAndRollback:installFromData 必须仍有
「备份旧目录 → extractPackage → 失败回滚」三件事
· TestValidatePackageChecksAllEntries:路径校验必须遍历全部条目,
且所在循环里不能有 break(历史缺陷:manifest 之后的条目漏检)
## 变异
去掉备份 → 咬("安装不再备份旧目录 —— 失败就没法回滚")
去掉回滚 → 咬
|
2026-10-18 10:51:46 +08:00 |
|
|
|
9b2c74bc49
|
refactor(config): 默认值单一来源 —— ConfigDef.Default,不再各调用点重复硬编码
## 起因(用户质疑)
我把 offload 的触发阈值从 5 分钟改成 1 分钟时,同一个默认值散落在四处:
defaultOffloadBusyAfter = 5 * time.Minute (agent/core 常量)
set("core.agent.offload_busy_after", "5m") (seedDBValues 播种)
reg(ConfigDef{... Default: "5m" ...}) (seedCoreDefs 定义)
GetDuration("...", 5*time.Minute) (kernelmod 读取点)
用户问:「为什么不是单一从配置读取,而是硬编码在源码中?」
答:这不是合理的分层,是重复。同一份默认值四处字面量,改一处漏两处
就是**静默的行为漂移** —— 不报错,只是某些路径用 5m、另一些用 1m。
## 改动
**① 默认值的唯一来源 = ConfigDef.Default**
GetInt/GetDuration/GetBool/GetString 新增 defDefault():
DB 里查不到行时,从已注册的 ConfigDef.Default 取,而不是用调用方传的字面量。
调用点现在写 GetDuration(key, 0) —— 那个 0 只是「连 ConfigDef 都没注册」
时的兜底,正常路径永远用不到。
**② 删掉 seedDBValues 里 54 行重复的 set(),改由 reg() 落库**
reg() 原本只 `r.defs[k]=&d`(只登记不落库),默认值另由 set() 写一遍。
现在 reg() = registerDefLocked(登记 + 落库)。
为此抽出 registerDefLocked:SeedDefaults 已持 r.mu,直接调 RegisterDef
会自死锁(sync.Mutex 不可重入)。
**③ 8 个键必须「只登记、不播种」,新增 regMeta()**
core.agent.system_prompt 播种会把版本号冻死在 DB 里
core.defaults.llm_endpoints 空默认值
core.memory.multimodal_space.options.* provider 私有选项
第一版没有区分,一改就红了两条既有判据:
TestSeedDefaultsDoesNotSeedSystemPrompt
TestSeedDefaultsDoesNotInjectIntoLegacyInstall
后者尤其重要:老安装升级被注入新默认值 = 静默加载 1.8GB 向量模型。
**④ seeded 集合:回退只对「本次真播种过」的键生效**
这是修 ③ 过程中暴露的更根本的问题。GetXxx 无条件回退到
ConfigDef.Default 会让**老安装**读到新版本的默认值 —— 即使 DB 里
根本没有那一行。加了 seeded map 区分:
全新安装 → 键已播种 → 回退 ConfigDef.Default(正常)
老安装 → 新键没播种 → 返回零值(不注入)
## 顺带修的
offload_busy_after 的 Type 从 "string" 改成 "duration" —— 它本来就是时长,
用 string 时 WebUI 只能给文本框,拿不到时长控件。
## 判据
沿用既有判据,不新增。新写的 offload_threshold_test.go 已删除 ——
它断言「四处默认值必须一致」,方向是**鼓励重复**,与本次目标相反。
|
2026-10-18 10:16:53 +08:00 |
|
|
|
ef08ebb7d3
|
chore(mascot): 立绘 v2 —— 真 alpha 素材 + 头部裁切头像 + 素材判据
老大给的 4 张新立绘里有 3 张是「棋盘格假透明」JPEG(像素是灰白格子,
不是 alpha)。直接进仓库会在深色面板上露出格子,所以先抠成真 alpha 再落盘。
## 为什么主图与头像要分两个文件
现役 mascot.webp 唯一的消费点是**聊天消息的 30px 圆形头像**
(dashboard.js 4 处 aiAvatar + GUI app.js 2 处),概览页并没有放立绘。
主图是坐姿全身,按现有 CSS(object-fit:cover + scale(1.5))裁成圆后
只剩裙子、看不到脸 —— 只换图不做头像版,等于把头像换成一张看不懂的图。
- assets/branding/mascot-xiaozhai.webp 1024² 主立绘(官方展示)
- assets/branding/mascot-xiaozhai-avatar.webp 512² 头部方裁(运行期源)
- internal/plugins/webui/mascot.webp 256² 头像版(原 133KB→24KB)
- cmd/gui/renderer/mascot.webp 128² 头像版(原 133KB→9KB)
参考稿另存三张(不参与运行):sheet(设定总图)/ portrait(半身)/
turnaround(三视图)。
## 抠图怎么做的(可复现,不是手搓)
背景是 20.45px 周期的棋盘格(237/254 两色)。判据取「中性灰 且 灰度≥228」
的像素做候选,再从图像边界做连通域传播 —— 这样白裙子/白围裙因为不中性
(带蓝调)不会被误判,避免了当年 site/README 记录的 flood-fill 渗色问题。
边界再内缩 1px 抹白边,0.7px 高斯羽化去锯齿。抠完透明占比 55.8%,
深/浅两种背景上目检均无残留格子与白边。
## CSS:scale(1.5) → 1.25
原来的 1.5 是为「全身图裁圆」补的放大。换成头部方裁后,1.5 会切掉两侧
发绳与呆毛 —— 按真实 30px 渲染放大对照看过 1.0/1.15/1.25/1.4 四档。
## 新增判据(含反向验证)
internal/plugins/webui/mascot_asset_test.go 直接读 WebP 容器头,
断言 VP8X 的 Alpha 标志位与 ALPH chunk **同时存在**,外加正方形/≥128px/
≤128KB。理由:这类退化在肉眼 review 里抓不住(缩到 30px 就是一团浅色),
而用户每天都在看;且 site/README.md 记着当年正因为「白底图没法抠」
干脆不放立绘,这个坑值得一个自动化判据。
反向验证(已实跑,非声称):把素材换成 HEAD 旧图,判据报红并准确指出
「Alpha 标志位未置位 / 缺 ALPH chunk / 体积超限」;换回新图转绿。
另有一条独立用例构造「带 VP8X 但不置 Alpha 位」的最小容器,
确认解析器不会对退化素材误判为绿。
## 顺带修的两处
1. assets/docs/{zh,en}/PLUGIN_DEV.md 的 mascot 引用路径是坏的:
`../../assets/branding/...` 从 assets/docs/zh/ 出发解析成
`assets/assets/branding/...`,文件不存在(旧版一直是坏的)。
已改为 `../../branding/...` 并逐条验证路径可解析。
同时指向新的头像版(那处是 20px 圆形内联小图标)。
2. gzip.go / gzip_test.go 注释里的「mascot.webp(133KB)」改为不带死数字
的说法 —— 体积会随素材变,注释写死数字只会变成误导。
## 验证
go build ./... → 0
go vet ./internal/plugins/webui/ ./cmd/gui/ → 干净
gofmt -l → 空
go test ./internal/plugins/webui/ -count=1 → ok(15.1s,含新增 2 个用例)
## 没做的事
- 没碰 main、没开 PR、没部署、没重启服务。
- **概览页仍未放立绘**。site/README 里「素材不可用」这条已不成立(素材有
alpha 了),但版面是否该放图属于设计取舍,不在本次范围 —— 文档已相应改写,
不再用「素材不行」当理由。
- 抠图对发丝边缘是「够用」不是「完美」:0.7px 羽化在 1024 下看不出来,
但若将来要印物料级大图,需要重出带原生 alpha 的源图。
|
2026-10-18 07:38:10 +08:00 |
|
|
|
5a3a8f484b
|
fix(agent): 工具超时不再一刀切 60s,支持工具自声明与按参数推导
## 缺陷(2026-10-17 生产日志实测)
`toolcall.go` 里写死 `time.After(60 * time.Second)`,与工具自己的超时
**互相矛盾**且让内层形同虚设:
23:13:38 执行 cmd_run(agent 传了 timeout="130" 级参数)
23:14:38 ★ 内核 60s 硬超时掐断 → 「执行超时(60秒)」
同一份日志里 healthcheck 声明默认 120s ⇒ 被砍掉一半,天生跑不完。
## 改动
1. `ToolDef.Timeout` / `BuiltinToolDef.Timeout`:工具自己声明单次上限。
留空 = 沿用 60s(存量插件**一行不改,行为逐字不变**)。
2. 内核取 `max(声明值, 调用参数 timeout)`,再夹到硬上限 10m:
· 声明修好固定时长的工具(healthcheck 5m)
· 参数推导修好时长可变的工具(cmd_run 的 timeout 参数)
· 硬上限防插件声明成 0/无限把队列堵死
3. 非法声明("130" 少单位、0s、负数)**回退默认并记日志**,不静默接受。
写错单位是最容易犯的错,默默变成 60s 会让人以为"声明没生效"却查不出。
4. healthcheck 声明 5m(它的 llm_timeout 默认就是 120s)。
★ 没有把 timeout 放进 `ToSchema()`:那份 map 是**发给模型**的 function
schema。把内核的执行参数混进去,模型会以为需要自己传 timeout 参数。
## 判据(6 条)
未声明→60s / 声明生效 / 参数参与取 max / 参数比声明小用声明 /
非法回退 / 超上限夹住。变异全部咬住。
## 附:这条路只治「工具真的需要更久」
向开发线投递(output_send__*)的两次 60s 超时**不是这个原因** ——
那是 commondev 把"投递"实现成了"同步等任务完成",已在 SDK 仓修成异步。
这里没有给 output_send 放大超时:投递应该瞬时返回,慢是 bug 不是需求。
|
2026-10-18 06:38:51 +08:00 |
|
|
|
683f0126af
|
test(webui): 内核按钮必须真的出现在**渲染后**的页面里
## 为什么需要这组判据
dashboard.html 与 dashboard.js 都是 `//go:embed` 进二进制的,再由 init
组装成 dashboardHTML(替换 {{DASHBOARD_JS}} 等占位符)。这里有两个
**各自都可能坏掉**的环节:
1. 按钮写进了 dashboard.html,但占位符没被替换 / 替换错位
⇒ 页面里看不到按钮
2. JS 函数写进了 dashboard.js,但被包在 IIFE 里(不在全局)
⇒ onclick="kernelRestart()" 点了报 "kernelRestart is not defined"
只查源码里有没有那个字符串,这两种情况都照不出来。
## 判据(3 条,都查组装后的 dashboardHTML / embed 源)
· TestKernelButtonsRenderedInPage:渲染后的页面含两个按钮的 id 与 onclick
· TestKernelFunctionsAreGlobal:4 个函数必须是**顶层**定义
(浏览器 onclick 走全局查找,jsdom 的行为测试做不到这点)
· TestKernelButtonsInNavFooter:两个按钮必须落在 nav-footer 内
—— 它们作用于整个服务,切到任何标签页都该能点到
实测:渲染页面 408057 字节,六项断言全在。
顺带把 dashboard.js 的格式化差异一并提交(-w 对比为空的纯格式调整)。
|
2026-10-17 21:26:12 +08:00 |
|
|
|
198811253a
|
feat(webui+guard): 内核重启/关机按钮,指令走心跳复用既有协议
## 需求
WebUI 加关机与重启按钮,内核提供对应内部接口。有些状态变更只能靠重启
生效(换 LLM 源、换主密钥),没有这条通道时用户只能 ssh 上去敲 systemctl。
## 设计:复用心跳,不新开通道(用户指出的方向)
guard 与 worker 之间已经有一条现成的、每 5s 一次的单向通报(心跳文件,
内容原本是 `t=<unix>`,只用来刷 mtime)。用户点按钮本质就是"我要你在
下一次心跳时知道这件事"——再开 unix socket / 信号 / 临时文件,只会多出
一套要防重、防丢、防竞态的机制。
WebUI 点按钮
→ POST /api/v1/kernel/{restart,shutdown}
→ sdk.KernelControlAPI(只投递,立即返回)
→ 下一次心跳把 `cmd=restart|shutdown reason=...` 写进 <data>/heartbeat
→ 重启:worker 清理 → exit 42 → guard 重拉(**既有机制,零改动**)
关机:guard 在 workerCleanExit 分支读到 cmd=shutdown
→ systemctl stop → 不再拉起
★ 关机为什么必须由 guard 做:
unit 是 Restart=always,guard 自己 return 会被 systemd 立刻拉起
(用户点了"关机"、几秒后服务又活了)。必须显式 systemctl stop。
而不让 worker 自己停:它是被守护方,且**只有 guard 知道这次该不该重拉**。
★ 指令必须一次性消费:guard 读完立即原子重写文件清除 cmd 行,
否则每次心跳都读到同一条 ⇒ 无限重启/关机。worker 侧还要检查
"指令写过但文件里没了" ⇒ 已被消费 ⇒ 清本地 pending,否则又写回去。
## 顺带修的真缺陷:心跳文件无限增长
生产实测 **14.6MB**(纯 O_APPEND,约 70 天)。心跳的唯一职责是刷 mtime,
历史行没有任何读者 ⇒ 每次 consume 要整读 14.6MB(实测 27ms)。
改为 O_TRUNC 滚动写:只保留当前一行。真机验证 15,322,164 → 13 字节。
consume 也加了尺寸守卫(大文件只读尾部 8KB)。
## 安全:能力分级 + 白名单(两道)
1. proc 桥的 method 能力表:新增 `CapKernelControl` 一档。
★ 判据 `TestCapability_AllMethodsClassified` 当场报出
"未登记 ⇒ 会按 CapCore 放行,绕过权限检查" —— 那意味着任何插件都能关机。
2. 内置插件走 `SDKConfig.KernelControl`,第一版我写成"有就发",
等于全部内置插件都能关机。改为按名字白名单(目前只有 webui)。
## 判据(7 条)
心跳:解析 / 消费后必须清除 / worker 不得写回已消费指令 / 未消费时必须续写
能力:只有白名单插件拿得到 / 未装配时返回 nil 不 panic / SDK nil 安全
心跳文件:不得无限增长 / 大文件只读尾部且读后收敛
变异 6/6 咬住。全量 53 包 ok。
## UI
导航底栏两个图标按钮(与主题/语言/退出并列):重启有确认框 + 轮询等它
回来并自动刷新;关机有**二次确认且写清后果**(必须人工 systemctl start)。
两者都先把响应写回浏览器并 Flush,再让内核退出 —— 否则用户看到的是
connection reset,无法区分"正在重启"和"真出故障了"。
|
2026-10-17 21:18:07 +08:00 |
|
|
|
134cd860b6
|
fix(webui): 聊天受理超时 15s 太短,误报「已取消(用户停止)」
## 内测用户报告
发一句话,界面回「已取消(用户停止)」,而且连着出现多次。
## 实测真相(用户提供的日志)
20:58:36 POST /api/v1/chat 开始,agent 开始干活
20:58:48 agent 仍在跑(terminal_create / terminal_read)
20:58:52 ★ 前端 15s ackTimer 到点 → ctrl.abort()
→ 服务端记 status=504,耗时 15.89s
20:59:29 ★ agent 正常跑完,52.5 秒、22 个工具、返回 response
也就是说:**agent 从来没有被取消过**。它一直在正常干活并跑完了,
只是前端自己掐断了连接,回复没法送回,UI 就把"我没等到"显示成了
"用户停止"——一个用户从没做过的操作。
## 根因
`sendChat()` 里把"受理确认"的等待写死 15 秒,而 agent 的正常任务
动辄 45-55 秒(工具越多越久;同一份日志里另一次是 45262ms)。
这个 POST 本来就是**触发式**的:真正渲染靠 SSE 流式(`armTurnWatchdog`
兜 120 秒)。在受理环节抢时间没有任何意义,只会误伤正常任务。
## 改动
1. **超时 15s → 600s**:不再用固定短超时。保留上限只为兜住
"连 TCP 都建不起来"这类真故障,避免请求永远挂着。
2. **文案改掉**:原来 abort 后提示「请求超时(可能已发送)」,
红字 error 样式。用户看到"超时"会以为消息丢了而**重发** ——
实测会造成同一句话产生两条任务。改为:
「已发出,AI 正在处理(复杂任务可能要一两分钟)。请等待回复,不要重复发送。」
并且不再用 error 样式(这不是错误)。
## 判据(2 条,源码扫描)
· `TestChatAckTimeoutIsGenerous`:超时 ≥ 5 分钟
(实测正常任务 45-55 秒,留足余量)
· `TestChatAckTimeoutMessageDoesNotBlameFailure`:提示不得出现
"失败/超时/Error/failed",必须说清"正在处理"且提醒"不要重发"
变异 2/2 咬住(改回 15s / 改回"请求超时"文案都当场变红)。
★ 为什么用源码扫描而不是行为测试:这是前端 abort 时序,没有可注入的
时钟。扫描至少能防住"有人把 15s 改回来"。
|
2026-10-17 21:16:49 +08:00 |
|
|
|
662d5472ee
|
fix(agent): 安全装饰器吃掉 provider 的模态能力声明(see_image 失效根因)
## 现象
调用 see_image 工具是通的、图片也确实注入了,但内核回:
当前模型不支持图片,且没有可用的视觉回退源
而配置明明是 `core.llm.sources.llmsproxy.vision = true`,后端也确实支持看图。
用户以为是配置没热生效、以为要重启,实际都不是。
## 根因:一条完整的静默失效链
vision=true(配置是对的)
→ LuaAdaptedProvider.Vision = true
→ ProviderManager.Register 把它包成 *secureProviderDecorator
(security.Global() != nil 时无条件包装)
→ ProviderSupportsVision(p) 做 p.(ModalProvider) 断言
→ ★ 装饰器只实现了 Name/MaxContextTokens/Chat/ChatStream,
**漏了 ModalProvider 的 SupportsVision/SupportsAudio**
→ 断言失败 ⇒ 返回 false
→ prepareToolBlocks 判定"主模型不支持图片"
→ see_image 的 image_url 被当成需要回退的块
→ resolveModalFallback 找不到可用视觉源
→ 「没有可用的视觉回退源」
装饰器只该**改写请求内容**(明文→别名防泄漏),不该改变能力声明。
## 修法
透传给内层:内层实现了 ModalProvider 就照实转,否则 false(不 panic)。
## 判据(4 条)
`internal/agent/api/provider_security_modal_test.go`:
vision 透传 / audio 透传 / vision=false 不得被污染成 true / 无 ModalProvider 不 panic
★ 我第一版判据是**假绿**的,值得记下来:
`WrapProviderForSecurity(p, nil)` 在 store==nil 时**直接返回原对象**
(见其实现),所以根本没包装,测的是未包装的 stub ⇒ 删掉透传也不红。
已改为构造真实 security.Store,并加**包装前置断言**
(`_, ok := wrapped.(*secureProviderDecorator)`),防止再次假绿。
变异 2/2 咬住:
· 删掉 SupportsVision 透传 → TestRegisterPreservesVisionCapability 红
· 一律返回 true → TestRegisterPreservesNonVisionCapability 红
## 实测(完整生产装配路径)
源 llmsproxy 类型=*api.secureProviderDecorator vision=true audio=false
默认源 "llmsproxy": ProviderSupportsVision=true
重启本机实例后:22 插件建链 / 0 panic。
|
2026-10-17 20:20:49 +08:00 |
|
|
|
a68d6a428c
|
feat: 模块化内核(编译期依赖图定序)+ 密钥防火墙 + 插件三态生命周期 (#10)
* feat(kernel): 模块化内核 + 编译期依赖图定序
把单体 init 序列拆成「模块自声明依赖 + 编译期生成依赖图 + 运行期只读图」。
## 为什么
原 cmd/homed/main.go 里 17 步 init 是**手写顺序**,依赖关系只存在于
注释和人脑里。实测踩到的坑(见 docs/zh/kernel-modularization-design.md):
· nlp.defaultParser 被 initONNXParser 写、被 distiller 读,两者相距 60 行,
只靠 ticker 10min 才没炸(隐式依赖,无声明)
· 加一个模块要人肉找"该插在哪",插错的表现是运行期 nil 解引用
· 关停是 defer ks.CloseAll(),而热更新路径 os.Exit(42) **跳过 defer**
⇒ docStore/distiller/textMem 的 flush 全部丢失(每次热更新都发生)
## 怎么改
- internal/kernelboot/:KernelState(一个结构体收全部包级共享变量,字段
nil 安全)、Module 接口、graphfile(依赖图编解码 + 与二进制 hash 对账)、
scheduler(按 BFS 层并行 Init)、syncbus(层边界写通知)、
lifecycle(Quiesce/Resume 可逆伪关机,区别于一次性 CloseAll)
- internal/kernelmod/<name>/:每个模块一个文件夹,Requires/Provides 自声明;
all/all.go 是唯一的 import 清单(Go 循环导入约束)
- cmd/homed-depgraph:编译期生成依赖图,Makefile 注入 digest
- 运行期只**读**图,不计算顺序(顺序是编译期决定的,源码里没有中央清单)
## 判据
kernelboot 8 条 + 图失败模式 7 条(全部拒绝并给准确错误)+ 模块目录数
与注册表一致性。变异测试 15/15 咬住,含:
· 删掉 AddCloser 的锁 → -race 报数据竞争
· Scheduler 同批跑去重 → 203 vs 200
· 图 hash 不匹配 → 拒绝启动
## 顺带修的缺陷
热更新路径漏调模块关闭 ⇒ 显式 stopKernelModules(),并用 AST 判据钉住
"必须在 os.Exit 之前"(字符串检查在本仓已骗过我三次)。
实测:冷启动 1630ms(5 层/17 模块,原 17 步串行);生产 22 插件全部建链。
* feat(security): 密钥防火墙 + 配置加密 + close 纪律
## 密钥防火墙
模型只见别名、不见明文;插件工具调用入口(StageHost.ExecuteTool)做入向
回填。三维能力:
· 出向:工具输出里的密钥被替换成 [[secret:name]] 别名后才进上下文
· 入向:模型传别名,入口处填回真值(插件完全无感知)
· 存储:config.db 里不存明文(0644 的库可读),敏感值独立 0600 文件
模式库「宁漏勿误伤」:宁可漏掉一次替换,也不把正常文本误当密钥。
## 配置加密
- internal/security/{codec,key,keyring,store,global}.go:AES-256-GCM,
enc:v1: 前缀,密钥轮换(OldKeys 解码 + 新钥加密)
- harden.go:数据目录权限收紧
- embedded.go:支持内嵌主密钥(可选,仍保留外部 key 文件路径)
- EncryptExistingSecrets / AuditSecrets:启动时幂等迁移 + 审计
## close 纪律(用户明确要求:只要调 close 就要有前置检查)
internal/safeclose:Safe / SafeFunc。全仓 37 处裸 close(chan) → 0。
关键教训(都写进注释和判据):
· `select { case <-ch: default: close(ch) }` **不是**并发安全的
(两个 goroutine 可同时命中 default)—— healthcheck 原先是这种写法
· `sync.Once` **不可重入**:X.Do(func(){ Safe(&X, ...) }) 会自锁,
症状是 go test 直接挂住(3 个包 600s 超时),栈里只有嵌套的 Once.Do
· map 里按请求持有的 channel(mcp/stdio.go、clawhubadapter/sidecar.go)
改成 *pendingRPC 包装,每通道一个 once
## 判据
security 全量 + TestNoBareCloseOutsideSafeclose(AST 级扫描,含注释剥离)
+ TestNoNestedSelfOnceDeadlock(正则修正:\w+ 不匹配点号,捕获组要含 '.')。
变异 2/2 咬住。全量 52 包 ok。
* feat(plugin+deploy): 插件三态生命周期、数据目录迁移、发行包修复
## 插件三态生命周期(用户要求:自动启动 / 启用但不自动启动 / 禁用)
- Manifest 新增 requires_facilities / auto_start(*bool,用于区分
"未声明"与"显式 false")
- plugin-host 模块(internal/kernelmod/pluginhost):用 kernelboot.Scheduler
按依赖时机拉起,**不再手写两阶段**
- 运行态停启 StopPlugin/StartPlugin **可逆**:不写 disabled_plugins
(不改用户决定)、不置 shuttingDown(之后还能加载)、会摘掉注册面
- 与 StopAll 的区别写进注释 —— StopAll 置 shuttingDown 后**永久无法加载**,
那是进程退出用的,误用于"暂停"就是关了就死
## 数据目录迁移(伪关机 → 迁移 → 唤醒)
- kernelboot/lifecycle.go:Quiesce/Resume(可逆,区别于一次性 CloseAll),
逆序暂停、逆序恢复、Pause 失败回滚
- kernelboot/migrate.go:BeginMigration/End 编排(先停插件 → 再停内核写者
→ 等在途归零)
- distiller/textmem 增 Pause/Resume 门(不是 cancel —— cancel 不可逆,
恢复要重建整个对象)
- 模块 migration:体检 + 生成可执行迁移脚本
★ 迁移脚本的三个实测教训(都写成了判据):
1. 无条件改 /etc/systemd/system ⇒ 演练把**生产** -data 改成了演练目录。
加 --dry-run(零改动)+ 强制校验 systemd 当前 -data == OLD
2. 本机没装 rsync,脚本在"服务已停、数据未搬"处硬失败 ⇒ cp -a 回退,
并明确警告不保硬链接/ACL(生产 plugindev 有大量 git 硬链接)
## 发行包修复(全部经实测确认)
1. package-linux.sh **完全漏打 depgraph.bin** ⇒ 内核 exit 1
("生产构建(内嵌 hash 非空)但读不到依赖图")
2. build_homed 不走 Makefile ⇒ 单独修 Makefile 无效,必须同时生成图+注入
3. build.sh 的 LDFLAGS 用**历史模块路径 gitcode.com** ⇒ -X 静默 no-op
⇒ --version 显示 unknown
4. Makefile -ldflags 用**单引号** ⇒ 注入了字面 $GRAPH_HASH
## ★ build.sh 的版本号必须校验(真实事故)
原 `VERSION="$(git describe --tags --dirty)"`。无 tag 分支上它返回**分支名**
`backup/pre-identity-rewrite-20261005-51-g03d87a4e-dirty`,被注入 meta.Version
⇒ 插件握手判据 MajorVersion() 把整串当主版本 ⇒ **22 个子进程插件全部
握手失败**("主版本不匹配")。内核活着、systemd active、日志每条都像
插件的问题 —— 实际是发行包带着非法版本号。
修法:git describe 只在形如 vX.Y.Z 时采用,否则回退 meta.Version(权威源),
并对最终值做形态守卫。deploy/packaging/version_guard_test.go 3 条判据,
变异 2/2 咬住。
## deb 的 systemd unit 改为 guard 模式
ExecStart=/usr/bin/homed --role=guard --data /var/lib/homeagent
(worker exit 42 → guard 重拉,不经过 systemd、不计失败)
含 RestartSteps/RestartMaxDelaySec/TimeoutStopSec + 硬化
(NoNewPrivileges/ProtectSystem/ProtectHome/DevicePolicy/ReadWritePaths)。
## 其他
- internal/plugin/proc:非法 JSON 帧的报错**可定位**(内容摘要 + 可能原因 +
修法)。生产实测 1038 条非法帧(插件往 stdout 打日志 = 污染协议通道)
- 示例插件 4 个 14 处 fmt.Printf → log.Printf
- hmapdev 模板同步修复;scaffold 判据剥离注释(模板注释里就含 fmt.Printf)
* chore: 收尾(主密钥轮换命令、IPC/知识库加固、SDK 对齐)
- cmd/homed-key-rotate:把 security 里的 RotateKey/RotateKeyFile/RekeySecrets
串成一条**可执行**的运维路径。没有它,E-1 就只是库里的能力而不是
"出事时能做的事",而轮换的价值恰恰在于能执行。
- cmd/homed:kmod_runtime(模块装配入口)、plugins_scheduled(failback
白名单)、memcandidate、bootstrap_order_test / shutdown_flush_test
- internal/ipc、internal/devicebridge、internal/knowledge:配套加固
- third_party/homeagent-sdk/sdk:PluginMgrAPI 增三态能力(mgrStop/mgrStart/
mgrIsRuntimeStopped)+ settings 对齐
判据:bootstrap_order_test(启动顺序不依赖手写序列)、
shutdown_flush_test(停机的两条路径必须对称 —— exit 42 会跳过 defer,
热更新路径下模块关闭/flush 曾全部丢失)。
* test(security): 测试夹具改用不可误认的占位值(修 push protection 拦截)
GitHub secret scanning 拦住了推送:三处测试夹具用了"看起来像真密钥"的
字面量:
internal/security/prefilter_test.go:39
internal/security/store_test.go:177
internal/security/survey_test.go:20
它们全是**假密钥**(`AKIAIOSFODNN7EXAMPLE` 是 AWS 官方文档的标准示例,
其余是 `abcdefghij...` 这类可读占位符),但扫描器无法区分 —— 它只看形态。
★ 不绕过(不用 unblock URL、不加 allowlist):直接改夹具。
原因:一旦允许"假密钥可以通过",真密钥混进来时就没有最后一道门了。
改法:保留**模式库匹配所需的结构**,把可识别内容换成 FAKE:
glpat-abcdefghijklmnopqrst → glpat-FAKE-TEST-TOKEN-NOT-REAL
sk-abcdefghij...012345 → sk-FAKE-TEST-KEY-NOT-REAL-0000000
AKIAIOSFODNN7EXAMPLE → AKIA0000FAKE0000TEST (保持 20 字符)
ASIAIOSFODNN7EXAMPLE → ASIA0000FAKE0000TEST
xoxb-123456789012-abcdefghijklmnop → xoxb-FAKE-TEST-TOKEN-NOT-REAL
★ 中途踩到一次:AWS 键的模式是 `\b(AKIA|ASIA)[0-9A-Z]{16}\b`(共 20 字符),
我第一次替换成 19 字符的 `AKIAFAKETESTKEY0000` ⇒ 模式匹配不上 ⇒
TestSurvey_WhatIsCaught / TestRecognizesCommonShapes /
TestPrefilterNeverBlocksRealMatches 三条当场变红。
判据正确地拒绝了"为了过扫描器而削弱覆盖"。
顺带核实:全仓 grep 无任何真实密钥泄漏。
* fix(packaging): package-linux.sh 的版本解析与 build.sh 统一
上一版只修了 build.sh,漏了 package-linux.sh —— 它仍只看 git describe:
VERSION="${VERSION:-$(git describe --tags --dirty)}"
无 tag 分支上 git describe 返回**分支名**
(backup/pre-identity-rewrite-20261005-56-g98850e1),守卫会正确拒绝,
但要求人**手动传 VERSION** 才能打包。而 build.sh 早已能自动回退 meta.Version
⇒ 同一个版本,走 build.sh 打得出来、走 package-linux.sh 打不出来。
危害不只是"要手动传参":运维为了跑通会去掉守卫,而分支名一旦进
meta.Version 就是**插件协议主版本**的来源 ⇒ 22 个子进程插件全部握手失败
(这是真实发生过的事故,见 3efa4e5)。
修法:两个脚本用同一套规则(git describe 仅在 vX.Y.Z 形态时采用,
否则回退 meta.Version),并对最终值做形态守卫。
判据:TestBothPackagingScriptsShareVersionRules —— 逐个读两个脚本,
要求都能从 meta/meta.go 取权威版本、都校验 git describe 形态。
变异(把 package-linux.sh 改回旧规则)咬住并准确指出是哪个脚本。
实测:不传 VERSION 跑 package-linux.sh,输出
[PKG] warn: git describe 给出非版本串(...),改用 meta.Version=1.4.0
Version: 1.4.0
* fix(test): 跨仓判据在 CI 上应 skip 而非 fail(修 PR 双红)
CI 上 "Go build / vet / test" 与 "Build linux/amd64" 双红:
--- FAIL: TestHmapdevTemplateImplementsAllPluginMgrMethods
bridge_sync_test.go:170: 读模板: open
../../../third_party/homeagent-sdk/tools/hmapdev/templates/proc_main.go.tmpl:
no such file or directory
## 根因:判据依赖了一个**被 gitignore 的路径**
`.gitignore:33` 有 `third_party/homeagent-sdk/tools/`(SDK 仓自治,本仓不跟踪)。
于是:
· 本机(完整 SDK 检出)有该文件 ⇒ 判据通过
· CI(只 checkout 本仓)没有该文件 ⇒ Fatalf ⇒ **必红**
我提交时只在本机跑测试、没看 CI,所以这个问题拖到 PR 才暴露。
## 修法
文件缺失时 `t.Skipf` 而不是 `t.Fatalf`。
★ 这不是"放弃检查":
· 跨仓一致性由 **SDK 仓自己的判据**守着
(tools/hmapdev/scaffold_stdout_test.go 等),那里才是模板的真实归属地
· 本判据在本机开发时**仍然生效**(已验证:有文件时 PASS,缺文件时 SKIP)
## 验证
· 本机(有文件) → PASS(判据真跑)
· 模拟 CI(移走 tools/)→ SKIP(不再 fail)
· 全量 53 包 ok / 0 fail
## 顺带
同目录还有 e2e_template_test.go 也读 tools/,但它是 `//go:build` 门控的
e2e 判据,CI 默认不跑,无需处理。
---------
Co-authored-by: JianFeeeee <JianFeeeee@users.noreply.gitcode.com>
|
2026-10-17 19:18:24 +08:00 |
|
|
|
03d87a4e21
|
docs(commondev): 主管式多仓库开发流水线的设计稿(未实现)
记录一次关于「主管式多仓库开发」的完整设计讨论,供接手者使用。
**状态是设计稿,尚未实现** —— 文档头部已明确标注。
## 为什么写这篇
用户提出:以后由 HomeAgent 作开发主管,人只与主管对接;主管通过一个
commondev 插件管理多平台凭据、项目/分支台账、临时工作目录,并把「拉起
编码 agent → 改 → 自审 → 复审 → 复核 → 提 PR」串起来。
## 已核实的事实(均有可复现命令,见文档 §10 自检清单)
- 本机有 Gitea 1.24.2;TrueAgent 自身 remote 为 gitcode + github
- 插件凭据现状:`config_<plugin>` 明文、`config.db` 权限 644
- **拉起 agent 的三条路**(关键):
- `pi -p` ✓ cwd = 进程 cwd(已实测读出 marker)
- `dsh headless` 逻辑同 CLI,本机凭据失效
- `a2a_query`/`acp_query` ✗ **参数里没有 cwd**,且只对**已在运行**的
agent 生效 —— A2A/ACP 是长驻 agent 模型,与「按任务拉起」不匹配
- 端口 14010/14011 由 pi-web-sessiond 监听,**不是** general pi 的能力
- 内核 `spawn_child` 与本题无关(它是工作式轻量子,参数只有 task/max_turns)
- `core.agent.workdir` 是**全局单值**且只是 cmd_run 的 fallback ——
前序讨论中「给专属 agent 配它」的说法是错的
## 核心设计主张
1. **复核必须两步:正向 + 变异**。用户原话「我定义过单元测试后,插件复核即可」
隐含了「GREEN = 改对了」这个不成立的假设 —— 插件只能跑预先写下的判据,
判据没覆盖的 bug 照样打勾。自动化不会发现假绿,只会让假绿跑得更快更可信。
文档引用了本仓当日实证(协议判据 M5 首轮未报红)作为依据。
2. **主管复审审的是「判据够不够」,不是逐行读 diff** —— 否则主管会变成
更贵的高级码农。
3. **权限模型要让「必须有主管复审」成为机制而非礼貌**:执行侧凭据只能 push
`feat/*`、不能 push main;主管侧才能合 PR。
4. **落地顺序**:第 0 刀先修「能拉起 agent」(当前成功率存疑,与 QQ 会话中
agent 自身遭遇的 403/503 同源),再谈单平台单项目跑通一条真实链路;
测试模板库放最后(它最容易写,也最容易变成摆设)。
文档还列了 5 个需用户拍板的未决问题(独立仓库 vs worktree、主管自己的模型
可用性、判据落地形态、并行任务间依赖、责任边界)。
|
2026-10-13 16:21:31 +08:00 |
|
|
|
cf3d3769b1
|
perf(starmap): WebUI/GUI 星图卡顿修复 + 抽出共享「布局数学」单一源
## 症状与实测
WebUI 星图建图阻塞主线程 9339ms、draw calls 7514;GUI 建图 2466ms、
draw calls 3477。两者在空闲时仍在空转渲染。
## WebUI(internal/plugins/webui/dashboard.js)
- 力导向布局 O(n²) 两两互斥改**网格近邻**,并移入 **Web Worker**
⇒ 建图阻塞 **9339ms → 57ms**
- 每节点光晕 3753 个 draw call → 1 个 InstancedMesh
- 边 966 → 4 个 LineSegments
- 空闲帧跳过 + 星空时间驱动
- 结果:draw calls **7514 → 3760**,空闲阻塞 **99% → 3%**
## GUI(cmd/gui/renderer/app.js)
- draw calls **3477 → 1809**,建图 **2466ms → 943ms**
- 首屏可见性守卫(GUI 默认 chat 视图,星图不该在后台初始化)
- 空闲 12fps → 0.2fps
修 GUI 两个自引 bug:
1. 抄 WebUI 的 `starmapMorphSet`(GUI 实际用 `state.starmapGrown`)⇒
每帧 ReferenceError、星图 0 帧;
2. `camKey` 读 `starmapCam.position` 无守卫(`switchChatPanel` 同步调
`starmapAnimate` 时 cam 为 nil)。**同一缺陷 WebUI 也存在,两处均已补守卫。**
## 共享源(方案 A:只共享「布局数学」这一层)
新增 `shared/starmap/layout.js`(3 个纯函数,零 DOM/全局引用)+
生成器 `tools/starmap-sync.mjs` + 门禁 `make check-starmap-shared`(已进
make test)。三处(共享源 / dashboard.js / app.js)的标记块必须逐字节相同。
**为何只共享这一层**(实测依据):web 星图 54 个函数、gui 24 个,两者同名
函数逐字节相同数为 **0**。GUI 有设备桥/computer use/托盘/自有面板,WebUI 有
侧栏页签体系 —— 共享受众完全不同的 UI 代码是错的,能安全共享的只有布局数学。
**为何用生成器而非运行时加载**:GUI 用 file:// (main.js 的 loadFile),可能
离线;让其拉服务端 dashboard.js 会把「星图能否初始化」绑到网络与登录态上。
**为何 GUI 不用 Web Worker**:file:// 下外链与 Blob Worker 实测都能跑,但不把
「星图能否初始化」押在打包环境的 Worker 行为上。
## 判据
- `cmd/gui/starmap-perf.test.mjs`(21 条,接入 npm test)
- 扩充 `internal/plugins/webui/starmap_perf_test.go`(同源判据 +
checkStarmapSharedBlock)
- 变异 M1/M2/M3 全部报红
## 附带
cmd/gui/renderer/app.js 同时含上一提交的 GUI `system` 事件消费(协议修复),
因同文件交织未再拆分,见提交 fix(clients)。
|
2026-10-13 16:21:04 +08:00 |
|
|
|
50ef27891f
|
fix(clients): 三方客户端与内核的 SSE 协议一致性 —— 幽灵类型、发了没人收、客户端漏接
一次三方(CLI/GUI/鸿蒙)与内核的协议审计,发现四处**全部静默**的不一致
(无任何测试或日志会报),本次一并修复并把它们变成可执行判据。
## 四处不一致
1. `agent_error` 是**幽灵类型**:SSE 白名单订阅了它、鸿蒙还写了处理分支
(显示「本轮处理失败」),但全仓**无任何发布者** —— events 包与 SDK 都
没有这个常量。于是那段分支永远不可达,而真正的失败帧叫 `error`
(webui/cli 都发 `{type:"error"}`),鸿蒙从未接住。
2. `system` **发了没人收**:internal/plugin/dynamic_proc.go 一直在发插件崩溃
(注释还写着「webui/诊断插件可订阅」),却不在 SSE 白名单里 ⇒ 事件发出来
就掉地上,插件崩溃对用户完全不可见。
3. CLI 缺 `stage`/`system`:CLI 是唯一看不到阶段进展的客户端,agent 跑长任务
时终端长时间静默,用户分不清「在干活」还是「卡住了」。
4. 鸿蒙设备桥**静默丢弃**:只处理 bind_ack 与 cmd,服务端下行的
push / cmd_speech_start / cmd_speech_end / hello_ack 直接 return ——
「收到了但不支持」与「根本没收到」在日志上完全一样,无从排查。
## 修法(注意:三方客户端的通信模型不同,清单不可一刀切)
CLI 走 InjectTextSync **同步拿最终回复**(response 帧)+ 事件订阅做过程输出,
**不做 SSE 重连** ⇒ 它需要 stage/tool_call/reasoning/delta,
但**不需要** agent_output(正文来自同步回执)与 sync_required(那是 SSE
断线重连专用的补拉信号)。GUI/鸿蒙是真 SSE 客户端,两者都需要全量对话事件。
第一版判据按同一份清单要求三者,误报了 CLI —— 那是判据的错,不是代码的错。
- handler_chat.go:subTypes 删 `agent_error`、补 `system`
- dashboard.js / ChatSse.ets:补 `system` 消费(WebUI 用 toast,鸿蒙用 setStage)
- ChatSse.ets:幽灵分支 `agent_error` 改为真实类型 `error`
- cli/plugin.go:补 `stage`(过滤 `_consolidation_`)与 `system` 订阅
- cmd/waiter:补 Phase/Event/Plugin 字段与 stage/system 渲染
- DeviceBridge.ets:静默丢弃改为带 hilog 日志(区分「不支持」与「没收到」)
## 判据(internal/plugins/webui/handler_sse_protocol_test.go,7 条)
TestSSESubTypesAreRealEventTypes(幽灵类型)/
TestEveryPublishedEventHasAnOutwardPath(发了没人收,例外须显式声明理由)/
TestClientsRecognizeServerEventTypes(按客户端分档)/
TestNoPhantomFrameTypes / TestStageFrameFieldsMatchServer /
TestSseWhitelistParsesToExpectedSet(**解析器反向自检** —— 解析器失效会让整组
判据静默变成空集并全绿,是最危险的假绿)/
TestDeviceBridgeOpCoverage。已接入 `make check-client-protocol`(进 make test)。
变异 M1–M8 全部验证。其中 M5 首轮未报红:判据只查前缀 `json:"phase`,
把 tag 改成 `phaseX` 仍匹配 ⇒ 改为匹配**完整 tag** 后才咬住。
## 端到端实测(不只看测试绿)
- 真实 SSE 流收到 stage×15 / content_delta×111 / memory_access×3 /
agent_llm_chain / tool_call×2,**agent_error 0 次**(幽灵已清)
- kill 一个 plugin.bin → SSE 收到 `event: system`
`{"event":"plugin_crashed","plugin":"a2a"}`,日志确认「自动重启 a2a」
- WebUI 真弹出 toast「插件 acp 异常退出,内核正在重启它」(MutationObserver 捕获)
|
2026-10-13 16:20:28 +08:00 |
|
|
|
ec035951cc
|
fix(agent): 批内附加消息延迟到整批落完,修复上游 tool_calls 配对 400
生产实测(llmsproxy tier 1,每 5 秒持续复现,非偶发):
api error 400: An assistant message with 'tool_calls' must be followed by
tool messages responding to each 'tool_call_id'.
(insufficient tool messages following tool_calls message)
根因在 stepToolAfter:它在**批内循环**里对每个工具各调一次,却当场追加
两类附加消息(多模态注入 mediaMsg=user、记忆召回=system)。于是 N 个工具
的批变成
assistant tool_calls=[c1,c2]
tool c1
user ← 插在中间
tool c2 ← 被挤开,与 assistant 不再相邻
上游按「紧跟 assistant 的连续 tool 消息」计数,只数到 1 条却声明了 2 条。
修复:TaskFrame 增加 batchAttachments / batchRecall 积压附加消息,
新增 flushBatchAttachments() 只在**整批 tool 消息全部落完之后**调用。
两个调用点:并行路径 stepToolBatch 收尾循环之后;串行路径 stepToolBegin
发现批已耗尽时。
这也落实了「多模态注入点应在当前轮工具调用之后」这一设计约束。
判据(internal/agent/core/toolcall_adjacency_test.go):
- assertToolCallsAdjacent —— assistant(tool_calls=[c1..cn]) 之后必须**紧接**
n 条 role=tool 且 id 一一对应。既有 TestBatchToolCallIDsAllPaired 只断言
**数量**配对(其注释明确写着「不断言消息的物理排列」),而数量对、排列错
上游照样 400。
- assertMediaNotSilentlyDropped —— 注入的媒体不得静默丢失。
- 三条用例覆盖串行路径 / 并行路径 / 最小回归。
变异验证(M1 当场追加 media / M3 删并行 flush / M4 删串行 flush)全部报红。
其中两轮暴露「判据太松」并已补强:
1. 测试工具未声明 ParallelSafe ⇒ 整批降级走串行,并行判据形同虚设。
补并行专用判据 + 前置断言(不满足即 t.Fatalf,不静默跳过)。
2. 仅断言相邻性时,删掉 flush 反而"更相邻"(附加消息压根没进来),
判据全绿而媒体被**静默丢弃** ⇒ 补 assertMediaNotSilentlyDropped。
教训:只断言一个性质会漏掉另一个,相邻性与不丢失必须同时断言。
验证:core 包三次全绿 + -race 干净;全仓 48 包 0 FAIL;
端到端真实多工具并行对话正常完成,日志 insufficient tool messages = 0 次。
|
2026-10-13 16:18:13 +08:00 |
|
|
|
c7da8fde82
|
perf(webui): 星图力导向布局改网格近邻 + Web Worker,消除主线程 10.9s 阻塞
生产库 3754 块时,buildChatStarmapGraph 在内联脚本里同步跑 50 轮全对全
(O(n²))斥力:每轮 704 万次配对,50 轮共 3.52 亿次。CDP Profiler 实测
单次阻塞主线程 10.9 秒,页面启动首帧 11.8 秒;PerformanceObserver 在
40 秒窗口统计到累计 40,005ms 长任务。
两条独立问题叠加,本提交分别解决:
① O(n²) → 网格近邻(O(n·k))
斥力体里本来就有 `if (d < 25)` —— 只对近邻施力,全对全是白算。
改为 3×3×3 网格分桶,只遍历邻近格。实测 3754 节点 7016ms → ~120ms。
② 同线程 → Web Worker
即使算量合理也不该占主线程。计算整体移入
static/starmap-layout-worker.js,主线程零阻塞。
配套:迭代数按 n 自适应(小图跑满 50 轮,大图退到 14 轮)+ 迭代间早停
(阈值取平均间距的 0.2%,随规模自适应)。
★ 串行回退(file://、老浏览器、Worker 构造失败)必须用**同一算法**。
若回退走旧全对全,两条路径会给出不同布局 —— 「有 Worker 和没 Worker
看到的图不一样」极难排查。故两边各写一份逐字对应的实现。
★ 另修:renderChatStarmap 必须在容器不可见时提前返回(offsetParent)。
starmapActiveContainer 会 fallback 到聊天面板,导致未打开星图也被迫
建图,把聊天页签一起拖死。
判据 TestStarmapNoPerFrameFullScan 同时覆盖两处实现,经三向变异验证:
① 回退改回 Object.keys 全对全 → FAIL
② Worker 去掉 CUTOFF 网格法 → FAIL
③ 删除 Worker 文件 → FAIL
|
2026-10-13 10:12:57 +08:00 |
|
|
|
ceb8a7b165
|
refactor(proc)!: 版本身份只由版本号本身承载,删除全部自定版本号
内核侧配合 SDK 仓 b2872f3:兼容性不再依赖任何自造版本号。
删除三处自造号:
- ProtocolVersion(协议号)→ 改为比对主版本段(MajorVersion)
- shmVersion(共享段 header)→ 改为校验描述符个数(field_count)
- unifiedVersion(SuperBlock)→ 结构自描述,不再校验
握手契约随之调整:
HandshakeParams: protocol(int) → core_version(string)
HandshakeResult: protocol(int) → sdk_version 承担判据
内核比对 MajorVersion(meta.Version) == MajorVersion(res.SDKVersion),
跨主版本即拒并提示重编(主版本跃迁 = 破坏性变更的定义)。
为什么废除:额外的号需要人去记得升,而它没有任何结构性约束兜底,
必然腐烂。实证:ProtocolVersion 常量在多次布局变更里只升过一次,
对应的守卫测试长期失败无人察觉(期望 18/1,实际 19/2)。
新增判据:
- TestProcess_MajorVersionMismatchRejected:跨主版本被拒(testdata/badproto)
- TestProcess_SameMajorDifferentMinorAccepted:同主版本放行(新增 testdata/oldminor)
后者是 §七「1.x 内纯追加、不调新面不用重编」规约的落地 —— 若误用全等
比较,未重编的存量插件会被无谓地全部拒绝。
两条均经变异验证:改成全等比较 → 前者过/后者挂;删掉校验 → 前者挂。
|
2026-10-13 09:39:34 +08:00 |
|
|
|
31de3921ed
|
Merge branch 'fix/proc-extra-frame-passthrough' into refactor/block-union-payload
|
2026-10-13 08:51:11 +08:00 |
|
|
|
1d843daf2a
|
fix(guard): 心跳提前启动 + 首次同步写入 —— 启动竞态的 worker 侧根修
562ba6bd 修了 guard 侧(给旧 mtime 加启动宽限,不误杀),那只是防御。
**根因**在 worker 侧:心跳文件要等全部重型初始化完(原 main 尾部)才
首写,且首次写是异步(在 goroutine 里,与多模态空间/知识库重建竞争
调度)。停机 >30s 后重启时 mtime 停在上一代 worker,guard 的 5s ticker
先到就 SIGKILL。
两层独立生效:
worker 侧(本提交,治本):dataDir 一确定就启动心跳,首次写同步
guard 侧(562ba6bd,防御):保留启动宽限期容忍旧 mtime
## 改动
- main.go:startHeartbeat 从尾部(190)移到 dataDir 解析后(54),
用 context.Background()(心跳生命周期=进程,与 agent ctx 解耦)。
- bootstrap.go:startHeartbeat 首次 writeHB() 提到 goroutine 之外
同步执行——返回前 mtime 必已变新,main 往下做慢事之前 guard 看到的
是新心跳。
## 判据(TestHeartbeatFirstWriteSynchronous,变异测试通过)
(a) 返回后立即查 mtime:必须是新的(同步首写),不许等到第一个 tick
首写移回 goroutine(异步)→ (a) 当场 FAIL,已实测
(b) 5s 后 mtime 再变(ticker 在跑)
(c) stop() 后不再更新(hbStop 生效,不泄漏 goroutine)
判据覆盖完整生命周期:启动即新 → 周期更新 → 停止即停。
cmd/homed 包全量测试通过;全仓生产编译 GREEN。
|
2026-10-12 19:32:04 +08:00 |
|
|
|
562ba6bd98
|
fix(guard): 启动宽限期 —— 停机重启后不再误杀加载中的 worker
## 故障(2026-10-12 部署实测,6 连杀 + 服务降级)
停机 34s 后重启:heartbeat 文件 mtime 停在**上一代 worker**(已远超
30s 阈值)。新 worker 要先做完全部重型初始化(chineseclip 512 +
两个静态词向量 + 知识重建,冷启动 5–8s)才写第一次心跳
(startHeartbeat 在 main.go:190,位于所有初始化之后)。
而 guard 的 5s ticker 先到、拿旧 mtime 判 stale → 把还在加载的
worker SIGKILL → 循环重启,guard 进入 failback Safe-Mode:
19:00:28 worker up pid=931403 boot=normal
19:00:33 heartbeat stale for pid=931403 ... SIGKILL ← 加载中
19:00:33 worker up pid=931557 boot=normal
19:00:38 heartbeat stale ... SIGKILL
...(6 连杀后)
19:00:43 3 failures >= max 3, entering failback (Safe-Mode)
worker 自身日志完全健康(graph/multimodal/knowledge 依次就绪),
纯死于"还没来得及写第一次心跳"。
## 根因
heartbeatStale 对「文件不存在」宽限,但对「文件存在、mtime 是
上一代的」不宽限——停机超过 heartbeatTimeout 的每一次重启都会踩。
旧内核未暴露只因它从未停机这么久(上一次是系统重启,mtime 新鲜)。
既有缺陷,block-union 重构未触碰该逻辑(main 分支同病)。
## 修法
spawn 后给一个 heartbeatTimeout 的**启动宽限期**:期间即使心跳
文件 mtime 极旧也不判杀。宽限期满后恢复原判定(卡死 worker 靠
宽限躲不过去)。决策抽成纯函数 shouldKillStaleHeartbeat。
## 判据(TestStartupGraceAllowsInit,变异测试通过)
(a) 宽限期内 + mtime 极旧(上一代遗留)→ 不许杀
(末沿 -50ms 也钉住:ticker 恰在 deadline 前触发是常态)
(b) 宽限期满 + mtime 极旧(真卡死)→ 必须杀
(c) 宽限期满 + mtime 新鲜(健康)→ 不许杀
变异(移除宽限判定、直接 heartbeatStale)→ (a) 两条断言当场 FAIL;
恢复后全绿。TestHeartbeatStale(三分支既有判据)不受影响。
cmd/homed 包全量测试通过;全仓生产编译 GREEN。
|
2026-10-12 19:16:57 +08:00 |
|
|
|
baf2ecd7c9
|
test(memory): 块 union 的并发压测(字节与块同表之后的并发热点)
块 union 化把「元数据」与「负载」合并进同一行(memory_blocks.data BLOB),
原本分属两个存储(media.db 元数据 + blobs 文件)的读写现在全压到同一张表、
同一把 g.mu。这条改动是否引入锁争用/丢更新/字节错位,只有并发实测能回答。
7 条压测(全部 -race 通过,约 26 万次并发读、0 撕裂 0 丢失):
1. ConcurrentPutSameContent —— 16 写者同内容:只留 1 行且字节完整
2. LeanRewriteMustNotBlankData —— ★ 瘦块(blockColumns 不含 data)重写
已回填块时,ON CONFLICT 不得把 data 覆盖成空。这正是生产踩过的坑,
变异(把 `CASE WHEN ... THEN memory_blocks.data` 改成 `= excluded.data`)
当场被这条判据抓住。
3. ConcurrentReadWriteNoTear —— 8 读者 + 4 写者(改向量):16456 次读
0 撕裂 0 丢失
4. BackfillConcurrentWithRecall —— 回填(单事务)与读取并发:101033 次读
0 错字节;终态候选清零
5. DeleteConcurrentWithRead —— 用真实删除路径 DeleteEntity:143k 次读
只能看到「完整内容」或「不存在」;终态块与负载一起消失(union 两半同生共灭)
6. ConcurrentSetVector —— 12 写者并发写向量:仍是合法 JSON、维度正确、字节未动
7. ManyBlocksLargePayloads —— 120 块 × 256KB = 30MB 反复读写逐字节校验
★ 为何删除压测不用媒体块:生产**没有**按块 ID 删媒体块的路径。删除发生在
实体级(DeleteEntity 按 text_content 匹配)与孤块清理(PurgeOrphans 只扫
text_content != ''),所以用 DeleteEntity 这个真实路径。
|
2026-10-12 17:37:08 +08:00 |
|