|
|
e8d7bb4c06
|
feat(webui): 通道归属合并进拓扑图,整张图改 SVG(少文字、多图形)
上一版把「通道分配(按归属)」单开一段,等于把同一件事拆成两张表——
而通道属于谁是**拓扑的一部分**(左边这些输入口分别被谁接管),拆开反而
看不出关系。按用户要求合并,并整体改成图形化:
- 整张拓扑用 SVG:左侧按归属画出输入通道容器(根=青色虚线框,驻留子=彩色
实线框并标轮次/上下文满),→ 汇集母线 → 内核 → 输出母线 → 右侧输出通道。
**连线即路由**。
- 信息全部改用图形编码:归属=容器/配色、容量=节点内细条(默认容量不画填充)、
输出能力=五个彩色圆点(text/file/image/audio/structured)。
- 文字降到最少:去掉四个数字块的副标题、排队队列那行只留 "FIFO"、
通道行不再写"回程由来源决定"这类说明。
- 段名改为「通道拓扑(连线即路由;左框 = 归属)」。
验证:node --check 通过;go build ./... 干净;webui 测试全绿。
|
2026-09-14 11:22:20 +08:00 |
|
|
|
75f377fd4d
|
fix(remotedevice): 心跳 pong 忘了 Flush —— 修「设备通道每 60 秒掉线重连」
真因(实测定位):服务端 writePong 只调 writeFrameHeader,**不 Flush**。
pong 只有两个字节,且设备空闲时没有任何别的写会顺带把 bufio 缓冲刷出去 ——
于是 pong 永远留在服务端缓冲里。
链路:客户端每 30s 发一个 ping(pingLoop)→ 服务端算出 pong 却没发出 →
客户端的读循环设的是「2 倍 ping 间隔」读超时(默认 60s)→ 每 60 秒准点
i/o timeout → 桥断开 → 3s 后重连 → 服务端 markOffline 注销 outputch,
重连后再注册。
生产日志就是这个指纹(online :20 → offline 下一分钟 :20 → 重连 :23,
连续数小时无一次例外);面板上表现为设备通道/工具凭空消失又出现,
/devices 列表跟着闪。
改法:writePong 复用 writeFrame(它 Flush)。另把客户端读循环退出时的
静默 return 改成带错误与 opcode 的日志 —— 此前断线真因在设备侧完全不可见,
只能靠对端日志倒推,正是这次排查一开始卡住的地方。
回归用例 TestWSPingGetsPongWhileIdle:只发一个 ping,随后什么都不发,
要求 2s 内必须收到 pong。**反向验证过**:把修复改回 writeFrameHeader,
用例即以 `read tcp ...: i/o timeout` 失败(与生产症状一致)。
|
2026-09-14 11:15:21 +08:00 |
|
|
|
8756f8d77f
|
fix(webui): 通道归属把「根 agent id」与驻留子分开(根不再被标成「驻留子 main」)
实测(创建一个驻留子 uitest 并把 timer 划给它)暴露的归类错误:
inputch 的 owner 在登记表里可以是**根 agent 自己的 id**(如 "main")——
child/<id> 这条就是 owner="main"。前端只按「owner 非空」判为驻留子,
于是根自己那条被标成「驻留子 main」,而同一条通道在 residents 里根本不存在。
改法:
- /api/v1/runtime 补 agent_id(根 agent 的 id);
- 前端把 owner == 根 id 与 owner == "" 归一成同一组「根 agent / 内核默认」,
只有既非空又非根 id 的才是子容器。
|
2026-09-14 11:08:27 +08:00 |
|
|
|
d502fc1bf5
|
fix(webui): 运行态面板逐段更新(真修「一闪一闪」)+ 补第五条排队队列
1) 上一版只做了整体签名缓存,实测仍会重建:设备通道列表本身就在来回变
(远程设备通道 11→9 条),签名一变就整块 innerHTML,没变的段落(含条
transition)也跟着推倒重来——视觉上仍是闪。改法:外壳只建一次,之后
**逐段**(tiles/levels/stack/owners/topo)比较 HTML,只替换真正变了的那段。
2) 设计是「四条中断队列(L1–L4)+ 一条排队队列」= 五个队列,面板只画了四条:
排队输入这条线在运行态里凭空消失。补第五行「排队(无级别)」,用中性色 +
虚线分隔(它不是优先级,而是另一**类别**),并把它计入条形归一化基准。
段标题从「中断队列(按级别)」改为「队列(四级中断 + 排队)」。
3) 顺带把累计计数(入队/执行/抢占/挂起恢复/拒绝/背压)显式列在数字块下方——
背压是新指标,之前只能看接口看不到面板。
验证:node --check 通过;go build ./... 干净;webui/core/sdk 测试全绿。
|
2026-09-14 11:03:37 +08:00 |
|
|
|
43536ba326
|
fix(webui): 运行态面板不再闪、通道分配带归属(含驻留子)、改图形化
三个用户可见问题,逐个说明根因与改法。
1) 首页「一闪一闪」——运行态每 3s 轮询一次,renderRuntime 无条件重建
#rt-panel 的 innerHTML:数据没变也把整块 DOM(含各级条的 transition)
推倒重来。改法:缓存数据签名(**不含 uptime**——它每秒都变,带上等于没缓存),
签名相同直接 return,一个字节都不动。另:renderOverview 会整块重建
#rt-panel(面板本身是空的),所以那里必须让签名失效,否则空面板填不上。
2) 通道分配只显示内核/根 agent,看不见驻留子——根因是状态面只暴露了设备能力
(KernelStatus.Channels,来自 iom.ListChannels),而「这条输入归谁」是
ChannelRegistry 的属性(InputChannel.Owner/Capacity/Output),从未出过内核。
而登记表本来就是根 agent 与驻留子**共用同一份**,所以数据一直都在,只是没画。
改法:KernelStatus 新增 InputChannels(+ sdk.InputChannelInfo),
/api/v1/runtime 带出 input_channels;前端把它按 owner 分进「归属容器」,
驻留子即使一条 inputch 都没划到也照样出现在图里(否则"子存在但看不见"
与"子不存在"无法区分),并显示其 allowed_outputs / 轮次 / 上下文满标记。
3) 「这些信息明明可以图形化」——四级中断的登记/抢占由纯文本改成并排迷你条;
通道分配用归属容器 + 容量滑块(轨道/填充/把手/读数),并把回程通道、
注册插件做成胶囊标签。设备能力拓扑(原有)保留。
验证:go build/vet 干净;go test ./internal/agent/... ./internal/plugin/...
./internal/sdk/... ./cmd/... 全绿;node --check dashboard.js 语法通过。
TestRuntimeEndpoint 扩展为同时钉住 input_channels 的归属与「驻留子划走的那条」。
|
2026-09-14 10:55:45 +08:00 |
|
|
|
c321388a21
|
fix(scheduler): 安全点重新求值中断队列 + 抢占/背压计数修正 + 停机补终态
对照 docs/zh/input-scheduler-design.md 原文修四处(前两处是真缺陷,后两处是
观测面与设计承诺不一致),均配回归用例:
1. §4.3/§5.2「临界区结束后的第一个安全点重新求值」此前**没有实现**:
全仓唯一的武装点是 registerInterrupt,凡被拦成「入队」的中断只能等当前任务
自然结束。可达症状:WebUI 终止按钮连按两次,第二次落在 2s 抢占冷却窗内 →
入队 → 再也不会被求值。修:runTaskSteps 的安全点先 rearmPending()——
判据与 registerInterrupt 完全同一套(canPreempt + 冷却 + 临界区闸门)。
2. PreemptsByLevel 的语义是「进入 immediate 槽的次数」,但计数发生在
setImmediateLocked 之前:immediate 是单槽,同一安全点前到达的两条同级中断里
被降级的那条也被计成抢占。修:setImmediateLocked 只在真占住槽时返回 true,
计数随之为真;同时把「降级入队」的责任收归调用方,消除同一任务被入队两次的
隐患(实测该隐患会让中断任务执行两次、Executed 虚高)。
3. 状态面 Preempted 此前拿 Stats.Suspended 顶替,与 preempts_by_level 自相矛盾。
修:Preempted = Σ PreemptsByLevel[1..4]。
4. §4.4/Q4「满时阻塞发送方 + 计数并打日志」只做了阻塞:pumpInbox 满时直接返回,
一个字都不计。修:新增 Stats.Backpressure(+DTO 字段) 与只报一次的状态翻转日志;
同时显式处理 enqueue 返回值(静默丢弃会让同步调用方永久挂起)。
另:Stop() 停机前排空待办——给从未运行与已挂起的、带 ResponseCh 的任务补
skipped 终态,否则 cli/clawhubadapter 这类无超时同步注入方永久挂起(§7 I5、§11.3 X4)。
emitResponse 的 ResponseCh 写入改为非阻塞 + 告警,避免一行写错就卡死调度器 goroutine。
验证:go build/vet 干净;go test -count=1 ./internal/agent/... ./internal/plugin/...
./internal/sdk/... ./cmd/... 全绿;go test -race ./internal/agent/core/ ./internal/sdk/ 干净。
新增 scheduler_rearm_test.go 六个用例(冷却期满重新求值/同级降级不计数/Preempted 求和/
停机补终态/背压计数与翻转/pumpInbox 满计数)。
|
2026-09-14 10:39:25 +08:00 |
|
|
|
1ce3a917a5
|
feat(status+webui): 运行态图形化 —— 排队/四级中断队列/中断栈/驻留子/通道拓扑
需求:首页不该只有文字,要能一眼看出内核在忙什么——排队消息数、各级中断
排队与中断栈、驻留子 agent 数量;这些要向**内部 SDK 暴露接口**,供 WebUI 等应用
展示;通道划分也要能画出来。
## 一、内核状态面(internal/sdk,内部 SDK,不受公开 SDK 冻结约束)
* SchedulerStatus 补:
- interrupt_queues[5]:**四级中断队列各自的深度**(下标即级别 1..4,下标 0 恒 0,
这样 level 能直接当数组下标用)。此前只有 pending_interrupts 总数,
看不出"堵在 L1 还是 L4"——四级是抢占优先级,堵在哪级是完全不同的运行状态。
- immediate:刚抢占成功、下一个安全点立即运行的那个中断(此前完全不可见)。
- suspend_frames:中断栈的帧(栈底→栈顶,只给任务标识),depth 之外还能看出
"谁被谁打断"。
- interrupts_by_level / preempts_by_level:各级累计登记数与抢占成功数。
* KernelStatus 补 residents(驻留子运行时视图:状态/轮次/上下文已满/输入通道/允许输出)。
刻意**不带**每个驻留子的 inputch 登记明细——状态面会被反复轮询,明细会让
每次 /status 背上几十 KB;只给表大小,要明细走专门接口。
* ChannelInfo 补 direction(in/out/io)、description、tools、output_caps、caps_text。
此前 collectKernelStatus 只透传 Name/Type,把描述/工具/能力**全丢了**,
前端只能画出一排光秃秃的名字。
## 二、WebUI
* 新增只读 `/api/v1/runtime`:只回运行态三件事(scheduler/residents/channels),
实测 **1.0KB**(/kernel 是 30KB 级)——所以能 3 秒轮询做"实时"感,
而不必反复拉全量状态。
* 首页新增「运行态」面板(纯 CSS + 内联 SVG,前端仍无构建链):
- 四个数字块:排队任务 / 待处理中断 / 中断栈(深度/上限) / 驻留子 Agent,带占比条;
- 四级中断队列条形图:每级"深度 · 登记/抢占",四级语义**照抄内核**
(L4 内核独占 / L3 交互 / L2 消息 / L1 后台),不自己起名字;
- 中断栈层叠图(栈顶在上)+ ⚡立即运行项;
- 通道拓扑:输入通道 → 内核 → 输出通道,双向通道两侧都出现,能力以胶囊标签显示。
* 3 秒轮询只在总览页可见时才发请求;切回总览时 renderAll 会立刻补一次。
## 验证
* 新增 TestSchedulerStatusExposesLevelsAndStack(四级队列/立即项/栈帧/各级计数映射,
并断言"未使用的级别必须为 0"与"下标 0 恒 0")、TestChannelInfoCarriesTopology、
TestRuntimeEndpoint(形状 + 不携带 tools/plugins + 无状态源时 503)。
* go build / vet / agent+core+sdk+plugin+webui 全量测试绿。
* 真实浏览器实测(CDP 驱动,注入运行态样本走真实渲染路径):
数字块 [3, 5, 2/4, 2];四级条 L4=1/L3=2/L2=1/L1=1 与数据一致;
栈帧按"栈底→栈顶"渲染且标出栈顶;通道左右分列、io 通道两侧都出现。
|
2026-09-14 09:05:14 +08:00 |
|
|
|
c0e9dc1818
|
fix(webui): 聊天记录不再"每次都发完整记录",并修掉视口跳顶
两个都是你指出的症状,都定位到了具体代码路径。
① 「每次都发完整聊天记录」
a) API 缺省值错了:/api/v1/chat/history 的 limit 缺省是 0 = **不限制**,
于是任何不带 limit 的调用每次都拿到整段记录。实测(126 条):
不带 limit 635,297 字节;现在默认只回一页 180,668 字节。
显式 limit=0 仍可整取(逃生口)。WebUI/GUI 本来都带 limit,不受影响。
b) 前端 30s 轮询(以及每次 SSE 报错)都直接拉一页 40 条:
浏览器实测单次 180,813 字节。现在先做"尾巴探测"(limit=1,362 字节),
尾巴一致就直接跳过;不一致才拉整页。
② 「聊天记录会跳到顶部」——两条会导致视口丢失的路径都堵上
a) syncChatFromHistory 在"找不到重合点"时直接 `state.messages = serverMsgs`:
服务端只回一页,而本地可能已经向上翻了好几页;一覆盖,容器立刻变矮,
视口被夹回顶部,用户翻过的旧消息也凭空消失。现在只在服务端页**不短于**本地时
才整体替换。
b) renderChat 全量重建 innerHTML 后,仅在粘底时滚到底;非粘底(用户正在向上读)
时位置没人管。改为重建前记住 scrollTop、非粘底时原样还回去。
浏览器实测(CDP 驱动真实页面,126 条历史):
* 15s/30s 定时器跑满 40s:聊天区滚动位置 **0 px 变化**,未跳顶;
* 期间 chat/history 请求:limit=1 × 2(各 362 字节)+ 首屏 limit=40 一次;
* 控制台无报错;新增消息后轮询仍能正确并进来(尾巴探测→拉整页→合并)。
测试:新增 TestChatHistoryDefaultIsPaged(默认一页 / has_more / limit=0 整取 / 显式分页)。
顺带修测试串味:迁移用例往 os.TempDir() 写共享历史文件,会让其它用例的
NewHandler 加载到脏历史(表现为条数多 1);现在各用例用自己的临时文件。
|
2026-09-14 08:06:16 +08:00 |
|
|
|
764939ed90
|
refactor(webui): dashboard.html 6637 行拆成「外壳 + 样式 + 脚本」
前端刻意没有构建链(纯 CSS + Vanilla JS,go:embed 进二进制),所以拆法是:
外壳 dashboard.html 留 {{DASHBOARD_CSS}} / {{DASHBOARD_JS}} 两个占位符,
init() 启动时把两份资产原样填回去 —— **发出的 HTML 与拆分前逐字节一致**,
但 6637 行的单文件变成三份,便于编辑与评审。
dashboard.html 168 行 外壳(head/body 结构 + 两个占位符)
dashboard.css 2099 行 样式
dashboard.js 4371 行 脚本
逐字节校验(三重):
* 组装结果 sha256 == git HEAD 里拆分前的 dashboard.html;
* 真实实例 GET /(带 API key)返回体 sha256 同上:0ef14b49…c053;
* 新增 TestDashboardAssetsSplit:占位符必须存在、样式/脚本不得再内联回外壳、
组装结果不得残留占位符且必须含样式与脚本特征串。
按行号切片时踩过一次坑并已修正:`</style>`/`</script>` 两个闭合标签被切掉
(正好少 27 字节)——正是因为当时少了逐字节校验,现在把它固化成断言。
|
2026-09-14 07:17:23 +08:00 |
|
|
|
4cbfdc970c
|
perf(webui)+feat(config): 聊天记录写盘节流 + 配置库空闲页回收
两条都是我上一封里点出、你说继续的问题。
① 聊天记录:每条消息都整段重写 → 节流合并写
原来 persistChatLocked 每次变更就整段重写记录文件,而一轮对话会触发多次
(用户消息、每个工具事件、收尾消息)。200 条上限下文件可达数 MB,单轮就能
放大出几十 MB 写。文件里还留着一个 chatSaveThrottle=3s 常量——声明了但从未
被使用(疑似上次 revert 的遗留),等于节流从来没生效。
现在:persistChatLocked 只置脏 + 唤醒写盘协程;chatPersistLoop 去抖
chatSaveThrottle(3s)、并以 chatSaveMaxDelay(10s) 兜底(持续输出也不会无限拖延);
写盘前把快照拷出来,**不持 chatMu 做文件 IO**;写失败重新标脏下轮重试。
插件 Stop 里调 Handler.Close():停协程 + 强制落最后一次(幂等),否则丢最后一轮。
实测(临时实例,连发 3 条消息):3s 窗口内记录文件**尚未创建**(节流生效);
SIGTERM 后文件出现且 6 条(3 用户 + 3 助手,无 LLM key 故为错误回复)全在
——关停落盘没丢。
② config.db:SQLite 的 DELETE 不缩文件 → 空闲页够多时 VACUUM
新增 ConfigRegistry.MaybeCompact(minFreeBytes, minRatio):空闲页 >= 1MB 且
占页数 >= 25% 才做一次 VACUUM,避免每次启动都重写整库。库里是 WAL 模式,
VACUUM 之后必须再 wal_checkpoint(TRUNCATE),否则主库文件看着没变小。
调用点放在插件加载**之后**(大值的搬走/删除发生在插件 Start 里,之前调没意义)。
实测(一个刚被搬走 5MB 聊天记录的实例):
freelist 1288 页 × 4096B;启动日志「配置库已压缩: 5394432 -> 118784 字节」
config.db 5,394,432 → 118,784 字节;记录文件 5,279,491 字节完好未动。
测试:TestChatPersistenceIsThrottled(节流窗口内不写盘 + Close 必落盘 + Close 幂等)、
TestMaybeCompactReclaimsFreePages(删大值后文件确实变小 + 数据完好 + 阈值不达标时不白做功)。
|
2026-09-14 07:14:55 +08:00 |
|
|
|
642e1c39b1
|
refactor(webui): handler.go 2993 行按资源拆成 11 个同包文件
拆法:按「资源面」搬家,每个顶层声明(func/type/var/const)整体搬到目标文件,
声明体一字未改,各文件按实际用到的包重新生成 import。文件头加一行说明本文件负责哪一面。
handler.go 骨架:嵌入前端资源、Handler/构造、路由表、鉴权会话日志中间件、静态页
handler_chat.go 对话面:消息模型与内存历史、SSE 事件订阅、对话/历史接口
handler_upload.go 上传面:handleChatFile / handleUploads / 中断对话
handler_memory.go 记忆面:图/文档/文本记忆、知识库、LLM 源、变更追踪
handler_agents.go 内核与代理面:状态、kernel、人格、代理/快照/回滚
handler_settings.go 设置与插件面:配置读写、插件列表详情(含 pluginmgr 反代)
handler_terminal.go 终端面:终端会话、终端接口、命令历史
handler_sse.go SSE 环形缓冲(断线重连补发)
handler_openai.go OpenAI 兼容面:/v1/chat/completions
handler_device.go 设备网关反代(HTTP + WS 升级透传)
handler_files.go /files/ 与 /uploads/ 下载
零漂移校验:拿重构前的 handler.go 与新 11 个文件逐行比对(忽略空行、package/import 头),
**丢失行 0**;新增行恰好是 11 个文件头注释(14 行)。
顺带修掉 import 里两处假使用:handler_openai 的 sdk 只作为 Handler 字段名出现(h.sdk.),
handler_settings 的 fmt 只出现在注释里 —— 都从 import 里去掉。
验证:go build ./... / go vet / webui+config+sdk 测试全绿;
起真实实例(沿用已有 data 目录)后 /status /settings /chat/history /plugins /terminals
/kernel /memory /config /login 全部 200,设置在注入 5MB 历史的情况下仍是 33,921 字节。
最大文件从 2993 → 706 行(handler_chat.go)。
|
2026-09-14 07:03:47 +08:00 |
|
|
|
c4998bb102
|
feat(webui): 聊天记录改为独立文件存储(位置可配)+ 存量自动迁移
起因:聊天记录原先作为插件配置项 plugin.webui.chathistory 存在 config.db 里,
带来三个后果(都在生产实例上实测过):
1. 整段记录 5,176,016 字节会被 GET /api/v1/settings 当普通配置项整块返回;
2. 每来一条消息就把整段记录重新 marshal 后写回 config 表,而那次写要拿
config registry 的全局写锁 —— 消息频繁时所有配置读写都被拖着排队;
3. 位置不可配(想放独立挂载盘只能改整个 data_dir)。
改动:
* 新增 internal/plugins/webui/history.go:
- historyStore:默认 <data>/webui_chat_history.json,写盘用同目录 tmp+rename
原子替换,崩溃不会留半截 JSON;读失败/JSON 损坏按空历史处理并告警
(聊天记录不是关键数据,不该让它拖垮 WebUI)。
- resolveHistoryFile:插件设置 history_file > 默认路径;相对路径按 data 目录
解析(可指向独立挂载盘),data 目录未知时落到系统临时目录而不是进程 CWD。
- LoadWithMigration:文件为准;文件为空而老配置项有内容时,把记录搬到文件、
搬成功才删配置项(删不掉就保留并告警,不丢数据);文件已有数据时顺手清掉
上次没删干净的遗留键。
* 新增插件设置项 history_file(设置页可见可改):留空 = 默认路径。
* handler.go:chatHistory 的读/写改走 historyStore,不再碰 settings;
顺带把「写失败静默忽略」改成告警。
存量迁移实测(拿仍持有 5,271,690 字节老记录的实例跑新二进制):
日志:聊天记录已迁移到独立文件 .../webui_chat_history.json(1300 条),并从插件配置表移除
迁移后:config_webui 里 chathistory 行数 = 0;记录文件 5,279,491 字节
GET /api/v1/settings = 33,921 字节(迁移前 8,244,108)
GET /api/v1/chat/history 正常(从文件读回 1300 条里最新的 3 条)
新增测试:TestResolveHistoryFile、TestHistoryStoreMigratesFromConfig(含二次加载
不重复迁移 + 损坏文件不 panic)、TestHistoryStoreSaveIsAtomicAndRoundTrips。
|
2026-09-14 07:00:07 +08:00 |
|
|
|
0beb389223
|
fix(webui): 设置接口不再吐内部数据;--webui 覆盖生效;端口占用不再静默成功
三处实测确认的缺陷:
① 设置接口整块吐出聊天记录
plugin.webui.chathistory 是 webui 自己持久化的整段聊天记录(生产实例
实测 5,176,016 字节),躺在插件配置表里被设置接口当普通配置项整块返回,
前端还会把它渲染成一个巨大的文本框。
修复:GET 跳过该键(按插件+键精确判定),PUT 直接 400,避免误改。
② CLI --webui 与 webui.listen_addr 一直是死配置
内核原本在插件加载前写 settings["addr"],但那时 config_<name> 表还没建
(表只在插件注册 def 时创建),PluginSettings.Set 的 INSERT 失败,而错误被
"_ =" 忽略了;随后插件 Start 里 RegisterDef 才建表并写入默认 :8080。
实测:传 "-webui 127.0.0.1:18099" 仍然监听 :8080。
修复:覆盖值改由插件自己接收(webui.SetListenOverride,loadPlugins 前调用),
优先级 CLI > webui.listen_addr(非默认值才算显式配置)> settings["addr"]。
实测修复后:"-webui 127.0.0.1:18099" 正确监听 18099,与生产的 :8080 并存。
③ 端口被占时 webui 静默死亡
Start 在后台 goroutine 里 ListenAndServe,先打印 "listening on" 再尝试绑定,
失败只留一行日志,Start 永远返回 nil → 插件仍被当成加载成功。
修复:net.Listen 同步做,失败即返回 error(交给加载器/守护),
成功后才起 Serve,并打印真实绑定地址。
A/B 实测(两个实例都撞生产的 :8080):
修复前:"listening on :8080" + "server error: address already in use" + LOADED: webui
修复后:"[plugin] start webui: webui: 监听 :8080 失败: ...",不再有 LOADED: webui
效果实测(同一实例,先注入 5,271,690 字节 chathistory):
GET /api/v1/settings 8,244,108 → 28,652 字节(约 1/288)
meta 条数 5,208 → 105,幻影键 0 条
设置页仍正常:?prefix=plugin.webui 返回 8 条 def;普通键 PUT 落库;
校验:GET/PUT 内部键被拒;-webui 覆盖真实生效。
新增测试:TestSettingsNoCrossPluginLeak(跨插件泄漏/幻影键/chathistory 读写)、
TestListenOverrideAndBindFailure(覆盖生效 + 端口占用必须报错)、
TestResolveListenAddrPrecedence(优先级)。
|
2026-09-14 06:54:16 +08:00 |
|
|
|
9e6627f0c3
|
fix(config): 插件 def 查询不再越界 —— ListDefs 作用域 + 新增 ListCoreDefs
两个方向相反的越界,合起来把 WebUI 设置接口的 meta 撑成 5208 条(96% 重复):
1) PluginSettings.ListDefs(prefix) 把 prefix 直接透传给全局 ListDefs,
等于「返回全仓所有 def」——调用方以为在问某个插件,实际拿到全部。
修复:限定到 plugin.<name>. 命名空间,并把 Key 剥回插件内局部键
(调用方看到的键必须与 Set/Get/ListPlugin 的局部键一致)。
2) DefsCore(prefix) → reg.ListDefs(prefix) 会连插件 def 一起返回,
于是 meta 里出现 plugin.<name>.<key> 的「核心侧副本」。
修复:新增 ConfigRegistry.ListCoreDefs,显式排除 plugin.* 命名空间。
生产实例实测(旧代码):GET /api/v1/settings 的 meta = 5208 条,
其中 core.agent.* 等每个 def 都被复制 28 份(每个插件命名空间一份),
并派生出 plugin.<a>.plugin.<b>.<key> 这类幻影键。
⚠️ 幻影键不只是脏数据:设置接口的 PUT 走 SplitN(key, ".", 3),
对 plugin.<a>.plugin.<b>.<key> 会解出 (a, "plugin.<b>.<key>"),
即按 UI 上的幻影条目保存会**写进错误插件的配置表**。
新增 TestPluginDefsAreNamespaced 钉住两条作用域。
|
2026-09-14 06:53:58 +08:00 |
|
|
|
3edab0fe68
|
refactor(homed): main() 696 行按启动阶段拆成 25 个阶段函数
main() 原本是一整条 696 行的启动脚本:日志、目录、记忆、配置、Lua、守护、
追踪、内核 API、文本记忆、LLM 源、文档/知识、人格、插件、Agent、ONNX、
IPC、心跳、关停全挤在一个函数里,变量跨 500 行互相引用。
现在 main() 只剩「顺序编排 + 就地交接」(**149 行**,低于 funlen 阈值 150):
opt := parseFlags()
logDir := setupLogging(opt.dataDir)
agentWorkDir := ensureDataDirs(opt.dataDir)
mem, closeMem := initMemoryStack(opt.dataDir)
...
共 25 个阶段调用,实现体在同包 bootstrap.go(一一对应)。
零漂移保证:
* 阶段体逐字取自原 main,只做机械替换(`*dataDir`→参数、`memIdx`→`mem.indexer`);
* 原 main 的每个 defer 都换成一个在**同一位置**注册的 cleanup,
LIFO 释放顺序不变;多资源阶段内部再按原注册顺序取反;
* 语句级比对:原 main 的 525 条可执行语句全部有对应,无遗漏。
仅 3 处为**有意**的结构改写(其余为同义替换):
1. initLuaVM / initTextMemory:「启动成功才 defer Stop」改为
「失败返回 no-op cleanup,成功返回 Stop」——调用点语义不变;
2. startIPCServer:同上(用 started 标志保证失败时不 Stop);
3. resolveBaseAPIKey:把三级兜底 API key 解析提成一个纯函数。
* defaultPrompt 提为包级 const defaultSystemPrompt(不含版本号字面量)。
验证(A/B 实测,不是只跑编译):
* go build ./... / go vet ./cmd/homed/ / go test ./cmd/... ./internal/agent/... ./internal/plugin/... 全绿
* 重构前后二进制各起一次(-data 临时目录,SIGTERM 收尾),日志集合**完全一致**:
63 个注册工具、同名插件全部 loaded、kernel ready、插件逆序关停、'stopped'
—— 差异仅为并发加载插件的打印顺序。
全仓非测试 Go 函数现状:≥300 行 **0 个**,≥200 行 9 个,≥150 行 16 个。
|
2026-09-14 06:30:45 +08:00 |
|
|
|
129a3aeb38
|
refactor(proc): coreHandler.Handle 550 行按 method 组拆成 14 个分部函数
原 Handle 是一个 550 行的巨型 switch(C ABI 51 个 case 的整块平移),
按协议面拆进同包 5 个新文件、14 个小函数:
corehandler_register.go handleRegister 注册面(tool/stage/output/api/input)
corehandler_inject.go handleInject IO 注入 + SetToolBlocks
corehandler_memory.go handleGraphMemory 图记忆
handleDocMemory 文档记忆
handleKnowledge 知识库
handleTextMemory 文本记忆
corehandler_settings.go handleSettings 设置(14 个 method 共用一条实现)
handleLLM LLM 源
handleSocial 社交图只读
handleLifecycle 生命周期开关
corehandler_runtime.go handlePluginMgr 插件管理
handleStageLocks 段锁仲裁
handleEvents 事件订阅
handleArena 共享槽池
Handle 保留 capability 强制检查,只做「method → 分部函数」一跳。
零漂移保证:case 标签由脚本从原文提取(不手抄常量名),case 体逐字搬迁,
逐函数比对确认 59 个标签 / 510 行 case 体与原文件完全一致(仅行首缩进经 gofmt 重排)。
验证:go build ./... / go vet / go test ./internal/plugin/... / -race 全绿。
|
2026-09-13 23:29:13 +08:00 |
|
|
|
dcae21b24c
|
refactor(lua): replaceSDKReal 633 行按 SDK 子表拆分
抽 luaReg 上下文(L/t/plg/s + subTable/pushVal/pushList/pushErr/pushNil
五个助手方法),把单函数拆成 registerRegistrars / registerInjectors /
registerDataAPIs / registerSettings / registerEventsAndMgr 五个方法。
被移动的代码体**逐字保留**:各方法头部把 L/t/s/plg 与五个助手注入为局部
别名,所以内部一行未改,行为零漂移。luaSyncUnavailable 提为包级函数
(原来它在被拆到另一个方法的作用域里会 undefined)。
实测:replaceSDKReal 从 633 行消失,最大子方法 265 行;非测试函数
>=300 行的数量 3→2。go build/vet/test -race 全绿。
|
2026-09-13 22:44:39 +08:00 |
|
|
|
62e1e02036
|
docs(ohos): README 两处事实修正(产物路径前置条件、≤400 行约定的边界)
1. **安装小节的产物路径**:`entry/build/` 是纯构建产物、不入库,干净 clone 或清理过
工作区时该文件不存在。补明"必须先跑完第 2 步",并说明目录名随
`-p product=<名字>` 变化、同目录还有 unsigned 版(`hdc install` 要用 signed)。
本次实际构建校验过路径:`entry/build/default/outputs/default/entry-default-signed.hap`。
2. **≤400 行约定补边界**:不到 400 行的文件不要为了拆分而拆分 ——
`@Component` 的 `build()` 只允许一个根节点,多节点 `@Builder` 改组件会多出一层
Column 包裹,布局等价是推理出来的、不是看出来的,每拆一次都要付一次
"未上机验证"的账。并记下 `pages/Index.ets`(396) 属于"不越线就不动"的一类。
无代码改动,纯文档。
|
2026-09-13 22:42:37 +08:00 |
|
|
|
8a58cde3ee
|
docs(ohos): 本地构建前置条件 + 运行时验证清单 + 工程结构表刷新
1. **oh_modules 前置条件**:该目录被 .gitignore 忽略但必须存在 —— hvigor 不会
自动 ohpm install,移走后构建直接报 arkts-no-untyped-obj-literals(依赖类型
声明缺失)且不重建目录。写明"clone 后先 ohpm install,别当垃圾清掉",
同时说明 entry/build 与 .hvigor 是纯产物、可随时删(冷构建 ~8s)。
2. **hvigorw 绝对路径**:仓库里的 ./hvigorw 是符号链接,启动脚本按
$(dirname $0) 定位,会报 File not found: <repo>/cmd/ohos/hvigor/bin/hvigorw;
统一改用 <command-line-tools>/bin/hvigorw(本机 /opt/huawei/command-line-tools/bin/hvigorw)。
3. **运行时验证清单**:把"构建通过 ≠ UI 行为不变"这件事写进仓库而不是留在邮件里
(流式对话/思考卡与工具卡/附件上传/设置与设备与插件的二级页/宽屏分栏),
并注明无提权环境起不来模拟器时需在真机补验证。
4. 工程结构表按前几轮拆分后的实际情况刷新(common/components/pages 新增文件与职责),
并记下"单文件 ≤400 行、页面只做壳"的约定。
无代码改动,纯文档。
|
2026-09-13 22:35:29 +08:00 |
|
|
|
e733d05a5e
|
refactor(ohos): DevicePage 560→307 行(入口列表/四个面板/模型拆分)
- common/DeviceModel.ets(73):device_id 兜底(桥 id 优先 → 持久化 → 生成并落盘,
顺序与原 aboutToAppear 一致)、/device/online 响应解析、四个二级页路由 id
- components/DeviceRootEntries.ets(92):一级入口(本机/通道两组 NavRow 列表);
宽屏高亮自己读 AppStorage 的 isWideScreen,页面只给 activeSub 与 onOpen
- components/DevicePanes.ets(316):DeviceLocalPane(基本信息 + 授权开关)、
DeviceCapsPane(能力清单)、DeviceGatewayPane(网关信息 + 刷新)、
DeviceListPane(在线设备列表)、DeviceKvRow;每个面板自带 SubPageLayer
页面保留导航栈、openSub/closeSub、授权开关、toast、refreshDevices 与
SubDestination 分发。所有文案、图标、颜色、过渡与失败分支逐字保留;
面板内容多了一层无 padding 的 Column 根节点(@Component 的 build() 只允许
一个根),宽度 100% + Start 对齐,与 SubPageLayer 内容槽的布局一致。
验证:hvigorw assembleHap BUILD SUCCESSFUL。
|
2026-09-13 22:34:20 +08:00 |
|
|
|
8a9fc05451
|
refactor(ohos): DeviceBridge 409→296 行(协议类型与帧构造移到 BridgeProtocol)
- common/BridgeProtocol.ets(168):hello/bind/cmd_result/cmd_data_start/
cmd_data_end/event/status 七个消息结构、CHUNK_SIZE、BridgeCmdHandler 回调类型,
以及 bridgeHelloFrame / bridgeBindFrame / bridgeResultFrame /
bridgeDataStartFrame / bridgeDataEndFrame / bridgeEventFrame /
bridgeStatusFrame / bridgeChunkSlices 八个纯构造/切片函数
- common/DeviceBridge.ets(296):只剩 socket 生命周期、重连代际、绑定状态机
与命令分发
**DeviceBridgeClient 的对外方法名与签名一字未改**(isConnected / getDeviceId /
setCmdHandler / setStateListener / connect / updateAuthorized / disconnect /
sendResult / sendDataChunked / sendEvent / sendStatus / send),
`send*` 仍是"拼帧 + 发出去"两步,拼帧那一步搬走;分块发送的
"start → chunks(失败即 break)→ end"顺序与 `bytes.slice` 语义保持不变。
外部只有 deviceBridge 单例被 import(BridgeRouter / DeviceBridgeSession /
DevicePage),无其它符号依赖。
验证:hvigorw assembleHap BUILD SUCCESSFUL;diff 中 DeviceBridge.ets 无
状态机/回调/重连逻辑改动。
|
2026-09-13 22:28:53 +08:00 |
|
|
|
31bbde9cdd
|
refactor(ohos): Attachment 463→344 行(纯函数与字节解码移出 components)
- common/AttachmentMeta.ets(106):parseAttachment / attachmentFromChannelOutput /
fileNameOf / formatBytes / oneDecimal / extLabel / sanitize,纯函数无平台依赖
- common/AttachmentImage.ets(37):loadPixelMap(沙箱 file:// 与远端 /files 两条路径)
components/Attachment.ets 只留 UI:AttachmentCard 与 AttachmentDetailContent。
**两者的导出名、@Prop 形状与 import 路径均未变**(ChatPage 仍从
'../components/Attachment' 取 AttachmentDetailContent),组件内部的布局、
过渡、提示文案与失败分支逐字保留,仅函数体搬走。引用方 5 处 import 路径同步更新。
验证:hvigorw assembleHap BUILD SUCCESSFUL;diff 中 Attachment.ets 只有
"-删除纯函数 + import 改写",无属性/布局改动。
|
2026-09-13 22:27:34 +08:00 |
|
|
|
e0514c3692
|
refactor(ohos): ChatPage 1962→207 行(状态机/SSE/气泡/输入区拆分)
- common/ChatStore.ets(388):消息数组、分页游标、新消息入场标记、
SSE 连接与重连、防抖刷新(50ms)统一成一个单例状态机
- common/ChatSse.ets(192):SSE 事件 → 状态的翻译层,逐分支照搬
channel_output/agent_output/reasoning/delta/tool_call/stage/agent_error/
sync_required;通过 ChatStreamSink 接口写入,避免与 ChatStore 形成循环依赖
- common/ChatSession.ets(176):POST /chat 与 POST /chat/file 的发送、
超时兜底、POST 响应与 SSE 的合并判定
- common/ChatFormat.ets(223):mime 推断、ForEach 键 structSig、工具卡
状态/颜色、渠道判定与头像配色
- common/ChatHistory.ets(96):历史载荷与 tool_calls 解析
- components/ChatBubble.ets(247):气泡(头像/渠道名/思考卡/工具卡/附件卡/正文)
- components/ChatToolCard.ets(253):思考卡 + 工具卡
- components/ChatStream.ets(210):消息列表 + 顶栏遮罩 + 底部淡出 + 触顶懒加载
- components/ChatComposer.ets(357):输入行、选图/选文件、沙箱落盘、发送
- components/ChatAttachBar.ets(139):加号菜单 + 待发送附件条
响应式语义刻意保持不变:数组不进 AppStorage,改用自增版本号 K_CHAT_REV
通知订阅组件重取快照(ChatStream 把它镜像进 @State messages,ForEach 每次
拿到的仍是新数组引用,与拆分前 this.messages = this.messages.slice() 等价);
structSig 仍不含 content,正文靠 MarkdownView 的 @Prop 流式更新;
markNew 仍不切片、入场动画交给紧随其后的 refresh();
animateTo 只能存在于组件里,所以折叠翻转的动作留在 ChatStream 内。
验证:hvigorw assembleHap BUILD SUCCESSFUL;SSE 各分支、ensureToolCall、
markNew、sendChat/sendWithAttachment 的守卫顺序均与原实现逐条比对过,
守卫从"页面方法内"移到输入区组件时保持了原有先后(连接判定先于清空输入)。
|
2026-09-13 22:19:05 +08:00 |
|
|
|
f79e0f82dd
|
refactor(ohos): SettingsPage 1463→389 行(模型/条目卡/四个面板拆分)
- common/SettingsModel.ets(313):设置载荷解析、分类归并、分页切片、
路由常量,纯逻辑无 UI
- components/SettingsEntryCard.ets(215):单条设置卡(值编辑/保存)
- components/SettingsRootEntries.ets(100):一级入口行 + 状态汇总卡
- components/SettingsHome.ets(95):一级页壳(顶栏/浮层/提示条)
- components/ConnectionsPane.ets(347):后端连接 CRUD(自带二级页壳)
- components/AppearancePane.ets(265):主题/背景/透明度(自带二级页壳)
- components/BackendSettingsPane.ets(172):分类与条目两个二级面板
页面保留 @State 集合、加载/保存编排与 SubDestination 分发;数组仍走
@Link 直传(未引入 AppStorage 数组)。相册选择、重启前台桥等既有行为
与提示文案逐字保留。
验证:hvigorw assembleHap BUILD SUCCESSFUL。
|
2026-09-13 22:18:56 +08:00 |
|
|
|
131c1ff0e9
|
refactor(ohos): PluginsPage 995→374 行(接口/状态/列表/详情/浮层拆分)
拆分方向按职责切,页面只留导航与数据编排:
- common/PluginApi.ets(174):fetchPluginRows / fetchPluginDetail
- common/PluginStatus.ets(78):状态判定、文案、颜色、副标题
- components/PluginListView.ets(166):列表卡 + 列表
- components/PluginDetailPane.ets(291):详情面板
- components/PluginsOverlays.ets(65):安装表单
- components/ToastBar.ets(48):提示条,PluginsToast 与 SettingsPage 的
toast 合并成这一个组件(此前两处各写一份)
UI 结构与文案逐字保留(含 92%/layoutWeight 等既有布局修法)。
验证:hvigorw assembleHap BUILD SUCCESSFUL。
|
2026-09-13 22:18:49 +08:00 |
|
|
|
414e6a1627
|
refactor(ohos): StaticMarkdown 604→332 行(Markdown 解析抽到 common/MarkdownParser)
把"文本 → 块/片段"的纯解析逻辑(MdBlock/MdSpan、parseBlocks、parseInline、
表格/分隔线/有序列表判定)整体搬到 common/MarkdownParser.ets(282 行),
StaticMarkdown.ets 只留 StaticMarkdownView 组件(332 行)。
解析规则逐字保留,未改任何排版行为;MdBlock/MdSpan 原本只被
StaticMarkdown.ets 使用(唯一引用方是 components/MarkdownView.ets,
它只用 StaticMarkdownView)。
验证:hvigorw assembleHap BUILD SUCCESSFUL。
|
2026-09-13 22:18:43 +08:00 |
|
|
|
431bb5a0c9
|
refactor(tooldefs): buildToolDefs 614→273 行(抽 toolDef 助手,schema 形状不变)
原实现每条工具都是 4 层嵌套的 map[string]interface{} 字面量(约 20 行/条),
40+ 条堆成 614 行的巨型函数。新增 toolDef(name, desc, props, required...)
助手消除外层样板;所有 name/description/properties/required 文本**逐字保留**
(用 go/ast 定位原样搬迁,非重新键入),schema 形状与 JSON 输出不变。
验证:go build ./... / go vet / go test ./internal/agent/core/ 全绿;
AST 实测 614→273 行,非测试函数 ≥300 行数由 4 降到 3。
|
2026-09-13 22:17:45 +08:00 |
|
|
|
6878f0126d
|
fix(lua): 同步注入在 Lua 中明确报不可用(避免自锁)+ 文档/mock 同步
sdk.inject_input_sync / *_opts / inject_input_media_sync* 在 Lua 里必然自锁:
Lua 代码只在 Start/工具/阶段/输出/事件回调中执行,这些路径都持有 plg.mu,
而同步注入要等本轮回复(回复路径上的回调又需要同一把锁)。原实现会挂死
直到超时;现改为立即返回明确错误,并在中英文 PLUGIN_DEV 里标注不可用 +
指向 Go 插件/异步注入。mock sdk.lua(SDK 仓为事实源)同步为同样的错误语义。
新增 TestLuaSyncInjectUnavailable 钉住不挂死。
|
2026-09-13 21:59:23 +08:00 |
|
|
|
52127a3323
|
fix(resident): CreateResident 注册入站 inputch 后加 defer 回滚
注册点与 residents 登记之间当前无可失败步骤,但缺回滚路径就是 child/<id>
残留那只 bug 的另一条入口。加 registered 标志 + defer:未走到成功返回就注销。
|
2026-09-13 21:59:10 +08:00 |
|
|
|
11d9038927
|
fix(io): ChannelRegistry 补 UnbindOutputTarget,Unregister 清理 outputTargets
outputTargets 只增不减:Unregister 一个 inputch 后,指向它的输出目标登记仍
留在表里,ResolveOutputTarget 会继续把消息路由到已不存在的 agent/inputch。
与刚修的驻留 inputch 残留同属「注册未注销」类。现补 UnbindOutputTarget
(幂等),并让 Unregister 顺手清掉显式绑定与同名回退两种目标登记。
|
2026-09-13 21:59:10 +08:00 |
|
|
|
a15e7c2dc1
|
chore(lint): 加 .golangci.yml(funlen/gocyclo/lll/dupl,warn-only)+ make lint-full
main 上曾有 4 个 >=300 行函数、12 个 >=200 行函数;没有复杂度 linter 是它们
长期存活的直接原因。先以 warn-only(issues.exit-code:0)立阈值、只出清单,
历史债下降后再收成硬门禁。测试与 testdata/third_party 排除长度类规则。
|
2026-09-13 21:59:10 +08:00 |
|
|
|
4518c3eb03
|
fix(lua): events.subscribe 改用内部 Subscribe + 订阅生命周期(修死锁/use-after-close)
上一版 Lua 对齐引入的 sdk.events.subscribe 有两个真问题,本提交修掉:
1) 用了公共 SDK 的 Events(),但本内核从未注入 event subscriber
(SetEventSubscriber 全仓无调用点),拿到永远是 nil ⇒ subscribe 只会
返回 "events unavailable"。改用内部 SDK 的 s.Subscribe——内置插件走的就是
这条路径(cli/webui/skillmgr 全用它)。
2) 自死锁:subscribe 会在 Lua 的 plugin.start(sdk) 回调里被调用,而
luaPlugin.Start 正持有 p.mu;原实现在 subscribe 里再 lock p.mu 追加 subs,
不可重入 ⇒ 测试实测 30s 超时。改用独立的 subsMu。
3) use-after-close:Stop 会 Close LState,但事件订阅此前无人取消,残留回调
再触发就会碰已关的 L。现在:Stop 先(不持 p.mu,避免与 Bus.Publish
锁序反转)取 subsMu 取消全部订阅,再置 closed 并关 L;事件回调持 p.mu 后
先查 closed,已进入等锁的旧回调会直接返回。
4) plugin_mgr 访问补 nil 保护(部分单测构造的 SDK 不含 pluginMgr)。
回归:TestLuaEventsSubscribeAndStopCleanup——订阅后 Publish 命中、Stop 后
再 Publish 不 panic。全套 Lua 测试在 -race 下通过。
|
2026-09-13 20:33:13 +08:00 |
|
|
|
44cb7243c5
|
fix(resident): 销毁驻留子时注销其入站 inputch(child/<id>)—— 修登记表脏数据累积
根因:residentInboundChannel 在 create 时把 child/<id> 登记进共享登记表
(Plugin=resident, Owner=父),但 teardownResident 只把划入的 inputch
(如 timer)归还为未分配,从未注销这条入站登记。于是每次 create/destroy
都在登记表里留下一条脏记录,且随次数单调累积。
实测(HomeAgent 侧,HΔ-Kernel v1.3.10 / 1b49365):
resident_agents destroy 之后,input_channels by_agent 仍列出 child/<id>,
归属 main;而 HomeAgent 没有任何工具能单独注销 inputch,只能重启 homed 清。
修法:teardownResident 里用纯函数 inboundChannelName 算出名字并 Unregister。
不能复用 residentInboundChannel——它有重新登记的副作用。
该路径同时覆盖 destroy / reclaim / StopResidents(父退出)。
测试:TestResident_LifecycleAndNoOrphans 增加两条断言——销毁后与父退出后
child/<id> 都必须从登记表消失。
|
2026-09-13 20:19:37 +08:00 |
|
|
|
162f33f81e
|
feat(lua): Lua 插件桥全量对齐 SDK 1.3.0(媒体/注入标志位/优先级/事件/通道注销)
内核 Lua 桥(internal/plugin/lua_plugin.go)此前停在 v0.8.0 时代能力面,
1.1/1.2/1.3 新增能力只在 Go 侧存在,而 PLUGIN_DEV.md 宣称『能力完全对齐』。
本补丁把 Lua 侧补齐到与公开 SDK 1.3.0 对齐:
- 1.1 媒体:memory.commit 支持 sentence_text/media_digests;
doc.insert_with_media + attachments;text_memory.append attachments;
set_tool_blocks / inject_input_media(_sync) / inject_interrupt_media。
- 1.2 注入语义:inject_input_sync(_opts)、六个 *_opts 变体
(no_memory/context_policy/cleaner_name/priority);
ToolDef/ChannelDef 解析 context_policy。
- 1.3 优先级与动态通道:priority 常量透传;unregister_output_channel。
- StageContext 暴露 reasoning_content/context_msgs/token_usage/memory/extra/errors。
- 新增 sdk.events.subscribe 与 sdk.plugin_mgr.*。
- sdk.lua mock 同步(单一事实源在 SDK 仓 sdk/lua/sdk.lua,内核副本由
third_party/homeagent-sdk/scripts/sync-lua-sdk.sh 同步)。
契约测试(lua_surface_test.go):
- 守住内核内嵌 mock 与 SDK 仓事实源一致;
- 守住 mock 承诺的每个函数都有运行时 RawSetString 绑定;
- 覆盖 opts/media/attachments 解析与 context_policy 透传。
文档:中英 PLUGIN_DEV.md 的 Lua API 表补齐并改为『对齐至 SDK 1.3.0』。
|
2026-09-13 19:56:32 +08:00 |
|
|
|
180b96e21a
|
docs: 记下 gitcode 附件的"同名只写一次"硬约束(校验和首次没传全就永远补不回来)
实测证据:同一个名字(ZZprobe.txt)传两次不同内容,下载端始终返回第一次那份;
资产列表里该名字只有一项。删除接口走不通 —— release JSON 不含 `id`,附件列表接口 404,
`DELETE .../releases/<tag>/attach_files/<name>` 返回 400「参数类型错误」(要数字 id)。
后果(1.3.1–1.3.10 全都踩了):首次上传 `SHA256SUMS` 时只有 linux/amd64 四个产物,
之后补 arm64/darwin/win 时"合并后重传"**全部无效** —— 线上那份至今仍是 4 项、带 `./` 前缀,
arm64/darwin/win 的产物没有校验依据。
⇒ 纪律:**打包全部平台后才第一次上传校验和**;分批上传时先传产物、最后传校验和,
校验和只传一次。补救只能换名(`SHA256SUMS.complete`)或重建 release(需重传全部产物)。
|
2026-09-13 18:50:06 +08:00 |
|
|
|
3fda1db9c0
|
fix(packaging): package-windows.sh 支持 NSI 覆盖 + payload 预检
上一步失败:`File "..\..\build\linux-payload\*.*" -> no files found`。
原因:NSIS 的 `File` 路径**相对 .nsi 所在目录**解析,而我用了主仓的 installer.nsi
(它去找主仓的 build/linux-payload),payload 却 stage 在 tag 的 worktree 里。
改法:`NSI` 可覆盖 —— 在 tag 的 worktree 里构建时用**该 tag 里的** installer.nsi
(与产物同源,也正是可复现发布该有的样子);另加 payload 空载荷预检(空载荷=装不上,
必须直接失败而不是打出一个没内容的安装器)。
|
2026-09-13 18:29:13 +08:00 |
|
|
|
8910c8c454
|
docs: 补两条流水线纪律(脚本必须 set -e;tag worktree 里的驱动脚本)
今天连踩两次,都是"脚本本身"的问题而不是打包逻辑的问题:
1. **没 `set -e`**:`package-windows.sh` 在 tag worktree 里找不到(新脚本只在 main),
bash 报 No such file or directory 之后**流程照旧往下走**,把只含 4 项的校验和
传上去覆盖了原本覆盖 10 项的那份 ⇒ 只能把产物下回来重建。
2. **驱动脚本不在 tag 里**:发布件在 tag 的干净 worktree 里构建,而刚补的脚本还没进 tag。
⇒ 让脚本支持 `DIST_LINUX` / `BUILD_DIR` / `DIST_RELEASE` 覆盖,用"主仓脚本 + 产物目录
指向 worktree"来解;文档写清这条约束。
同步进发布技能的同名小节(这两条和"分批上传要全量重算校验和"是同一类:**校验和的完整性
比产物本身更容易被流程吃掉**)。
|
2026-09-13 18:23:22 +08:00 |
|
|
|
c83eca5e88
|
fix(packaging): package-windows.sh 支持 DIST_LINUX/BUILD_DIR/DIST_RELEASE 覆盖
我在 v1.3.10 的 tag worktree 里调这个脚本,而它提交在 main(tag 里当然没有)⇒
`No such file or directory`;又因为脚本没加 `set -e`,它继续往下跑,把只含 4 项
(linux amd64)的校验和传上去,覆盖掉了原本覆盖 10 项的那份。
目录可覆盖后就能「用主仓脚本、产物目录指向 worktree」,两个坑一起消掉。
|
2026-09-13 18:22:33 +08:00 |
|
|
|
594496a225
|
feat(packaging): 补 Windows(WSL) 安装器的驱动脚本,并按变体定向 payload
用户要求:**Windows 的 homed 安装包应当是往 WSL 里安装**。口径本身早已落地
(installer.nsi 注释 + install-via-wsl.ps1 + build.sh 的 WSL 分支),但缺两样东西:
1. **没有驱动脚本**:`build.sh` 里没有 `makensis`,仓库里也没有任何脚本调用它 ——
build/ 下那几个历史 .exe 是手工打的。新增 `deploy/packaging/package-windows.sh
<server|client|full> [arch]`:按变体准备 payload、必要时编 waiter.exe、调 makensis、
把产物落到 dist/release。
2. **payload 不分变体**:`build.sh` 的 `stage_linux_payload` 把 dist/linux 下所有 deb+tar
全塞进 payload ⇒ 现在 server/full 的 deb 各带 ~719MB 模型,任何变体的安装器都会
膨胀到 ~2.4GB。而 WSL 侧脚本只取 payload 里的**第一个** `.deb`
(install-via-wsl.ps1:141)⇒ 按变体只放对应的那一个包。
同时:client/full 需要 Windows GUI payload(HAS_GUI=1),本机无 electron-builder 时
**明确失败并给出命令**,不产出"装完没有界面"的半残包。
docs/git-branching.md §七.5 补上这条口径与三条命令。
|
2026-09-13 18:13:04 +08:00 |
|
|
|
56ba1332f3
|
docs: 补「分批上传时后一轮必须全量重算 SHA256SUMS」
实测踩到:v1.3.10 先传 amd64 的 9 个资产(含只覆盖 amd64 的 SHA256SUMS),
后补 arm64 时按 arm64 那 4 个文件重算 ⇒ 同名附件覆盖 ⇒ amd64 的校验和消失。
校验和是附件的唯一完整性依据,丢了等于没有校验。已同步到技能的同名小节。
|
2026-09-13 17:35:02 +08:00 |
|
|
|
1d4f2beeea
|
fix(prompt): 去掉"每轮只能发一次 output_send"的凭空限制;type 缺省即 text
用户现场指出:**qq 插件的输出通道判据太严了**(那条判据在插件侧,已单独修:
`output_send__qq` 不再受"当前会话身份"限制)。同时内核提示词里还有一条**同类的凭空限制**:
「每轮对话**通常只需调用一次** output_send__{通道名} 即可完成回复。
仅在内容确实超过单条消息长度上限(如 >4000 字)时才拆分为多条」
可设计上输出是 agent 的**主动调用**:收到一次输入后,可以往**任意(已授权的)通道**
发**任意多次**(分段播报、先回执后结论、同时通知多个通道都合法)。这句话会让模型
自己收起合理的多次输出 —— 而且它不是任何机制的要求,只是当初为压 output-loop 写的
措辞(真正的防环机制是"回执只回 ok、不回传富结果",那条保留)。
改法:
- 提示词改为明确授权:**输出次数与目标通道由你自己决定**,没有「一轮只能发一次」的限制;
只保留两条真话:单条长度上限(超长拆完整段落)、别反复重发**完全相同**的内容。
- `output_send__*` 的 `type` 参数改为**可选**(缺省 text):判据该拦的是"不知道发什么",
不是"没写众所周知的默认值"——此前缺 type 会直接失败并让模型重试一次。
判据 3 条(新增 `output_rules_test.go`):提示词不得含输出次数限制且必须显式授权 /
省略 type 时按 text 发送成功且 schema 的 required 只有 payload / 空 payload 仍被拦。
(cherry picked from commit 17ea7fd5f0)
|
2026-09-13 16:04:16 +08:00 |
|
|
|
5160d8d4d1
|
chore(sdk-mirror): qq 插件 1.4.1(输出工具不再受当前会话身份限制)
镜像 SDK 仓的插件修复:被子的中断唤醒的一轮里,父带齐 meta 调 output_send__qq 也被
「可信 QQ 会话身份不完整」拒掉;输出改为先放行(目标由 meta 决定),读取类工具仍限当前会话。
|
2026-09-13 16:03:28 +08:00 |
|
|
|
e2500035d5
|
fix(resident): 子的「轮次」一直显示 0 —— info() 根本没填 Rounds
现象(用户线上联调实录 + 我复验):父侧 `resident_agents` 列出 `输入ch=[timer] 轮次=0
处理表=2` —— **处理表已有两条记录,轮次却是 0**,自相矛盾,容易被读成"子没干活"。
根因:`residentChild.info()` 构造 `ResidentInfo` 时**从来没有填过 Rounds 字段**
(结构体里有这个字段,于是永远输出零值),不是计数漏加。
改法:`Rounds = 已执行轮次数`(调度器执行计数,单调不减)。新增 `Agent.roundsExecuted()`
并写明为什么**不能**用 inputch 处理表条数当轮次:那张表记的是"当前上下文窗口内"的轮次,
压缩会清空(设计 §8.3)—— 用它会让父看到轮次倒退。
判据:inputch 路由测试里补一条断言 —— 子处理完输入后 `info().Rounds > 0`。
(cherry picked from commit cd88b2dfe5)
|
2026-09-13 15:41:07 +08:00 |
|
|
|
886ba78b11
|
fix(scheduler): inputch 划给子后输入只流向子 —— 补上「进内核之前」的输入路由
用户指出的语义(设计稿 §4.1 早已写明):
**inputch 是可分配资源**,「路由发生在**进内核之前**」—— 划给某个 agent 后,
该通道的输入**只流向那个 agent**;outputch 不同,授权是**非独占**的,
父依旧可以通过它发送内容。
而代码里 inputch 划拨只做了**登记**,没有做**路由**:
- 插件注入输入的 io 是**根 agent 的**(`cmd/homed` 里 `pluginReg.SetIOManager(iom)`);
- 唯一消费输入的是「该 io 自己的调度器」(`scheduler.go` 读 `a.io.InputChan()`);
- `ChannelRegistry.Assign` 只把 Owner 写进登记表,**没有任何转发动作**。
⇒ 现场表现(用户线上联调):子挂 `inputch=[timer]`,**timer 的输入却打在父身上**
(日志 `[agent] interrupt from timer/timer`),子侧 `轮次=0` 永远不动。
登记表里的 Owner 于是沦为标签。
改法(按 §4.1 把路由放回"进内核之前"):
- `IOManager` 增加 `InputRouter`(`SetInputRouter`),并把**五处直接入队**收口到
`deliverInput`:`InjectInput` / `InjectInputSync` / `InjectInputTo` /
`InjectInputSyncTo` / `InjectInterrupt`(排队与中断两条路都过路由)。
- 内核注入路由器 `Agent.routeInputByOwner`:查 inputch 的 Owner —— 归自己/未分配 ⇒
本内核处理;归自己的某个驻留子 ⇒ `DeliverRouted` 交给它(**不再进父的队列**);
归一个不存在的 agent ⇒ **不吞输入**,父兜底 + 留痕(吞掉输入比多处理一条更糟)。
- `DeliverRouted` 是"已路由"的投递口,不再二次路由(避免成环)。
- 同步输入的 `ResponseCh` 随事件一起走 ⇒ 回答由持有者写回同一回程(§4.3)。
判据(新增 6 条):
- io 层:被接管时排队/中断都**不入本内核队列**(且中断确实经过路由)/ 放行与未设
路由器时与历史行为一致 / `DeliverRouted` 不再触发路由
- 内核层:划给子的 inputch 输入进**子**(子 Executed>0)且**父 Enqueued 不变** /
归属到不存在的 agent 时父兜底(不吞)/ 未分配的 inputch 仍归父
(cherry picked from commit 4707b05498)
|
2026-09-13 15:35:59 +08:00 |
|
|
|
d0997f6279
|
docs(resident): 补写「子的 io 通道视图 = 对父的实时回退」(N3 落地口径)
输出通道在 io 层就是 Device,由插件登记在父的 IOManager 上;驻留子只共享了 inputch
登记表 ⇒ 子侧 childIO 空壳(现场:子调 output_send__cli 被判「通道不存在或不可用」)。
文档写清:继承方式(SetParentIO)、四个受影响的方法、为什么是实时回退而非快照、
以及回退只解决看得见、授权仍在白名单之后。附线上实测结果(result: ok)。
|
2026-09-13 15:16:58 +08:00 |
|
|
|
91c4bc01f1
|
fix(resident): 驻留子继承父的输出通道 —— 修「子侧 childIO 空壳、子不会发消息」
现场(用户在线上跑驻留子联调,日志实录):
父 agent 侧「通道装载完整」,子 `demo-resident` 侧 `childIO` 是**空壳**:
子的 `output_list_channels` 为空、`output_send__<通道>` 一律被判
「通道 [X] 不存在或不可用」,连 `output_send__*` 工具都不生成 ⇒ 子不会发消息。
根因:**输出通道在 io 层就是 Device**,而它们由插件登记在**父**的 `IOManager` 上。
`SpawnResident` 给子建的是全新 `IOManager`(它确实该有自己的输入入口与 outputCh),
却只共享了 inputch 登记表,**没有继承设备/输出通道视图**:
- `executeOutputSendTool` → `a.io.GetChannelCapabilities(ch)` 查的是 `devices[ch]` ⇒ 0
- 投递路径 `a.io.GetDevice(ch).Execute("output", …)` ⇒ nil
- 工具面 `tooldefs.go` 从 `a.io.ListChannels()` 生成 `output_send__*` ⇒ 空
改法:给 `IOManager` 增加**上级回退**(`SetParentIO`)——驻留子创建时把自己的 io 挂到
父的 io 上,`GetDevice` / `GetChannelCapabilities` / `ListChannels` / `ExecuteTool`
在自己没有时回退到上级。
为什么是**实时回退**而不是创建时复制快照:设备随资源生灭(远程设备上线/掉线以分钟计,
现场日志 60 秒一个来回),复制出来的表转瞬即过期;而回退永远与父一致。
**授权不受影响**:回退只解决"看得见",能不能用仍由各自的 `AllowedOutputs` 白名单把关
(`executeOutputSendTool` 的授权闸 + 工具生成时的过滤都在白名单之后);
自己的登记优先,子可以覆盖/屏蔽同名通道。
判据(新增 5 条):
- io 层:无上级时行为与以前完全一致 / 挂上级后看得见 / **实时**(父新登记立刻可见、
注销立刻不可见)/ 同名自己的优先且不重复列出 / `ExecuteTool` 同样回退
- 内核层:子看得见父通道 + 真能发出(父通道收到 1 次 output)/ 白名单外被拒且未送达 /
子工具面只生成授权通道(含 `_help`)/ 父后登记的通道立刻可见 / 默认即完整授权
(cherry picked from commit 8537577123)
|
2026-09-13 15:09:44 +08:00 |
|
|
|
fb2db2c304
|
docs: 两仓文档对齐当前版本状态 + 补发版产物清单;同步 SDK meta 路牌
用户指出:SDK 版本又带 patch 位、agent 自称 1.0.3、两个仓库文档都没更新。
docs/git-branching.md:
1. **§三 状态表**重写(停在 2026-09-12:main 还写 1.3.0、release/v1.2.x 还被当成"本条发布线、
尚无 tag"、SDK main 还写 1.2.0、完全没有 release/v1.3.x)。现按事实更新,并写明
`v1.3.0` 是已撤回的坏 tag。
2. **§2.3** 增补:发布线的 `meta.Version` 必须跟着该线已发的最后一个 patch 走;
只用 `-ldflags -X` 打版本而不改源码会让二进制与源码对不上账(1.3.1–1.3.4 就是这么打的)。
3. **反例表**补两行:人格文本在**播种时**固化版本(生产实例自称 v1.0.3)、
发布线路牌不随 patch 推进 / 给 SDK 误发 patch tag。并澄清"插值"必须在**渲染时**,
把算好的结果固化进配置库与写死没有区别。
4. **§七.5 新增"发版产物清单(可复现)"**:核心仓(tar.gz + 3 个 deb + SHA256SUMS,
校验和必须在全部产物生成后统一算)、SDK 仓(5 平台 hmapdev + SHA256SUMS)、
上传脚本的用法与两个坑(release 条目必须先存在;`hmapdev_*` 无扩展名不会被自动识别)。
起因就是本次"推了 tag 却没建 release、没打包"——推 tag ≠ 完成发版。
third_party/homeagent-sdk/meta/meta.go:镜像 SDK 仓 main 的路牌(1.3.0 → 1.4.0)与
版本语义注释更新(1.3.0 已定版 ⇒ 该号归发布线,main 推进)。
|
2026-09-13 14:41:24 +08:00 |
|
|
|
ba3ab5a7d9
|
chore(meta): main 的版本路牌推到 1.4.0(1.3.0 已归 release/v1.3.x 所有)
规范 §2.1 / §七.4:切出 release/v1.3.x 后,1.3.0 就归发布线所有,main 立即推进到
下一个未发布中版本。此前停在 1.3.0 属遗漏 —— 会让 main 构建出来的二进制自称已发布版本。
|
2026-09-13 14:34:46 +08:00 |
|
|
|
a4ebcb6e96
|
fix(config): 播种时不再把版本号写进人格文本 + 存量实例一次性去版本化
用户发现:agent 自报版本 **1.0.3**,内核早已 1.3.x。
根因(两层):
1. `SeedDefaults` 当年用 `fmt.Sprintf("…HΔ-Kernel v%s…", meta.Version)` **在播种时**
就把版本写进了 `core.agent.system_prompt` —— 装完即冻住,之后每次升级都不会
去改配置里的文本,于是实例终生自称装机那天的版本。默认模板用 meta.Version
插值本是"不写死"的做法,但**播种 = 把插值结果固化**,等于写死。
2. `core.agent.personal_prompt`(新人格机制)本身是对的(DefaultPersonaPrompt
无版本字面量,由 TestDefaultPersonaPromptHasNoVersionLiterals 钉住),
但旧键仍在系统提示词里说话,模型就照抄旧键的版本。
改动:
- **不再播种** `core.agent.system_prompt`:留空 → 组装时取 cmd/homed 的内置底座
提示词;人格由 personal_prompt 承载。全新安装不再预置会腐坏的文本。
- 新增 `migrateSeededSystemPrompt()`(在 SeedDefaults 最前,故不被播种标记早退):
只对"当年那段播种模板"(前缀 + `HΔ-Kernel v<数字>` 字面量双判据)做
`v<数字>` → `v{{kernel_version}}`;用户自己写的人格卡一律不碰。
一次性标记 `core.internal.system_prompt_deversion_v1` 守住幂等 ——
幂等语句不等于语义幂等,重复执行会把用户后来手写的版本号也改掉。
- 与 v1.3.5 的占位符展开配套:占位符在组装系统提示词时按真实构建展开。
回归判据 4 条(`prompt_migration_test.go`):存量卡被去版本化且正文不动 /
迁移只跑一次 / 用户自写卡不动 / 全新安装不播种该键。
|
2026-09-13 14:34:44 +08:00 |
|
|
|
f168862eaa
|
fix(prompt): 系统提示词支持版本占位符 —— 人格卡不再写死版本号
现象(用户发现):agent 向用户自报版本是 **1.0.3**,而内核早已 1.3.x。
根因:**人格卡是配置项**,线上 `core.agent.system_prompt` 里写死了
「HΔ-Kernel v1.0.3 型号的家政型 AI 管家助手」——那是当年装机的文本,
之后每次发版都不会去改它,模型于是照抄给用户。默认模板用 `meta.Version`
拼接(`registry.go` 的 `fmt.Sprintf`)所以一直是对的,**只要被自定义过就会漂**。
修法:在 `buildSystemPrompt` 组装处展开占位符,让这类文本跟随真实构建:
{{kernel_version}} → meta.Version(如 1.3.5)
{{kernel_commit}} → 构建 commit
{{sdk_version}} → 所兼容 SDK 版本(如 1.3.0)
- 未知占位符**原样保留**:写错了要看得见,而不是被静默换成空串;
- 不含 `{{` 时原样返回(提示词在热路径上);
- 覆盖所有路径:主 agent 与驻留子都经 `buildSystemPrompt`,
`persona_set` 写入的文本同样在读取时展开(存的是模板,不是渲染结果);
- 配置项描述里写明可用占位符,引导用户别再写死版本。
回归判据 `TestExpandPromptVars`:展开正确 / 内置占位符不残留 /
线上真实人格卡文本能被纠正 / 未知占位符不被吞 / 无占位符不改写。
|
2026-09-13 14:34:44 +08:00 |
|