|
|
7cef7df6f2
|
test: 判据改为面向接口输入输出 + 文档立规 + CI 补缺口
本提交是三件事的第一批,后续按计划分步推进。
## 一、判据改造(两条实证)
### 1. turn_hint 判据原是自证循环
原测试在测试里**手动重放**了真实注入逻辑:
f.Msgs = a.buildMessages(...)
hint := stageHint(f.StageCtx, "turn_hint")
f.Msgs = append(f.Msgs, Message{Role:"system", Content:"[Turn hints] "+hint})
// 然后断言"最后一条是 system、内容含 [Turn hints]"
它断言的是**它自己刚 append 的那一行** —— 把 task.go:656-661 抄进测试
再验证抄得对不对。真实代码那段删掉,它照样绿。
改为:把 turn_hint 放进 StageCtx,跑**真实的 stepPrepare**
(newTaskFrame 起始就是 StepPrepare),检查它产出的 Msgs。
变异验证:删掉 task.go 里 `if hint := stageHint(...)` 那段 ⇒ 实测红。
### 2. webui 文件下发判据原是白盒
原测试直接调未导出的 `stageWebFile(src, isImage)`、断言 `webFilesDir`
包级变量 —— 那都是**实现方式**。
改为从**输出通道 handler** 走(`RegisterOutputChannel("webui", …)`,
agent 发图/发文件时走的就是它):
输入:{payload: "<路径或URL>", type: "image"|"file"}
输出:{status:"ok", url:"/files/<name>.<ext>", size:N}
4 条判据(新版 handler_files_contract_test.go):
· 图片 → 可下载 URL(前缀/扩展名/size)
· 远端 URL 原样透传(不下载)
· 危险扩展名被中和(.html 保留会在 webui 域执行脚本)
· 空 payload 被拒
变异验证:丢扩展名 ⇒ 红;远端也拷贝 ⇒ 红。
## 二、文档立规:engineering-lessons 新增第五节
原来只有「判据会骗人」,没有「判据本身形状错了」。新增 6 小节:
5.1 判据必须把被测模块当黑箱(含"如果重写内部实现、接口不变,
这条判据还会绿吗"这个可操作判据)
5.2 禁止读源码做断言(附正确替代表)
5.2.1 判据不得以文档为依据
5.3 测试文件与业务代码分离(现状:521 文件 / 11.2 万行,
agent/core 一个包 196 文件里 144 个是测试)
5.4 CI 必须覆盖每个独立 module
5.5 CI 与本地必须跑同一套门
§5.2.1 的直接触发:`interrupt_invariant_test.go` 里有一条判据写着
「事实清单(来自探测,见 docs/zh/interrupt-subsystem-split-plan.md §1.5)」
—— 被引用方自己标着「状态:计划(未实施)」,而那条判据的实际断言
只是"入队后队列里 1 条",与声称的"反向依赖现状快照"毫无关系。
该判据已删除(入队行为已由 scheduler_preempt_test 的黑箱断言覆盖)。
## 三、CI 补两个缺口
### gofmt 门
提交纪律要求 `gofmt -l` 干净,但三个 workflow 里 `gofmt` 出现 **0 次**
—— 纪律只靠人自觉。补上后未格式化的代码进不了主干。
只查本仓自管目录:SDK 是独立仓,格式由它自己的 CI 管。
### SDK 判据步骤
`third_party/hologramagentsdk/` 有自己独立的 go.mod:
go list ./... | grep third_party → 0 个
主仓 `go test ./...` **永远不包含** SDK 任何测试。实测 SDK 有 30 个判据
文件,主仓 CI 一次都没跑过。
补的步骤只跑主仓 git 跟踪的文件(CI 上只有 22 个 vendored 文件,
工作机上是完整独立仓)—— 实测 `git archive` 出 22 个文件后
`go test ./sdk/ ./meta/` 全绿。
## 四、SDK 仓测试迁移(独立仓,已单独提交 3a38ef5)
`sdk/{plugin,proxy,stress}_test.go` → `sdk/test/`,包名 `sdk` → `test`。
关键:原测试直接写 `&PluginSDK{regStage: X, name: "test"}` —— **私有字段**。
而 `New(name, sett, regTool, regStage, regAPI, regOutput)` 本来就把所有
registrar 作为参数,等价且是生产路径。共改 19 处,0 处剩余。
主仓同步更新了跟踪路径(这三个文件主仓也 vendor 了一份)。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 62 包全过,0 FAIL
干净检出(worktree) build + 改造后的判据全绿
## 后续(按计划分步)
步骤 2 webui 29 个测试分类迁移(已实测出精确分类)
步骤 3 agent/core 739 处白盒引用先分类再动
步骤 4 SDK tools/hmapdev(package main,无法迁出,只加说明)
步骤 5 CI 补 coverage + staticcheck
步骤 6 文档双语(需定术语口径)
|
2026-11-04 18:11:06 +08:00 |
|
|
|
07d35297a6
|
fix(webui): 反代三处缺陷 —— 入口不显示、子域链接失效、前缀重定向逃逸
全部来自本机 systemd 实例的实测,不是推测。
1. 插件页「服务入口」永远显示"暂无"
TABS.plugins.fetch 漏了 proxyServices,而 renderPlugins 第一行就调
renderProxyServicesCard()(读 state.proxyServices)。于是该页签
永远不请求 /api/v1/proxy/services —— journalctl 实测:用户正常使用
期间零请求,只有我手工 curl 的那一条。
判据 TestProxyServicesReachesPluginsTab 钉住三环(A 调用 B、B 读
state、C fetch 含块名),已做变异验证(删掉该项立刻红)。
不用静态调用图做通用依赖检查:实测它在 dashboard.js 上不可靠
(中文注释引号破坏配平、分发器展开导致大量误报),已在注释里
写明原因,避免后人重复踩。
2. 面板生成的子域链接 100% 进不了反代
生成链接走 base_url 的主机名,路由匹配只认 base_domain。
生产实例 base_url=homeagent.jianfgit.xyz、base_domain=localhost,
于是链接落到门户 mux → 302 /login,而手拼 *.localhost 反而能进。
新增 proxyBaseDomains 返回全部候选基域(配置值 → base_url 主机名
→ localhost),serveProxyHost 用它匹配。两者都认,不是二选一:
*.localhost 是 RFC 6761 的零配置基座,不能丢。
3. 前缀模式下上游根绝对路径重定向逃出挂载点
实测 NapCat:上游 GET / → 301 Location: /webui,浏览器跟随到
https://<门户>/webui(已跑出 /napcat),门户 mux 接住 → 302 /login。
httputil.ReverseProxy 不改写 Location,在 ModifyResponse 里补:
只改以 / 开头且未带前缀的,避开 //host 与已经带前缀的(否则会出现
/napcat/napcat/webui)。
顺带纠正:base_domain 保持原配置不动,改由代码兼容两种基域 ——
改配置会抢走别的 *.jianfgit.xyz 子域,且 *.jianfgit.xyz 是单层通配,
huawei-smarthome.homeagent.jianfgit.xyz 本来就证书不匹配(实测
verify error 62)。要外网可用子域形态,应把 base_domain 配成
jianfgit.xyz,那是运维决策,不在本次改动内。
|
2026-11-04 17:15:43 +08:00 |
|
|
|
1b6cd57dca
|
fix(skillmgr): 内核旧版 skill 永不更新 —— 无 stamp 不等于"用户改过"
## 缺陷(实测,非推测)
给 kernel-plugins 补上 `skill_info(name, section=...)` 的用法说明后,
改了嵌入内容,但**生产上永远不会更新**:
/home/hologramagent/skills/kernel-plugins/ mtime 2026-10-26,无 .builtin-sha256
嵌入版本 1.1.0 vs 磁盘版本 1.0.0
`materializeBuiltinSkills` 的判断链是:
builtinSkillUnmodified(dir) → 读 .builtin-sha256
没有 stamp ⇒ false
⇒ continue("用户改过或无法确认,保留人的版本")
而 stamp 机制是 387ae94(今天早些时候)引入的。生产机上这份 skill
是**更早的内核**写的 —— 它没有 stamp,于是被永久判为"来历不明"。
后果不只是这一份文档:任何在 stamp 机制之前部署的机器,其内核自带
skill 都停在旧版。而 kernel-* 系列正是教模型怎么用工具/记忆/终端的
文档 —— 内核改进了文档,模型永远看不到。
## 修法:用 frontmatter version 区分两种"没有 stamp"
原判据把两件事混成一件:
用户改过措辞 ⇒ 必须保留
内核在 stamp 之前写的 ⇒ 应该升级
新增 builtinSkillOutdated(dir, embedded):只在**没有 stamp** 时被调用,
判据是磁盘 frontmatter 的 `version` 严格低于内嵌版本。
安全性分析(写在代码注释里):
人改措辞 通常不动 version ⇒ 不低于 ⇒ 保留 ✓
人把 version 改高 ⇒ 保留 ✓
内核旧版(无 stamp) ⇒ 升级 ✓(这正是要修的)
解析不出版本号 ⇒ 返回 false ⇒ 保留(保守)
误升级的唯一条件是"人把 version 改低",而那本身就意味着放弃内核版本。
原有的四条保守判据(含 NoStampIsConservative)全部保持绿。
## 判据抓到了我自己写的 bug
TestCompareVersion 立刻红了:
compareVersion("1.0","1.0.0")=-1,期望 0
原因:缺失段留成空串 ⇒ Atoi("") 失败 ⇒ 落到字符串比较 ⇒ `"" < "0"`
⇒ 版本号少写一段的 skill 会被无谓地重复升级。改为缺失段视为 "0"。
## 变异验证
builtinSkillOutdated 恒 return false(= 修复前行为)
⇒ TestMaterializeBuiltinSkills_UpgradesStaleKernelVersion 红:
"★ 没有 stamp 的内核旧版(version 0.0.1)没被升级"
compareVersion 缺失段改回空串
⇒ TestCompareVersion 红:"compareVersion(\"1.0\",\"1.0.0\")=-1,期望 0"
## 顺带:kernel-plugins v1.1.0
补一节「Reading a skill without burning context」,说明:
- 长文档返回目录 + 开头 30 行,目录每行带 (N 行)
- `section` 可按编号或标题子串取,大小写不敏感,容忍 `##` 前缀
- 顶层章节返回整棵子树
- 未命中会列出全部可用章节
- 章节边界尊重围栏代码块
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 62 包全过,0 FAIL
干净检出(worktree) build + skillmgr 全绿
(包数 63 → 62 是因 clawhubadapter 已在 a0077a2 迁出为外部插件,与本改动无关)
|
2026-11-04 15:50:39 +08:00 |
|
|
|
a0077a2bad
|
refactor(plugin): clawhubadapter 迁出为外部插件
## 为什么迁
它是唯一还留在内核仓的「兼容层」插件,而它的全部工作都是对接**外部生态**
(ClawHub/OpenClaw 的包格式),跟内核版本无关。留在仓里有两个实际问题:
· 外部生态的新包格式跟进要走内核发版
· 它必须跟着内核一起重新编译,而它并不依赖内核内部实现
## 迁移改动(逻辑一行没动,只换接口形态)
package clawhubadapter → package main
init()+RegisterFactory → NewPluginFactory(proc 运行时按名调用)
internal/sdk → hologramagentsdk/sdk
internal/safeclose.Safe → 自写 safeClose[T](泛型覆盖 3 种 channel)
InjectInputSync(4 参) → InjectInputSync(3 参, 返回 string)
s.Events().Publish → 删除(外部插件拿不到,也不需要)
## 为什么删掉 Events().Publish 而不是找替代
原实现靠它把纯 SKILL 条目移交 skillmgr。查清后发现**不需要**:
pluginmgr 在安装与每次启动时扫 `<插件目录>/skill/` 自己发 skill_detected
(internal/plugins/pluginmgr/skill_handout.go)。外部插件带 skill 走
工程根 skill/ 目录,与插件能不能发事件是两件事。
链路上没有一行来自插件自身,实测日志:
[pluginmgr] clawhubadapter: handed off skill clawhub-tools to skillmgr
[skillmgr] registered via skill_detected event: clawhub-tools
[plugin] loaded SKILL: clawhub-tools v1.0.0
## 验证
迁移工程 /home/program/clawhubadapter-plugin/clawhubadapter
go vet / gofmt 干净
go test -race ok(20 条原有 + 4 条外部形态判据)
hmapdev build packaged clawhubadapter_bundle.hmap
变异验证 4 条判据全部真红
隔离实例端到端(真实安装路径,非模拟):
registering tool: clawhubadapter_* × 10
[plugin] loaded: clawhubadapter
[plugin] loaded SKILL: clawhub-tools v1.0.0
内核仓:go build / vet / gofmt / go test -race 全绿,62 包
## 生产切换的注意点
本 commit 只删源码,**不改生产**。生产 homed 里仍编着内置版;
要真切换必须先装 .hmap 再重编 homed,否则等于把这个插件弄没了。
|
2026-11-04 15:23:53 +08:00 |
|
|
|
67be114647
|
feat(skillmgr): skill_info 支持按章节读取 —— 长 SKILL 不再一次吞一万 token
## 问题
skill_info 此前**整份返回** SKILL.md。生产最坏一例:
commondev-discipline v2.3.0 → 983 行 / 53810 字节 / ≈10600 token
模型为查「怎么开 PR」(§14,21 行)必须把 hmap 插件开发纪律(320 行)
和红牌一起读进上下文。一次误读一万 token,还挤掉了真正要用的历史。
拆 skill(一个插件声明多个)能解决"跨文档绑定",但解决不了
"单文档内部按需读" —— 本提交补的就是后者。
## 改法
skill_info(name, section=...) 只返回匹配的章节。不传 section 时:
短文档(<=200 行) 直接返回全文(加目录只是噪音)
长文档(>200 行) 返回「目录 + 开头 30 行」,模型据此决定读哪节
目录每行都标 `(N 行)` —— 行数不是装饰,它是模型在"一次吞一万 token"
和"先读目录"之间做选择的唯一依据。
## 三个实现要点
① **切分必须感知围栏代码块**
SKILL.md 里有大量 shell 注释:
```bash
# 期望 200;401/403 = 凭据无效
curl -s ...
```
硬按 `#` 切会把一条 curl 示例拆成两半 —— 模型拿到的是无法执行的
碎片,比不给还糟。parseSections 跟踪 ``` / ~~~ 围栏,块内不切。
② **返回行号区间,不复制字符串**
章节内容的唯一事实来源仍是文件本身,目录与正文用同一套边界计算,
不会漂移成两份。
③ **点名顶层章节给整棵子树**
问 `## Setup` 时要的是 §11–§16 全部,不是只有标题那一行。
边界 = 下一个**同级或更浅**的标题。
## 判据(均经变异验证)
TestSectionsRebuildEqualsOriginal ★ 主判据
所有区间拼接必须逐字节等于原文
一次同时抓漏行 / 重行 / 错位三类缺陷
变异:end 改 heads[i+1].line-1 ⇒ "原文 16 行,拼回 13 行"(实测红)
TestSectionsShellCommentIsNotHeading
变异:删掉 fence 判断 ⇒ "代码块内的 shell 注释被当成标题:
\"期望 200;401/403 = 凭据无效\"(行 4)"(实测红)
TestResolveSectionReturnsWholeSubtree
变异:`<=` 改 `<` ⇒ "Setup 越界吃进了 Usage 的内容"(实测红)
TestResolveSectionToleratesHashPrefix
模型常把 `## Setup: git 操作` 整行传进来,必须容错
TestRenderTOCListsLevels
目录只列到 maxLevel,且必须带行数
## 判据本身最容易写废的地方(已避开)
① 用人造短文档测切分 —— 那种文档没有 shell 注释、没有嵌套层级、
没有围栏代码块,测过了不代表能切真实文件。
主判据**用生产那份 983 行的真实文件**。
② 只断言"切出 N 个章节" —— 数量对不代表边界对。
## 真实数据验证
文档 983 行 → 切出 46 个章节(## 顶层 7 个)
区间拼接 == 原文(逐字节)
section="审批" → 21 行(§16)
section="Setup" → 123 行(含 §11–§16 全部子节)
section="hmap" → 320 行
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 63 包全过,0 FAIL
干净检出(worktree) build + skillmgr 全绿
|
2026-11-04 15:06:04 +08:00 |
|
|
|
a8ce5ece30
|
feat(agentcli): 补 skill 文档 —— 终端 vs 一次性命令的边界
## 为什么补这个
真实生产事故:agent 把 terminal_create 当成一次性命令执行器用。
terminal_create(command='cd /usr/lib/node_modules/@deepseek-ai && echo "=== 已安装的 dsh 包 ===" && ls -d */ | head -60 && …')
→ read /dev/ptmx: input/output error
→ 再开 term_7、term_8、term_9 …
三层成因:
① cmd 插件 auto_start=false ⇒ 工具表里没有 cmd_run,需先 plugin_start cmd
② terminal_create 的 command 参数描述写着"要执行的命令",语义上像"跑一条命令"
③ 没有文档说清"一次性命令该用 cmd_run、终端是给要持续输入的程序用的"
①③ 已分别修掉(cmd-tools skill + 本文件)。② 是设计上的引力:
参数名叫 command、描述说"要执行的命令",模型自然会塞复合命令进去。
文档要正面抵消。
## 实现方式
按 seq 的既有模式:内置插件实现 plugin.SkillProvider,
由内核统一写盘/幂等/事件/停用清理(internal/plugin/builtin_skill.go)。
· 新增 internal/plugins/agentcli/skill.go
· BuiltinSkills() 交出正文,生命周期与插件同生共死
(禁用插件后 skill 还留着 = 模型看得到说明、点进去工具却不存在)
## 文档的核心:可操作判据,不是抽象词
一句判据:**跑完就结束 → cmd_run。要一直活着、要看屏幕、要按键 → terminal。**
关键的三条(缺任何一条模型都会退回旧行为):
· 点明 cmd 插件要 plugin_start(否则模型找不到 cmd_run 就用终端)
· 点明复合命令(cd && echo && ls)属于一次性,不是"所以要用终端"
· 解释 /dev/ptmx 那个真实报错的含义(命令跑完终端已退出),
否则模型会以为终端坏了
索引行(模型每轮唯一能看到的一行)也带边界:
"交互式终端(vim/ssh/top/长驻进程)。一次性的 shell 命令不要用终端,用 cmd_run。"
## 判据(均经变异验证)
TestSkillProviderContract 编译期断言 SkillProvider + 正文非空
TestSkillSeparatesTerminalFromOneShot ★ 六条必备内容缺一即红
TestSkillIndexLineStatesBoundary 索引行必须指出替代方案
TestSkillBodyFirstParagraphStandalone 正文首段(真正注入提示词的那段)能独立看懂
变异验证(实测):把整份文档换成"终端可以执行各种命令"的弱化版本
⇒ 3 条同时红。
★ 第一版变异是"只改一句话",判据没红 —— 因为 cmd_run 在文中另有 13 处。
那次变异无效,不算验证。改成整份替换后才是真验证。
## 端到端验证(独立实例 + 假 LLM 捕获真实请求)
[plugin] agentcli: installed skill "agentcli-terminal" -> .../skills/agentcli-terminal
[clawhubadapter] skill agentcli-terminal detected, handed off to skillmgr
实际注入到 system prompt 的 [Skills] 段:
agentcli-terminal v1.0.0 - 终端给**要持续输入**的程序用(vim、ssh、top、REPL、
要按键确认的安装器);一次性的 shell 命令用 **cmd_run**(cmd 插件,需…
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 63 包全过,0 FAIL
干净检出(git worktree) build + 判据全绿
独立实例实测 真机装盘 + 真机注入提示词(见上)
|
2026-11-04 13:22:07 +08:00 |
|
|
|
5bdd0b5107
|
refactor(security): Store 复用 Codec 加密能力 + 删 globalCodec 单例
## 用户裁定
> store也应该调用security的加密能力!
## 改动前的事实(实测)
两套**完全重复**的 AES-256-GCM:
Store.seal/open store.go:372,388 裸 base64
Codec.Seal/Open codec.go:113,139 enc:v1: 前缀 + 域隔离 + 幂等
算法、nonce 布局(前置 + io.ReadFull(rand))、密钥环轮换逻辑全都一样,
只有 Codec 多了前缀与域隔离。
危害不是"代码重复"这么轻:
· 两套实现各自演化,任何一侧的改动不会被另一侧的判据发现
· Store 缺域隔离 ⇒ 变量库与配置共用密钥(安全倒退)
· Store 缺前缀 ⇒ 无法区分"旧密文"与"明文"(惰性迁移的障碍)
## 本次做的
### Store 委托 Codec
codecOf() 按当前 keys 环惰性构建(轮换后自动跟上)
sealWithRing() 新格式(enc:v1: 前缀)
openWithStoreRing() 兼容新旧两种格式,返回 needsMigrate
删除 Store 自带的 seal/open/openWithRing —— 现在**只有一处** AES-GCM。
### 域隔离(安全判据)
storeCodecDomain = "security/variables"
configCodecDomain = "config-at-rest"
两者必须不同:同域时一处明文泄漏可推导另一处密文。
### 存量密文惰性迁移
旧格式是裸 base64 无前缀,而 Codec.Open 对无前缀输入**原样返回明文**
(那是给 config 的存量明文兼容)。直接交给 Open 会把密文当明文返回,
症状是"凭据回填出一个 base64 串"。
所以 Store 走 openWithStoreRing:
enc:v1:... → Codec.Open
裸 base64 → Codec.OpenLegacy(解开后立刻回写新格式)
不做启动时全量迁移(21G 数据目录的教训:迁移窗口越长越危险),
也不让两种格式永久共存(判据永不能要求"落盘都是新格式")。
### 删 globalCodec 单例 → 归 Runtime
security 模块 → rt.SetCodec(c) → ks.Security
config 模块 → cfgReg.SetCodec(ks.Security.Codec())
ConfigRegistry 加 codec 字段(注入而非读全局),6 处 ConfigCodec() 改为
r.codecOf();PluginSettings 经 p.registry 转调。
sdk.SecretsAPI 也加 codec 参数(插件加密能力来自它的内核实例)。
### 顺带
· InitConfigCodec* / ClearConfigCodec 删除,改 NewConfigCodec 返回实例
· homed-key-rotate 改为 构造 + reg.SetCodec(不再装全局)
· bootstrap_order_test 的锚点从 InitConfigCodecWithOldKeys 改为
NewConfigCodec(守的不变量不变,只是生效点写法变了)
## 判据(均经变异验证)
TestNoPackageLevelSingletons 禁表补入 globalCodec
TestStoreWritesPrefixedCiphertext 新密文必须带 enc:v1:(旧实现裸 base64 ⇒ 红)
TestStoreAndConfigUseDifferentDomains 域隔离;改成同域 ⇒ 红(实测确认)
TestLegacyCiphertextStillReadableAndMigrates 旧格式可读 + 落盘已升级
TestRotationKeepsOldCiphertextReadable 轮换后旧条目仍可回填
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 63 包全过,0 FAIL
干净检出(git worktree) build + security/config 测试全绿
## 数据风险
加密实现切换有丢数据风险,因此:
· 先写往返判据,再改实现
· 惰性迁移(读到才升级),不做启动全量重写
· 旧格式与旧密钥环**都保留**读取路径
|
2026-11-04 12:22:08 +08:00 |
|
|
|
cc0eb51286
|
refactor(kernel): 统一挂载点 kstate + 插件声明式系统提示词段
## 起因(生产故障)
A2A 分诊助手(驻留子)把 terminal_create 当一次性命令执行器:
terminal_create(command='cd /usr/lib/node_modules/@deepseek-ai && ls -d */ | head -60 && …')
→ read /dev/ptmx: input/output error
→ 再开 term_7、term_8、term_9 …
链路拆开四层:
① cmd 插件 auto_start=false ⇒ 工具表里没有 cmd_run
② 需要 cmd-tools skill 说明"先 plugin_start cmd"
③ 但 skill 索引没进那个 agent 的提示词
④ 因为 skillIndex 只是根 agent 的字段、由 bootstrap 手工接线
④ 才是总根。而它之所以必须手工接线:agent/core 读不到内核的共享数据。
## 环的精确形状(实测,非推测)
kernelboot 因为持有 KernelState 而依赖 19 个业务包:
kernelboot ──▶ agent/core ──▶ …
──▶ plugin
──▶ security
──▶ memory / knowledge / text / coldcontext / …
而 kernelboot.go / scheduler.go / funcmodule.go / syncbus.go / lifecycle.go
**一行业务代码都不需要** —— 依赖全在 kernelstate.go 这一个数据文件里。
后果是四个底层包各自造通道:
core → Agent 字段(每种 agent 构造点都要记得传)
plugin → 12 个 SetXxx
sdk → SDKConfig 传参
security → 包级单例 Global() / RuntimeGlobal() / ConfigCodec()
四条通道是同一个根因的四个表现。
## 本次做的事
### 1. 抽出 internal/kstate(断环)
kernelstate.go / syncbus.go / lifecycle.go 搬出 kernelboot:
internal/kstate 纯数据 + 唯一挂载点(叶子,谁都能 import)
internal/kernelboot 装配引擎,零业务包
挂载点 API:Mount / Get(nil 安全)/ MountForTest(返回 restore,并行测试隔离)。
验证:core 现在能 import kstate(此前 import cycle not allowed)。
### 2. StageHost / Agent 字段存 any
它们是 core 自己的类型(core 是环的一极,这是真环)。按用户裁定的方向:
> 是其他包在kstate挂共享内容啊
存 any,消费方断言回具体类型 —— 与本仓既有的 PluginCatalog /
PluginScheduler / UpdateCapabilities 同型。
★ 其余字段保持**具体类型**,没有一律改 any。判据是"它会不会反向依赖
agent/core":实测 plugin / security / sdk 都不依赖 core,所以它们作为
具体类型不构成环,而类型化字段让"拼错/取错类型"是编译错误(本结构体的
设计原则)。
### 3. 插件声明式系统提示词段
SDK 新增 SystemPromptAPI(见 submodule 提交)。内核侧:
kstate.State.PromptSections *promptsec.Registry ← 唯一事实来源
Agent.renderSystemPrompt() 每轮重建时读它
skillmgr 从"实现 SkillIndexProvider 让内核来问"改为"声明自己的段":
DeclarePromptSection{Section:"skillmgr.skills", Order:100, Volatile:true}
并挂到全部变更点(启动全扫后、移交事件后、load/unload/create/install/
enable/disable 后、Stop 时撤销)。
### 4. 内核自身的提示词段也走同一管理器
按用户裁定「把现在内核所有 systemprompt 的相关注入都接入到这个统一的
systemprompt 管理器上」,把 buildSystemPrompt 里硬拼的十几块
(人格卡/任务提示词/记忆说明/输出规则/工具目录…)改为声明式:
kernelSections() 当轮计算 → 与插件段按 Order 合并渲染。
顺序由 Order 常量决定,改结构变成调常量而不是挪代码块。
## 判据(均经变异验证)
TestCoreCanImportKstate 真编译探针;环重现则红(实测)
TestKstateDoesNotDependOnCore 静态版,指向具体字段
TestKstateIsLeaf kstate 不得反依赖上层
TestRTChildSeesPromptSections 子 agent 能看到插件段
TestResidentSeesPromptSections 生产实际中招的那一类;变异后红(实测)
TestRootAgentSeesPromptSections 确认没改坏根路径
TestNoPromptSectionsMount 未装配时不 panic、不凭空出现段
TestDeclareTwiceRendersOnce ★ 回应"会不会多份":声明 5 次渲染 1 份
TestSectionRemovalTakesEffect Stop 后段消失
TestRuntimeUpdateVisibleToExistingAgent 同一实例下一轮看到新内容
TestSectionOrderIsRespected Order 生效且稳定
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l internal/ cmd/ pkg/ 干净
go test ./... -count=1 63 包全过,0 FAIL
干净检出(git worktree) build + 4 包测试全绿
## 未做(见 kernelplan.md)
T3 security 三单例收敛(含 Store 复用 Codec 的惰性迁移)
T5 plugin Setter 收敛
T6 防腐判据(部分已随本次落地)
|
2026-11-04 11:23:05 +08:00 |
|
|
|
387ae945df
|
fix(agent): 子 agent 继承技能索引 —— 修 agent 把终端当命令执行器用
## 缺陷(生产实测)
`SetSkillIndexProvider` 只在 cmd/homed/bootstrap.go 里对**根 agent** 调了一次。
rtchild / 驻留子各自 `New(AgentConfig{...})` 构造载体,都没传这个字段 ⇒
子的 system prompt 里**完全没有 skill 索引**。
症状(日志实录):A2A 会话由「临时分诊助手」(驻留子)接手,它手上没有
`cmd_run`(cmd 插件 `auto_start=false`),而 `cmd-tools` skill 正文里写着
「先 plugin_start cmd」。它看不到这条 skill,于是把 `terminal_create` 当成
命令执行器:每条 `cd … && echo … && grep …` 开一个新终端,终端随命令结束退出,
`terminal_read` 立刻撞 `read /dev/ptmx: input/output error`,然后为下一条命令
再开一个 —— 终端列表被填满,每一条都死在半路。
与本仓 Security 那次同型:父有的能力,子没继承。
## 修法
rtchild 与驻留子的 AgentConfig 各补一行 `SkillIndexProvider: a.skillIndex`,
与 `SystemPrompt` / `StageHost` / `Security` 同一组「必须继承父」的字段。
## 顺带两件事
### 1. 内置 skill 的更新永远到不了已部署机器
原落盘规则是「目标目录已存在就跳过」,本意是保住用户改动。但它把两件事混成一件:
用户改过的版本 ⇒ 应该保留
内核自己写过的旧版 ⇒ 应该随内核更新
两者都表现为「目录已存在」,于是 b850fea 把 `memory_recall` 改成
`stablemem_recall` 后,生产机的 kernel-memory 一直停在旧名,模型照着
旧名调工具必然失败,而日志里一个字都没有。
改为用 `.builtin-sha256` 摘要区分:
当前内容 == stamp ⇒ 没人动过 ⇒ 可安全覆盖成新版
当前内容 != stamp ⇒ 用户改过 ⇒ 保留
没有 stamp(老目录)⇒ 保守视为用户版本,不覆盖
### 2. 新增 kernel-terminal skill
工具描述只说「适用于交互式程序」,没说何时**不该**用;`command` 参数又鼓励
传整条命令。补一条内置 skill 讲清 cmd_run 与 terminal_create 的边界、
以及终端的 input/key/notify 用法。描述控制在 80 字符内(进索引不截断)。
## 判据(均经变异验证)
TestRTChild_InheritsSkillIndex 删掉接线 ⇒ 红(实测确认)
TestResident_InheritsSkillIndex 同上(生产实际中招的那一类)
TestRootAgent_SkillIndexStillInjected 确认没改坏根 agent 路径
TestChildWithoutSkillIndexIsNotBroken 父无索引时子不 panic
TestMaterializeBuiltinSkills_* 5 条:覆盖语义四条成组断言
(改回旧行为 ⇒ OverwritesUnmodified 红,实测确认)
判据测的是**生成的 prompt**,不是 `skillIndex != nil`:
后者接上了但注入那段被删照样绿。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 62 包全过,0 FAIL
干净检出(git worktree)单独验证 PASS
|
2026-11-04 10:17:45 +08:00 |
|
|
|
ba8a33dad8
|
fix(agentcli): 终端默认不推送 + input/key 可同传 + 失效终端结构化答复
## 一、默认不主动推送(用户裁定)
「agentcli 没必要进行直接推送,因为本质它就类似人类开启的终端窗口」
终端窗口的语义是:人敲命令 → 自己去看输出。输出不该主动跳到眼前。
主动注入虽然走 inputCh(排队、不抢占),但**每次都会唤醒 agent 跑一轮
推理**;`tail -f` / `npm install` 这类长输出命令会让它被反复唤醒。
改动:
- terminal_create 默认 watch 从 `{onExit:true}` 改为 `{onExit:false, quiet:true}`
- 结束/退出/超时清理一律静默,输出只由 terminal_read 主动读
- 需要推送必须**显式**声明 notify=exit / interval=Ns / buffer=N
- terminal_watch 的 clear 语义改为「回到默认(不推送)」,
不再自动恢复 exit 提醒
- 删掉「没配 interval/buffer/quiet 就自动 onExit」的兜底(它会让默认
静默被悄悄推翻)
## 二、input 与 key 可以同时提交(真 bug)
旧实现是互斥结构:
if key != "" { ... } else if input != "" { ... }
于是 `{"input":"ls","key":"enter"}` 只发出 `\r`,**input 被静默丢弃**。
模型收到 status=ok / bytes=1 以为成功,去读输出却是空的,于是反复重试
——这就是「终端没反应」的直接成因。
现在按声明顺序发送:先 input,再 key。整对写入在同一把会话锁
(新增 TerminalSession.writeMu)内完成,避免同终端并发写插进中间把命令拆坏。
## 三、失效终端给结构化答复
「终端不存在或已关闭」从一句文本改为:
{"error":"terminal_gone","terminal":"...","next_action":"调用 terminal_create 重建..."}
原因与 browser 插件同形态:只给普通失败时,模型会拿同一个 id 反复重试。
覆盖 terminal_write/read/resize/close/watch 五处。
## 四、通道注册不再吞错
- _ = s.RegisterInputChannel("agentcli", sdk.ChannelDef{})
+ if err := s.RegisterInputChannel(...); err != nil { return fmt.Errorf(...) }
注册失败时该通道根本不存在,之后所有 InjectText 都投进虚空 ——
通知静默消失且日志无痕。宁可启动失败也不静默降级。
## 判据(均已变异验证可红)
- TestWriteSendsInputThenKey 断言 input 与 \r 两段都真的写进终端
- TestWriteInputOnlyUnchanged 只传 input 时不额外发键
- TestTerminalCreateDefaultsToNoPush 默认 watch 必须不推送
- TestExplicitExitNotifyStillWorks 显式 notify=exit 仍然生效
- TestWatchClearRestoresNoPush clear 回到默认而非 exit
- TestTerminalGoneIsStructured 五个工具都返回 terminal_gone + next_action
变异验证:
- 默认改回 onExit:true → TestTerminalCreateDefaultsToNoPush 红
- input 分支不发送 → TestWriteSendsInputThenKey 红(实际 1 次: ["\r"])
- 错误码改成其它文本 → TestTerminalGoneIsStructured 红
## 验证
go build ./... → 0
go vet ./... → 0
gofmt -l internal/plugins/agentcli/ → 干净
go test ./... -count=1 -race → 62 ok / 0 FAIL / 0 DATA RACE
|
2026-11-04 09:51:22 +08:00 |
|
|
|
2b2707cb89
|
test(agent): 让 rt 跨进程 e2e 真正跑起来 + 策略撤销改为行为判据
## 一、e2e 一直 SKIP,等于从未验证
`TestExternalRTChild_*` 4 条判据长期 SKIP(缺插件产物)。本次用
`third_party/hologramagentsdk/example/rtvoice/build/plugin.bin`
实跑,立刻暴露两处**测试与插件不同步**:
rtvoice_inject 插件已改名为 <name>_say ⇒ 注入失败
rtvoice_dump 插件从来没有这个工具 ⇒ 观测失败
改名发生在插件侧,而判据长期不运行,所以没人发现。这正是
"SKIP 判据等于没有判据"的实证。
修法:
· 注入改用 `rtvoice-e2e_say`(插件当前唯一调试入口,走与麦克风
完全相同的 p.Push → InjectInterruptTextOpts 路径)
· 输出观测改为对 outputch 设备做一次**跨进程 RPC**(channelDevice
句柄 → 插件进程 → 拿回执),不依赖插件内部私有状态
## 二、策略撤销判据从源码扫描改为行为断言
`TestChildTeardownRevokesPolicy` 原先扫源码文本,找停止函数体里有没有
`revokeChildPolicy`。R2/R3 把撤销移进骨架 Cleanup 后文本匹配失效 ——
但行为是对的,判据自己变红。
这暴露出它抓不住的真实风险:`StopDeps.RevokePolicy` 是**注入**的,
漏填时 Cleanup 里的 nil 检查会静默跳过,而字符串仍在别处,源码扫描照样绿。
现在分两层:
· kernelmod/all 保留源码断言,但钉**两个环节成对**:
① Cleanup 真的调用 deps.RevokePolicy
② 各类型真的注入它
并增加"单数与复数必须共用骨架 Stop"的结构护栏
· agent/core 新增行为判据 childpolicy_test.go:
声明策略 → 走真实停止路径 → 主体表里必须消失
## 三、验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 62 包全过,0 FAIL
go test ./... -count=1 -race 0 FAIL
rt 跨进程 e2e(4 条,不再 SKIP):
TestExternalRTChild_FullChain PASS(rtchild 跑了 1 轮)
TestExternalRTChild_ReuseDoesNotLeakTranscript PASS
TestExternalRTChild_BulkStopUnregistersInbound PASS
TestExternalRTChild_RepeatedCyclesDoNotAccumulate PASS
变异验证:断开 routeInputByOwner 的 rt 分支 ⇒
FullChain 立即红,并准确报出"输入没有到达 rtSlot"
干净检出(git worktree)单独 build + vet + test + e2e PASS
|
2026-11-04 08:56:47 +08:00 |
|
|
|
81a0f5f598
|
feat(security): 插件按工具声明内容策略 + 目录名统一
## 一、插件声明层(用户裁定)
> 「敏感词策略也应该是插件声明啊,每个插件声明自己轮次以及自己工具的
> 敏感词策略」
SecurityProfile 新增两个字段:
ToolSensitiveWords map[string][]string // key=工具名模式(尾 *),value=词表
DetectInjection bool // 开启内核内置注入迹象识别
语义是**追加不覆盖**:整轮 SensitiveWords 仍然生效。
取宽不取窄的理由:当前处置是 B 档(标记+警示,不阻断),
多标一条只是多一句警示,漏标则是安全缺口。
## 二、修掉我自己造的「莲藕式分层」
前一版把 *security.Round 从 runInputTask 一路搬到工具链
(prepareInputTask(evt, round) → TaskFrame.SecRound →
executeToolCallOutcomeWithRound),并在 rtchild 里另补一条。
那是把**分层策略实现成跨层传参**:每多一条工具执行路径就要求调用方
多传一次句柄。profileActivator 的设计注释早就写明了正路:
> 工具调用发生在 LLM 决定之后,此时插件已不在调用栈上。
> 唯一可靠的传递方式是**内核在输入进来时记住**,在该轮的工具判定里使用。
改为 security 自取:
Runtime.ScanContentForAgent(agentID, isChild, ins, text)
→ ForAgent(agentID, isChild) // per-agent 视图,天然不串档
→ EffectiveProfile() // session > longtime
→ 合并 全局词表 + 档位词表 + 工具词表 + DetectInjection
core 只递交**事实**(我是谁 / 哪个工具 / 什么文本),不认识 Round。
与 CheckToolForAgent 同一套身份推断,不引入第二个入口语义。
TaskFrame.SecRound / executeToolCallOutcomeWithRound 全部撤除。
## 三、目录名统一
third_party/homeagent-sdk → third_party/hologramagentsdk
module 与远端仓早已是 hologramagentsdk,只剩物理目录是遗留名。
同步改了 go.mod replace、构建/打包/部署脚本、5 处测试里的分段路径。
注意:这里改的是**主仓 vendored 的 SDK 源码路径**,example/ 由 SDK
独立仓自己跟踪,主仓不涉及。
## 判据
TestToolSensitiveWordsAppendOnlyForThatTool 词表只对目标工具生效 + 追加语义
TestToolSensitiveWordsUseAgentScope security 按 agent scope 自取生效
(变异验证:删掉 ForAgent 查询 → 红)
## 验证
go build ./... → 0
go vet ./... → 0
go test ./... -count=1 -race → 62 ok / 0 FAIL / 0 DATA RACE
gofmt -l internal/ → 干净
|
2026-11-03 09:51:11 +08:00 |
|
|
|
cc07adb20b
|
style(webui): dashboard.js 格式化(换行与引号风格)
编辑器/格式化器在保存时改的,只动换行与引号,无语义变化:
· 一行多条件的 if 拆成多行
· 含双引号的字符串改用双引号包裹(避免转义)
· 长字符串拼接拆分
13 insertions / 4 deletions,逐行核过无逻辑改动。
|
2026-10-31 16:20:47 +08:00 |
|
|
|
f9ee58a914
|
merge: pr31 — update 回滚安全(原子写 + 回读校验),解 19 处冲突
pr31 与 main 是**双向分叉**,不是单向落后。`ba281a8` 已把它的多数工作
在 main 上重新实现(dataDir 守卫、gc.go、dataDir 解析),但两边各自又
往前走了,所以 6 个文件 19 处冲突要逐条判。
## 合进来的(main 缺)
- **标记原子写**:main 是 `os.WriteFile` 直接覆写,掉电留半截标记;
改为 `写 .tmp → fsync → close → rename → fsync 目录`
- **H4**:`publishResult` 里 `if errMsg != "" { _ = errMsg }` 把失败原因
丢了,订阅方只看到 ok=false。现在 error 字段恒定存在
- **回读校验补最低可用性**:main 对无 digest 的旧 marker 只比"内容与备份
一致",而**零字节备份内容也一致** ⇒ 判成回滚成功、不清 marker 的时机
也错过了。补"非空 + 带可执行位",刻意**不**要求 `--version`
(老内核没这个 flag,那正是 PR #31 的回归)
- `kernel_apply_test.go`(836 行,含 B2c `TestMarkPendingUpgrade_PreservesOldMarkerOnFailure`)
## 保留 main 的(pr31 在这几处是旧版或更差)
- **回滚校验用 digest**(`verifyRestored`)而不是 `--version`:
pr31 要求回滚后能读版本,而 1.3.x 及以前没有 `--version`,
而它们恰恰是最常见的回滚目标 ⇒ 从旧版升级失败后永久回滚不了
- **`restoreDepGraph` 三态**:原来有+备份有→还原、原来有+备份无→不算成功、
原来无→删掉新装的。pr31 只处理第一种,会导致「旧 homed + 新 depgraph」
的崩溃循环
- **marker 在还原完整后才清**:pr31 在还原前就清,还原失败即失去第二次自愈
- **CLI 不重启服务**:pr31 仍带 `restartUnit`,会杀掉整个 cgroup 连带 guard
- **`resolveDataDir`**:pr31 的 `upgradeDataDir` 是它的子集,多扫几个候选前缀
## 冲突处理中的三处重复声明
两边各加了同名函数,自动合并把它们都留下了:
- `readBinVersionStrict` —— 删 pr31 的(main 在 77 行有)
- `kernelDataDirMu/kernelDataDir/SetKernelDataDir/KernelDataDir` —— 删 kernel.go
里那份(main 在 install.go)
- `newPkgRoot` —— pr31 的重命名 `newPkgRootFromBin`,两套测试各自可用
- rollback.go 里 `verifyRestored` 与 `readBinVersionStrict` 两段校验并存 —— 删后者
顺带修掉删除时误伤的 `ShortSum` 注释,并删掉 pr31 引入但无人调用的
`DataDirEnv`(main 明确不再拿 env 当 dataDir 来源)。
## 验证
go build ./... ✓
go vet ./... ✓
go test ./... -count=1 62 ok / 0 fail
go test -race ./internal/update/ ... ✓
gofmt -l 干净
|
2026-10-31 15:56:10 +08:00 |
|
|
|
acaf0887c7
|
fix(webui): 聊天图片"加载成功却显示加载失败"——HTML 属性里的反斜杠转义
## 缺陷
生产截图:图片加载成功了(img 元素在、src 正确),界面却多显示一行
蓝字「图片加载失败"/>」。
根因是内联事件属性里用了反斜杠转义:
onerror="this.parentElement.innerHTML='<span class=\"att-err\">图片加载失败</span>'"
**HTML 属性值里没有反斜杠转义** —— 那个 \" 就是引号本身,属性在
`<span class=` 之后就闭合了。浏览器切出来的是:
onerror = "this.parentElement.innerHTML='<span class="
剩下的 `att-err">图片加载失败</span>'"/>` 变成**文本节点**直接显示。
症状极具误导性:看起来像"图片坏了",实际图片是好的;而那个 onerror
因为语法错误**永不执行** —— 真失败时反而什么都不显示。
## 修法
不用内联属性(那要求以后每次拼 HTML 都记得转义规则),改成
**捕获阶段的事件委托**:
- error 事件**不冒泡**,只能在捕获阶段拿到(注释里写明了)
- 用 createElement + textContent 构造占位,不再拼 innerHTML 字符串
- 只处理 .chat-attachment-img,不影响正文里的 markdown 图片
## 顺带:两个类没有 CSS
`chat-attachment-img` 与 `att-err` 只出现在 JS 里,CSS 里一条都没有。
图片靠内联 style 撑着还能看,失败占位则完全没有样式。已补。
## 判据(2 条,变异验证)
- `TestChatAttachmentImgHasNoBrokenInlineHandler`:不许内联 onerror、
必须用 DOM API、必须有 error 监听
- `TestDashboardNoEscapedQuoteInHTMLAttributes`:通用扫描,
任何 `name="...\"...` 形态都会红(下次别人写内联事件也拦得住)
变异:把内联 onerror 放回去 ⇒ 两条同时红。
★ 写判据时踩到的坑(都已修):判据被**自己的注释**打红(注释里必须写出
错误写法来解释),需先用已有的 stripJSComments 剥离;另外我先自己写了一个
stripJSComments,与 dashboard_js_scope_test.go 的重名 —— 应复用已有的。
## 顺带修一条脆弱判据
`TestRTChild_RapidInputCollapsesToLatest` 全量跑时偶发红
("连发 50 条却跑了 5 轮")。
探针查实:连发 50 条只花 ~80µs,而测试 provider 的 ChatStream **同步瞬间
返回**,两者同量级 ⇒ 消费方能在连发过程中抢到几次执行。探针显示
dropped=48,collapse 明明生效了。
所以 `Rounds > 3` 是在测调度抖动,不是测契约。改为断言**覆盖主导**
(dropped >= burst/2)。真实通话每轮几十毫秒,连发微秒级,生产里
"轮次≈1" 自然成立。
变异验证:让 dropped 不计数 ⇒ 判据红(dropped=0(连发 50 条))。
go build/vet/gofmt 干净;go test ./... -count=1 -race = 62 ok / 0 FAIL / 0 RACE
|
2026-10-31 10:03:43 +08:00 |
|
|
|
fbccfe17ed
|
fix(rtchild): 同名复用串线 + 批量停止漏注销
三处缺陷,全部在「回收/复用」这条线上 —— 单实例 e2e 天然测不到:
1. RTTranscript 读序(主线)
原实现先查留档、命中即返回。留档以 id 为键而 id 由模型给,于是
回收后同名重建,rt_transcript 返回的永远是上一通电话的原文,
且完全不报错。改为「在跑实例优先,无活实例才落回留档」。
2. StopRTChildren 漏注销入站登记
单数路径会 Unregister(child/rt-<id>),复数路径没有。登记表共享、
按 name 全局唯一,残留随 create/destroy 单调累积,而外部没有任何
工具能单独注销一条 inputch。
3. 撤回一个错误的修法
一度在 SpawnRTChild 里 delete 留档。变异测试证明删掉它判据仍绿
(治不了串线),且有害:留档的意义正是「回收后父按需取原文」,
提前删掉会造成「推了信号却拉不到正文」。现仅在 CloseRTChild 写
同名键时自然覆写。
判据(均通过变异验证,去掉修复必红):
- internal/agent/core/rtchild_reuse_test.go(进程内,5 条)
- internal/plugins/rtchild_reuse_e2e_test.go(跨进程真插件,3 条)
★ 写判据时踩到两个坑,一并记在注释里:
- 前两版判据假绿(先塞留档被 Spawn 清理掉 / 新实例不 push 导致
「读到旧内容」与「读到空」都是 len==0),靠变异才发现没测到真东西
- 等 Rounds 不等于历史已入账(rtRunOnce 先 round++ 后 rtAppendHistory),
与 dff745c 同一个坑;已改为等 history 条数
|
2026-10-30 21:54:40 +08:00 |
|
|
|
bcf45498a0
|
feat(skill): 补 kernel-rtchild + 两条判据锁住 rt_children 静默失败
## kernel-rtchild skill
`resident_agents` 有 `resident-subagent-hygiene` skill,而
`rt_children`/`rt_transcript` 这一对整个**没有 skill** —— 模型只能靠
工具描述猜语义。
最容易踩的坑:**工具执行期不可打断**。模型一旦进入工具执行期就听不见
新输入,所以调工具前必须先用 outputch 说一句"稍等",否则用户会觉得
"它没在听我说话",而实际上它只是卡在一个 3 秒的命令里。
skill 覆盖:三种子 agent 的分界、通道必须成对且两侧声明 rt、打断语义、
只出文本(TTS 是插件的活)、窗口裁剪但原文保留、回收后用 rt_transcript
分段取、闲置零 token 但必须 stop、四条回收判据、五种常见错误对照表。
## 两条判据(都来自一次真实事故)
生产 e2e 时我用 `input="..."`(工具声明的是 `inputch`)创建 rtchild:
`strArg` 只查精确键名 ⇒ 读到空串 ⇒ **本应报"必须成对给出",但它成功了**,
还把 output 通道划给了新子。之后灌语音,该子轮次永远是 0。
症状是"通道注册了、插件活着、推送 ok、但子没反应",
与真因(参数名拼错)隔了三层。
- `WrongParamNameIsRejectedNotSilent`:写错必须失败、必须指名正确参数名、
必须无副作用(不建子、不动通道归属)
- `PairAssignmentIsAtomic`:成对分配第二个失败时第一个必须回滚
(原来那句 `rollback()` 没有判据覆盖,是死代码)
变异验证:让代码对 `input`/`output` 静默兜底 ⇒ 第一条变红
(`参数名写错却创建成功了:[typo-child] —— 这会得到一个收不到输入的哑子`)。
go build/vet/gofmt 干净;go test ./... -count=1 -race = 62 ok / 0 FAIL / 0 RACE
|
2026-10-30 18:09:50 +08:00 |
|
|
|
eb1deab5dd
|
feat(agent): rtchild 实时子 agent + 四类 agent 独立模型源 + 残余任务四态
## rtchild(R1–R13)
第三种、**独立**的子 agent 类型,面向语音/视频通话:内核只要文本流,
不定音频(TTS/播放由插件负责)。
- R1 CapRT 能力位 + AgentConfig.IsRealtime;AssignRealtime 两侧校验
- R2 rtSlot 单槽覆盖(latest-wins,从不排队)
- R3 独立 ctx + rtLoop + SpawnRTChild/StopRTChild
- R4 ChatStream 逐块 → outputch(rt).Execute(text)
- R5 打断 = 原地截断,不重试
- R6 工具执行期不可打断(suppressedCancels 让守卫可观测)
- R7 普通上下文 + 窗口裁剪(旧内容不进模型,**原文保留**)
- R9 CloseRTChild → L4 信号 + rt_transcript 按需分页取
- R10 notify_parent(L4,注册一次,停即注销)
- R11 闲置零 token
- R13 rt_children 管理工具(父专属)
刻意**不做** rtPage/anchor/degraded_resets(过设计,已裁定);
不用四级中断阶梯(preemptCooldown+同级不抢占会让 ~75% 的 rt 打断
静默退化为排队)。
## 四类 agent 各自可配模型源
core.agent.llm.{main,resident,rt,child}_provider,语义是**优先不是独占**:
该源排到候选链最前,其余源仍作回退。硬锁定会让"首选源抖一下"变成
"这个 agent 彻底不工作",而多源回退链正是本仓为此建立的机制。
名字写错 ⇒ 回退默认源 + 记日志(不静默)。子 agent 未配置时继承父的。
★ 顺带修掉一个真缺陷:spawn.go 原来直接 a.provider.Chat(),
**普通子 agent 完全绕过 provider 回退链** —— 它是唯一绕过的地方:
默认源抖一次整条子任务就"执行失败",而主 agent 同刻能自动换源。
## 驻留子残余任务四态 + 通道三态
残余任务:keep / takeover(接手,keep 的明确别名)/ stash(暂存到各自
原通道)/ drop。stash 必须**逐条按各自 OutputChannel 归位** —— 一个子
可能同时收 qq 和 webui,混存会让"接回 qq 的人"拿到本该给 webui 的活。
通道:release(零值,向后兼容)/ suspend(挂起,暂存保留)/ takeover
(接回父并立即释放暂存)。原来是无条件 release,于是只有两种坏结局:
通道一直没人分配(消息无人处理且无提示),或刚好被别的 agent 抢到。
一次定完不留窗口:FinishResidentWith → DestroyResidentWith →
teardownResidentWith。任一环退回不带 disp 的旧函数,disp 就被丢掉、
通道被无条件释放 —— 而 stash 的暂存正挂在那条通道上(实际踩过)。
## 修复"手写字段白名单"丢值(7 处)
同一类缺陷:不传整个结构体,而是手写一份"允许通过的字段清单"。
危险在于**写漏了不报错** —— 字段静默归零,故障出现在很远的地方。
- corehandler.go outputRegister: ChannelDef{NoMemory,Cleaner} → 整体传
- pubSdkInjectOpts: 逐字段摊平丢 ScenePolicy/SecurityProfile → 收整个 injectParams
- injectParams/injectMediaParams: 结构本身缺字段 → 补齐 + asInjectParams()
- 配套反射判据 TestSegment_RoundTripEveryField(取代手写断言,
原判据漏了 Phase/LLMText/FinalText/ToolCalls/ExtraKeyMediaBlocks/ExtraBag)
## 压缩偏离设计
CompressResident 原为截断,与设计(LLM 摘要替换)不符。改走
PlanCompaction → summarizeForCompaction(用子自己的 provider)→
ApplyCompaction,失败回退 TrimKeepRecent。顺带修 ApplyCompaction 的
锁重入 panic 与双重解锁。
并修一处真实生产数据竞争:ctxFullSignaled 由父 goroutine 写、
子调度 goroutine 读,加 ctxFullMu。
## 判据
新增约 70 条。全部验证过能变红(变异测试),包括:
- TestSegment_RoundTripEveryField 的防腐蚀:fullStageContext 反射断言
每个字段非零,否则新字段永远不会被覆盖("假绿")
- TestPreferredProvider_PutsSourceFirstAndKeepsFallbacks 成对断言
"在首位且回退仍在"—— 只断言前者的话,硬锁定也会绿
## 判据自身的坑(两次偶发红,都不是产品缺陷)
1. SpawnResident 末尾 Start(),子会在两次 enqueue 之间取走一条
→ "暂存 0 条"。修法:cancel + waitSchedulerStopped。
(offload_test.go 早有记录,我没先读就重踩了一遍)
2. TaskPrompt 异步投递,可能在清空队列之后才到 → "多 1 条"。
靠探针打印内容才认出是 TaskPrompt。修法:本文件用 spawnQuietResident。
go build/vet/gofmt 干净;go test ./... -count=1 -race = 62 包 ok / 0 FAIL / 0 RACE
|
2026-10-30 17:00:47 +08:00 |
|
|
|
b850feaef2
|
feat: 段页式记忆流转、coldcontext 重命名与安全治理
记忆层:
- MemoryBlock 承载 FrameTS/PageSeq/PageKind/BlockRole/CallID
- coldcontext 归档与回填按完整页传递,工具调用参数和配对键不丢失
- 召回先估算总 token,超预算时按全局页相似度标记 inactive
- inactive 仅是 context 渲染期状态,不落盘、不进入 coldcontext,Prune 后重置
- Peek/确认/Remove 两阶段召回,失败保留冷上下文副本
- stablemem 记录 last_recalled_at,删块连带清理边
命名:
- internal/memory/document → internal/memory/coldcontext
- Doc/DocEvent/DocHit → ColdDoc/ColdEvent/ColdHit
- ContextToColdContext/RecentColdContexts/FindColdContexts
- 保留既有 JSON 标签、doc_ 文件前缀与 doc_count 状态键,兼容存量数据
安全治理:
- 工具策略门禁、安全配置、事件总线发布者身份与入站脱敏
- 声明式 SecurityAPI 与 QQ owner/guest profile
- 日志保留策略与相关 e2e/回归判据
验证:go build ./...、go vet ./...、go test ./...(62 包);
生产副本 e2e:7013 块迁移后 ID/向量/指纹 0 变化、0 缺失。
|
2026-10-29 23:53:48 +08:00 |
|
|
|
ba281a855a
|
feat(update): 差分 OTA + 安装形态分派 + 事件总线统一受安全监控
三条主线,由 e2e 实测驱动。
## 一、差分 OTA(internal/update/delta.go)
实测:改一行逻辑,88MB 的二进制逐字节只差 80 字节,而 OTA 一直下整包。
用 rsync 族的滚动哈希块匹配(4KB 粒度),不是 bsdiff —— 后者需 17×N
峰值内存(88MB → 1.5GB),而升级恰可能发生在内存紧张时。
真实二进制实测:88,036,728 字节 → 补丁 77,316 字节(0.088%)。
三道闸:应用前验旧文件 sha256(错基线会静默产出垃圾)、应用后验结果
sha256、COPY/LITERAL 越界防护。任何失败**退回整包**,不向上抛 ——
差分是优化,不是可用性前提。
接入:Source.Resolve 优先试 delta;CheckRelease 按 -from-<commit8>.delta
匹配(不猜基线);CLI 新增 -make-delta;打包脚本 build_delta(需
PREVIOUS_HOMED),delta 进 SHA256SUMS。
## 二、安装形态分派(internal/update/install.go + channel.go)
内核对「能不能自己升级」此前只有一个答案:直接替换二进制。而 deb/rpm
形态下那样做会让 dpkg/rpm 数据库与实际内容不一致(下次 apt upgrade
覆盖回旧版,或 dpkg 拒绝后续升级)。
新增探测:deb/rpm/tar/container/source 五类形态 + 变体(full/server/
client/toolchain)+ 目录写权限。判据全部来自可观测系统事实(dpkg 的
info 目录、容器标记文件),不做路径推断。
三端统一走 PlanUpgrade 分派;形态闸门放在**下载 880MB 之前**。
## 三、事件总线统一受安全监控(internal/events + security)
用户指正:「eventbus 上所有事件都要接受安全模块的监控」。
此前 EventPolicy 只被 internal/sdk/events.go 调用,而那是四条路径中的
一条。s.Publish / s.Subscribe / s.EventBus() 拿的是裸 *events.Bus,
任何插件都能绕过策略发任意事件(含伪造内核事件)。
现在闸门在 Bus.Publish/Subscribe 本体上(Guard 接口注入,避免 events
↔ security 成环),四条路径全部覆盖。sdk.EventBus() 改返回带身份的
BusHandle。内核身份免授权,但载荷照样过凭据闸门。
白名单按主体收窄:agent_output/stage/terminal_output 只允许承载者
(webui/cli/agentcli)发布 —— 它们是内核向外的输出面,谁都能发就等于
能伪造 agent 说的话。
## e2e 暴露并修掉的三个真实回归
[bus] subscribe denied: pluginmgr type=update_request
[bus] subscribe denied: skillmgr type=skill_detected
前两个让升级请求静默丢失、职责链交接断掉。内部协作事件必须可订阅
(安全边界在发布侧,订阅只是「听得到」)。
## 顺带的止血修复
- 回滚判据从 `--version` 改为 digest:旧内核(1.3.x 及以前)没有该
flag,而它恰是最常见的回滚目标 —— 原逻辑让「从旧版升级失败」永久
无法回滚。
- CopyFile 补 fsync(替换 homed 本体时掉电会留半截文件)。
- restore 完整还原才清 marker(原先先清再还原,还原失败就失去第二次
自愈机会)。
- 回滚时移除升级中新增的 depgraph(否则「旧 homed + 新 depgraph」
会因 digest 不匹配崩溃循环)。
- homed-upgrade 不再硬编码 systemctl restart(丢 guard 状态、unit 名
漂移);改用内核已有的 restartCh → exit 42 语义。
- homed-upgrade 不再用 CGO_ENABLED=0 编却依赖 go-sqlite3(stub 下
静默失效,该路径从未工作过);改用 internal/config。
- 服务单元补 User=hologramagent + 三条安装路径补建用户(此前发行物里
完全没有落实降权)。
- rpm 的 fpm 调用补 --after-install(此前 rpm 装完根本不建用户)。
## 判据
新增 30 条:差分 8(往返/压缩比/错基线/截断/篡改/边界)、通道 8、
执行器 6、事件闸门 8、按主体授权 8。全仓 go test ./... 0 失败。
|
2026-10-29 09:54:55 +08:00 |
|
|
|
1319d6619c
|
fix(update): dataDir 取内核权威源 + 回滚可信校验 + marker 原子写
承上一个 commit(回退 85fd38b)。本 commit 是 R2 主体:B0/B1/B2 三项修复,
以及为 P1–P4 补齐的**行为级**判据。
━━━ B0 · dataDir 不再推导,取内核真实 -data ━━━
`internal/update/kernel.go`
· 删除 ResolveDataDir(env → `<exe>/data` 兜底)。
· 新增 SetKernelDataDir / KernelDataDir:值由内核适配层在启动时
用 ks.DataDir 注入。进程内调用方(pluginmgr / webui)直接取,
不做任何推导;取不到返回**空串**。
· DataDirEnv 保留但降级为诊断参考(不再是合法来源)。
`internal/kernelmod/update/mod.go`
· Init 里 upd.SetKernelDataDir(ks.DataDir);为空时打响亮告警
(不阻断启动 —— 第 0 层职责是回滚判定,不是参数校验)。
`internal/plugins/pluginmgr/kernel_update.go`、`internal/plugins/webui/kernel_update.go`
· kernelDataDir / dataDirOf 都改为 return update.KernelDataDir()。
`cmd/homed-upgrade/main.go`
· upgradeDataDir(explicit):① `-data <dir>`;② config.db 的
core.daemon.data_dir(只读连接 + PRAGMA query_only=1,不建表不迁移)。
· 两者都拿不到 ⇒ 返回空串 ⇒ `-apply` 在动手之前就 fail 并说明怎么修。
→ 这样 P1 的空值守卫重新变成**活的**:拿不到真实 dataDir 就真的会拦。
━━━ B1 · 回滚可信:回读校验是唯一判据(判据 B1)━━━
`internal/update/rollback.go` — RollbackIfUnconfirmed
· rollback.go:315 `rb, verr := readBinVersionStrict(binPath)`
替换后**必须**回读成功且 rb.Version == rec.FromVer,否则
rolled=false + error + **不清 marker**(下次启动再试,并让人看到日志)。
CopyFile 返回 nil 只代表字节搬完了,不代表搬过去的东西能执行。
· rollback.go:254 确认分支同样改用 readBinVersionStrict。
这里曾经用 ReadBinVersion —— 它含 InferVersionFromRunning 回退,
已实测:binPath 是不可执行残文件、marker.ToVer 恰好等于本机 waiter
版本时,会从 waiter **借到**版本号 ⇒ confirmed=true ⇒ 清 marker、
不回滚 ⇒ 机器带着跑不起来的二进制继续启动且失去自愈机会。
该缺陷环境相关(CI 没装 homed/waiter 会绿),故判据不能依赖本机环境。
· 备份缺失 / 回读失败 / 版本不符 / depgraph 回滚失败 —— 四种情形
一律不清 marker。
· 新增 readBinVersionStrict(kernel.go):只认「这个文件自己能报出版本」,
无任何回退;ReadBinVersion 改为其包装器(保留探测用回退),
注释写明它**不能**用作回滚校验。
━━━ B2 · marker 原子 + 损坏不卡启动(判据 B2)━━━
`internal/update/rollback.go` — MarkPendingUpgrade
· 旧实现 os.WriteFile 直接覆写(O_TRUNC),掉电留半截 JSON。
· 现改为:写 <path>.tmp → fsync(文件) → rename(原子) → fsync(目录)。
目录 fsync 失败仅告警(容器/tmpfs 下 EINVAL 不该阻断升级)。
rollback.go:112 `if err := f.Sync(); err != nil {` 即这条链路的第一道闸。
· RollbackIfUnconfirmed 遇到解析不了的 marker:quarantineMarker 改名
留存(不删,保留现场)+ 告警 + return false,"","",nil
⇒ 当作「无待确认升级」继续启动。把内核卡在启动阶段是最差选择:
能执行到这行已证明当前二进制可执行。
`internal/kernelmod/update/mod.go`
· 补注释钉住语义:rerr != nil 必须中止启动;但 marker **损坏**不算
失败(已由 RollbackIfUnconfirmed 吞掉并改名留存)。
━━━ 判据补齐(行为级,修掉 M6 真空洞)━━━
`internal/update/kernel_apply_test.go`
· P1 TestApplyRefusesEmptyDataDir 加强:切到 t.TempDir() 并拍快照,
断言拒绝升级时**根本没写任何文件**(旧用例只查「目标二进制没变」,
把守卫挪到预检之后依旧绿 —— 因为 filepath.Join("", prefix) 会把
备份目录建到 CWD)。
· P3 新增 TestApplyPrunesOldBackups:预置 5 个历史备份后跑真实 Apply,
断言旧的被 GC。★ 这是补 M6 的真空洞 —— 旧的变异脚本只跑
`-run TestPruneOldBackups`(只证明函数能跑,3 毫秒),把 Apply 里
那一行 PruneOldBackups(dataDir) 删掉它照样绿;新脚本必须同时跑
TestApplyPrunesOldBackups,删掉调用后立刻报红。
「函数能跑」≠「Apply 真的调了它」。
· B1 新增 TestRollback_RefusesCorruptBackup(零字节/坏备份 ⇒ 必须
回滚失败且不清 marker)、TestRollback_RefusesVersionMismatch
(备份版本 ≠ FromVer ⇒ 同样失败)、
TestRollback_ConfirmedBranchIgnoresInferredVersion 与
TestConfirmedBranch_StrictProbeBitesOnUnparseable(确认分支不得
使用推断值;用可注入的 inferHomedPath/inferWaiterPath 造出
「能借到版本」的场景,不依赖本机是否装了 homed/waiter)。
· B2 新增 TestMarkPendingUpgrade_IsAtomic、
TestRollback_CorruptMarkerDoesNotBlockBoot。
`internal/update/rollback_test.go`
· TestRollback_RestoresBackup / TestRollback_RollsBackWhenRunningOldVersion
的备份从纯字节 "OLD-GOOD" 改为**真正可执行**的假二进制(1.4.0),
并按版本而非按字节断言。旧的写法只能测「字节搬对了」,测不到
「搬过去的东西能跑」—— 而后者才是回滚能救机器的前提。
`internal/update/kernel.go`
· inferHomedPath / inferWaiterPath 提为包级变量(生产默认值不变),
唯一目的是让「确认分支不得使用推断值」这条判据可稳定复现。
━━━ 判据锚点自检(grep -F 命中数均为 1,行号已核对)━━━
P1 kernel.go:348 if dataDir == "" {
P2 kernel.go:370 backup := filepath.Join(dataDir, BackupDirPrefix+ts)
P3 kernel.go:449 PruneOldBackups(dataDir)
P4 kernel.go:445 publishResult(bus, "kernel", "", true, …)
P4 events.go:69 "error": errMsg,
B1 rollback.go:254 if cur, cerr := readBinVersionStrict(binPath); cerr == nil {
B1 rollback.go:315 rb, verr := readBinVersionStrict(binPath)
B2 rollback.go:112 if err := f.Sync(); err != nil {
验证:go build ./... 通过;go test ./internal/update/ 通过。
|
2026-10-28 17:51:52 +08:00 |
|
|
|
a8ecb8257c
|
revert: 回退 85fd38b(ResolveDataDir 从 exe 猜 dataDir,生产必挂)
This reverts commit 85fd38b7d1.
## 为什么回退
85fd38b 把三端 dataDir 推导统一成 update.ResolveDataDir(exePath):
`HOMEAGENT_DATA_DIR` → `<exe 所在目录>/data` 兜底。方向(三端必须算出
同一个值)是对的,但**兜底那一项在生产上是错的**:
ExecStart=/usr/bin/homed --role=guard --data /var/lib/hologram
ResolveDataDir("/usr/bin/homed") == "/usr/bin/data" ← 与真实值不符
`HOMEAGENT_DATA_DIR` 在现有部署里并没有设置,所以实际生效的就是这个兜底。
后果两个,都是致命的:
① Apply 第一步要在 dataDir 下建备份目录,而 /usr/bin 在
ProtectSystem=strict 下只读 ⇒ EROFS ⇒ 升级必挂
② pending 标记落到内核读不到的位置 ⇒ 升级启动失败后静默不回滚
## 附带问题:空值守卫变成死代码
ecde8dd 的 H1(P1)加的是「dataDir 为空就拒绝升级」。85fd38b 的
ResolveDataDir 兜底**永不为空**(exePath 为空时还会自己去取
KernelBinaryPath),于是三个调用点传进来的 dataDir 恒非空 ——
守卫还在,但守的是一个永远非空的常量,实际不会再拦任何东西。
「不知道」伪装成「知道」比没有守卫更危险:守卫看起来在守,出事时才发现
它从来没生效。所以判据 P1 在 85fd38b 之上是咬不住的。
## 回退范围
只回退 85fd38b 的三个文件(CLI / pluginmgr / webui 的推导逻辑),
回到 ecde8dd 的状态;不影响 ecde8dd 本身。回退后:
· ResolveDataDir 及其所有调用点消失,无悬空引用
· go build ./... 通过
· 三端暂时回到「各端自己推导」——这正是紧随其后的下一个 commit
要替换掉的形态(改为取内核权威源,不做任何推导)
回退 + 后续修复合起来才完整:单看本 commit,dataDir 解析又散回三处;
下一个 commit 用「注入内核真实 -data」取代全部推导。
|
2026-10-28 17:51:16 +08:00 |
|
|
|
85fd38b7d1
|
fix(update): 统一 dataDir 推导,三端共用 update.ResolveDataDir
范围说明:本 commit 与 H1 属同一缺陷的另一面。Apply 现在会拒绝空
dataDir,而三端各自的推导逻辑此前会**真的返回空**(或写死生产路径),
不一起改则 H1 会把所有升级都拒掉、或让 pending 落到内核读不到的地方。
三处原来的推导:
· cmd/homed-upgrade/main.go env → 写死 /home/newqqagent
· pluginmgr/kernel_update.go env → Dir(pluginDir)
· webui/kernel_update.go env → 空(由调用方报错)
问题:① 硬编码 /home/newqqagent 在目录/用户迁移后指向不存在的路径;
② 从 pluginDir 反推与 webui 侧不一致,同一台机器两条升级通道算出
两个 dataDir ⇒ 标记落在 A、第 0 层 update 从 B 读 ⇒ 升级启动
失败时静默不回滚(症状是崩溃循环,极难反查到标记)。
现在三端统一调用 update.ResolveDataDir(exePath):
① HOMEAGENT_DATA_DIR(部署方显式声明,唯一可靠来源)
② <exe 所在目录>/data 兜底(替代原先的硬编码生产路径)
并删掉 cmd/homed-upgrade 里写死的 /home/newqqagent。
本 commit 可单独回退:回退后仅剩「各端自己推导」,与 H1 组合会退化为
「空 dataDir 一律拒绝升级」(安全侧失败,不会产生无回滚能力的升级)。
|
2026-10-28 17:20:40 +08:00 |
|
|
|
1dc32be736
|
fix(memory): 召回可用性——L2 片段带正文 + WebUI 上下文走双路
① L2 预填充此前只给摘要行('已回复老大:原来是这个谐音梗'),模型读得懂
'我回复过'却不知道梗是什么 ⇒ 召回命中仍答不知道。现取两段候选
(事件文本块 + 工具回执),JSON 回执先提取字符串值再压成单行。
② WebUI「上下文」页只显示 L3,而 L3 有 72h 冷化延迟 ⇒ 冷化没跑起来时
面板永远'无上下文',看着像召回彻底坏了(实际 L2 一直有命中)。
现 IndexerAPI 带上 L2 片段(与模型预填充同一 QueryScored),前端两路并列。
判据:internal/agent/core/crossmodal_body_test.go
|
2026-10-27 13:04:19 +08:00 |
|
|
|
981b969f58
|
feat(sdk): 受限事件总线的内核实现 + clawhubadapter 改用它
- internal/sdk/events.go:实现 pubsdk.EventsAPI
· publishAllowed:白名单精确匹配 或 本插件命名空间 plugin.<name>.<后缀>
· subscribeAllowed:只开只读事件(tools_ready/security_ready/device_ready)
· 载荷出向经 security.PrepareOutbound(明文→@{别名}),入向经 ScrubInbound
· 键数上限 64;越界**报错**不静默
· Source 由 SDK 固定成插件名(插件不能冒名)
- PluginSDK.Events() 访问器
- clawhubadapter 的 skill_detected 改走 s.Events().Publish,不再 import internal/events
⇒ 它为外部化的唯一阻塞项(事件发布)已消除
判据(internal/sdk/events_test.go,6 条):
白名单放行 / 越界拒绝(raw_input、agent_output、update_request、stage、system、
别的插件命名空间、空类型)/ 自己命名空间放行 / 订阅越界拒绝 / 载荷凭据被替换成别名 / 键数上限
|
2026-10-27 11:16:40 +08:00 |
|
|
|
c33199cc5a
|
fix(webui,plugin): 内核控制按钮晚绑定 + 按钮布局;plugin_start 报错给候选名
1. 内核控制通道晚绑定(真机 503 实测)
POST /api/v1/kernel/restart 一直返回 503:webui 由 plugin-host 在装配期
拉起,而 main() 里 SetKernelControl 还没执行 ⇒ 插件 Start 拿到 nil。
依赖图保证不了 main 里的语句顺序,所以改成让插件拿**解析器**
(lazyKernelControl),调用那一刻再取真实通道;授权仍在每次调用时
按 kernelControlAllowed/kernelRestartAllowed 复核,不因晚绑定放宽。
2. WebUI 重启/关机按钮布局
原先用 .theme-btn(顶栏图标样式:pill + margin-right + 无文字)混在
侧栏底部的文字按钮里 ⇒ 大小不一、挤在一起、关机看不出是危险操作。
改为与「退出登录」同族的 btn-sm,带图标+文字,关机用 danger 悬停。
3. plugin_start 失败报错
原来只说加载出错,或插件不存在,一个可用名字都不给,模型只能盲猜
(实测把 files 写成 file)。现在给:失败原因分类(目录不存在/没清单/
自身加载失败)、最接近的名字(Levenshtein ≤2)、已安装清单(最多 12)。
4. scripts/migrate-plugins-to-org.sh:插件迁到 GitHub 组织独立仓的脚本收口
测试:go build ./... 通过;go test ./internal/plugin ./internal/plugins/webui 全绿。
|
2026-10-27 01:43:19 +08:00 |
|
|
|
55fa2685ae
|
feat(security,device): security 成为独立内核模块(闸门+策略+限式),device 接管内置工具路由
security(新模块 internal/kernelmod/security)
- 凭据闸门:出向拦截成 @{别名}、入向清洗、执行前回填(internal/security/{gate,runtime}.go)
- 工具黑白名单 + 沙箱(读写根、网络、.. 逃逸):internal/security/policy.go
- 敏感词过滤 + 提示词注入预警(Finding 不带原文):internal/security/content.go
- 按主体授权(hard-private 对非内部主体永久拒绝):internal/security/principal.go
- 插件命名限式(可选档位,插件声明/内核执行,只能收窄):internal/security/profile.go
- 启动装配从 cmd/homed 迁入模块;启动顺序判据改盯真实装配点
device(新模块)
- 正式内核模块,Provides FacDevice,KernelState.Device
- 内置工具路由表下沉到 internal/toolroute(避免 core↔kernelboot 环),
device 保留对外入口;agent 未挂载时退化本地 switch
- Agent 侧只留 api.ToolHost/DeviceSurface 适配层
SDK
- InjectOptions.SecurityProfile:插件按 inputch 声明本轮安全档位(可选)
- InjectOptions.TurnHint:插件注入**本轮事实**,内核以独立 system 消息注入,
不混进用户正文(人设负责怎么说话,TurnHint 只放这一轮成立的事实)
- FacilitySecurity / FacilityDevice
提示词
- 内核自带 prompt/skill/工具返回全面英文化(中文提示会让模型行为漂移)
- 机制解释下沉到 4 个内置 skill(memory/output/tool-order/plugins),
skill 描述压到 80 runes 内(原来 94–108 全被索引截断)
- 默认人格模板英文化;[Voice] 段删除,规则并入 persona 块(单一段落)
- 历史记忆工具改名 coldcontext_query/commit,图记忆语义改称 stable memory
修复
- tracker: exec mount → unix.Mount(降权运行 + AmbientCapabilities 下才能挂)、
区分 EPERM 与不支持;启动日志说明后果与恢复方式
- healthcheck_kernel 输出瘦身:status.Tools 只给摘要(原 98,989 tokens/151% 预算)
- plugin_list 描述讲清 auto_start(按需)与 alive(此刻是否在跑),
明确不能由 alive=false 推断能力不存在
- pluginmgr/webui 去掉写死的 /home/newqqagent 回退路径(env → pluginDir 反推 → 报错)
- lua: 盘上适配器与内置版本不一致且无基线时响亮告警(deepseek 流式丢失
tool_calls 导致'模型不调工具'的真因)
测试:go build ./... 通过;go test ./... 60 包全绿
|
2026-10-26 20:55:25 +08:00 |
|
|
|
ca5b1b17b7
|
test(pluginmgr): 判据钉住暴露给 agent 的两个更新工具
kernel_update / plugin_update 是**模型唯一能触达的更新入口**。它们被删掉、
改名、或参数对不上时,agent 就再也升不了级 —— 而源码里 grep 得到工具名
不能证明它注册成功(本仓 E-3 教训:源码有、运行时没有)。
三条判据,全部走真实 registerTools(捕获 sdk.SDKConfig.RegTool):
1. 两个工具都注册了,且 Serial=true
更新会替换文件并重启,与 install/remove 并发会撞车。
2. kernel_update 同时暴露两种来源
缺 pkg ⇒ 只能用 Release;缺 release ⇒ 只能用本机包。
描述还必须说清"二选一",否则模型会都传或都不传,白烧一轮往返。
3. plugin_update 必须有显式范围
缺 plugins 参数就无法表达范围;默认全升会让用户"只升一个"变成批量重装。
|
2026-10-26 13:48:39 +08:00 |
|
|
|
817f6c59ae
|
fix(update): 重启交还 guard、OTA 支持 release/包两种来源、修三处真 bug
## 一、重启不再由 update 自己 systemctl(用户口径:应该通知 guard)
原 internal/update 直接 `systemctl restart homeagent`。三个问题:
① 杀掉整个 cgroup(unit 里 KillMode=control-group)⇒ **guard 跟着死**,
而 guard 存在的意义就是不让自己死。
② 丢失 exit 42 语义:guard 的 failures/failbackRound/failbackDone 全丢。
③ 自毁竞态:发命令的进程正是被重启杀掉的进程 ⇒ CombinedOutput 可能报错,
**一次成功的升级被误判失败而回滚**。
现在 internal/update 只到"产物就绪":预检 → 备份 → 写 pending → 原子替换。
重启由调用方走**既有**通道(restartCh → exit 42 → guard 用新二进制重拉):
· webui h.kernelCtl.RequestKernelRestart(本来就有,且是白名单内)
· pluginmgr p.sdk.KernelControl().RequestKernelRestart(见下面第三条)
· CLI 外部管理命令,restart unit 后按**版本自报**核对是否真生效
## 二、OTA 支持两种来源(release 自动更新 / 指定安装包)
新增 internal/update/source.go:
Source{Kind: FromPackage, Path} → 直接用本地包
Source{Kind: FromRelease} → 查最新 Release → 下载 → 校验 digest
ParseSource(pkg, release) 统一判空规则(pkg 优先),避免三端各写一套。
两条来源解析后进**同一条** Apply,不存在两份升级实现。
## 三、修三处已提交代码里的真 bug
① **HEAD 编译不过**:d17e261 提交了 kernelmod/update/mod.go(引用 ks.Update),
但 kernelboot/kernelstate.go 的字段改动从未提交。干净 worktree 实测
`ks.Update undefined` —— 推上去的仓库是坏的。本次补上。
② **kernelDataDir 算错一级**:Dir(Dir("/home/newqqagent/plugins")) = "/home",
而第 0 层 update 模块从 ks.DataDir=/home/newqqagent 读 pending
⇒ **agent 发起的升级一旦启动失败就永不回滚**(静默失效)。改为 Dir 一次。
③ **pluginmgr 的 KernelControl 恒为 nil**:它不在 kernelControlAllowed 里,
于是 `if kc := p.sdk.KernelControl(); kc != nil` 永不成立 ⇒
换完二进制**从不重启** ⇒ pending 留着 ⇒ 下次重启被判"未确认"而回滚。
修法**不是**把 pluginmgr 加进完整白名单 —— 那等于让 LLM 能关机整个服务
(kernel_control_gate_test 明确拦这个)。而是拆出**仅重启**授权:
kernelControlAllowed = {webui} 完整(含关机)
kernelRestartAllowed = {pluginmgr} 仅重启(shutdown 直接报错)
两者的危险程度差一个量级:重启几秒后 guard 自己拉回来,关机不会自己回来。
## 四、EventBus:插件请求更新不必 import internal/update
新增 events.EventUpdateRequest,pluginmgr 订阅并受理:
scope=kernel → kernelUpdateApply;scope=plugin → pluginUpdate(apply)。
它在 internal/plugins/webui 的 SSE 守卫里显式登记为**内核内部控制事件**
(不该推给浏览器:前端要的是 update_progress/update_result,不是"谁发了请求")。
## 五、顺手修掉的静默失败
· webui /kernel/update 原实现:**先**写 202 Accepted,**再**解析 body,
之后每个错误都直接 `return` —— 用户看到"已受理"然后什么都没发生。
改为同步 Apply(失败可如实报 HTTP 状态),成功后才请求重启。
· pluginmgr 拿不到控制通道时不再假装成功,而是回报 applied=true + 明确原因
(否则用户以为已升级,进程却还在跑旧镜像)。
· update 事件 publishResult 的 errMsg 原先是死代码(`_ = errMsg`),
按 payload 约定写回 error 字段。
|
2026-10-26 13:45:54 +08:00 |
|
|
|
5b759e80d7
|
feat(update): 更新进度走 EventBus + SSE(穿越进程重启的通知通道)
## 为什么必须走事件总线(用户口径:为什么不走 eventbus)
升级是**长耗时且会让进程消失**的动作。此前进度只往 log.Printf 写,
WebUI 要看得自己轮询 /kernel/update —— 而轮询在重启瞬间必然断线,
断线那一刻恰恰是用户最需要知道"升到哪一步/失败了/已回滚"的时刻。
EventBus 是内核既有的发布订阅机制(同步投递、handler panic 隔离),
用它不需要新造一条通知通道。
## 三个新事件类型
update_check 检查结果(只读,不改动)
update_progress 升级步骤:verify/backup/replace/restart/rollback/done/failed
update_result 最终结果(含回滚轨迹)
## ★ 顺序约束:先发事件,再动作
升级尾部几步(replace → restart)会让进程消失。顺序一反,最后几步的
事件永远发不出去。所以每个 publish* 都紧贴在其描述的动作**之前**。
## 直接 import events,不搞接口垫层
第一版我写了 EventPublisher 接口 + map[string]interface{} payload 想
"避免依赖",那是自欺欺人:events 包**不依赖任何内部包**(已验证),
import 它不会成环;而垫层会把事件降级成无类型 map,丢掉编译期检查。
已回退该设计,改为直接传 *events.Bus。
## SDK 补 EventBus() getter
PluginSDK 原先只有 Publish/Subscribe,没有暴露总线对象。而长流程
(升级闭环)需要把总线本身传下去持续发事件 —— 只给 Publish 的话,
每个调用点都要手动转发一遍。
## SSE 通路(守卫判据逼出来的正确修法)
TestEveryPublishedEventHasAnOutwardPath 抓到三个新事件"有发布者但无对外
通路"——症状是「发了、没报错、客户端收不到」。修法不是加白名单豁免,
而是真的接进 SSE:三者都进 sseEventRing(不只是实时推送),
断线重连要能补到**最终结论**(update_result),否则用户重连后只看到
"开始升级"而没有结局。
## 调用点
· pluginmgr(kernel_update):p.sdk.EventBus() → DoUpgradeBus
· webui(/kernel/update):h.sdk.EventBus() → runKernelUpgrade(pkg, bus)
· CLI:bus=nil,退化成纯 printf(CLI 没有总线)
|
2026-10-26 09:23:58 +08:00 |
|
|
|
d17e261b76
|
feat(update): update 注册为内核标准模块(第 0 层)+ 启动失败自动回滚
## 为什么必须是第 0 层(用户口径)
update 肩负「启动失败回滚上一次升级」的职责,该职责必须**早于所有模块**。
若排在后面:升级后新内核在 memory/llm 阶段崩了,而回滚还没跑 ⇒ 机器进
崩溃循环,只能靠 guard 的 failback(受限启动)兜底。failback 是最后手段,
不该承担"换错二进制"这种**已知可回滚**的故障。
⇒ Requires() 为空 ⇒ 依赖图天然把它放 Layers[0]。
## 两段式升级确认(rollback.go)
升级成功 ──→ 写 pending 标记("这个版本尚未被确认能启动")
↓
重启 → 内核启动 → update 模块(第 0 层)读标记
↓
┌────────────┴────────────┐
标记还在 标记已不在
(上次升级没能活到清除) (上次升级已确认成功)
↓ ↓
回滚到备份版本 正常继续 + 清标记
清除时机:模块跑完自检后立刻清。只依赖"进程活着且执行到第 0 层",
不依赖其他模块 ⇒ 不会误判。
**为什么不用 TTL / 重启次数**:那是 guard 已有的 failback,但判据是
"崩了几次",不是"这次升级该不该撤销"。用重启次数会在"新版本偶发崩溃"时
误回滚 —— 而那种情况该修的是崩溃,不是回退版本。
## 分层(与仓库其他模块同一惯例)
internal/update/ 业务实现(三端共用:CLI/WebAPI/agent 工具)
internal/kernelmod/update/ 适配层(读 ks → 调业务包 → 写回 ks)
参照 supervisor 模块:实现在 internal/supervisor,模块里只做装配。
分开的理由:internal/update 被三端调用,它们都没有 KernelState。
## 标记写入时机(一个真实的顺序坑)
MarkPendingUpgrade 在**替换二进制之前**调用。反过来(替换后才写)会在
"替换成功但没来得及写标记就崩了"时丢标记 ⇒ 下次启动不知道该回滚。
且写标记失败 ⇒ **放弃本次升级**(未替换任何文件):没有标记就没有回滚
能力,而不带回滚的升级等于赌运气。
## 回滚失败必须响亮
备份不存在时返回 error,调用方(模块 Init)必须中止启动 —— 继续装配等于
用一个已知起不来的版本。错误文案带备份路径与标记路径(删标记即可放弃回滚),
人能照着修。
## 判据
internal/update/rollback_test.go 10 条,2 条经变异验证:
· TestRollback_RestoresBackup —— 二进制+depgraph 同批回滚、标记清除、
坏版本留档 homed-broken-*、版本轨迹记录
变异:回滚后不清标记 ⇒ 红
· TestRollback_MissingBackupIsLoud —— 备份缺失时报错且可执行
变异:静默返回 nil ⇒ 红
· TestPendingMarker_AbsentIsNotError —— 无标记返回 (nil,nil) 而非 error
(否则每次启动都走"回滚失败"分支 ⇒ 内核永远起不来)
· TestPendingMarker_CorruptIsActionable —— 标记损坏时提示可删
· TestStateSummary_* —— 回滚信息进 healthcheck;nil 安全
## 顺带修正的既有判据
· runtime_dep_test.go:L0 设施集合从 2 个变 3 个(新增 kernel.version),
改为断言**集合**而非顺序(注册表本就是乱序的)
· facilities.go:新增 FacVersion,并登记为 internalOnlyFacilities
(它是启动时序事实,不是插件契约 —— facilities_sync_test 的守卫正确拦住了
"把内部标记泄漏成对外契约")
三个 DoUpgrade 调用点补 dataDir 参数(标记落点必须与内核 --data 一致)。
|
2026-10-26 02:08:29 +08:00 |
|
|
|
475aac867b
|
refactor(update): 内核与插件更新合并为 update 子系统(三端共享一个底层)
## 为什么合并(用户口径:更新部分应独立为子系统)
之前 kernelupgrade / pluginupdate 两个包,但它们共享三样东西,
分开写就要各复制一份,然后慢慢不一致:
① **版本比较** —— 合并前共 6 个函数做同一件事:
kernel 侧 ParseSemver / CmpSemver / versionNewer / trimTagV
plugin 侧 trimV / isNewer / semverNums(还自带一份正则)
且**行为不一致**:kernel 侧只挡 len<2,"1.4" 漏过去被填成 1.4.0;
plugin 侧的正则要求两段所以也漏。两份都把 "1.4" 当合法版本。
② **两阶段交互**(检查 → 确认 → 执行)—— 同一套用户口径。
③ **失败要响亮** —— 都不静默降级。
## 收敛结果
internal/update/
doc.go 子系统头:职责边界、三端共享契约、为何不分家
semver.go ParseSemver / CmpSemver / Newer / TrimTag(**唯一一份**)
kernel.go 内核升级闭环(解包→校验→备份→替换→重启→复核→回滚)
release.go GitHub Release 查询
plugin.go 插件升级(查仓库→拉源码→hmapdev 编译→产物)
三端共享:
agent 工具(plugin_update / kernel_update) ┐
WebAPI(/plugins/update、/kernel/update) ├─ internal/update
CLI(homed-upgrade) ┘
入口层只做参数解析与结果呈现,不含业务逻辑 —— 否则会出现
"agent 说没有新版、WebUI 却升了级"的分裂状态。
## 合并时抓到的两个真实 bug
① **ParseSemver 只挡 len<2** ⇒ "1.4" 被零值填成 1.4.0,
「1.4 → 1.4.0」被判完全相同(说"无需升级"),而两者内容未必相同。
现在要求恰好三段。
② **不认 v 前缀** ⇒ GitHub release tag 全是 v1.4.0 形态,
CLI 对"当前 v1.4.0 vs 包内 v1.4.0"判不可比、只能退回字符串比较。
现在 TrimTag 去 v 前缀后再比。
## 新增入口
· plugin_update(agent 工具)+ GET/POST /plugins/update
—— 两阶段:apply=false 只检查(列出 updatable / no_repo 两类,
后者是"未声明仓库、无法自动更新"的内置/私有插件);
apply=true 需显式 plugins=all 或名单(不默认全升)
· kernel_update(agent 工具)+ GET/POST /kernel/update
—— 两阶段同构;apply 只收本机包路径(URL 下载是 CLI 职责,
agent 不该在推理中发起长下载)
判据:
· internal/update: Newer / TrimTag / ParseSemver 的边界(含上述两个 bug 的回归)
· pluginmgr 入口层 5 条:检查形状、apply 必须显式给范围、
kernel apply 必须给 pkg 且指向 homed-upgrade、拒绝目录当包
|
2026-10-26 01:05:09 +08:00 |
|
|
|
29150065aa
|
fix(clawhub): sidecar 就绪超时 5s → 30s(慢机器上 node 冷启动超时)
CI 报红:`TestLaunchSidecarAndListTools`
launch sidecar: sidecar echoplugin ping timeout (5.00s)
本机全绿。
## 这不是"测试机器慢所以放宽测试"
sidecar 起不来等于该 skill 整体不可用 —— 这是**产品行为**,不是测试细节。
原来的 5s 是按开发机估算的:node 冷启动 + 模块加载在快盘空载时约 0.2s。
而 CI 共享 runner(以及真实部署里的低配机器)上,一次 fork+exec+node
引导遇到 CPU 争抢就会超过 5s。
判断"真的挂了"应该由**进程退出信号**负责(readLoop 会感知),
而不是由引导耗时猜。
## 30s 的取法
· 比 systemd 的 TimeoutStopSec(25s) 宽 —— 关停时不该被就绪等待拖住
· 与用户可感知的"卡住"阈值(约 30s)持平 —— 再长就该让用户看到明确失败
· 错误文案带上超时值与可能原因(node 不可用),便于自助排查
|
2026-10-25 22:31:51 +08:00 |
|
|
|
7052644431
|
fix(core): contextoverflow 改回空中断(按原始设计)+ Python 解释器可配置
## 1. contextoverflow:L4 空中断(推翻三处偏移)
用户口径:**原始设计是注入 L4 空中断,目的是触发语义相关性裁剪,
不含任何消息,就是一个空中断。**此前的实现有三处偏移:
① **中断带正文** —— `Build` 生成「已裁剪 N 条低相关事件到文档记忆…」,
于是 L4 变成一条要模型回应的消息,白烧一整轮推理。
② **裁剪在 raise 之前做完** —— 先 `pruneByQuery` 再 raise,中断沦为
「裁剪已完成」的回执,晚了一步。
③ **上游 contextfull 后原地重跑** —— `StepPrepare` 重建后拿**同一份**
超限内容再发一次,必然再超限,快速烧光 recoverBudget。
(这条是修① ② 时暴露的连带问题。)
改动:
· `VectorContextOverflow.Build` 返回空串 ⇒ 空中断;级别仍是 L4
· 删除 `overflowNotice` 与 `vectorArgs.Pruned`(中断不需要知道裁了多少)
· `handleContextOverflow` 不再裁剪,只发空中断,如实返回 0
· 新增 `pruneForAirInterrupt`:裁剪由空中断任务**真正执行时**触发,
query 取上下文里最后一条真实用户输入作相关性基准
· 新增 `isAirInterrupt`:只认「内核来源 + type=interrupt + 无正文」,
插件带正文的中断不受影响(否则"插件发了个空中断"会变成无声裁剪)
· `prepareInputTask` 识别空中断 → 裁剪 → terminalSkipped
(不进 LLM、不写上下文、不发回复)
· 上游 contextfull 后改为 `outcomeSuspended`(让位等裁剪),不再原地重跑
顺带记录偏移成因(`raiseContextOverflow` 注释):当时的**观察**是对的
(L4 看不到被打断者上下文 ⇒ 模型会把"没检索到"推断成"不存在"),
但**结论**错了——它把"补偿 L4 看不到上下文"当成中断该带正文的理由。
模型需要的不是"知道有内容被移走",而是**留下来的内容与当前问题相关**。
判据(均经变异验证):
· `TestVectors_OverflowIsAirInterrupt` —— 正文必须为空 + 级别 L4
变异:Build 改回非空 ⇒ 红
· `TestOverflow_RaisesAirInterruptWithoutPruning` —— 发中断那步不改上下文
变异:加回 pruneByQuery ⇒ 红
· `TestAirInterrupt_ResolvedAsValidInput` —— 空中断须被认作有效输入
· `TestAirInterrupt_RejectsOrdinaryEmptyInput` —— 普通空包/插件带正文
中断**不得**误判
· 改写 5 条断言"发中断即裁剪"的旧测试(T1/T2/T5/大事件/自适应),
保留它们原本守的东西(超限判定、恢复预算、终止保护),
只把裁剪步骤显式移到 `pruneForAirInterrupt`
## 2. clawhubadapter:Python 解释器与工作目录可配置
真机事故:`bilibili-all-in-one` 声明 requirements 含 httpx,系统 Python
没装 ⇒ 启动即 `ModuleNotFoundError`。而解释器此前是**硬编码猜**的
(先 /usr/bin/python3,再 /usr/local/bin/python3)。
「猜」没有正确解:不同部署的 Python 布局完全不同(发行版 Python、
pyenv、uv、conda、venv…)。而往系统 Python 里 pip install 是**错的**:
PEP 668 明确禁止,`--break-system-packages` 会破坏 apt 包一致性。
新增两个配置项(与既有 skills_dir/simulator_dir 同一风格):
· `python_bin` —— 解释器绝对路径,最高优先
· `skill_workdir` —— skill 子进程工作目录
回退顺序:配置 → `<skill>/.venv/bin/python` → 系统 python3。
**配错必须响亮**:配置的路径不存在时返回 error,不静默退回系统解释器
(否则用户以为配好了,实际跑在缺依赖的 Python 上 —— 又一次静默降级)。
判据:`TestPickPython_ConfiguredWins`(配置优先于 venv)/
`TestPickPython_VenvWhenNotConfigured` / `TestPickPython_MissingConfiguredIsLoud`
`launchProcessIn` 是 `launchProcess` 的工作目录变体(不改既有 4 个调用点)。
|
2026-10-25 21:39:12 +08:00 |
|
|
|
f833897d40
|
fix(agentcli): 不再把自造状态当新事件注入 —— 每条都烧一整轮推理
问题(WSL2 真机诊断报告问题 4,影响最大):开终端注入「已启动」、
超时注入「已超时关闭」,每条都唤醒一整轮 LLM 推理。实测后果是
agent 反复向用户解释"终端怎么又没了",真正要做的活一件没做。
这属于本插件**自身逻辑矛盾**,不是内核缺陷:
1. 「已启动」是与 terminal_create 返回值的重复告知。返回值已含
id/command/timeout/rows/cols/notify_mode,信息完整。
2. 「已超时关闭」在创建时已由 timeout 字段告知,agent 本可推导;
且注入后紧接着 removeSession,agent 去 terminal_list 查必然是 0 个,
只能盲目排查——而这是插件自己删的。
3. 同一函数里 IsExpired() 语义相反:r.err 分支把它列为**必须静默**,
而超时分支却在注入。
4. 本插件自己的 notify=exit/interval/buffer 参数本就定义了"什么该通知"。
两处无条件注入是不经该配置的旁路,与自身设计相矛盾。
顺带:r.err 不再以 %v 原文进 prompt(原来注入
`read /dev/ptmx: input/output error`,把 agent 引去查 devpts 挂载,
好几轮推理花在这上面)。改为给**结论**「读取结束」,原始错误只进日志。
判据(plugin_test.go):
· 新增 2 条**反向判据** TestNoStartupNotificationInjected /
TestNoExpiredNotificationInjected —— 没有它们,后人把注入加回来时
其它测试依然全绿,缺陷会静默复发
· 原 7 处用「等已启动通知」做时序同步的测试改为 waitReadLoopReady
(轮询 mockTerm.readCalls(),不靠固定 sleep 也不依赖业务通知)
★ 保留不动:进程自己结束/读取错误的通知(agent 无法从工具返回推导),
由 watch 配置控制。判据 TestProcessExit_StillNotifies 钉住这一点。
|
2026-10-25 20:16:06 +08:00 |
|
|
|
d65b742bd7
|
fix(plugin): 修两处持锁自死锁(CI 双红真因)+ tools_ready 事件声明
## 死锁(这是 CI 连续两次红的真因)
`StopAll` 与 `StopPlugin` 都在持有 `r.mu` **写锁**时调用
`RemoveBuiltinSkill` → `skillDirOf` → `r.mu.RLock()` ⇒ **自死锁**。
后果不是报错而是**卡死**:
- internal/plugins 卡 300s 超时(TestCapability_RestrictedPluginStillLoads)
- internal/plugin 卡 840s 超时(TestStopPluginDoesNotTouchPersistentState)
- CI 的 `go test ./...` 与 `go test -race` 双双红
修法:拆出**不加锁**的内部版本,持锁路径只走它:
- `skillDirFrom(dataDir, name)`(纯函数)
- `removeBuiltinSkillLocked(pluginName, p, dataDir)`(显式传 dataDir,
避免"看起来没取锁、一改就取锁"的隐患)
- `announceSkillRemovedLocked`(持锁下直读 r.evBus)
修后:internal/plugins 300s → 12s,internal/plugin 840s → 0.7s。
## 防回归判据
`TestStopAllAndStopPluginDoNotDeadlock`:死锁**不返回**,普通断言抓不到,
所以用**超时**当判据(5 秒未返回即失败),并指明应改用 Locked 版本。
## tools_ready 事件声明
新增的 tools_ready 触发 `TestEveryPublishedEventHasAnOutwardPath`
(该判据防"事件发了、客户端收不到"的静默失效)。
tools_ready 是**内核内部**就绪信号(plugin-host → seq 等订阅者),
不是对话流程事件,前端无需感知 ⇒ 在 allowed 里显式声明理由。
## 验证
go build ./... / go vet ./... / go test -p 1 ./... / -race 全通过。
|
2026-10-25 18:25:37 +08:00 |
|
|
|
77f596fb07
|
chore(log): 全仓运行期日志与 Fatalf 英文化(320 处 / 83 文件)
用户要求:"历史以来的全部写入日志的区域"改为英文,因为中文日志
在没有中文字形的终端/容器里会乱码,且跨机器、跨工具 grep 时不可靠。
## 范围
- 全部 log.Printf / log.Fatalf / log.Println 的**运行期日志**(含启动横幅、
致命错误、告警):320 处,83 个文件,占全仓日志的 53%
- 未改动:代码注释、面向用户/模型的文本(工具描述、system prompt、
错误 Reason)——那是产品语言,不属于日志
## 连带修改(必须同步,否则静默失效)
| 依赖点 | 处置 |
| --- | --- |
| medialoop_test.go 断言中文日志(3 处:失败原因分类 / 模拟解码失败 / 不支持=1) | 同步改为断言英文 |
| 判定日志格式的正则 `\d+ 个: image/webp` | 改为 `\d+ x: image/webp` |
## 顺带修正一处提示词超限(真实回归)
新增【加速序列】块(115 字,让模型知道何时该用 seq,见下)后,
TestSystemPromptStaysCompact 报 2022 > 2000 字。
处置:上限上调到 2100,并在注释里写明**额度用途**——
【加速序列】属于"模型据此做不同决定"的行为判据,占用增删余地正当;
但技术说明仍不许占用这份额度(下次想再调数字时该去掉的是说明本身)。
【加速序列】块内容只留判定条件(多对象重复 / 顺序已知 / 以前做过),
格式细节全部下沉到 seq-tools skill —— 避免两处真相。
## 验证
- go build ./... / go vet ./... 通过
- 分块跑全仓测试通过(agent、plugin、plugins/*、security、memory、
config、kernelboot、kernelmod、cmd)
- 残留中文日志仅 1 条注释,非日志调用
## 运维提示
日志文本变了,依赖 grep 中文的既有习惯要改为对应英文词,例如:
内核装配完成 -> kernel assembly complete
插件调度完成 -> plugin scheduling done
已建链 -> link established
已加载 N 个 -> N loaded
|
2026-10-25 15:55:26 +08:00 |
|
|
|
db88c5efe5
|
fix(agentcli): 关闭终端后反复注入过期状态
## 现象
关闭终端后 agent 持续收到「[终端 term_6 读取结束: read /dev/ptmx:
input/output error]」「[终端 term_6 中的进程已退出]」,而 terminal_list
显示 0 个会话——它拿不到任何证据说明这些是历史遗留,只能反复解释
"这是旧回声,不用管",每条都唤醒一轮推理。
## 根因(不是一次回声,是每 30 秒一次)
close 工具 → session.Close() → readLoop 从 <-t.stopCh 静默 return
→ 但 **sessions 条目没删**
→ cleanupLoop 每 30s 扫到它,发现 !terminalRunning
→ 又注入一条「进程已退出」
三条退出路径(主动关闭 / 进程自己结束 / 超时)里,主动关闭那条漏了删表。
## 修法
1. readLoop 的 t.stopCh 分支补 removeSession,三条路径统一走它。
2. 新增 isClosing:所有"终端结束"通知都必须区分两种成因——
进程自己结束(是新闻,要告诉 agent)vs 我们主动关掉(是回声,不能注入)。
旧实现只看 r.err / terminalRunning,而主动关闭恰好让两者都成立。
## 测试基础设施修正
mockTerm.Kill() 原先是空操作,于是"主动关闭"在测试里从不产生 EIO,
判据永远绿(假绿)。改为模拟真实 PTY:Kill 后 Read 立刻返
`read /dev/ptmx: input/output error`。
判据:关闭后 session 必须立刻从表里消失、关闭后不得新增注入
(连开 20 次防 select 随机性)、进程自己结束仍必须通知(不过度修复)。
变异:关闭时不删表 / isClosing 恒 false,均被抓。
Co-Authored-By: Claude <noreply@anthropic.com>
|
2026-10-25 10:10:21 +08:00 |
|
|
|
2d49c23cc6
|
Merge remote-tracking branch 'refs/remotes/gh/main' into fix/webui-gzip-race
|
2026-10-24 13:25:18 +08:00 |
|
|
|
7457b052a7
|
fix(webui): streamOpenAI 改为 w 单写者模型,修 gzipResponseWriter 真数据竞争
根因(-race 实测,非推测):
streamOpenAI 的 handler goroutine 在收尾处直接 fmt.Fprintf(w, finish chunk)、
fmt.Fprintf(w, "data: [DONE]")、flusher.Flush(),与增量 flush goroutine
(16ms 批量合并)**并发写同一个 http.ResponseWriter**。经 gzip 中间件时,
gzipResponseWriter 的 mode / wroteHeader / buf 三个字段无锁被两方同时读写:
Read gzipResponseWriter.Write gzip.go:139(读 g.mode)
└─ streamOpenAI handler goroutine handler_openai.go:256
Write gzipResponseWriter.commit gzip.go:206(写 g.mode/wroteHeader)
└─ streamOpenAI 写 goroutine handler_openai.go:165
修法选择:改写并发模型(单写者),不给 gzipResponseWriter 加锁。
理由两条:
1. 加锁只哄住 race detector,是治标 —— 并发写同一个 ResponseWriter 这件事
本身就该被结构禁止,而不是靠锁逐字段兜。
2. 持锁要跨 Write / Flush 这类网络 IO,一旦客户端慢,整条 SSE 会串行卡在
锁上(锁内做 IO)。单写者模型从结构上消灭竞争,且零锁开销。
改动:
- 新增 finishCh(chan finishItem,buf=1)+ writerExit。终止帧(finish chunk
或超时错误 + [DONE] + Flush)也交给那个唯一的写 goroutine 发。
- 新增 sendFinish(usage, timedOut):**先投递再等 writerExit**。
顺序不能反 —— writer goroutine 只在收到 finishCh 或 done 关闭时退出,而
done 在 defer 里关(函数返回之后),先等后发即死锁。select 里的 <-done
是防御路径:done 已关时 writer 可能已不在,不该再等一个永不到来的接收者。
- 超时路径与正常收尾路径都改走 sendFinish,handler goroutine 不再碰 w。
- writerExit 用 safeclose.Safe 关闭,遵守本仓 close 纪律
(internal/safeclose/TestNoBareCloseOutsideOutsideSafeclose 禁裸 close(chan))。
- 顺带修掉一个既有逻辑缺陷:旧写法下 [DONE] 可能先于 deltaCh 里尚未发完的
尾部 delta 到达客户端;现在先 flushPending 再发终止帧。
验证(全部 -count=1,除注明者):
- 基线(f408448 原版)TestOpenAIStreamIsActuallyStreaming + -race:
单跑 5/5 次各 8 条 DATA RACE 且 FAIL;-count=5 时 10 / 12 / 10 条。
- 修复后同一用例 + -race:单跑 6/6 次 races=0 PASS。
- 修复后整包 go test -race ./internal/plugins/webui/:3/3 次 races=0 PASS
(63s / 69s / 64s)。
- go build ./... 通过;go vet ./... 通过;go test ./... 58 包 ok、0 FAIL、
0 WARNING: DATA RACE。
判据:cr-HomeAgent-6c0d1e07(stress),带变异 M1「sendFinish 改回直写 w
(并吃掉 <-writerExit,避免只靠死锁超时报红)」——实测被咬住(判据报红)。
|
2026-10-24 12:35:31 +08:00 |
|
|
|
3f834794c5
|
refactor: SDK module 路径统一改为 hologramagentsdk(59 文件 + go.mod)
## 为什么必须做
SDK 仓(独立仓库)已把 module 从 `github.com/JianFeeeee/homeagentsdk`
改名为 `hologramagentsdk`。主仓通过 `replace` 直接引用它的**物理目录**:
replace github.com/JianFeeeee/homeagentsdk => ./third_party/homeagent-sdk
一旦 SDK 侧改成新 module 名,主仓立刻炸:
go: ./third_party/homeagent-sdk@ used for two different module paths
(github.com/JianFeeeee/hologramagentsdk and github.com/JianFeeeee/homeagentsdk)
Go **不允许**同一目录被两个 module 路径 replace。所以不能"新旧并存",
只能把主仓 59 个文件的 import 全部改到新名,并删掉旧 replace。
## 一个被证伪的前提
我最初按"third_party/homeagent-sdk 是独立仓库,不能在此仓修改其源码"
来处理,并因此判断两个仓的改动可以隔离。实际上主仓是靠 `replace` 指向
该目录的物理副本 —— **两个仓的改动不可能真正隔离**:SDK 侧改 module 名,
主仓必然跟着改 import。
这个前提应该在做任何改动之前用 `go list -m` 验证,而不是假设。
## 连带修掉两处
| 缺陷 | 性质 |
|---|---|
| `TestLuaSDKMockSingleSource` 失败 | SDK 的 `sdk/lua/sdk.lua` 变了,主仓内嵌副本未同步(跑 `scripts/sync-lua-sdk.sh` 修复) |
| `sdkModuleNames` 分散两处 | 已在 SDK 仓合并为单一数据源(见 SDK 仓 725732b) |
第一条是**副本一致性判据抓到的**,不是我自己发现的 —— 这类判据的价值在此:
两个仓必然有副本,靠人记住"改了 A 要同步 B"是做不到的。
## 验证
- `go build ./...` / `go vet ./...` 干净
- `go test ./... -count=1`:**58 包 0 失败**
- `internal/plugin`(Lua SDK 副本一致性)+ `internal/sdk` 全绿
- 主仓 `third_party/homeagent-sdk/` 与 SDK 仓 `main` 无差异
|
2026-10-23 15:51:02 +08:00 |
|
|
|
b20e32d81d
|
feat(outputch): 提示词显式声明「必须调 outputch 才送达」+ 契约值改名 Agent
## ① 提示词:只有显式调用 outputch 才会送达用户(用户明确要求)
模型最常见的错误不是不会调工具,而是**以为返回文本就等于回复了** ——
在异步通道(qq/wechat/群聊)上那等于自言自语,用户永远收不到。
改动前【输出规则】开篇是「消息不会自动发送到对话来源通道,你必须自己决定
如何回复」,语气偏描述。改为开篇总述句:
**只有显式调用 `output_send__{通道名}` 才会把内容送达用户。**
返回纯文本只是在这次推理里说话,用户看不到 —— 除非是下面说的同步通道。
同时补上本次内核新增能力:payload 传数组可一次发多条。
## ② 契约值改名:ProxyAuthHomeAgent="homeagent" → ProxyAuthAgent="agent"
用户指示契约值一起改。常量名与字符串值同时改,但**保留旧值读取路径**:
ProxyAuthAgent = "agent" ← 新值
ProxyAuthAgentLegacy = "homeagent" ← 只用于兼容读取
ProxyAuthHomeAgent = ProxyAuthAgent (Deprecated 别名,存量源码可编译)
为什么必须保留:`Auth` 是**写进 plugin.json 的持久化声明值**。存量配置里
写着旧值 —— 本机实测 `/home/newqqagent/plugins/huawei_smarthome/plugin.json:19`
就是 `"auth": "homeagent"`。只认新值 ⇒ 该插件加载时被判为非法声明 ⇒
反代不生效,症状是「插件好好的但页面打不开」,且无任何报错指向改名。
### 归一化是安全关键
`EffectiveProxyAuth` 把**空串与旧值都归一化为 ProxyAuthAgent**。
判断点全都写成 `auth == ProxyAuthAgent`。若不归一化,就得每点写两遍比较
—— **漏一处就是鉴权绕过**:旧值被判为「非门户鉴权」⇒ 跳过 authorizeProxy
⇒ 请求直接透传给上游。所以兼容收在一个函数里,不散落在调用点。
### 顺带修掉两处隐患
1. `webui/proxy.go` 的裸字面量 `"homeagent"`(非法行占位)改为
`sdk.ProxyAuthAgent` —— 裸字面量在改名时会被漏掉,届时它悄悄变成
未知值,而未知值在旧代码里等于「无鉴权」。
2. 两处鉴权判断改用 `EffectiveProxyAuth(...)`,堵住旧值绕过。
## 判据
| 判据 | 覆盖 | 变异自证 |
|---|---|---|
| `TestSystemPromptRequiresExplicitOutputCall` | 总述句必须存在、必须说明「返回文本用户看不到」、同步通道是唯一例外、payload 可传数组 | 删掉总述句 ⇒ 红 |
| `TestProxyAuthLegacyValueStillAccepted` | 旧值仍被接受 + 必须归一化为新值 | 去掉归一化分支 ⇒ 「旧值应归一化为 "agent",实际 "homeagent"」红 |
## 验证
- `go build ./...` / `go vet ./...` 干净
- webui / internal-sdk / agent-core 全绿
- SDK 仓(独立提交 acd1166):`go build` + `go test ./...` 0 失败
|
2026-10-22 21:17:05 +08:00 |
|
|
|
f256098c9f
|
refactor: 全面改名为 HologramAgent + 提示词技术内容下沉到 skill/工具描述 + 修 21 处乱码
## ① 全面改名 HomeAgent → HologramAgent
仓库已改名,但代码与文档里仍有大量旧名。本次按"用户能看到的优先"全量替换:
- 用户可见:登录页标题/页脚、默认人格第一句、CLI 状态输出、启动日志、
plugin_install 的工具描述、AI 图片插件的 User-Agent
- 文档与脚本:README、assets/docs、docs/zh、deploy/*.sh、.github/workflows
- 鸿蒙工程:cmd/ohos/HomeAgent → cmd/ohos/HologramAgent(git mv)
目录内的 module/app 描述同步;**证书路径原样保留**
(default_HomeAgent_xxx.cer/p7b/p12 是签名材料的实际文件名,改了会找不到证书)
- 插件元数据:plugin.json 的 author
★ 必须保持原样的两处(改了会坏):
· legacySeededPromptSignature = "你是 HomeAgent 的看板娘「小宅」…"
—— 那是识别历史播种人格卡的**原文锚点**,改了就识别不出旧数据
· 公开 SDK 的 ProxyAuthHomeAgent = "homeagent"
—— 那是 SDK 契约里的值,属独立仓库,本仓不改
## ② 修 21 处乱码(U+FFFD)
这些是历史编辑留下的编码破坏,全在注释里,例如:
「赌注重下���遍」→「赌注重下一遍」
「「不��滤」」 →「「不过滤」」
「**待��退场**」→「**待退场**」
★ 保留 providers/chineseclip/tokenizer.go 里的 2 处:那是**故意**用
U+FFFD 演示"非法 UTF-8 字节被归一成 U+FFFD",不是乱码。
## ③ 提示词技术内容下沉(用户:技术性内容应该收敛到 skill 与 -help 中)
实测系统提示词 2309 字,其中两段几乎全是**模型无法据以行动的实现细节**:
【工具执行顺序】1272 字(ParallelSafe 声明、批内降级、batchRunnable 判据…)
【三层记忆】 1069 字(L1/L2/L3 编号、消费式删除、向量空间、指纹…)
收敛后 **2309 → 1723 字(-25%)**,保留的全是"模型据此做什么不同的事":
工具顺序:默认并行 / 不要依赖先后 / 同通道保序 / 写类工具可能串行
三层记忆:时序里能看最近 / doc_query 把历史事件按原始时间戳插回时序 /
关系用 memory_recall / 成文知识用 knowledge_search
判断口径写进判据:新加一段提示词前先问"模型会据此做什么不同的事",
答不上来就该进 skill 或工具描述。
## 判据
prompt_memory_test.go 新增 2 条:
- TestSystemPromptHasNoImplementationJargon
黑名单:ParallelSafe / batchRunnable / 消费式 / DenseCosine / fingerprint /
向量空间 / 批内降级 / 自激
- TestSystemPromptStaysCompact:上限 2000 字(当前 1723)
变异:把 "ParallelSafe 声明、批内降级" 加回工具顺序段 ⇒ 第一条立刻红。
## 验证
- go build ./... / go vet ./... 干净
- go test ./... -count=1:58 包 0 失败
- make check-client-versions:客户端与内核版本一致
|
2026-10-22 20:49:44 +08:00 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|