|
|
94c74b2ee6
|
fix(devicebridge): 修复设备反复掉线/静默失联 —— ping 路径断连 + bind 结果无人处理
用户要求全面修复「设备桥自动链接」这条链路上的问题。三个真实缺陷,
前两个是**服务端/客户端真 bug**(生产日志实证),第三个是我起初误判的。
## 缺陷 1(最严重):未 bind 时收到 ping → 服务端直接关连接
原实现:
err := r.wsWriteLocked(curID, writePong)
if err != nil { return } // ← 关连接
而 conns 表**只在 bind 成功后才写入**(bind 前刻意不暴露连接给查询/命令
路径)。于是「握手完成、bind 尚未到达」这个窗口里来的 ping 找不到写入口,
函数返回错误,读循环 return —— 把连接关掉了。
生产后果(journalctl 实证):客户端每 30s ping 一次,只要有一次落在未 bind
窗口就断连。日志里同一设备 20 秒内多次 "ws connected",online/offline 与
输出通道注销/注册反复交替:
17:29:14 ws connected → 17:29:17 ws connected → 17:29:24 ws connected
→ 17:29:29 → 17:29:35 → 17:29:40 online → 17:30:18 offline → ...循环
修法:pong 直接写本连接的 writer。此时该连接尚未进入 conns(没有 Push* 会
碰它的 writer),不存在并发写风险;已 bind 时才取写锁(Push* 可能正在写
同一 buffer)。
判据 TestPingBeforeBindDoesNotDropConnection 直打 bug 点(只握手、不发
hello/bind、发 ping、要求 pong),修复前报 `EOF`,修复后通过。
## 缺陷 2:bind_ack 的 ok 完全没被检查 → 失败静默失联
原实现(客户端):
case "hello_ack", "bind_ack":
log.Printf("... device=%v", msg["device"])
两处错:
- **取错字段**:服务端成功时回 {"op":"bind_ack","ok":true},没有 device
字段,于是日志永远显示 `bind_ack device=<nil>`。这让我起初误判成"绑定
失败",实际连接是好的(直连与经反代现象完全一致)。
- **不看 ok**:bind 被拒时服务端回 ok:false + error 并关闭连接,客户端既不
报错也不重连,设备静默失联 —— TCP/WS 通但从未登记进网关。
修法:分别处理两种 ack;bind 判 ok,失败记原因并通知宿主。新增
Bridge.Bound() / BindError() / OnBoundState():**连接成功 ≠ 设备可用**,
只看连接状态的健康检查会给出假阳性。
判据 TestBindFailureIsObservable / TestBindStateCallback。
## 缺陷 3:-chat 一次性模式下桥存活时间过短
不是我最初以为的"bind 失败"。真因:`-chat` 走进 oneshot 后立刻 return,
触发 defer stopDeviceBridge(),桥只活几百毫秒,设备来不及完成 hello→bind。
修法:退出前等 bind 确认(最多 3s);bind 明确被拒则打印原因,不静默丢弃。
## 真实验收(隔离实例,命名 netns + 独立 data + 18080)
真 waiter 经**反代自动发现**连接,保持连接期间查询服务端:
device gateway discovered: ws://127.0.0.1:18080/api/v1/device/ws
bind 成功,设备已登记
/api/v1/device/online → waiter-mainserver, online=true, caps=[11 项]
长连接稳定性:70 秒(跨 2 个 ping 周期)三次采样设备始终在线,
无 read loop exit / bind rejected 日志。
## 附:反代通路本身的判定性对照
裸客户端(直接构造 hello/bind 帧)**经反代**与**直连 9890** 返回逐字节
一致(bind_ack ok=true、设备注册、online=true)。所以这条链路上反代
不背锅,问题全在 remotedevice 服务端与客户端自身。
|
2026-09-26 13:49:59 +08:00 |
|
|
|
7f5bf1670f
|
feat(clients): 设备桥自动链接改用服务端发现 + 路径挂载(无 DNS 依赖)
配套 webui 反代改造:网关现在可由 HomeAgent 反代出去,客户端不能再靠
「门户地址同 host 拼 /api/v1/device/ws」猜地址——基域名与子域标签都是
**服务端配置**,客户端无从得知。
## 服务端:/api/v1/device/gateway 发现端点
客户端问「网关在哪」是唯一不会漂移的做法:子域标签可改(插件声明)、
基域名可改(webui.base_domain)、实例可换形态,客户端都不用跟着改。
⚠️ **不返回设备令牌**:本端点用门户凭证鉴权,而设备令牌能执行设备命令;
把令牌塞进来等于「门户只读凭证 → 设备执行权」的越权。令牌仍由客户端
自配。已有判据钉住「不得泄漏凭证字段」。
## ★ 实测发现:*.localhost 只有浏览器能解析
这是本轮最重要的发现,直接决定了设计:
| 环境 | devices.localhost 解析 |
|---|---|
| 浏览器 | ✓(RFC 6761 内置) |
| curl | ✓(内置特例) |
| getent / Go / Node | ✗(系统 nsswitch 是 files,dns,无 nss-myhostname) |
设备客户端(waiter / GUI 主进程 / 嵌入式固件)用的正是系统解析器。
实测 waiter 报「lookup devices.localhost on 192.168.2.1:53」。
因此**两处**设计变更:
1. 发现端点同时返回两种形态,并标 preferred:
- url(子域)—— 浏览器用
- url_portal(门户同源,同一 host、同一端口,走路径挂载)—— 非浏览器用,
无任何 DNS 依赖
2. SDK 的 ProxyDecl 新增 **Path**(路径挂载前缀):让同一服务同时挂到
门户自身 host 的路径下。remotedevice 声明 Path="/api/v1/device",
设备客户端因此能沿用**它已硬编码的路径**,不需要知道反代存在。
路径挂载语义:请求路径**原样保留**(不剥前缀),上游按真实路径注册即可。
边界卡在路径分隔符上(/api/v1/device 不匹配 /api/v1/devicefoo)。
## 客户端
- **waiter**:新增 discoverGateway(),仅在用户配了门户地址时尝试,失败回退
自配地址(老版本 HomeAgent 无该端点)。抽出 normalizeGateway() 纯函数,
显式钉住「已带子域/完整端点的地址不得被改写」。
- **GUI**:renderer 新增 loadDiscoveredGateway(),renderDeviceChannel 优先用
发现值、回退旧口径。顺带修掉此前插入函数时 anchor 不匹配导致调用点
找不到定义的问题。
- **鸿蒙**:discoverGateway() + resolveGatewayUrl(),优先 url_portal。
- 三者都**优先 url_portal**(system resolver 的现实约束)。
## 遗留路由鉴权修正
`/api/v1/device/` 的旧路径反代原被 requireAPI 包裹 —— 但其调用方是设备
(带设备令牌而非门户凭证),套上门户鉴权会把它们全挡在 401(**真实实测**:
waiter 经此路径升级握手 401)。去掉这层包装,鉴权交给上游 remotedevice
自己的 requireToken,安全性不降级。
## 判据
webui +6 条、waiter +7 条。
★ 其中一条是**真实回归**:/api/v1/device/gateway 曾被 Path="/api/v1/device"
的路径挂载接走(那服务 auth=none),于是发现请求被转给上游、回 401,
客户端再也发现不到网关。修法是发现端点先于路径挂载判定,并补判据
(走完整生产链,同时确认同前缀的真实设备路径仍归反代)。
## 真实验收(隔离实例,命名 netns + 独立 data + 18080)
真 waiter 客户端 + 真 remotedevice 网关:
device gateway discovered: ws://127.0.0.1:18080/api/v1/device/ws
device bridge active: waiter-mainserver authorized=true
hello_ack / bind_ack 均经反代往返成功
说明:`bind_ack device=<nil>` 与在线列表为空的现象,**直连 9890 绕开反代
完全一致复现**,属 remotedevice 与 waiter 之间既有的握手细节,与本次
反代改造无关(反代侧职责已证:连接建立 + 双向帧往返都通)。
|
2026-09-26 13:49:59 +08:00 |
|
|
|
5d278cffdb
|
fix(webui): 反代两处真实故障 —— 凭证头按 auth 区分 + Host 分发先于门户路由
两处都是**隔离实例上跑真实端到端**才暴露的,单测(用不校验凭证的假上游、
直接调 serveProxyHost)全绿却线上出错。记录在此以免重蹈。
## 故障 1:auth=none 路由的凭证被无条件剥掉 ⇒ 设备链路全 401
Rewrite 里原本无条件 Del("Cookie"/"Authorization"/"X-API-Key")。但
auth=none 的语义是"请求原样交给上游",凭证本来就是给**上游**的——
remotedevice 的接入令牌正是走 X-API-Key 传的。
实测症状:带设备令牌经反代访问 /api/v1/device/online → 401;
直连 127.0.0.1:9890 → 200。差异极难定位,因为两侧状态码语义相同。
修法:按路由 auth 分流。
- auth=homeagent:凭证是门户的,剥掉(避免泄漏给插件)
- auth=none:保留(上游要用)
复验:经反代与直连**逐字节一致**(HTTP 200 / 17B,cmp 相同)。
## 故障 2:插件子域被门户路由截走 ⇒ 401 且响应体是门户的 JSON
原实现把 Host 分发放在 mux 的 "/" 兜底里。但 stdlib ServeMux 是**最长前缀
优先**:任何更具体的模式都先命中。插件子域上的 /api/v1/device/online 被门户
为「旧路径反代」注册的 /api/v1/device/ 接走(requireAPI 包裹)→ 401。
判据(响应体格式)是定位关键:
webui requireAPI → {"error":"unauthorized"} 25B ← 实际拿到
remotedevice requireToken → "unauthorized" text/plain 13B
看响应体格式就能区分是谁拒的,比看状态码有效。
修法:Host 分发提为**最外层中间件**,包在整个 mux 之外,先于任何路径匹配。
## 顺带:消除判据与生产接线错位的可能
新增 Handler.Handler() 返回生产用的完整链(Host 分发 → 日志 → mux),
plugin.go 与测试共用同一条。本次踩过:测试自己组装 mux、中间件却挂在
plugin.go,判据全绿而线上 401;共享同一条链可结构性避免。
(写这条判据时还发现测试里 h.mux 为 nil 导致 panic——也正是这种错位的表现。)
## 新增判据 2 条
- TestProxyCredentialHeadersDependOnAuth:auth=none 必须转发上游令牌、
auth=homeagent 必须剥掉门户凭证(两个方向都钉)
- TestProxyHostTakesPrecedenceOverPortalRoutes:走**完整生产链**,确认
插件子域上的 /api/v1/device/online、/api/v1/status、/api/v1/plugins/ 都
归反代;同时确认门户自身的 /api/v1/status 仍返回门户 JSON(没被反代吞掉)
## 真实验收(隔离实例:命名 netns + 独立 data + 端口 18080)
- 设备网关:令牌经反代 200,与直连逐字节一致;WS 升级 101
- 未声明子域:404 且错误信息含具体标签
- huawei_smarthome(用新 hmapdev 重打包、真装载):
匿名 401 + 可操作提示;带门户 key 拿到真实 UI(9444B,
<title>华为智慧生活管家);页面内根绝对路径 /api/status 正确透传
|
2026-09-26 13:49:59 +08:00 |
|
|
|
1e58af79d3
|
feat(webui): 通用反向代理 —— 插件声明服务,HomeAgent 按子域反代出去
用户要求:外部只装 HomeAgent 即可使用自带反代能力;用户只需穿透一个
webui 端口就能访问所有内部插件服务;认证与 WebSocket 支持都作为插件
的可声明项;插件 UI 要有可直接点击的入口。
实测 huawei_smarthome 插件的前端用**根绝对路径**(api('/api/status') →
fetch('/api/status'))。挂在 /p/<name>/ 这类路径前缀下,这些请求会打到
HomeAgent 自己的 /api/status —— 静默错路由;做 HTML/JS 内容重写对拼进
JS 字符串的绝对路径只是"按概率能用",会产生"页面能开、某个按钮就坏"的
静默故障。子域路由下根路径天然正确,**插件前端零改动**。
且它天然匹配"只穿透一个端口":webui 监听 0.0.0.0:8080 按 Host 分发,
外层 frp 单端口 TCP 隧道**一行都不用改**。
默认基座 localhost:RFC 6761 规定 *.localhost 强制解析到 loopback,
现代浏览器原生支持 ⇒ <标签>.localhost:8080 **零配置可用**,不需要 DNS、
证书、/etc/hosts。远程部署改 base_domain 即可。
- 外部插件 → plugin.json 的 proxies(静态可发现:插件没起来也能报
"声明了 ui 但目标不可达",而不是静默 404)
- 内置插件 → s.DeclareProxy()(remotedevice 是内置的、没有 plugin.json,
却最需要被反代出去)
反代层在 webui 侧读清单:webui 已能拿到插件目录(PluginManager.PluginDir),
因此**无需给内核接口加方法**。manifest 解析忽略未知字段,加 proxies 对
"旧内核读新插件"与"新内核读旧插件"都无害。
新增 sdk/ProxyDecl 与配套校验(ValidProxyAuth / ValidProxyHostLabel /
NormalizeProxyHost / ValidateProxyDecl);新增运行期 ProxyDeclarer 通道。
hmapdev 的 writePluginJSON 是**白名单 map 重建**——不同步加字段会让声明
被打包静默丢弃(插件作者本地正常、装上去失效),因此 PlgConfig 与
writePluginJSON 同时加,并在打包前校验声明(插件作者本地就能发现写错)。
auth=homeagent(默认,安全的默认):门户会话 / X-API-Key / ?__token=;
auth=none:信任上游自身鉴权,供设备与嵌入式客户端使用——它们不可能持有
浏览器会话,强制走门户鉴权会把设备链路挡死。remotedevice 声明 none,
因为它自身用 ws_token 强制校验。
未声明时升级请求**明确拒绝**(400 + 原因),而不是静默降级成普通请求
(后者表现为前端不断重连、日志看不出原因)。
1. 不跟随上游 3xx:旧实现用 http.DefaultClient(默认跟最多 10 跳),
上游 302 到内网地址时反代自己跟过去、失败回 502 并把内网 URL 泄给
客户端。httputil.ReverseProxy 默认不跟随,3xx 原样透传。
2. 逐帧 flush:旧实现 io.Copy 导致上游流式响应被缓冲到上游关闭才下发
(实测 3 帧 200ms 间隔的流,客户端在 +600ms 一次性收到全部)。
设 FlushInterval=-1。
另补齐 X-Forwarded-For/Host/Proto(旧实现完全不注入,上游无法判断真实
来源),并剥掉上游 Set-Cookie 的 Domain(防止插件 cookie 打到主门户域)。
插件页新增「服务入口」卡片:列出全部被反代的插件服务(含被拒条目与
不可达原因),点「打开」直接访问。链接带 ?__token=<api_key>,因为子域
与门户不同源、浏览器不会自动带会话 cookie。
webui +35 条、SDK +4 条、工具链 +4 条。关键几条:
- 根绝对路径必须原样到上游(选 Host 路由的核心理由)
- 上游 302 必须原样透传、且反代不得跟随(旧缺陷)
- 已知 Content-Length 的慢速响应必须逐帧到达(**这条经过变异验证**:
把 FlushInterval 改回 0 后判据挂死 → FAIL,还原后回绿。
说明:最初写的 SSE/chunked 版本是假判据——ReverseProxy 对
text/event-stream 与 ContentLength=-1 会自动立即 flush,与
FlushInterval 无关,变异抓不到,已改正)
- 子域标签冲突不得静默覆盖(后者保留可见并带原因)
- 非法声明不进路由但必须可见(配置页要能看到原因)
- 未声明 websocket 的升级请求必须 400
- auth 逐条生效:none 放行匿名、homeagent 与默认档 401 且给可操作提示
- 自动发现:显式 host 不得被自动编号覆盖(**测试抓到的真 bug**:
remotedevice 声明的 "devices" 会被改成 "devices-2" 而静默失效)
- 真实端到端:生产实例 huawei_smarthome 的 UI(9444 字节)与其
/api/status 经反代正确透传
go build ./... 通过;相关包全量测试通过。
internal/plugin/proc 的 TestStreaming_PublishLatencyFlatAcrossSubscribers
是**预存在的不稳定测试**(同一份代码 10 次跑 9 过 1 败,且本改动完全
未触及该包),非本次引入。
|
2026-09-26 13:49:59 +08:00 |
|
|
|
4e40597954
|
merge: 内核编解码层 C 化 + C 基础设施门禁(feature/c-core)
## 内容
- C 化第一刀 L1 纯函数层(ha_codec):token 估算/截断/上下文窗口推断
- 零分配 JSON 扫描层 ha_json_scan(scan/extract 两段分离,黄金对照 + fuzz)
- SSE 协议导航层 ha_sse(**默认关闭**,见下)
- C 基础设施门禁六项(make check-csrc / check-csrc-full,已接进 make test)
- 共享内存与 IPC 的成本地板基准(纯测量)
- 分词器热路径分配优化(差分 oracle 验收)
## 实测(main 同机对照,50000 次迭代)
| 场景 | main | 本次 | 提升 |
|---|---|---|---|
| Truncate zh_1k | 6570ns / 2 allocs | 174ns / 0 | 37.8× |
| Truncate long_zh | 5984ns / 2 allocs | 179ns / 0 | 33.5× |
| Truncate ascii_1k | 1580ns / 2 allocs | 95ns / 0 | 16.6× |
| Estimate ascii_1k | 445ns | 51ns | 8.7× |
| Estimate zh_1k | 2262ns | 1172ns | 1.93× |
| Estimate short_zh | 22ns | 35ns | -58%(cgo 边界固定成本) |
几何平均 4.20× / 中位 1.93×;**变快 7 项、变慢 2 项**(短串受 cgo 边界拖累,
如实记录未掩盖)。调用点在热路径:process.go 对每个上下文事件都调
EstimateTokens,tooldefs.go 的工具定义裁剪调 TruncateByTokens。
## 默认关闭的部分(实测更慢,不当作成果)
- chunkFastEnabled = false:SSE 分块快速路径。首版更慢 52~79%;
返工(5+ 次 cgo 边界压成 1 次)后为「三项赢、一项输」,toolcall 仍慢 15%
⇒ 不打开。TestChunkFast_BenchGate 断言该开关必须为 false。
- ha_json_scan 未接生产路径:库已验完(119 契约断言 + 黄金对照 5 组 +
4948 万次 fuzz 零崩溃 + 6 万+ 差分用例),作为可复用底座留存。
## 为什么共享内存没有 C 化(附成本分解基准)
编解码占端到端 34%,但 **C 的甜区(字节搬运)仅占 0.2%~2%**
(1KB 拷贝 18ns、16KB 210ns;Go copy 已 44~71 GB/s),大头是 JSON 反射 34%。
另:段内读是**不可信偏移**(offset 由插件转述,伪造会破坏块链),
保留 Go 边界检查 / panic / -race / 模糊测试覆盖比省 0.2% 更值。
IPC 的真正地板是 OS 调度:cat 管道 echo 就要 16µs,占最简 RPC 的 62%。
## 验证
- 全量 go test -count=1 ./... 38 包 0 FAIL
- C 六门禁全过:gcc+clang 零告警(-Wconversion 必备)、ASan+UBSan、
arm64 交叉编译、头文件自包含、libFuzzer 零崩溃、ABI 版本自述
- 端到端启动实测:14 插件 / 63 工具 / kernel ready / 0 panic
- make build-linux-arm64 → ELF aarch64
- SDK 公开接口 diff = 0 行(csrc/ 是内核 C ABI,不属 SDK 冻结范围)
- git-release-discipline 体检 FAIL=0
## 纪律
- meta.Version 未被污染(未动 internal/meta)
- main 上无 merge 来自 release 分支(仅本 feature 合入)
- 本分支未部署任何生产环境
|
2026-09-26 13:32:36 +08:00 |
|
|
|
241f5fac04
|
fix(knowledge): 拒绝越出知识根的知识名(可致整个数据目录被删)
sanitize 只做小写/去空格/换下划线,**不过滤 ".."**,而 Remove 直接把
sanitize 的结果 filepath.Join 到知识根后 os.RemoveAll。
后果(实测):
- Remove("..") → RemoveAll(<data>),把整个数据目录连同 memory/
documents/media 一起删掉;且 os.RemoveAll 对已不存在的目标返回 nil,
调用方(含 knowledge_delete 工具)会回报"已删除"。
- Remove("../..") → RemoveAll(<data 的父目录>)。
- Add("../../x") → 内容写到知识根之外;重启后 scanAll 扫不到该目录,
条目既不在盘上正确位置也无法重建 ⇒ 幽灵条目(内存有、索引有、盘上没有)。
- Add(".hidden") → 写到隐藏目录,scanDir 明确跳过隐藏目录 ⇒ 同样的幽灵。
修复:
- 新增 checkSafeName:拒绝空段、"."、"..",以及以点开头的段。
Add 与 Remove 在拼接路径前都过它。
- 双保险:拼接后用 filepath.Clean 复核结果仍在知识根内,
防止 checkSafeName 将来被改宽而重新引入越界。
反向验证:临时拆掉这两处防护后重跑新测试,Add/Remove 对 .. 与隐藏名
全部"成功",测试稳定变红;恢复后全绿。
影响范围:该缺陷存在于 release/v1.0.x ~ v1.3.x 四条发布线(各自的
internal/knowledge/knowledge.go 的 Remove 均为同一写法),本次修复需按
hotfix 纪律 cherry-pick 回流 main 并前向传播。
|
2026-09-26 13:11:08 +08:00 |
|
|
|
a006237105
|
test(proc): 建立进程间通信的成本地板(判定「优化 IPC」的空间)
目标:回答「工具调用往返 30µs 里,非编解码的 ~20µs 花在哪、能否优化」。
方法:先立地板 —— 任何跨进程方案都有 OS 调度决定的下界。
三层对照(同机同会话,3000 次迭代):
| 层 | ns/op | allocs |
|---|---:|---:|
| ① OS 调度地板(cat 子进程管道 echo,无协议无 JSON) | **16071** | 0 |
| ② 最简 RPC(无载荷、不经共享帧) | **25840** | 20 |
| ③ 纯编解码(共享段 write+read+compact,纯内存) | 10200 | 84 |
## 两条判据
1. **① 已占 ② 的 62%**:一个什么都不做的 echo(两次进程唤醒 +
两次管道读写)就要 16µs。⇒ RPC 层的成本主要是 **OS 调度**,
不是协议解析或 JSON 序列化。
2. **②−① 只剩约 10µs**:这才是协议层(JSON 帧 + pending map +
channel 握手)可优化的全部空间,且其中还包含一次真实的 JSON
编解码往返。⇒ 「优化 IPC」的理论上限约为端到端 30µs 的三分之一。
## 结论
跨进程数据面的**协议侧已接近其地板**。若要把端到端再压下去,
方向不是「优化协议」,而是**改变通信形态本身**:
· 批量调用(一次往返做多件事,摊薄固定调度成本)
· 或对高频小调用改走共享内存 + 自旋/事件通知(绕过两次进程唤醒)
两者都是架构级改动,不是参数调优。
★ 这也解释了此前几轮的困惑:为什么 C 化数据面收益总是很小 ——
因为真正的大头(OS 调度 16µs)与语言无关。
验证:基准可复现;internal/plugin/proc 全量测试绿。
|
2026-09-26 13:06:10 +08:00 |
|
|
|
84d2d7c313
|
perf(chineseclip): 分词器热路径分配优化 + 差分 oracle 验收(含一处真实语义修复)
嵌入式模型推理(ONNX)本身已是 C++,**可优化的 Go 侧是分词器与预处理**。
本轮先测出成本分布,再改,且**不假设 C 更快**。
## 实测(合成词表,无需 CHINESECLIP_MODEL_DIR)
| 场景 | ns/op | allocs |
|---|---:|---:|
| short_zh | 25681 | 60 |
| short_en | 19100 | 43 |
| mid_en | 117343 | 451 |
| mid_zh | 189260 | 1047 |
| punct_heavy | 283265 | 1388 |
| long_zh | 757746 | 4233 |
## pprof 指出的分配源(alloc_objects,mid_zh)
- splitOnPunctuation **33.5%**(每 token 都做 []rune + string(cur))
- wordpiece **32.3%**(内层每轮候选都 string(runes[a:b]),多数未命中)
- stripAccents/NFD 33.6% cum
- basicTokenize 自身只 1.4%
## 改了三处
1. `basicTokenize`:`len([]rune(token))` → `utf8.RuneCountInString`
(原为「数个长度」就把整个 token 转 rune 切片)
2. `splitOnPunctuation`:去掉整串 []rune,改逐 rune 扫描 + 一次 flush
3. `wordpiece`:预建 rune 边界表,按字节区间取 substring,
消除「每轮候选都构造 string」
## ★★ 差分 oracle 抓到一处**真实语义缺陷**(非测量噪声)
本机无模型产物,权威的 TestTokenizerMatchesOfficialReference 会 **SKIP**
⇒ 仅靠现有测试,我的重写**没有被有效验证**。故把改动前的实现原样内联为
oracle 做差分(split/wordpiece/basicTokenize/Encode 四组 + 随机字节 2 万组
+ 随机 rune 5000 组)。
它立刻抓到:`"\xbc\xef=..."` 旧实现得 `["��" ...]`,新实现得 `["\xbc\xef" ...]`。
根因是 `[]rune(s)` 会把**非法字节归一成 U+FFFD**,而纯字节切片原样保留坏字节。
⇒ 真实差异(会进日志/去重/hash),已改为对非法序列写回 RuneError,与旧行为逐值一致。
## 诚实的收益结论:**基本没有**
改动后:mid_zh 189260(改前 186777)、long_zh 757746(改前 786416)、
mid_en 117343(改前 120422)。分配数 mid_en -40%、其余基本持平,
**时间无实质改善**(部分场景还略慢)。
复查原因(不掩盖):重新做 CPU profile 后发现 **~25% 的样本是
runtime 锁/抢占**(unlock2 8.1% + lock2 6.8% + procyieldAsm 6.8% +
asyncPreempt 5.4%),而 utf8/unicode 相关不足 20%。
且 GOMAXPROCS 敏感:1→375365ns、4→209922ns、12→189543ns
⇒ **大量时间花在调度与 GC 而非分词算术**。
⇒ 结论:Go 侧微优化这条路**已到头**。真正的杠杆在别处:
① 提高 GOMAXPROCS/减少 GC 压力 ② 批量分词(降低每条输入的固定开销)
③ 减少送入模型的 token 量。三者都不是 C 能解决的。
改动本身保留(正确性等价、有 oracle 守护),但**不应据此宣称性能收益**。
与 C 化那几刀同一条纪律:没有数据支撑的优化不算优化。
验证:差分 oracle 6 组全过(含非法 UTF-8);providers/... 全绿。
|
2026-09-26 11:44:49 +08:00 |
|
|
|
4340232bb0
|
test(proc): 共享内存数据面成本分解基准(纯测量,判断 C 化是否值得)
针对「共享内存应由 C 实现」这个直觉做量化。结论:**收益判据不成立**。
基准拆出三类成本,只有分开测才知道哪类是 C 的甜区。
## 实测(small:2 toolResults + 2 ctxMsgs)
| 项 | ns/op | allocs | 占比 |
|---|---:|---:|---:|
| ③ 描述符记账(18 个 Slice) | **51** | 0 | **0.4%** |
| ① 段内字节搬运 1KB | **18** | 0 | **0.2%** |
| ① 段内字节搬运 16KB | **210** | 0 | **2%** |
| ② JSON (Marshal+Unmarshal) | **3900** | 15 | **34%** |
| 整体 write+read+compact | 10376 | 84 | 100% |
字节拷贝 1KB=18ns / 16KB=210ns(3 次重复,稳定在 ±5%):
Go 的 copy 已达 **44~71 GB/s**,接近内存带宽上限,**无余量可榨**。
## ★ 更正一处我自己的错误口径
我先前报「编解码只占端到端 9%」——**那是单次非成对采样,是错的**。
成对重测(各 3 次取中位):
| | ns/op |
|---|---:|
| 工具调用往返(inline/small,跨进程) | 30305 |
| 纯编解码(small) | 10376 |
| **占比** | **34%** |
即编解码其实是**端到端的三分之一**,比 9% 重要得多。
但结论**不变**,且理由换成更有力的两条:
1. **C 的甜区恰好是最小的那块**:字节搬运仅占 0.2%~2%。
真正的大头是 **JSON 反射占 34%**、描述符记账 51ns 占 0.4%。
2. **JSON 恰是本轮三刀反复验证「跨语言重建语义不划算」的领域**。
顺带记:这轮我又被自己的**测量方式**坑两次 ——
① 基准脚本用 `CLOCK_MONOTONIC` 却只取 `tv_nsec`(漏 `tv_sec`),
算出 -4201ns 负值;② 用 `grep 'ns/op'` 批量取数时,
把 `JsonOnly/empty`(0.4ns)误当成 `small`(3310ns)读了进来。
⇒ 拿荒谬数值先怀疑工具;批量取数要确认匹配到的是**哪一行**。
## 三条判据
1. 大头是 JSON 反射(34% 时间、15 分配),不是内存带宽。
2. C 的甜区(memcpy 类)只占 0.2%~2%,无榨取空间。
3. 段内读是**不可信偏移**(arena.go 注释自述 offset 由插件转述,
伪造会破坏块链;payload 可达 MB 级)—— Go 的边界检查 + panic +
`-race` + 模糊测试覆盖它;C 越界是静默堆破坏。
## 另一条架构判据
格式常量在两仓各写一份(内核 `shm.go` 与 SDK `proc_main.go.tmpl`
各有 `shmStageFieldCount=18`/`sliceSize=8`/`offMagic`)。
C 化一旦动 ABI 须走 SDK 发版 + 两仓版本对齐(MIT vs AGPL),
否则「新内核 + 旧插件」静默错位。本文件只是基准,不涉及 ABI 变更。
## 「C 是共享内存原生语言」在本项目为何不成立
1. **插件侧根本不用指针**:SDK 模板只做 `syscall.Mmap` 拿 `[]byte`,
全程相对偏移 `{off,len}`、零指针重解释、零 unsafe。
跨进程 mmap 到不同虚拟地址 —— 这正是必须用偏移的原因,
C 的指针模型在这里用不上。
2. **真正原生的部分是 Go 更强的地方**:memfd 惰性物理内存
(未触碰页不占物理内存,MB 级 arena 近零常驻)+ first-fit +
邻块合并 + owner 校验;OS 语义 cgo 一样要调。
3. C 化还会破坏「整个新架构零 cgo」这条已达成的不变量。
验证:go test -benchtime 全绿,无回归。
|
2026-09-26 11:22:55 +08:00 |
|
|
|
84c7559b0e
|
chore(api): 删除批量定位改造后遗留的 6 个未使用函数
首版逐字段设计(findKey/firstElem/scanArray/decBuf 路径等)在批量定位
改造后已完全被取代,保留它们会让人误以为这些路径仍在生效。
删除:findKey / firstElem / scanArray / locateChunkBatchInto /
sseABIVersion / argStringC。
保留 C_size(rootSpan 在用)与 stringifyC(文本数组路径在用)。
go vet 与 go test 均不报未使用的包级函数,故用调用点计数核验
(每个符号的非定义调用数均为 0)。build + 全量 api 测试绿。
|
2026-09-26 10:44:18 +08:00 |
|
|
|
7a1322c97f
|
perf(api): SSE 导航层返工 —— 5+ 次边界压成 1 次(三项赢,仍默认关闭)
按 sse-codec-c.md §6.4 的架构改造方向返工。**部分成功**:从「五项全输」
变成「三项赢 / 一项持平 / 一项输」,且所有场景分配数都下降。
## 改造内容
| 项 | 前 | 后 |
|---|---|---|
| cgo 边界次数 | 5+(每字段一次 findKey) | 1(ha_sse_chunk_locate) |
| 键查找 | 每键各扫一遍对象(6 趟) | 单趟分派(遍历成员表一次即分发) |
| 解码 | 每字段一次往返 + 各自 decBuf | 同一趟内写进一块 sbuf(1 次分配) |
| 成员表遍历 | 6 趟 | 2 趟(顶层 + delta) |
顺带修掉两处自造的浪费(都是「先扫一遍拿个数、再扫第二遍拿首元素」):
choices 数组的「数个数 + 取首元素」合一趟;choice0 内的 delta/finish_reason
合一趟。成员遍历实测 107ns/趟,省一趟就是省 107ns。
新增 ha_sse_chunk_locate:一次调用完成根校验 + 顶层分派 + choices[0] +
delta 分派 + content/reasoning/finish 解码,输出写调用方持有的 C 结构体
(C 结构体无 Go 指针 ⇒ 可安全传指针,消除 out-param 逃逸)。
choices_count>1 时直接回退(Go 侧 Unmarshal 会解析全部元素,本层只认 [0],
其余元素可能类型不符而让 Go 整块作废 ⇒ 无法保证等价)。
## 实测(50000 次 × 3 轮取中位)
| 场景 | Entry | GoOnly | 判定 |
|---|---|---|---|
| content_zh | 1540ns / 5allocs | 1871ns / 13allocs | 快 18%,分配 -62% |
| content_ascii | 1250ns / 5allocs | 1304ns / 13allocs | 持平,分配 -62% |
| finish | 820ns / 6allocs | 921ns / 12allocs | 快 11% |
| usage | 2530ns / 9allocs | 2591ns / 12allocs | 持平偏快 |
| toolcall | 3450ns / 20allocs | 3000ns / 21allocs | 慢 15% |
## toolcall 仍输的根因(已定位,非猜测)
分解测量:C 侧纯 C 零边界 = 766ns;Go 侧 []openAIToolCall unmarshal =
1305ns/15allocs;对照 Go 整块 unmarshal ≈ 2980ns。
问题在第二行:tool_calls 元素是对象,Arguments interface{} 需要真实的
map[string]interface{},必须走 encoding/json 的反射建树。
而为了定位已先做了一遍 C 扫描 ⇒ 同一份数据被解析了两次。
⇒ 不是 C 慢,是「扫两遍 vs 扫一遍」。
标量字段(content/reasoning/finish)C 能一次到位 ⇒ 那些场景赢;
需要建树的字段(tool_calls/usage)C 的定位是纯开销。
## 为什么仍默认关闭(理由充分,不是保守)
1. toolcall 是真实负载最常见的一类块(任何一次工具调用流),仍慢 15%
2. 18% 收益不足以抵消「与 encoding/json 语义并存的第二实现」的风险
3. 本刀原始动机在 toolcall 场景没有兑现:分配数 20 vs 21 几乎没降
⇒ 前提是先做「按字段类型决定是否 C 化」,让 toolcall 也不输,再重测。
## 正确性
6 万+ 差分用例(协议形态/真实负载/随机 JSON 3 万/随机字节 3 万)全过。
基准测量也修了:先前 C 基准脚本用 CLOCK_MONOTONIC 却只取 tv_nsec,
算出 -4201ns 的负值 —— 测量工具本身出错会直接毁掉结论。
## 验证
ASan+UBSan PASS;gcc+clang 零告警;arm64 交叉 0 告警;
libFuzzer 66 万次零崩溃;全量 go test 38 包 ok / 0 FAIL
|
2026-09-26 10:41:52 +08:00 |
|
|
|
7748ec450e
|
perf(api): SSE 分块 C 导航层 + 差分等价验收(实测更慢 ⇒ 默认关闭)
第三刀:把 ha_json_scan 接进 parseOpenAICompatibleStreamChunkFull。
**结论是否定的** —— 实测比原实现慢,故默认关闭并如实记录。这条提交的
价值在于「已钉死的正确性 + 已定位的根因 + 一条防静默回退的断言」。
## 设计:只做「结构导航」,序列化留在 Go
接线前实测出两条 wire 语义,它们让「整条解析全 C 化」不成立:
§5.1 重复键是**字段级合并**,不是替换:
{"choices":[{content:a}],"choices":[{reasoning:r}]} → 两个都保留。
机制:json.Unmarshal 的 object() 收尾做 v.SetIndex(i, subv.v),
而 subv 拿到的是**已存在元素的指针** ⇒ 第二次是叠加。
§5.2 stringifyContent 的 default 分支 = json.Marshal(interface{}),
即**重新序列化**:{"b":1,"a":2}→{"a":2,"b":1}(键排序)、
1e2→100、<→\u003c、大 int 先舍入成 float64。
逐值一致 = 复刻 Ryu 最短浮点 + map 键排序 + HTML 转义 + int 舍入。
两条都只在**取值**阶段需要,故 C 只回答「值在哪里」(零分配零解码),
类型检查靠「用相同的 Go 类型 unmarshal 相同形状的子树」保证,不靠 C 复刻规则。
## 实测:新路径比原实现慢(20000 次迭代)
| 场景 | 新路径 | 原实现 |
|---|---|---|
| content_ascii | 2016ns / 20allocs | 1325ns / 13allocs |
| toolcall | 5854ns / 33allocs | 3270ns / 21allocs |
| usage | 3170ns / 24allocs | 2832ns / 12allocs |
分配数**也变多**(20 vs 13),与「消除 GC 抖动」的初衷相反。
根因(逐项测量,非猜测):裸 cgo 调用 168ns;**每次带 out-param 的键查找
205ns + 2 allocs**(out-param 逃逸到堆);一次解析需要 5+ 次查找
⇒ 边界与分配成本约 1µs,恰好吃掉全部收益。Go 侧只需**一次** Unmarshal。
一句话:**用很多次廉价调用换一次昂贵调用,在这个尺寸上不划算。**
## 天花板实验:方向对,但当前实现没到
假设拿到 span 完全免费,只测设计中必须由 Go 做的部分:
我的 Go 侧 505ns/7allocs vs 原实现 1239ns/13allocs
⇒ 边界归零后仍有 2.4× 时间、46% 分配的空间。故问题在**逐字段往返**
这个交互方式,不在 C 本身。正确改造:一次调用返回全部字段 span +
结果写调用方栈结构体 + 仅在确需重新编码时回退。
## 正确性:6 万+ 差分用例全过
同一批输入跑两条路径逐字段比对(Content/Reasoning/Done/Finish/ToolCalls/
Usage + bool),5 组:协议形态(含全部回退触发条件)、真实负载、随机 JSON
30000 例、随机字节 30000 例、优化有效性。
★ 差分测试当场抓出 4 个真实缺陷(其中一个正是「优化压根没生效」):
1. ha_sse_arr_first 里「重新 init 到 sc.s+sc.i」使 base 变了 ⇒ start 恒 0
⇒ 返回的是**数组本身**而非首元素。症状是**快速路径永远不生效**——
而若只看「结果与 Go 一致」,这个 bug 会**完全隐形**(回退总是对的)。
⇒ 这就是必须单独断言「优化确实被走到」的原因。
2. chunkAssemble 的 bool 被丢弃 ⇒ 空对象被判 true(原实现 false)
3. 键匹配层级搞错:delta 是 **struct**(字段名 CI),不是 map。
我一度「推理」成 CS 并以为差分测试会通过——错的。
教教训:哪层是 struct、哪层是 map 要**回原实现读类型**,不能凭字段名推断。
4. cgo 边界:out-param 逃逸到堆
另修:C 代码从 cgo 前言移进 csrc/ ——前言里的 C **逃出全部 C 门禁**
(告警/sanitizer/交叉/模糊测试),而它恰是本刀最易出错处。
## 防静默回退
TestChunkFast_BenchGate 断言 chunkFastEnabled 必须为 false。
后来者看到「快速路径写得全 + 差分测试全过」,很自然会以为它已生效并打开它
—— 而实测更慢。断言把这个事实钉住,改动即判红。
## 实测汇总
- C 契约 119 项断言、黄金对照 5 组、差分 6 万+ 例:全过
- ASan+UBSan PASS;gcc+clang 零告警;arm64 交叉 0 告警(3 个源文件)
- 全量 go test -count=1 ./... 38 包 ok / 0 FAIL
- libFuzzer 4948 万次零崩溃(上一刀)
教训(与第一刀同源):**「C 比 Go 快」不是前提,是待验证的假设。**
第一刀被 C.CString 的 82% 自找开销推翻一次,这一刀被逐字段往返推翻一次。
两次都是测量推翻直觉。
|
2026-09-26 10:32:20 +08:00 |
|
|
|
4d3962a845
|
feat(csrc): 第二刀 —— 零分配 JSON 扫描/取值层 ha_json_scan(含黄金对照)
C 化第二刀:为协议编解码层铺 JSON 底座。**本刀只交付库 + 验收,
未改 Go 生产路径**(接线是独立一步,库先验完再换产线)。
为什么是它:SSE 单块解析(parseOpenAICompatibleStreamChunkFull)是每个流式
chunk 都要跑的最热路径,实测 1937ns/13allocs(content 块)、3122ns/21allocs
(toolcall 块),而纯字节扫描理论下限 133ns/1alloc —— 差距 15~23×。
一次 1 万块的会话 = 1~2 万次堆分配,正是 GC 抖动的来源。
为什么不复用 SDK 的 remotedevice/ha_json.c(实测三缺陷,不可直接复用):
① 无 \u 解码:\u4f60\u597d → ?0?d?d?0(非 ASCII 全靠转义时内容直接损坏)
② 只有 _get_int 无浮点:temperature:0.7 静默变 0
③ null 与「键缺失」不可区分
外加它是 DOM + malloc,与本层「不 malloc / 零拷贝 / 纯函数」正交。
设计:scan(结构,零分配零解码)+ extract(取值,按需解码)两段分离。
content 可能是很大的多模态数组,而 stringifyContent 只需要 text 字段拼起来;
若 scan 就解码并分配缓冲,等于把成本付给不需要它的调用方。
★ 被测试抓出 7 个真实缺陷(写 C 时同一逻辑我读三遍都认为正确):
1 代理对合成成功后未跳过 unconditionally 的 U+FFFD 发射(😀 → 两个 FFFD)
2 过长编码检查用了只含首字节位的 cp(「你」→ 6 个 FFFD)
3 members_next 只报值起点不消费值 → 游标停在值前(模糊测试第一轮抓到)
4 扫描阶段不校验转义字符合法性({"a":"\q"} C 判合法、json.Valid=false)
5 扫描阶段不校验 \u 后四位十六进制(同上)
6 get_int 接受前导零(007 / 00)
7 cgo 桥接把 C 结构体声明为 Go 局部变量 → 运行时 panic
(cgo argument has Go pointer to unpinned Go pointer)
其中 4 个是「静默分叉」——不崩、不报错,生产里表现为「内容少一个字符」
或「某些块被静默丢弃」,极难归因。这正是黄金对照不可省的理由。
★ 另纠正我自己两次错误的「真值」(比代码 bug 更危险,会变成错误规格):
第一版真值表里 content:{} 的花括号少了一层,把「我写错 JSON」误读成
「Go 对 content 严格」。修正后实测发现一对方向相反的语义:
content 走 interface{} 宽松({}→"{}"、true→"true"),
reasoning_content/usage/finish_reason 强类型严格(123 ⇒ 整块作废)。
照错误表写 C 会产出「比 Go 更严格」的实现,静默丢弃本该生效的块。
两个由缺陷倒逼的设计决定:
- members_next 返回**完整值 span** 并内部跳过 ⇒ 「返回 1」蕴含「成员良构」。
要求调用方自己推进游标的 API 是错的:忘一次就解析到上一个值且不报错。
- members_complete() 区分「正常扫到 }」与「输入畸形」,否则无法复刻 Go 严格性。
同时修两个基础设施目标对「多源文件/多测试」的适配:
- csrc-sanitize:每个契约测试各自链接(多个 main 合链会 multiple definition,
而报错被吞后会被误报成「本机无 sanitizer」——一个假的 SKIP)
- csrc-cross:多源文件改用 -fsyntax-only 逐文件(gcc 不支持多源单 -o)
实测(全部当场可复现):
- C 契约测试 119 项断言全过;黄金对照 5 组全过(语法/成员/解码/整数/随机字节)
- libFuzzer 4948 万次运行零崩溃(121s)
- ASan+UBSan PASS(两个契约测试各跑);gcc+clang 零告警;arm64 交叉编译 0 告警
- 全量 go test -count=1 ./... 0 FAIL;make build-linux-arm64 → ELF aarch64
- 纪律检查 SDK 公开接口 diff = 0 行(未触碰 SDK)
决策关闭(jianf 本轮裁决):C 实现留主仓 csrc/(它本就是替换内核 Go 实现,
SDK 从未被触碰,跨端复用才需进 SDK 而它们不调用本层);ha_json.c 不复用;
下一刀即协议编解码层。
|
2026-09-26 10:07:27 +08:00 |
|
|
|
d9f60bcd68
|
docs(plan): 记录 C 基础设施门禁落地 + 下一刀决定 + ha_json.c 不可复用的实测证据
- §七 新增「C 基础设施门禁」小节(六个门禁 + 地基上线抓出的 5 个自身 bug)
- 记录首次把「C 函数体成本」与「cgo 边界成本」分开测出的基准表
- 下一刀定为协议编解码层,附 SSE 单块解析实测(1937/3122/2464ns vs 133ns 下限)
- ★ 推翻旧判断:SDK ha_json.c 有三个致命语义缺陷(\u 不解码 / 无浮点 / null 不可区分),
且 DOM+malloc 与设计约束正交 ⇒ 建议 csrc/ 新建零分配 ha_jsonscan.c,不建议内核热路径依赖别仓
待 jianf 拍板三项仍未决,未擅自开工。
|
2026-09-26 09:45:49 +08:00 |
|
|
|
8070844fe9
|
build(csrc): C 基础设施门禁(ABI 版本 / 告警 / sanitizer / 交叉编译 / 模糊测试)
C 化从「一把刀」推进到「可持续推进」,本轮先把基础设施建起来:
不建它,后续每个 C 切片都在裸奔(无告警门禁、无内存安全检查、
无交叉编译验证、无 ABI 漂移检测)。
新增门禁(make check-csrc / check-csrc-full,已接进 make test):
- csrc-lint gcc+clang 双编译器 × -Wall -Wextra -Wpedantic -Wshadow
-Wconversion,零告警才算过(-Wconversion 是为 cgo 窄化
准备的:size_t→int 截断在默认档下是静默的)
- csrc-abi ABI 版本运行期自述 + 荒谬值检查
- csrc-headers 头文件自包含性(每个 .h 能单独编过)
- csrc-sanitize ASan+UBSan 跑 C 契约测试
- csrc-cross arm64 交叉编译(homed 的真实发布目标)
- csrc-fuzz libFuzzer:内存安全 + 6 条不变式(可 CI 门禁)
新增 ABI 契约(csrc/include/ha_abi.h):
- ha_codec.h 声明「签名冻结」,但冻结只写在注释里;现改为
HA_CODEC_ABI_MAJOR/MINOR + 运行期自述 + Go 侧常量,
三方交叉断言,版本漂移在测试期判红而非线上表现为行为诡异。
- 门禁当场抓出我自己的两个真 bug:①_Static_assert 是 C11 而项目
是 -std=c99(-Wpedantic 报的);②##msg 不能拼接字符串字面量,
导致两个断言共用一个 typedef 名(clang 报的)。
基础设施当场抓出的三个真实缺陷(都是「本机 gcc 能编过、别处会炸」类):
- bench 用了 POSIX clock_gettime,而 CMake 刻意 C_EXTENSIONS OFF
(严格 c99)→ 头文件未声明;补 _POSIX_C_SOURCE(须在任何头之前)
- 头文件缺 include 时只在「恰好被别的头先包含」处静默编过
- Go 不允许在 _test.go 用 cgo ⇒ C 侧 const 桥接只能放非测试文件,
且 cgo 生成的 *_Cvar_* 不是 Go 常量(「两边一起错成一样」的盲区,
改用 C 函数返回 + 编译期 _Static_assert 补上)
实测(全部当场可复现):
- 双编译器 × c99/c11 零告警;ASan+UBSan PASS
- libFuzzer 91s 跑 3329316 次、零崩溃(6 条不变式全过)
- 变异测试:改 C 侧宏 / 让 Go 常量与 C 函数「一起错成一样」,
均被对应断言抓住(证明门禁不是摆设)
- C 侧纯函数基准(首次把函数体成本与 cgo 边界成本分开测):
ascii_1k 121.7ns/8.4GB/s;zh_1k 1434ns;truncate 两者均约 128-134ns
- 全量 go test -count=1 ./...:57 包 0 FAIL
- make build-linux-arm64 → ELF aarch64
- 纪律检查 FAIL=0;SDK 公开接口 diff = 0 行
未动:csrc/ 是内核 C ABI,不属 third_party/homeagent-sdk 公开接口。
|
2026-09-26 09:44:17 +08:00 |
|
|
|
cbe024f6a6
|
docs(plan): 两条端口/限流 flaky 已修(17e7094),标记关闭
|
2026-09-25 17:57:26 +08:00 |
|
|
|
17e7094967
|
fix(plugins): 修三处端口/监听缺陷 + 让测试用临时端口(消除既有 flaky)
排查内核 SIGSEGV 时用 A/B 对照(我的树 20 轮 vs 干净树 20 轮)确认了
两条**既有** flaky,与 C 化改动无关。本提交把它们修掉。
## 缺陷 ①(真 bug,不只是测试卫生):pluginmgr 监听地址是包级可变全局
`var HTTPAddr = "127.0.0.1:9876"` 是包级可变全局,`Start()` 还把 settings 读到的值
**反写**回它,`startHTTPServer` 再读它。后果:
- 多实例互相污染:后启动的实例把地址写进全局,先启动那个读到的是**别人的**地址
(实测与生产 homed 抢 9876)
- 全局读写无同步,属数据竞态
修法:改为实例字段 `p.httpAddr`(默认走 `const defaultHTTPAddr`),
不再有可被任意代码改写的包级状态;并新增 `HTTPURL()` 访问器。
## 缺陷 ②:remotedevice 用 ListenAndServe,监听失败静默且 :0 无法回报端口
`p.server = &http.Server{Addr: p.addr}` + `ListenAndServe()` 在后台 goroutine 里报错,
端口被占时只打一行日志、`Start()` 仍返回 nil —— 插件表面「已加载」而网关根本没跑。
且 `:0` 下拿不到真实端口。
修法:改为 `net.Listen` + `Serve`(与 webui/pluginmgr 同形):
- 监听失败**同步**返回,交给加载器
- 用**实际绑定**地址回写 p.addr,日志与诊断面显示真实端口
## 测试侧:全部改用 :0,不再抢固定端口
新增 `ConfigRegistry.SetPluginConfig(name, key, value)`:插件表原本只在
`RegisterDef`(插件 Start 时)创建,导致「想在插件加载前预置配置」无从下手
(直接 Set 会因表不存在而失败,错误常被忽略)。新方法先建表再写,填补该时序缺口。
`setupIntegration` 在 `Load()` 前预置:
- pluginmgr.http_addr / remotedevice.listen_addr → `127.0.0.1:0`
- webui 走已有的 `SetListenOverride("127.0.0.1:0")`(它有独立旁路)
实测三个插件现在各自绑到 OS 分配的空闲端口(41895 / 35855 / 34021)。
## 缺陷 ③:deepsearch 测试把「上游限流」当成功能回归
`TestRealPlugin_DeepSearchInvoke` 的断言会在上游限流时失败,但插件此时返回的是
**正常结果**(err==nil,content 含 "未返回结果" 与无响应引擎列表)——那是外部条件。
实测失败信息:`brave(Suspended: too many requests), duckduckgo(CAPTCHA), google cse(...)`。
更糟的是它**不可控地随机红**:干净树连跑 20 轮复现 2 次,与代码改动无关。
这种判据会让真正的回归淹没在噪声里。
修法:区分「上游不可用(限流/CAPTCHA)⇒ t.Skip 并说明理由」与
「其他异常 ⇒ fail」。不用静默 return,避免环境退化时判据无声失效。
## 由此发现并修掉的真缺陷:监听地址被硬编码在三处
`127.0.0.1:9876` 曾硬编码在 pluginmgr / cli / webui 各一份。cli 与 webui 后来改为
运行时读 `pluginmgr.http_addr` 设置(本次核实),pluginmgr 自己却仍是全局 —— 三处
口径现在统一为「读设置 + 实例字段」。
## 验证
- 新增 `TestTwoInstances_ListenIndependently`(pluginmgr):两个实例同时监听、
各自 HTTPURL 指向自己端口、两个地址都真的可连。
★ 经**忠实变异**验证有牙:复原「包级全局 + Start 反写 + 读全局」后该测试判红
(我第一版测试只断言字段不共享,变异证明它没牙,已重写为端到端判据)。
- `internal/plugins` 连跑 **30 轮:30/30 全过**(修复前干净树 18/20)。
- 全量连跑 3 轮:38 ok / 0 FAIL / 0 bind 冲突。
- go build ./... / go vet ./... 干净。
|
2026-09-25 17:57:15 +08:00 |
|
|
|
10a8d04deb
|
docs(plan): 修掉重复块 + 补两条既有 flaky 的实测归属
## 修掉我引入的结构错误
plan.md 此前有**两套 §三~§七**(第 468 行起是第 265-410 行的完整副本),
是我早前替换 §七 时遗留的旧版本。现合并为一套:保留新版 §七
(含优化后基准与「完全 C 化」裁定),吸收旧版里仍有价值的
「已定事项(不再挂账)」小节,删掉过时内容:
- 旧的「C 比 Go 慢」基准表(该结论已被推翻)
- 旧的「按长度分派(短走 Go)」建议(已被 jianf 裁定否决)
行数 685 → 498,`grep '^## '` 不再有重复章节。
## 补两条既有 flaky 的实测归属(排查 SIGSEGV 时顺带确认)
用「我的树 20 轮 vs 干净树 20 轮」A/B 对照确认两者**均与 C 化改动无关**:
1. **测试间固定端口冲突**:pluginmgr `9876`(且是包级可变全局 `var HTTPAddr`)、
remotedevice `9890`、webui `8080`。干净树同样复现 2/20 失败
(`bind: address already in use` + `signal: terminated`)。
2. **TestRealPlugin_DeepSearchInvoke 依赖外部 SearXNG**(`127.0.0.1:8888`):
上游限流时必红(实测报错 `brave(Suspended: too many requests)`、
`duckduckgo(CAPTCHA)`)。干净树同样复现 2/20。
两条都建议改用 `:0` 或区分「服务不可达(skip)/ 真回归(fail)」,
但不属本次「排查崩溃」范围,记入待办。
|
2026-09-25 17:27:55 +08:00 |
|
|
|
b4fb254bf5
|
fix(proc): 修 arena use-after-unmap 导致的内核 SIGSEGV(关停竞态)
## 症状
全量 go test 偶发 SIGSEGV,整个测试二进制被杀(recover 捕不到 runtime
致命错误)。崩溃栈(2026-09-25 实测捕获,完整):
readLoop (process.go:334)
→ markExited → once.Do
→ onExit → Plugin.handleExit (plugin.go:209)
→ Host.ReclaimOwner (host.go:342)
→ arenaRegion.ReclaimOwner → blockBase → getU32
→ SIGSEGV 读已 munmap 的内存
## 根因(两个叠加缺陷,同一后果)
**① markExited 里 close(exited) 早于 onExit**
close(p.exited) // :394 —— 先关闭,唤醒所有等待者
p.sup.untrack(p.name)
p.onExit(...) // :399 —— 回调里要读共享内存
onExit(内核侧 Plugin.handleExit)会调 Host.ReclaimOwner 回收该插件残留的
共享槽,那是要读共享内存区域的。而 exited 一关闭,Stop()/Kill() 就返回
(process.go:566/587),StopAll 随即返回,调用方(Host.Close)立刻
freeShm 解除映射 —— 此刻 onExit 还没跑完,ReclaimOwner 就成了读已 munmap
的内存。
修法:把 onExit 提到 close(p.exited) **之前**,并明确 exited 的语义是
「**完全**收尾完毕」而非「进程已死」——任何等待者看到它关闭后,都可安全
释放共享内存、卸载资源。
**② StopAll 超时分支 `go p.Kill()` 发射后不管**
go p.Kill() // 不等待
本函数返回后调用方就 unmap,而 Kill 内部要等 markExited 跑完(含 onExit)。
改为等全部 Kill 完成(Kill 自带 killReapTimeout 上限,不会无限拖住关停)。
**③ 同类的第三处:事件环订阅在关停时从不退订**
EventRing.Subscribe 注册到 Bus 的 handler 会 ring.WritePush(写共享内存),
而 Host.Close 会 munmap 整块区域。此前:
- handleEvents 把 EvtRingSubscribe 的取消函数**直接丢弃**(corehandler_runtime.go:103)
- EventsUnsubscribe 是 no-op,注释还写着「子进程 Stop 时由内核统一清理」,
但 closeProcHost 根本没有退订
于是每个订阅过的插件都在 Bus 上永久留了一个写共享内存的 handler,
munmap 后任意一条事件经过 Publish 就会写已解除映射的内存 ⇒ 同类 SIGSEGV。
修法:EventRing 记为 unsubs、新增 EvtRingSubscribeTracked(订阅即登记),
Host.Close 在 freeShm **之前**调用 evtCloser.Close 统一退订。
## 回归测试(3 个,均经变异验证「修复前判红」)
1. `TestProcess_OnExitCompletesBeforeExitedCloses`(proc)
断言 Exited() 关闭时 onExit 必须已返回。变异(把 close(exited) 挪回
onExit 之前)→ FAIL。这是本次崩溃的直接判据。
2. `TestHost_CloseUnsubscribesEventRing`(proc)
断言 Host.Close 调用了 evtCloser.Close。变异(撤掉退订)→ FAIL。
3. `TestEventRing_CloseUnsubscribesFromBus`(plugin)
端到端:订阅 → Publish 有写入 → Close → Publish 不再写入。变异
(Close 不做事)→ FAIL。
顺带给 EvtRing 加了 Written() 访问器(诊断 + 上述测试的可观察量)。
## 验证
- 三个新测试全绿;-race 下 ./internal/plugin/... 全绿
- proc 包连跑 12 轮、internal/plugins 连跑 20 轮:SIGSEGV 0 次
- 全量 go test -count=1 ./... → 38 ok / 0 FAIL
- go build ./... / go vet ./... 干净
## 另有两个**既有** flaky(本次未动,与 C 化无关,单独记录)
排查过程中用「我的树 20 轮 vs 干净树 20 轮」对照确认了归属:
- 测试间固定端口冲突(pluginmgr 9876 / remotedevice 9890 / webui 8080):
并行或残留实例时报 bind: address already in use。干净树同样复现。
- TestRealPlugin_DeepSearchInvoke 依赖外部 SearXNG(127.0.0.1:8888):
上游限流时(brave "too many requests"、duckduckgo/quark CAPTCHA)断言失败。
干净树同样复现。属外部依赖,不是代码缺陷。
两者都不属本次「排查崩溃」的范围,已记入 plan.md 待办。
|
2026-09-25 17:25:43 +08:00 |
|
|
|
7799ca4559
|
perf(c-core): 完全 C 化 + 消灭初版的 malloc/拷贝开销(C 从「更慢」变「明显更快」)
上一提交的基准结论是错的:"C 比 Go 慢" 不成立 —— 那是我把自己的
malloc/拷贝开销误当成了 cgo 的固有成本。本提交先拆解成本、再逐项消灭。
## 成本拆解(同机、百万次 benchtime)
| 场景 | ns/op |
|---|---:|
| cgo 边界(零拷贝传指针 + 空函数体)| 31.9 ← cgo 真实固有成本 |
| + 一次 C.CString + C 侧 strlen | 105-111(多出 ~75ns)|
| 初版 ModelContextWindow(另加 lower_dup malloc + 16×strstr)| 175 |
即 82% 开销是自找的。而初版还违反了自己写在设计文档 §四 的原则第 1 条
「C 接口只吃 const char* + 长度」——它没传长度,让 C 侧 strlen 再扫一遍。
## 逐项修复
1. C.CString(malloc+整串拷贝)→ unsafe.StringData 传指针+长度,零拷贝
2. C 侧 strlen 再扫一遍 → 长度由调用方传入,不扫
3. truncate 的 malloc 输出缓冲 + GoStringN 拷回 → C 只返回**字节数**
(结果必然是输入前缀),Go 侧 s[:n] 完成切片,全程零分配
4. lower_dup 每次 malloc 模型名 → 栈缓冲折叠,超长走零分配回退
5. 逐字节 utf8_next 函数调用 → 字级(8 字节)ASCII 检测
6. truncate 扫完整串才判断 → 数满 keep 个 rune 立即返回(提前短路)
7. 纯 Go 侧 len([]rune(s))/[]rune(s)(1KB 分配 4KB)→
utf8.RuneCountInString / DecodeRuneInString 游走,零分配
## 结果
| 基准 | 初版 C | 优化后 C | 纯 Go | 提升 |
|---|---:|---:|---:|---:|
| ModelContextWindow | 175 | 76.5 | 46.8 | 2.3× |
| EstimateTokens / 1KB ASCII | 2318 | 80.8 | 326 | 28.7× |
| TruncateByTokens / 1KB ASCII | 2594 | 71.7 | 411 | 36× |
| TruncateByTokens / 1KB 中文 | 3923 | 70.1 | 3097 | 56× |
## 完全 C 化(jianf 裁定)
撤掉我一度加的「短串 <32B 走回 Go」按长度分派:那会同时存在两份语义
可能分叉的实现。C 是唯一实现。
代价如实记录:EstimateTokens("qq") 这类极短串上 C 约 47ns(几乎全是
31ns 边界成本)vs 纯 Go 约 3ns,慢约一个数量级;绝对值纳秒级
(0.000047ms),单次请求尺度可忽略。若某循环对极短串高频调用,
正确应对是**把该循环 C 化(批量传一次)**,而不是按长度分派回 Go。
## 顺带补的正确性缺口(初版是真错的)
初版 C 的 UTF-8 解码只按首字节推断长度、**不校验后续字节**,
因此对畸形序列会与 Go 分叉:例 "\xE4\x41\x41",Go 判 3 rune,
初版判 1 rune ⇒ rune 计数偏差 ⇒ token 预算与截断点偏移。
这类偏差**只影响计数、不会崩**,不测发现不了。
现在 C 侧做与 utf8.DecodeRuneInString 等价的完整校验(含过长编码、
代理对、超 U+10FFFF、截断序列),语义边界逐条注释。
代价:中文密集输入比初版慢(1467 vs 840)—— 这是刻意的正确性代价,
且仍比纯 Go 快 2×。
新增测试:
- TestGolden_InvalidUTF8:3000 组**任意字节**(含畸形序列)对拍,
覆盖初版会分叉的输入类别
- TestGolden_TruncateAlwaysPrefix:截断结果必为原串前缀且不超长
- C 契约测试从 21 项扩到 40 项(含非 NUL 结尾、超长名、畸形 UTF-8)
## 包现在要求 cgo 才能编译
删除 codec_nocgo.go:CGO_ENABLED=0 下整包构建失败(错误直指缺失符号)。
不保留回退的理由:只验证过一条路,就不该存在第二条。
实测这不影响任何构建 —— go list -deps 证明只有 cmd/homed 依赖本包,
而 waiter/initconfig/memgc/mock-server 均不依赖(逐个验过),
且 homed 本就强制 cgo(sqlite3 + gojieba)。仓库无 CI。
Makefile 把「不许有第二条路」变成可执行断言:check-codec-cgo-only
(断言 cgo 下全绿 **且** CGO_ENABLED=0 下必须失败)。
## 验证
- C 契约测试 40/40(gcc -Wall -Wextra 零警告)
- 黄金对照 6 个测试全绿(含 2000 组随机 + 3000 组畸形字节对拍)
- 变异测试:改 C 侧返回值后 go test 立即 FAIL(确认真的走 C)
- make check-codec-cgo-only 两项断言通过
- go vet ./... 干净;全量 go test -count=1 ./... → 38 ok / 0 FAIL
## 已知既有 flaky(与本改动无关,单独记录)
internal/plugins 在全量并发下偶发一次 SIGSEGV,栈在
internal/plugin/proc/{unified.go:234,arena.go:197}(arena 的 getU32)。
该两文件最后修改于 09-10,本提交 0 处触及;随后连跑 5 次单包 +
2 次全量均通过。初步判断是 arena/shared-region 的既有竞态,需单独排查。
|
2026-09-25 15:40:20 +08:00 |
|
|
|
5ccdf24f18
|
test(c-core): 补跨语言开销基线 —— 数据反驳「C 比 Go 快」的直觉
codec.go 的注释写着「EstimateTokens 是否该留在 C 侧由 codec_bench_test.go
的实测数据决定,不要凭直觉断言」,但那个文件此前并不存在(悬空引用)。
plan.md 与设计文档也都在说「需先有真实延迟基线(当前没有)」。本提交把它补上。
## 数据(ns/op,benchmem)
| 基准 | C(经 cgo)| 纯 Go | 谁快 |
|---|---:|---:|---|
| ModelContextWindow(短 ASCII)| 175 | 38 | Go 快 4.6× |
| EstimateTokens / 空串 | 100 | 0.43 | Go 快 230× |
| EstimateTokens / 短 ASCII | 115 | 6.5 | Go 快 17× |
| EstimateTokens / 短中文 | 100 | 29 | Go 快 3.4× |
| EstimateTokens / 中 200 字 | 229 | 509 | C 快 2.2× |
| EstimateTokens / 1KB 中文 | 840 | 2870 | C 快 3.4× |
| EstimateTokens / 1KB ASCII | 2318 | 332 | Go 快 7× |
| TruncateByTokens / 短中文 | 233 | 54 | Go 快 4.3× |
| TruncateByTokens / 1KB 中文 | 3923 | 6918 | C 快 1.8× |
(已用 -count 复测确认稳定;ascii_1k 的异常已单独隔离复测 3 次)
## 三条结论
1. **cgo 固定开销约 95–100 ns/次**,小输入下完全压倒算法差异。
2. C 只在**长中文**(UTF-8 步进重)上领先;长 ASCII 反而 Go 快 7×
(Go 的 utf8.RuneCountInString 对 ASCII 有快路径,C 侧逐字节跑)。
3. ⇒ 判据应是「哪个在**真实输入分布**下真能变快」,不是「哪个看起来更底层」。
## 对后续 C 化的影响(已写进 plan.md §七 与设计文档 §7.1)
- EstimateTokens 的真高频点在 process.go:476 的逐事件循环与 resident.go:552。
字段分布不单一:Source 是短标签(Go 快 17× 那一档),
Input/Response 是对话文本(长中文 C 快、短文本与长 ASCII Go 快)。
⇒ 当前一刀切走 C 会让短串净亏;正确做法是按长度分派,
但须先用真实长度分布复测,不要凭推测动手。
- ModelContextWindow(provider.go:333)与 TruncateByTokens(tooldefs.go:38)
调用点单一、非热路径,开销在单次请求尺度上无关痛痒。
这不否定 C 化方向:协议编解码(JSON 解析、SSE 分片)处理长文本,
才是 C 的主场,也是比「把短函数搬过去」更合理的下一步。
|
2026-09-25 14:51:10 +08:00 |
|
|
|
7351c6ca2e
|
feat(c-core): 内核编解码层 C 化第一刀 —— L1 纯函数层落地并打通构建链
第一刀只做三个纯函数(窗口推断 / token 估算 / token 截断),
价值不在功能(Go 版没问题),而在打通「Go → cgo → C」全链路并
建立可复现的对照范式,后面每扩一个函数都复用它。
## 为什么是这三个
按「无状态 → 有状态」分层,L1 协议编解码最安全:纯 string in → struct out,
不碰网络、不碰 Lua、不碰 goroutine。三个函数更是同一组纯算术,最小可验证切片。
## 关键设计:包内符号链接,不链接静态库、不 include 包外源
三种做法都实测过,只有一种同时满足「可构建 + 可交叉编译 + 缓存可跟踪」:
1. ❌ 链接 `csrc/build/libha_codec.a`(原方案)
- .a 是构建产物、不入库(.gitignore 的 build/ 命中 csrc/build/),
而发布脚本原先并不产出它 ⇒「不入库 + 不生成」两头空,
实测报 `cannot find .../libha_codec.a`
- 交叉编译 linux/arm64(homed 真实发布目标)时,宿主 x86-64 的 .a
被链进目标产物,实测报 `file in wrong format`
2. ❌ `#include "../../../csrc/src/ha_codec.c"`(包外相对包含)
★ Go 构建缓存**不跟踪包外被 #include 的 C 文件**。实测:包外源把返回值
7→8,`go test` 依然通过(缓存命中、静默沿用旧代码);同样改动落在包内
文件时立即判红。对「逐步推进 C 化」这是致命的——改 C 源码不生效且无报错。
(包内 shim `#include` 包外源同样漏跟踪,已实测排除。)
3. ✅ 包内符号链接 `internal/agent/api/ha_codec.{c,h}` → `csrc/`
文件在包目录内 ⇒ 缓存按内容正确跟踪;只有一份权威源 ⇒ 无副本漂移,
也不需要「同步 C 源」的 make 目标。
## 不需要额外 build tag
ha_codec 是零依赖纯 C99 源码内联编译,不需要外部库或工具链前提;
而 homed 本就强制 cgo(sqlite3 + gojieba),故 C 路径自然生效。
只用 `cgo` / `!cgo` 一组约束(对比 onnxruntime:那个需运行期 .so,故必须显式 tag)。
## 顺带修掉的既有缺陷(非 C 化引入,但一直缺覆盖)
- Makefile 的 arm64 目标缺 CC/CXX:cgo 回退到宿主 g++,报
`gcc_arm64.S: no such instruction: 'stp x29,x30,[sp,'`
(deploy/packaging/build.sh:49 一直是对的,Makefile 漏了)
- Makefile 的 arm64 目标缺 .syso 隔离:cmd/{homed,waiter}/*.syso 是 Windows
COFF 资源对象,Go 会把同目录 .syso 无条件链进任何目标,交叉到非 Windows
平台报 `file format not recognized`(build.sh 有 hide_syso_for_target)
## 验证(每条可复现)
- `make build-linux-arm64` → ELF 64-bit LSB executable, ARM aarch64(82MB)
- `bash deploy/packaging/build.sh linux/arm64 homed` → ELF aarch64(79MB)
- 移走 csrc/build/ 后 homed(cgo)与 waiter(CGO=0)均能构建
- 变异 C 源(131072→777)后**同一缓存**下 go test 立即 FAIL(改前:仍报 ok)
- `make check-codec-paths` 两条路径 OK;`make csrc-test` C 契约测试 100%
- 黄金对照:手写用例 + 2000 次随机对拍,C 与纯 Go 逐值相等
- 全量 `go test -count=1 ./...` → 57 包:38 ok + 19 无测试 + 0 FAIL
新增 `make check-codec-paths` 防回归:只测一条路径时,另一条的破坏不会被发现。
## 文档
- `docs/zh/c-core/llm-orchestration-c.md` 同步为「已落地」,并更正因
「Windows 原生已放弃」而过时的 §2.3(回退路径的理由需重述)
- plan.md 的 P0-1 标记为已修复,附实测证据
|
2026-09-25 14:45:08 +08:00 |
|
|
|
b6027f3ff8
|
docs(plan): 按源码核实重写——剔除过时/跨仓项,只留 10 项真待办
旧 plan.md(1474 行)是「历史工单 + 路线图」混合档,混入两类错误:
1. 已实现却仍标 TODO(§13.12 L3 多模态、§11.4 Lua 快照锁、§12.2 setToolBlocks、
§13.11 主体 11 项)
2. 根本不属于本仓的跨项目工单(§13.9 在 llmsproxy 仓、§13.10 在 agentmail 仓)
本次回到源码逐项核实(grep 符号存在性 / go build / go test / go test -race),
新档只保留经证据确认的本仓待办,每条附 file:line 与可验证的验收条件。
- §一 结论速览(10 真待办 / 4 生产验证 / 8 已关闭 / 2 跨仓)
- §二 真待办 P0-P2:RuntimeManager 分组 worker、reload 语义说谎、arena 无
Grow/Shrink、事件环零真实负载、工具超时措辞、handleAgentAction 501、
Lua 独立 ABI、Windows 无真机验证、homed heap 常驻、3 个测试缺陷
- §三 已关闭 8 项(附源码证据,防复活)
- §四 生产部署后验证 4 项(代码已就绪)
- §五 跨仓工单归属说明
- §六 明确「不做」的决定
|
2026-09-25 09:17:11 +08:00 |
|
|
|
39a0c626b6
|
docs: 补站点基础设施运维手册(不含任何敏感信息)
本项目的生产部署是「多站点 + 统一入口 + 两层 TLS 终止」,此前只存在于
人和 agent 的临时记忆里 —— 换个人接手要重新摸索一遍,我这次就因此在
同一类问题上反复踩坑(误报「已修好」两次、并造成一次全站 TLS 故障)。
把机制写下来。
## 内容
- §0 拓扑:两层 TLS 终止(公网入口机 + 内网网关机),并指出「改一层 ≠ 改完」
- §1 部署一个静态站的完整流程(内网 drop-in → 证书 → 公网 drop-in)
含若干踩过的坑:为何用 drop-in 而非改生成的主配置、
为何静态站必须 `try_files ... =404`(回落 index.html 会让不存在的路径
返回 200,监控与链接检查全都「通过」)、反代为何要关缓冲攒包
- §2 验收:**必须用 `--resolve` / `--host-resolver-rules` 走真实公网路径**。
内网 DNS 会把域名解析到内网机,直连 curl 测的是另一条路 ——
这正是我此前误报「已修好」的根因。附 Playwright 片段
(强调 `ignoreHTTPSErrors` 必须为 false,否则等于没测)
- §3 证书续期:cron → 续期 → 钩子(内网 reload + 推公网),全自动;
以及**推送脚本必须保留的四道防线**(见下)
- §4 管理后台接口:先取 schema 别猜字段名;路由表是**整表替换**;
写完要轮询验证(apply 是异步的);系统管理的分组不能手工加条目
- §5 已知陷阱与事故复盘(6 条,均来自本项目真实故障)
- §6 给 agent 的直读入口(llms.txt / 每页 .md,含两个必踩的坑)
- §7 脱敏约定
## 关于事故复盘
§5.1 记录了我这次造成全站 TLS 故障的根因:把 acme.sh 的证书目录名
(**字面含 `*`**)交给了 glob,glob 展开后误匹配到别的证书并写进全局默认
证书。由此提炼两条硬规则:字面 `*` 绝不用 glob;写生产前必须断言
「读到的是什么」。§5.3 记录了「删证书记录前先查全盘引用」——
我漏了 /opt/ 导致推送脚本失效。
## 脱敏
全文无真实域名、IP、token、云 AK/SK、实例 ID 与内部组件名,
一律占位符(`<PUBLIC_IP>` / `<GATEWAY_DATA>` 等),
取值指向本机私有笔记与基础设施面板。已用脚本对 8 类模式做终审扫描:
仅 `127.0.0.1` / `0.0.0.0` / `example.com` 这类结构性取值保留。
技术论断均与线上实际配置或上游源码逐条核对(drop-in 指令、钩子行为、
后台接口语义),文档本身不构成新的未验证声明。
|
2026-09-24 15:04:02 +08:00 |
|
|
|
0ca1b96cd8
|
chore: 忽略 SDK 文档站的主题覆盖目录
上一批漏了 overrides/(只覆盖 footer.html 以补备案号)。它与 docs/、
mkdocs.yml 同属文档站,核心仓不应跟踪——规则漏一条就会在 git status 里
冒出来,正是之前 example/ 那类问题的来源。
|
2026-09-24 13:31:39 +08:00 |
|
|
|
a2cba0ceb1
|
docs: 更正 PluginMgr 的能力描述 + 挡住新文档站目录
## 更正一处实测证伪的断言
`assets/docs/{zh,en}/PLUGIN_DEV.md` 都写着「`PluginMgr()` 仅内置插件可用,
外部动态插件无法直接调用」——**这是错的**。
建 SDK 文档站时对照 `hmapdev` 桥接模板实测:
- `proc_main.go.tmpl:692` 显式 `base.SetPluginMgrAPI(procPluginMgr{})`,即桥接运行时
**为外部插件注入了** PluginMgr;
- 公开 `sdk/plugin.go:282` 的注释本身就写着「PluginMgrAPI 提供插件管理能力
(外部插件可调用)」。
真正的区别是**方法数**,不是有无:
| 接口 | 位置 | 方法数 |
|---|---|---|
| `sdk.PluginMgrAPI` | 公开 SDK | 3(ReloadOne / ListLoadedPlugins / IsPluginDisabled)|
| `internal/sdk.PluginManager` | 内核内部 | 9(另有 Enable/Disable/Remove/ReloadPlugins/IsBuiltinPlugin 等)|
两个接口名字相似但**不是同一个**,这正是混淆的来源。已在中英两版改写为准确描述,
并保留原有的「内置插件 API」代码块(那段示例用的确实全是内部面,加注说明)。
## 挡住新文档站目录
SDK 仓新增了 `docs/`(mkdocs 站点源码)与 `mkdocs.yml`。它们与 `README*`、`tools/`
同类——属于 SDK 仓,核心仓不该跟踪。规则带前导路径限定,核心仓自己的 `docs/`
不受影响(实测 `git check-ignore` 命中,工作区不再出现这两个路径)。
SDK 仓同批提交 0a6e2b7(文档站本体)。
|
2026-09-24 12:10:10 +08:00 |
|
|
|
4f3de61d8f
|
license: 明确内核 AGPL / SDK MIT 的边界(插件不受 AGPL 传染)
SDK 仓改为 MIT(同批提交 e97cafc)后,核心仓几处「插件静态链接 SDK 故须同许可」
的论述不再成立,一并更正。
## 边界(现在写清楚了)
- **AGPL-3.0-only 覆盖内核与随包内置插件**:homed、internal/、internal/plugins/
下 18 个内置插件。§13 网络条款照旧适用。
- **MIT 覆盖公开 SDK**:sdk/ 零外部依赖、只依赖 Go 标准库,不引用内核任何代码。
- 因此**外部插件不是本项目的衍生作品**,作者可自行选择许可(含闭源、商业、
私有),既不必同许可、也不受 §13 约束。
## 改动
- README.md / README_EN.md「许可」章节:改写静态链接那段,写明上述边界与理由
(决定许可的是被链接的 SDK 代码,而它是 MIT;子进程隔离不再是关键论点)
- README.md / README_EN.md changelog:**按本仓惯例保留历史原文**,在既有更正
注记区追加一条「许可口径已变更(2026-09-24)」,而不是去改写 v1.2.0 的原文
—— 避免伪造当时的语境
- site/index.html footer:状态栏拆成「内核许可:AGPL-3.0-only」+「插件 SDK:MIT」
两行;声明区分「AGPL(内核与其内置插件)」与「SDK 以 MIT 发布,插件不受传染」
- .gitignore:example/ 注释里的计数 20 → 21(实际 10 个示例 ×2 + qq/plugin_test.go;
规则本身与「有意保留」的说明不变)
验证:go build ./internal/meta/ ./internal/agent/core/ 通过;官网 1440px 无横向溢出、
无控制台错误,footer 许可两行正确渲染(/tmp/lic-footer.png)。
|
2026-09-24 10:55:23 +08:00 |
|
|
|
11144c62b5
|
chore(sdk): 同步 SDK 仓 SetAutoRestart 文档修正
SDK 仓 5af2a86 把 `SetAutoRestart` 的「崩溃后自动重载」改准为
「崩溃后自动重启」,并补上真实约束(线性退避 1s→2s→3s、
5 分钟窗口内第 4 次崩溃即停止、与「重载」是两回事)。
主仓以 vendored 方式跟踪该文件(third_party/homeagent-sdk/sdk/plugin.go,
主仓只跟踪其 40 个文件、不含 README),故此处同步同一改动,
保持两处一致。
|
2026-09-21 10:35:12 +08:00 |
|
|
|
d05161ac8d
|
fix(plugin): 退避注释里无实测支撑的「感知不到工具缺席」
`scheduleProcRestart` 的注释写:
线性退避:1 次→1s,2 次→2s,3 次→3s。崩溃循环时不至于打满 CPU,
又足够快到用户感知不到工具缺席。
前半句是事实(退避确实只为防崩溃循环打满 CPU),后半句是主观断言:
首次重启就要等 1s,这 1s 内该插件的工具是缺席的、调用会直接报错。
「用户感知不到」既无实测支撑,也会让读代码的人误以为是无感恢复。
这正是另一处文档(README「崩溃到恢复 <1s」)同源的问题 ——
实测退避为 1s/2s/3s,故 <1s 从未成立(`procRestartBackoff = time.Second`
由 02cc74c 引入,且该提交是 v1.0.0 的祖先)。
改为写明真实代价与插件侧的正确做法(在 OnStart 里自建重连与状态重建),
与 SDK 仓 README 刚补的说明保持一致。
|
2026-09-21 10:34:41 +08:00 |
|
|
|
7587bd82d8
|
site: 正名「驻留子 agent」+ 删净页面小字注解
用户两点意见:
1. 那个东西不叫「分诊助手」,叫**驻留子 agent**
2. 页面上加的小字注解全是废话,全删
## ① 正名:分诊助手 → 驻留子 agent
「分诊」只是 offload.go 里对**职责**的描述(triage),
`resident.go` 顶注给的正式名是「驻留子」,`docs/zh/resident-subagent-design.md`
也把它列为独立概念(根 agent / 驻留子 / 工作式轻量子三类)。
7 处全部改正,含图内 SVG 文字、`aria-label`、架构流程图、FAQ 正文:
| 位置 | 原 | 现 |
|---|---|---|
| 图 5 框标题 | 分诊助手 | 驻留子 agent |
| 图 5 框内第二行 | 临时驻留子 | (删,与标题重复) |
| 图 5 的 aria-label | 转投给分诊助手 | 转投给驻留子 agent |
| 正文 | 转给一个临时驻留的**分诊助手** | 转给一个**驻留子 agent** |
| 架构图转投分支 | → 转投临时分诊助手 | → 转投驻留子 agent |
| FAQ | 这正是「分诊助手」的用途 | 这正是驻留子 agent 的用途 |
| CSS 注释 | 分诊助手接到消息 | 驻留子 agent 接到消息 |
## ② 删掉 11 处注解小字
原则:**复述图上已画出来的、或标题已说过的** → 删;
**承载数据**(插件版本 / 源码路径 / SVG 图内必要标签)→ 留。
- `.duty-note` ×6:架构图里 6 个阶段下面那行灰字
(「构建消息与工具表 · 插件可在此短路」等)—— 阶段名本身已经说清,
而这 6 行会把循环框右半边撑空
- `.flow-note`:重复解释 `on_input` / `before_output` 跑在什么时候
- `.dv-caps` ×3:图 4 的「任何中断都能插它前面」「被打断的现场压入中断栈」、
图 5 的「不必干等」—— 图上插队动线和回执 chip 已经把它们画出来了
- `.dv-note` ×2:图 2「实测:statically linked,无动态依赖」、
图 3「实测整段写入+读回 3.5µs」—— 数字重复
- `.dv5-tnote`:「默认关闭,需部署方打开」—— FAQ 里有完整说明(含理由)
- `.duty-ev` ×4 + `.sm` ×4:证据行/注释行,数字并回正文
(「实测整段写入 + 读回 3.5µs。」等)
## ③ 删小字的连带修正
- **11 处死 CSS 全部清掉**(`.duty-ev` / `.flow-note` / `.dv-caps` /
`.dv-note` / `.dv5-tnote` / `.sm`),单文件不留无人引用的规则
- 循环框删掉 6 行灰字后**右半边空了一大块** → 改为 `fit-content` 收窄居中
## 自己踩到并改掉的坑
- 循环框收得太窄(16rem),回边徽标「有工具调用则回到 ①」**压住**
`after_toolcall` → 加 `min-width` 并加大底部 padding
- 想让循环框占满宽度,试过**阶段项排两列** → 截图发现编号被 grid
按行填充排成 `1/3/5 · 2/4/6`,线性流程顺序全乱。已回退单列,
并把这条教训写进 CSS 注释(免得后人再试一次)
- 图 5 触发条件框删掉小字后留白偏大 → 高度 56→42
## 验证
- 三档宽度(1440/768/390):无横向溢出、回边徽标与阶段项**零重叠**、
文字未被截断
- 深浅双主题 / JS 禁用 / reduce 回归全绿;控制台零错误
- 死 CSS 计数全部归零;「分诊」全站残留 0
- CSS 括号平衡,文件 141,847 字节(原 143,960,净减 2.1KB)
|
2026-09-21 10:23:06 +08:00 |
|
|
|
7b7d405463
|
site: 重做第 3/5 张示意图 + 删冗长文案 + 修中文排版
用户反馈:3/5 页面图片不够精致、动画单调;hero 与架构节两句文案啰嗦;
图 1 那句注释要删。
## ① 删掉两处冗余文案
- 图 1 的 `<p class="dv-note">箭头只进出插件 —— 内核一列都没有</p>`:
图上已经画出来了(光球只打插件、内核周围无连线),注解是重复
- 架构节副标题的「插件可改写或短路」:与组件表里 `Stage` 那行完全重复
(那行还更具体,带 1/4/2 的阶段分布)
## ② hero 副标题重写
原文「内核只管编排、记忆与调度,消息 / 文件 / 网络 / 设备一律交给插件。
插件崩了不牵连内核 —— 换掉一个二进制就热重载。」三重堆叠、句子太长。
改短后又发现**两处与源码不符**,一并改准:
- 「热重载」不是崩溃后的行为。崩溃走的是退避重启
(`scheduleProcRestart`:1s/2s/3s,`AutoRestartEnabled` 默认 true),
`ReloadOne` 是换二进制那条路径。混成一句等于说错。
- 改为「崩了自己重启,波及不到内核」。
## ③ 图 3(共享内存):从稀疏线框重画
原图 19 个元素、层次扁平。重画后 53 个元素、三层递进:
- 两个进程做成带玻璃高光的卡片,各标自己的**虚拟地址**(0x7f2a… / 0x55c1…)
- 中间一句桥接:「不同虚拟地址 · 同一物理页」——这才是零拷贝的关键
- 物理页外框呼吸描边 + 极淡填充
- 段内标出 `header`(魔数·版本)与 `arena`,并加**字节刻度**,
把「一片区域」讲成「有结构的区域」
## ④ 图 5(长任务转投):填掉大片空白
原图下三分之一完全空着,且「两种出路」只写在文案里、图上没有任何体现。
- 主 agent 加**忙碌进度条**(一直爬不到头,呼应「占住很久」)
- 积压消息由 2 条改 3 条、宽度递减成「一摞」,并逐条被取走
- 右上空白填入**触发条件**,给的是源码实测默认值
(`defaultOffloadBusyAfter = 5min`、`defaultOffloadMinPending = 3`),
并注明「默认关闭,需部署方打开」
- 新增回执区:`① 简单 → 直接办完,发回原通道` /
`② 需主 agent → 回「忙碌中,请稍候」`,并用光点示意回执送达用户
## ⑤ 排版:中文之间夹空格
改文案时实测发现正文里有「实时 渲染」这类中文间空格 —— 源码换行被浏览器
渲染成一个空格。找出真实渲染有空格的位置并修掉(改用 innerText 判定,
textContent 会把 `<em>` 等块边界误判为空格,实测误报了 2 处)。
## 自己踩到并修掉的几个坑
- 「各自的虚拟地址」两句标签被竖向虚线穿过(压字)→ 改为一句居中桥接
- 图 3 标题靠左时被 `x=72` 的写入虚线穿过 → 改居中
- 玻璃高光整块铺满像蒙了层白纱 → 收到上半部、透明度 .3
- 巡行光点框看起来像页内多了一层框 → 去掉,改为外框呼吸
- 内层标题与外层桥接都在说「同一物理页」→ 内层改为描述段布局
## 验证
- 深浅双主题逐图截图核对(图 3、图 5 各两套)
- 动画:图 3 从 5 → 11 个动画元素、图 5 从 8 → 10;元素总数 19→53、17→45
- reduce 下新增的 CSS 动画(physBreathe、dv5-pile、忙碌条)全部停;
SMIL 仍靠 pauseAnimations,8/8 SVG 已暂停
- 回归:深浅主题 / JS 禁用 / reduce / 390·768·1440 三档,零横向溢出、无控制台错误
- 结构:CSS 括号平衡、HTML 无未闭合标签
|
2026-09-20 23:37:16 +08:00 |
|
|
|
5d395116d1
|
site: 修正 FAQ 三处与源码不符的描述
用户指出「常见问题部分描述不太符合事实」。逐条对着源码核了六条,
查出三处不准(第 4 条是实质性误导),另修掉一个由此暴露的滚动缺陷。
## ① 第 3 条:「L4 是内核保留的」漏了一半
源码 `interruptLevel(evt, privileged)` 有**两条**放行 L4 的路径:
`privileged=true`(内核)与 `isKernelLevelSource`(编译期内置插件)。
`scheduler.go:74` 的注释也写明「只给内核与编译期内置插件」。
原文只说「内核保留」,会把内置插件的能力说没。已补上。
(「外部插件声明 L4 会被夹到 L3」这句本身没错,`clampPluginLevel` 确实如此。)
## ② 第 4 条:自动转投默认是关闭的 —— 原文读起来像默认行为
`DefaultOffloadOptions()` 返回 `Enabled: false`,源码注明理由:
「默认关闭、由部署方显式打开,与『显式才是特权』同一条理由」。
原文说「内核拉起临时驻留子接手」,通篇没提这个前提,读者会以为开箱即用。
已补上「默认关闭」并给出默认阈值(实测 `defaultOffloadBusyAfter = 5min`、
`defaultOffloadMinPending = 3`,原文只说「忙超阈值」「积压够多」,没给数)。
## ③ 第 5 条:「媒体跟着记忆块走」不完整
`payloadHeld()` 的存在说明有**共享**一说:同一份字节可能被多个块持有,
此时**不删**。两个删除点(`medialoop.forgetPayloads` 与 `sdk/memory_impl.go`)
都各自做了 `stillHeld` / `payloadHeld` 检查,注释写明
「同一张图可能被多个块引用」。「跟着块走」会让人以为按块计数即可。
已改为「如果同一份字节仍被别的块共享,则不删」。
## 核对无误的四条(未改)
- 第 1 条:`现场保存/恢复`、`中断栈` 都是 `scheduler.go` 的原词;四级中断、
上下文预算、蒸馏与召回均在
- 第 2 条:三通道准确(`proc/shm.go` 顶注:`stdio JSON-RPC` 控制面 /
`shm + 偏移` 数据面 / `eventfd` 通知面「事件环 post-and-forget」);
「摘除工具、阶段与通道」对应 `UnregisterPluginTools` /
`UnregisterPluginStages` / `releasePluginChannels`;`onProcCrash` 注释即
「摘注册面、喂健康计数、排一次重启」
- 第 6 条:`go:embed adapters/*.lua` 真嵌入;9 个名字与 `internal/lua/adapters/`
下文件逐一对应(`server.lua` 是网关脚本,不计入)
## ④ 顺带修掉:FAQ 全部展开后滚不到底
改动后我照例量了末尾能否到底,发现**全部展开时卡住、页脚不可见**:
展开后 faq 712 + footer 367 = 1079 > 一屏 900,`mandatory` 又把滚动锁住。
这里走了两次弯路,都记进注释了:
- 先想只让 faq 退出吸附 → **死锁**:它下方没有吸附点,而 mandatory 只允许
停在吸附点,实测卡在 plugins 的吸附位(y=9455)不再前进
- 阈值先误用「视口高」→ 展开后 faq 高 712 < 892,判不出超限,类根本加不上
最终改为「展开超过『视口高 − 页脚高』时整页切到自由滚动」,
因为 faq 是最后一个吸附节,它一旦装不下,末尾就必须整体自由。
## 验证
- 折叠态:到底=true,页脚可见,faq 标题不被导航遮挡(h2 顶 210 > 67)
- 展开 1 条 / 展开全部 6 条:两种状态都到底=true、页脚可见
- 一滑一页仍生效(8/8 次停靠不同节,72px 偏移为导航高度)
- 深浅双主题 / JS 禁用 / reduce / 390·768·1440 三档:全绿,零横向溢出,无控制台错误
|
2026-09-20 22:49:56 +08:00 |
|
|
|
fc8f153e34
|
site: 修首屏滚不到底 + 交错行出入方向 + 让示意图真动起来
用户反馈三点:① 拉不到最下面 ② 文字与图片没有进出动画,单调
③ 图片仍是「框框住文本」,且希望有「外界光球打到插件上」。
## ① 滚不到底:真 bug,且是两个独立根因叠加
实测(滑到底后读 scrollY):停在 10283,上限 10669,**差 386px**,
页脚完全不可见。两个原因:
- **末尾两节合计超过一屏**。faq 高 635 + footer 367 = 1002px > 一屏 828px。
`scroll-snap-type: y mandatory` 只允许停在吸附点,于是浏览器被迫二选一:
要么 faq 标题对齐(则页脚滚不到),要么页脚可见(则标题被导航压住)。
实测两种坏法都出现过:h2 顶 = -57 / -38(导航底 67)。
修法:FAQ 六条改双列(省下约 190px),收尾段取消整屏高度,
合计压到 762px,两个要求即可同时满足。
- **只在 footer 单独设吸附点会形成死锁**。试过给 architecture 设
`scroll-snap-align: none`(它高 1180px),结果前一个吸附点把它自己吸回来、
后面又无吸附点接住,实测卡死在 y=5791,怎么滑都不动。
教训写进注释:高节退出吸附不是解法,「装得进一屏」才是。
## ② 交错行:方向与布局相反(结构性错误)
原文给每行写死 `slide-l`(文)与 `slide-r`(图)。但翻转行里图在**左边**,
却仍从右侧飞入 —— 图穿过文字进场,方向与布局相反。5 行里 3 行错。
修法不是逐行改正,而是**从布局推导方向**:`.alt:not(.flip)` 文左图右、
`.alt.flip` 图左文右,方向跟着 `.flip` 走。这样不可能再写错。
退出时不加 `.in`,transform 回到同侧 ——「从左进就从左出」是自动的。
## ③ 示意图:从静态线框变成有语义的动画
现状实测很糟:5 张图共 116 个元素,**只有 4 个在动**(各 1 条虚线),
图 2 完全静止 —— 用户说「框框住文本」是准确的。
按每张图的语义给它自己的动作(13 种 keyframes,47 个动画元素):
- **图 1 隔离**:外部光球从画面外飞入,四种颜色命中四个插件,
激起涟漪 + 插件闪一下;内核一列都没有。这正是「外面的一切都落在插件上」。
- **图 2 零依赖**:四项依赖被**逐条划掉**。用 `pathLength=1` + `stroke-dashoffset`
让线自己「划」过去,不是淡入。
- **图 3 共享内存**:写入/读回两个包沿链路跑,arena 上有光带自左向右扫过。
- **图 4 优先级**:金色包沿四条曲线**插队**到 L1–L4 之前;
四级强度条依次点亮;队列条逐条被取走(先到先处理)。
- **图 5 长任务转投**:积压消息被接走飞向分诊助手,收到时闪一下。
配套修掉三处我自己引入的缺陷:
- `.dv-pkdot` 原用 `pkPulse` 动 `r`,与行进包叠加会闪烁 → 改只动透明度
- `.dv-ripple:nth-of-type(2n)` 按 `<circle>` 计数,误配到数据包圆点,
四个涟漪全被染成金色 → 改显式逐色
- 涟漪原画在插件框**内部**,像框里画了个圆 → 移到框下层,从背后漾开
## ④ 顺带发现并修掉的真问题
- **CSS 的 `prefers-reduced-motion` 管不到 SMIL**:实测 reduce 下 6 个
数据包照跑。加 JS 调 `svg.pauseAnimations()`,实测 8/8 SVG 已暂停。
- **窄屏横向溢出 26px**:源于我这次加的入场位移(`translateX(42px)`)
在未入场时探出视口。修法是 `overflow-x: clip`(不用 hidden,避免新建滚动容器)
并窄屏收到 ±20px。实测 390/768/1440 三档溢出均为 0。
## 验证
- 滚动:到底=true,页脚可见,faq 标题不被遮挡(h2 顶=210 > 导航底 67)
- 方向:5 行全部「文在左侧就从左进」,翻转行正确反向
- 入场:逐节停留 13 节全部入场,0 未揭示;
滑到底后 23 个 opacity=0 的元素**全在视口上方**(退场生效),无一是「场内却不可见」
- 回归:深浅双主题、JS 禁用、reduce、390/768/1440 三档 全绿,无控制台错误
- 结构:HTML 解析无未闭合,CSS 括号平衡
|
2026-09-20 22:37:06 +08:00 |
|
|
|
3374e7dbd9
|
docs: 更正三处无实测支撑的性能断言
用户指出现有文档里的性能数字可疑。逐个实测后发现三类问题,都不加改原文地
标注更正(历史条目保留原文,仓内已有此惯例)。
## ① 「崩溃到恢复 <1s」——从未成立
写于 v1.0.0 发版说明。但**当时的退避代码就已是 1s**(查 v1.0.0 tag 的
`procRestartBackoff = time.Second`),首次重启就要等 1s。
实测(新增临时测试测量 scheduleProcRestart 延迟):
第 1 次崩溃 → 1s 第 2 次 → 2.001s 第 3 次 → 3.002s
顺带纠正我自己刚在站点写错的阈值:并非「崩 3 次停下」。实测第 **4** 次
才停(`procMaxRestarts=3`,判定为 `n > 3`),前 3 次都会重启。
## ② 「RPC 往返 p50 24.1µs」——量级对、数字不符
实测 `BenchmarkToolInvoke`:inline/small **30.4µs**、frame/small 51.5µs、
inline/large 767µs、frame/large 398µs。原文与实测同为几十微秒量级,
但具体值对不上,且未注明测的是哪种 payload。
## ③ 「CLIP 实测常驻 1.15GB」——采样点不对(6 处)
实测加载 chineseclip 两塔,RSS 会**自己降下来**:
加载前 0.00 GB
两塔加载后 1.59 GB ← 峰值
GC + 静置 0.89 GB ← 稳态(内核回收未用页)
1.15GB 落在两者之间,既不代表峰值也不代表稳态。线上稳态实测 0.39~0.58GB
(更长时间静置后更低)。同源问题:qwen3vl 的「常驻 9.4GB」实为**峰值**,
其视觉塔本就是按需加载(源码注释:每张图约 1.6GB,故按需)。
6 处全部改为「稳态 X(峰值 Y)」双值,消除口径歧义:README 中英、
docs/zh/multimodal-space.md、config/registry.go(2 处 + 1 处注释)、
providers/chineseclip/tokenizer.go。
## 验证
- `go build`(含 `-tags onnxruntime` 与不带)与 `go vet` 均通过
- 全仓 `grep 1.15GB` 已清零
- 测量用的临时测试文件已删除,无残留
|
2026-09-20 19:56:49 +08:00 |
|
|
|
ca6f4c510c
|
feat(site): priority 图补上「排队输入」这一类(用户指出的漏项)
## 漏项
文案写「输入走两条路」,但图上只有 L1–L4 —— **排队的完全没出现**。
源码 `scheduler.go` 开篇就写明是**两类别**:
- TaskInterrupt(InjectInterrupt*):带 L1..L4,可抢占
- TaskQueued(InjectText*/InjectInputSync*):**无级别**,可被任何中断打断
「级别只属于中断」是这套调度模型的关键一句,图里不表达就等于漏了一半。
## 改法
图改成左右两列,标题直接写清差异:
中断 · 带级别 排队 · 无级别
├ L4 内核独占 ├ 队列条(先到先处理)
├ L3 需及时处理 └ 任何中断都能插它前面
├ L2 消息类 │
└ L1 完全可等 │
└───────────┬──────────────┘
正在跑的任务
- 排队列用**虚线框 + 素色条**(不发光),与左侧的实线发光级别条刻意区分 ——
「无级别」这件事本身就该在视觉上体现
- 两路汇入「正在跑的任务」,并标注「同一时刻只一个」
## 顺带修的两处
- **浅色下排队条看不见**:原先复用 `gCard`,而浅色下 gCard 是白的,
白底白条等于没画。改用独立的 `--dv-queue-item` 令牌
- **横向虚线穿过队列条**:原想表达「处理方向」,结果画在了条内部。
改为右侧竖线 + 向下箭头
## 验证
- 双主题:控制台错误 0、横向溢出 0、图元零越界
- 一滑一页仍生效;JS 禁用 24/24 可见;reduce 下吸附停用
- 390/768/1440 溢出均 0
|
2026-09-20 10:39:53 +08:00 |
|
|
|
88923ed663
|
feat(site): 示意图升级为玻璃面板 + 修 priority 图两处错误
## 根源(用户指「图太平,没有高级 UI 效果」)
问题不在画得不够花,而在**手法本身就平**:纯 SVG rect + 平面文字浮在平坦
背景上,本质上就是工程草图。换基础:
- **玻璃面板**:每张图裹一层带内上高光、内下厚度、外分层投影的玻璃底 +
`backdrop-filter: blur(8px)`,图形才像「浮在界面上」而非浮在空背景上
- **顶部柔光 + 细颗粒**:径向渐变给受光面,`feTurbulence` 噪点抿掉矢量图的塑料感
- **渐变节点 + 投影**:所有节点从纯色填充改为竖向渐变面 + `feDropShadow`
- **悬停提亮**:`fLift` 滤镜(更远的阴影 + 青色泛光)
- **流动虚线**:连接线 `dashFlow` 动画,看起来「有东西在跑」
- 全部走主题令牌,深浅各自调参(浅色受光方向相反)
## 修 priority 图两处真错误
**① 文案与源码语义错位**(用户报的)
原文写「可以等的 / 不能等的,不能等的再排 L1–L4」——
但源码 `LevelBackground = L1` 的注释是「**完全可等**」。我把 L1 归进了「不能等的」。
实际是:**排队无级别(谁都能插它),中断才带 L1–L4**。已改。
**② L4 文字溢出框外 57px**
`dv-ts` 是 `text-anchor: middle`,但我按左对齐给了 x=46,长的那行
(「L4 内核独占 · 立即打断」)以 46 为中心向两边展开 → 左溢 57px。
新增 `.dv-tsl`(`text-anchor: start`)专供左对齐行。实测四行现均整齐落在框内。
## 顺带修
- 「抢占」标签贴边 99.8%,玻璃面板内边距会裁掉它 → 整条线内移
- 抢占方向原为「从 L4 顶部绕出去悬在半空」,既没连上目标也读反了 →
改为「从级别条右侧指向正在跑的任务」
- 图 4 内容仅占 viewBox 70%×71%(其余图 88–93%),显空 →
重画为「强度条背景 + 级别徽标 + 场景标注 + 抢占回边」,现 81%×90%
## 验证
- 双主题:5 面板、控制台错误 0、横向溢出 0
- **全部图元与文字零越界**(含 viewBox 内边界检测)
- 一滑一页仍生效;JS 禁用 24/24 可见;reduce 下 snap 与流动动画均停
- 390/768/1440 溢出均 0
|
2026-09-20 10:33:20 +08:00 |
|
|
|
631963fdc7
|
feat(site): 总起页改为「真正的 AgentOS 长这样」+ 修页脚状态栏不可点
## 总起页:从对比改为展示(用户要求)
前几版是对照表(「别人说 X → 我们要做到 Y」)。用户指出重点不是对比,
而是**把自己作为「真正的 AgentOS 样例」展示出来**。改为四张并列卡片,
每条给「机制 + 可测数字」,不做比较。
标题「真正的 AgentOS,长这样」;四题:隔离 / 调度 / 通信 / 资源 ——
操作系统躲不开的四道题,逐题给答案。
## 文案改了第三轮(用户指「读着太难受」)
按「短句、有节奏、不堆从句」重写四段正文。举一例:
旧:插件不是进程内的一个库,是内核 spawn 的独立进程。崩了就把它的
工具、钩子、通道一并摘掉,其余照跑。
新:插件不在内核里,是另一个进程。崩了就把它注册的东西一并摘掉,其余照跑。
## 修页脚「状态」栏不可点(用户报的 bug)
三行原为 `<span class="muted">` 死文本,点不动。改为链接:
- 最新发布:v1.3.x 线 → /releases
- main 在研:1.4.0 → blob/main/internal/meta/meta.go(版本号的实际来源)
- 许可:AGPL-3.0-only → blob/main/LICENSE
## 一处自查纠正
我一度在卡片里写「插件崩溃 → 恢复 <1s」,那是照抄 README 的旧说法。
查源码 `dynamic_proc.go` 发现退避实为 **1s / 2s / 3s**(`procRestartBackoff=1s`,
5 分钟内崩 3 次 `procMaxRestarts=3` 即停手等人),**<1s 不成立**。
改为如实写明退避序列与停手机上阈值。README 那句待另开一轮核实。
## 验证
- 双主题:4 卡片、控制台错误 0、横向溢出 0
- 一滑一页仍生效(6 次滑动偏差恒为 72px = scroll-padding-top)
- JS 禁用 24/24 可见;reduce 下 snap 自动关闭
- 390/768/1440 溢出均 0
- 页脚 9 个 gitcode 目标逐个对照 origin/main 的树:**全部存在**
(不只看 HTTP 200 —— gitcode 对错误路径也返回 200,此前踩过)
|
2026-09-20 10:11:02 +08:00 |
|
|
|
4078f3ac0a
|
feat(site): 重写首屏文案 + 交错图文布局 + 一滑一页
## 口号与文笔(用户指「不够响亮、部分文笔不好」)
Hero 改为「是…更是…」句式:
是记得住的管家 / 更是从不让你干等的搭档
## 五个设计决定:从卡片改为交错图文
原来 4 张卡片平铺。改为 5 行交错(文/图左右互换),每行配一张内联 SVG
示意图,纯 CSS + 主题令牌,深浅自适应,零外部依赖。
**换掉一条、新增一条**(用户指出「插件跑在独立进程」不算特色 —— MCP、LSP
都这么做,不是差异点):
| | 内容 | 依据 |
|---|---|---|
| ② | 部署,从未如此便捷 | 实测插件 `statically linked`、`not a dynamic executable` |
| ③ | 数据如水,随流,随改,随走 | 共享内存 + 相对偏移零拷贝;实测整段写入读回 3.5µs |
标题按用户给的句式写(②③ 原文照用),正文不给形容词、给可核验的做法与数字。
## 一滑一页
`scroll-snap-type: y mandatory` + 每节 `min-height: 100svh`。
为此把 features 的 5 条决定各拆成独立 section(原 2.74 屏塞 5 行,
mandatory 下会锁死底部),architecture 的流程图也单独成节。
现 13 节,实测连续 8 次滑动精确停在第 1..8 节,间距 828px = 一屏。
## 两处实测纠正(都是我先判断错、再被数据推翻)
1. **`proximity` 做不到「一滑一页」**:实测滑 500px 落点就是 500,离最近
节边界 395px,不触发吸附 —— 只是「有时粘一下」。改用 mandatory。
2. **我误报 architecture「底部锁死」**:按 `h > innerHeight` 判定,忽略了
溢出行仍可滚动。用真实 wheel 实测 13 节末元素全部可达(含该节 828 < 900)。
所以没有锁死,压缩 vertical rhythm 是顺带的,不是修复。
## 验证
- 深浅双主题:13 节 / 5 交错行 / 5 示意图、控制台错误 0、横向溢出 0
- 一滑一页:8 次滑动停在 8 个不同节,落点间距精确 828px
- 72px 落点偏移经查是 `scroll-padding-top`(导航高 67px),确保标题不被遮挡 —— 有意为之
- JS 禁用 24/24 可见;reduce 下 snap 自动关闭(`prefers-reduced-motion`)
- 390/768 无 snap(窄屏强制一屏反而难受);1024/1440 启用
- 真人式滚动(wheel 与 400px 步进两种)未揭示元素均为 0
注:本轮前期用了几个 Python 补丁脚本改 HTML,用户指出「不好」。后续改为
直接编辑以产出可审阅的 diff,脚本已删除。
|
2026-09-20 09:46:18 +08:00 |
|
|
|
0c9a3900b8
|
fix(release): .hmap 纳入发布产物白名单 + 路径解析
## 白名单(真问题)
`upload_assets.py` 的 ARTIFACT_SUFFIXES 只有 .tar.gz/.zip/.deb/.rpm/.pkg/_win64.exe,
**没有 .hmap** —— 即使插件包已经构建好放在 dist/ 下,上传时也会被静默跳过。
这正是「release 里一个插件包都没有」的直接原因之一。
补 `.hmap` 与 `SHA256SUMS.plugins`(插件包的汇总校验和,与内核包的 SHA256SUMS 分开,
避免混用)。实测 is_artifact() 现能正确识别两者、仍跳过 README.md。
## 测试路径解析
`HMAP_BUNDLE_DIR=dist/plugins` 这种相对仓根的写法原先会失败:测试的 cwd 是包目录
(internal/plugins/pluginmgr),相对路径解析到包内,报 "no such file or directory",
看起来像产物不存在。改为相对路径按仓根解析(向上找含 go.mod 的目录)。
实测三种调用都正确:相对路径、绝对路径、不设时 skip。
|
2026-09-20 09:07:33 +08:00 |
|
|
|
a35f2126a1
|
test(pluginmgr): 校验发布用插件包能被内核真实安装
配套 SDK 仓新增的 scripts/build_plugin_bundles.sh:**能构建出来 ≠ 内核装得上**,
这个测试用内核自己的 extractPackage 把产物真解一遍,验证三种包形态都落成规范入口。
覆盖的三种形态(都由真实产物验证过):
- 多平台 bundle:`plugin.bin.<os>.<arch>` → 按当前平台挑出并**重命名为 plugin.bin**
- 单平台包(qq 的 plg.json 是 bundle:false):只有 `plugin.bin`
- Lua 包(luademo):入口是 `main.lua`,不编译 Go
不设 HMAP_BUNDLE_DIR 时 skip(不作为常规 CI 的必跑项,避免依赖 hmapdev 工具链):
HMAP_BUNDLE_DIR=/path/to/plugins go test ./internal/plugins/pluginmgr/ \
-run TestBuildPluginBundlesInstallable -v
实测 21 个真实产物全部通过(含 Lua 与单平台两种非 bundle 形态)。
|
2026-09-20 09:02:42 +08:00 |
|
|
|
9b26db45bc
|
fix(site): 逐条对照源码修正描述(含两处真错误)
上一版有几处表述与源码不符。这轮把页面上每条可核验的说法都对着代码重新查一遍,
改掉 15 处,其中两处是**事实错误**而非措辞问题。
## 事实错误
**① L4 的归属说反了(FAQ)**
原文让读者「用更高级别的中断(如 L4:内核与内核级插件)」插队,暗示插件能用 L4。
源码 `scheduler.go` 的 `clampPluginLevel` 把 **>L3 一律夹到 L3**,注释也写明
「L4 由内核独占(panic、内核事件 selfip)」。照原文写插件会静默拿到 L3。
改为:插件可声明 L1–L3,L4 是内核保留的「立即打断」。
**② 驻留子的父侧动作列错**
组件表写「父可查看/收发/压缩/回收」。"收"不存在 —— 源码的动作集是
`list | create | send | inspect | compress | reclaim | destroy`,
子持有状态面由**父 pull**(resident.go 开篇注释:父持登记表,子持 inputch 处理表,
父 pull 不打断子)。改成「查看/发送/压缩/回收/销毁,子是父拉取而非推送」。
## 措辞不准确(12 处)
- **Context 层**「最近若干条受保护」→ 源码 `pCount := 10` **写死十条**;
「预训练词向量 → 余弦相似度,TF-IDF 回退」→ 实为优先稠密向量余弦、
未配置时退到稀疏词向量(TF-IDF / fastText);「自动下沉」→ 归档进 Document 层
- **PluginSDK「四通道」**→ 不是四个"通道",是三面接口(工具/钩子/事件)+ 输出通道声明
- **管道「7 个阶段钩子」**→ 会被读成都在管道内。实际分布是进管道前 1(on_input)、
轮次中 4、收尾 2(before/after_output),两处都标明
- **sanitizer** 只写了"清工具调用残留",漏了它更常做的是洗坏 UTF-8/U+FFFD/ANSI
(而这类字节会被模型复读),且不注册工具只挂钩子
- **rss**「推送通知」→ 实际是按间隔轮询 + 中断注入;补上"订阅时记历史条目,
所以订一个源不会把旧文章全推一遍"
- **memo** 补上可核验的机制:每 5 分钟检查未完成待办
- **ocr / bili** 补外部依赖(tesseract + chi_sim / yt-dlp)—— 不写清楚装完才发现缺
- **mc**「两阶段激活」原样照抄没解释;实为「想连着(意图)」与「确实连着(连接)」
两个状态分开,所以 bridge 被 kill -9 后能自动重登恢复会话
- **qq** 一句话太单薄,补 20 工具 + 权限模型要点(身份绑帧、取交集、前缀拒绝)
- **a2a / music / weather** 分别补:两个方向与端点、只读无副作用、NoMemory 取舍
## 顺带修掉两个我上一轮引入的 HTML 缺陷
用行替换时失手:Context 卡丢了一个 `</p>`、mc 卡多了一个 `</span>`。
这次写了栈式配对检查才发现(简单的计数对比看不出来)。
## 验证
- 栈式标签配对:p/span/div/button/code/section/h2/h3/details/ul/ol/a/li **全部平衡**
(修复前 p 差 1、span 差 -1)
- 事实终检 10/10:内置插件 16(all.go 导入数)、LLM 适配器 9 且**逐个名字对上**、
Go 行 109241→109k、go.mod 1.25.0、L4 归属、resident 动作、Stage 分布、备案号
- 浏览器回归:深浅错误 0、JS 禁用 47/47 可见、reduce 动效停、390/768/1440 溢出 0、
滚到底未揭示元素 0
- mc 工具数:本写「12 个动作工具」,实测 `tp+"act"` 去重后 activate/deactivate/status
之外是 **11** 个,已改
|
2026-09-20 08:48:15 +08:00 |
|
|
|
47052cb115
|
fix(site): 更正媒体机制描述 + 页脚补备案号
## 描述性错误(用户指出)
1. **「引用计数 GC」已不存在**。「有引用绝不删」「媒体靠引用计数 GC」
两处都在讲一个已废弃的账本 —— 实测源码里已无 media_refs/ref_count,
`internal/memory/media/media.go` 明确写「这不是 GC,也不看引用计数」。
现行规则是「删除持有它的记忆块即删内容」,与文本块同一套
(medialoop.go 的 payloadHeld 只在确认无块共享时才删字节)。
2. **「描述才是持久语义」整张卡已过时**。旧实现靠视觉模型生成的描述当索引;
现已弃用 —— `mediaref.go` 写明标签「不再包含任何生成的描述文本」,
图片改按统一空间向量检索,`graphmedia.go` 还带一个把旧描述式实体
迁移成原生记忆块的迁移函数。卡片改为「图片靠自己的向量被检索」。
## 备案号
页脚补 豫ICP备2024074105号-1 与 豫公网安备41070202001579号,
链接到 beian.miit.gov.cn / beian.mps.gov.cn。取值来源是现网
门户配置(/root/portal/dashy/conf.yml),未凭记忆编造。
## 验证
深浅双主题下渲染正确、两条链接 href 实测无误、无 JS 错误。
|
2026-09-20 00:00:23 +08:00 |
|
|
|
09298886a2
|
feat(site): 「一条消息进来之后」改为真正的流程图
原来是 ASCII <pre> 图。它有三个问题:
1. **画不出循环**。真实执行序是 7 步状态机,其中工具循环要回到开头
再来一轮 —— ASCII 只能表达上下关系,这一点只能靠文字暗示。
2. **7 个钩子排成一行是错的**。on_input 在进管道前跑,
before/after_output 在**全部轮次结束后**才跑一次;把它们与管道内的
钩子并列,读起来像一条直线。
3. 漏掉了 post_action 之后才发生的工具调用,以及"上下文裁剪 + 相关记忆召回"
这一步(在 after_toolcall 里)。
## 现在的结构
五层节点 + 分支 + 循环体:
外部输入 → 输入调度器(三条分支:入队列 / 抢占 / 转投)
→ 处理管道 →〔① pre_action ② LLM ③ post_action ④ before_toolcall
⑤ 执行工具 ⑥ after_toolcall〕↻ 循环 → 三层记忆 → 输出通道
事实全部对照源码核过(不是照抄旧图):
- 7 个 Stage 常量取自 SDK `third_party/homeagent-sdk/sdk/plugin.go`
- 顺序取自内核 `internal/agent/core/task.go` 的 Step 状态机
(StepPrepare→StepLLM→StepToolBegin→StepToolExec→StepToolAfter→StepTurnEnd)
- 脚本末尾补一句说明 on_input / before_output / after_output 的时机
## 视觉
节点用色与三层记忆的三色一致(蓝=Context/青=管道/金=Graph,紫=转投);
连接线上的光点错峰下行,序号依次点亮。全部是内联 SVG-free 的纯 CSS,
无外部依赖。
## 关键取舍
- **删掉了贯穿全图的中轴线**:节点背景是半透明令牌,轴线会直接透出来,
实测在「处理管道」里穿过整个编号列表,看着像画错了。连接线本身就是主轴。
- **循环回边改为内嵌徽标**:先做成从框底绕出的弧线,但它会压到下一条
连接线 —— 同样像画错。
- **给连接线补了静态箭头**:动画关掉时(reduce)方向也要看得出来。
## 验证(独立 headless 实跑)
深浅双主题控制台错误 0、页面溢出 0;图内溢出 0(390/620/900);
**JS 禁用下 14 个节点全部可见、7 个钩子名齐全**(流程图是内容不是装饰);
reduce 下光点/图标动画确为 none 而静态箭头仍在(宽 7px/2px);
**7 个钩子名与 SDK 常量逐一比对通过**,防止文案漂移;
滚动到底未揭示元素 0。
|
2026-09-19 22:38:06 +08:00 |
|
|
|
db8534e315
|
feat(site): 文案精简 + 插件可点击 + 版面精致化
## 文案(净减约 25%,信息量不变)
删的是解释性赘语与重复限定,不是信息:
- 「内核不直接读写任何外部世界…于是「内核有多可信」与…」→「内核不碰任何外部世界…可以分开评估」
- 「大多数框架先写功能再补边界。HomeAgent 反过来:先把边界和调度定死,再往上加能力。」
→「先定边界与调度,再加能力。」
- FAQ 六条逐条收紧;副标题从句子改回短语
- AI 声明与许可段去重复(两段都在讲同一件事)
## 插件徽章从装饰变为可交互(这是用户报的「无法点击」)
每个徽章现在是真按钮:点开显示该插件的**版本 + 用途**(取自各 plugin.json,
共 20 个),可多开、可收起,末尾「展开全部 20 个」一次全开。
键盘可达(Enter/Space),选中态用 aria-pressed 表达。
初版是纯 <span>,带 hover 效果却不可点 —— 看起来能点但点了没反应。
## 修正一处事实错误
统计卡原写「**36 外部插件**」。实测 36 是**加载总数**(16 内置 + 20 外部);
外部插件实为 20 个。同时:
- 「110k Go 代码行」→ 109k(实测 109,241)
- 「10 LLM 协议适配器」→ 9(server.lua 是 zen 网关脚本,不是厂商适配器)
每张卡补一行小字说明口径,避免再被误读。
## 版面
section 统一 4.5rem 节奏、卡片内边距与标题间距收敛、组件表代码列定宽对齐、
统计卡加口径小字、三层记忆卡收紧。插件选中态从实心青底(20 个齐亮像一堵墙)
改为淡青底 + 主色描边,并给 color-mix 加了 rgba 回退。
## 验证(独立 headless 实跑,全部通过)
深浅双主题 console 错误 0;JS 禁用 47/47 可见且插件卡默认全隐(0 张);
reduce 下粒子停、全可见;主题切换刷新保持、首绘无闪白;
390/768/1440 横向溢出均 0;移动端点插件正常展开;
插件交互逐项验过:单击展开 → 再点收起 → 多开 3 张 → 全展开 20 张 → 收起。
★ 一度报「19 个元素未揭示」,查证是我测试脚本没滚动所致 ——
逐步滚到底后实测 0 个未揭示,非真回归。
另把取数命令写进 site/README.md,并注明「36 = 加载总数」这个易错点。
|
2026-09-19 22:24:03 +08:00 |
|
|
|
fab27a1194
|
feat(site): 深浅双主题 + 动效层,并按用户要求移除立绘
## 深浅双主题
跟随系统偏好,导航栏按钮可手动切换(存 localStorage)。首绘前在 <head> 里
定好 data-theme,无闪白(实测 reload 首绘即正确背景色)。
语义色全部令牌化,:root[data-theme="light"] 只覆盖取值;品牌三色两主题共用
(对应三层记忆,换主题不该换语义)。
## 动效层(1 → 11 个 keyframes)
极光漂移 + 细网格背景、粒子网络(近邻连线,密度按面积自适应、上限 72)、
三色滚动进度条、标题渐变流动、分块上错落入场、卡片聚光 + 3D 微倾、
三层记忆色条自上而下灌注、架构图流光带 + 节点脉冲、数字滚动到位、
分节标题下划线展开。
三条硬约束(都吃过亏):
1. **内容默认可读** —— 初始隐藏只在 .js-fx 下生效,而 .js-fx 仅当 JS 真跑起来才加。
实测 JS 禁用时 30/30 元素可见(旧版把 opacity:0 写默认样式里 → 26/30 永久不可见)。
2. **尊重 prefers-reduced-motion** —— 不启粒子、不画进度条、元素直接可见。
3. **装饰不得产生滚动条** —— canvas 改用 documentElement.clientWidth
(window.innerWidth 含滚动条,实测多出 15px 撑出横向滚动),body 加 overflow-x: clip 兜底。
## 移除立绘(用户要求)
删掉 HTML/CSS/JS/资源/令牌全部痕迹,Hero 改单栏。README 记下为什么最终不放图:
原图是不透明 WebP(mode=RGB 实测),白底与角色白裙子同色,flood-fill 会渗进轮廓
让约 42% 身体透明 —— 这类素材要么出透明图,要么就别放。
## 顺带修掉 3 个真 bug(都是主题化后暴露/复核出来的)
1. **代码块换行全丢** —— .code 缺 white-space: pre,实测整段命令挤成一行。
(这个 bug 在我这次改动之前就存在)
2. **浅色下导航看不清** —— header 背景硬编码 rgba(11,16,32,.78),改用 --nav-bg。
3. **浅色下立绘处有灰块** —— .mascot::after 硬编码深色,改用 --mascot-fade
(该规则已随立绘一并删除)。
另把 .badge / .btn-ghost:hover / 按钮光泽里 3 处 rgba(255,255,255,…) 令牌化为
--hover / --sheen,否则浅色下是白压白。
## 验证(共享 Chromium 实跑)
深/浅首屏 + 记忆段 + 架构段截图逐张看过;console 错误 0;坏图 0;
JS 禁用 30/30 可见;reduce 全可见且粒子/进度条已停;主题切换 → 刷新后保持;
390/768/1440 三档横向溢出均为 0;CSS 花括号平衡、8 个 keyframes 无孤儿。
|
2026-09-19 21:49:59 +08:00 |
|
|
|
923d5d595f
|
feat(site): 新增产品官网落地页(单文件 · 零构建 · 零外部依赖)
用户要求写一个官网介绍页面。做成纯静态单文件,与仓库 WebUI 的既有做法一致
(原生 HTML/CSS/JS,无打包步骤)。
## 内容(每一条都对着源码/运行实例核实过)
- Hero:一句话定位(常驻型个人 Agent 框架)+ 看板娘立绘
- 四个设计决定:内核零 IO / 插件独立进程 / 输入有级别 / 忙时有人顶班
- 三层记忆:Context → Document → Graph,含媒体一等节点、描述即语义记忆、统一多模态空间
- 架构:一条消息进来之后的完整路径图 + 核心组件表
- 数字(**实测值,非估算**):36 外部插件 / 340 工具 / 110k Go 行 / 10 LLM 适配器
- 上手命令、插件徽章墙、6 条 FAQ、页脚(文档/深入/项目/状态)
## 两个设计决定,都有理由
1. **配色取自品牌指南**:蓝/青/金正好对应三层记忆,故三层记忆那节直接用三色做色条。
2. **立绘按「有意的圆角卡面」呈现,不抠图**:原图是白底 + 蓝紫渐变外框,
而白底与角色的白裙子同色 —— 连通域分析显示 flood-fill 会让 42% 的身体变透明
(围裙、发丝高光被吃掉)。改为圆角 + 发光边框 + 底部渐隐,方形图与深色页自然衔接。
## 修掉一个真实的可访问性缺陷
初版把 `opacity:0` 写在**默认样式**里、由 IntersectionObserver 加 `.in` 揭示。
实测:30 个 .reveal 元素里 26 个停在不可见 —— **JS 被禁用或报错时整页永久空白**。
改为渐进增强:内容默认可见,仅当 JS 真跑起来才加 `.js-reveal` 接管动画。
复测两种场景均 30/30 可见(正常滚动 + 禁用 JS)。
## 验证(用共享 Chromium 实跑,不只是看代码)
- 控制台错误 0;两张图均加载(logo 400x400、立绘 1024x1024)
- 移动端 390px:无横向溢出,导航折叠,立绘置顶
- FAQ 手风琴展开正常(open=true)
- a11y:图片 alt 齐全、单一 h1、lang=zh-CN、无空文本链接
- 标签配对全 OK;34.6 KB;**无任何外部依赖**(无 CDN/字体/JS 库)
|
2026-09-19 21:15:00 +08:00 |
|
|
|
71faf8d9ad
|
docs(readme): 按源码修正 README 的过时事实(中英同步)
上一轮只补了 v1.3.x 变更日志,没系统核对全文。本次逐条对照源码,修掉 5 处硬错误:
1. **消息时序图漏掉输入调度器**(最严重):还画着 `IO->>EV: inputCh` 直连
eventLoop,而当前输入必须先进调度器。补 participant 与调度阶段
(两类别+四级中断、同级不排队/更高级抢占、转投分诊助手)。
2. **图里的 `drainInterrupts` 已不存在**:实测该函数在源码中查无此项,
改为「安全点:中断求值/让位」(真实机制见 scheduler.go)。
3. **内置插件数 11 → 18**:漏列 ai_image / data / localuse / multimodal /
remotedevice / skillmgr(实测 `ls internal/plugins/` = 18)。
4. **Lua 适配器 8 → 10**:漏列 ollama / server(实测 = 10)。
5. **`agent/api/` 描述错误**:它只有 provider.go,不含 Lua 适配器
(适配器在 internal/lua/adapters/);改为如实的「provider.go 调 vm」。
另修一处**自相矛盾**:构建章节写「依赖 Linux/Windows」,而下载章节说
homed 已放弃 Windows 原生(`package-windows.sh` 明确「不往 Windows 装 homed」,
只建 waiter.exe + 引导 WSL2)。改为「依赖 Linux」并说明 Windows/macOS 的真实边界。
并给「设计要点」补上两个当前核心机制(此前只有域分离与三层记忆):
输入调度(两类别+四级中断)与驻留子/分诊助手。
验证:全仓文档断链 0;6 个 mermaid 图块配对全 OK;上述数字逐条实测复核。
|
2026-09-19 20:59:42 +08:00 |
|
|
|
c0274b71d5
|
docs: 删除迁移期临时文档,现行内容搬进正式文档
用户指出迁移评估那批是**过程性临时文档**,迁移已完成就该退场。
## 删除(38 个文件)
- docs/zh/架构迁移评估.md(1621 行)—— 评估稿。开头的「❗现网正在发生的问题」
(output_send 永远成功 / cgo 超时泄漏 26 次 / stage 污染)**全部已修复**,
留着是误导性告警。其 §三「目标架构」已被 ARCHITECTURE.md 完整覆盖
(且后者更细,含子进程生命周期管理)。
- docs/zh/plugin-interface-matrix.md(428 行)—— 迁移基线矩阵。
- docs/zh/experiments/(36 文件)—— 18 项可行性实验,验证的是"该不该迁移",
迁移早已完成;实测无任何构建/测试依赖它。
## 现行内容先搬走(不能随临时文档一起丢)
- plugin-interface-matrix §九「接口扩展规则」→ 搬进 docs/git-branching.md 新增 §八
(只增不减/签名不改、新增必须"插件调用内核实现"方向、hmapdev 模板必须同步接线
否则全体插件编译失败、"接口纯追加"≠"无需重编"、合回 main 的同步清单)。
- git-branching §六 原写「接口冻结是合回门禁」—— 冻结是**迁移期**约束,v1.1.x 起
已到期,改为标注失效并指向 §八。
## 引用清理
8 处引用全部改指现行文档:plan.md ×3、两篇设计文档各 ×1、
4 处源码注释(proc/shm.go、proc/process.go、dynamic_proc.go、entry_dispatch_test.go、
proc/bench_test.go)。仅 third_party(SDK 独立仓)保留 1 处,不动。
## 验证
- `go build ./...` 通过;`go test ./internal/plugin/...` 两个包全绿
- 本项目文档**断链 0**(另 2 处断链在 oh_modules 第三方依赖内)
|
2026-09-19 19:22:48 +08:00 |
|
|
|
7213edd181
|
docs: 全面按当前源码更新文档 + 删除已过时文档
## 删除(内容已落地/已被替换,保留只会误导)
- demo.md ................... failback 与 recoverydiag 均已实现,0 引用
- docs/defect-qq-output-send-loop.md .. 已修复(本身也标了「已修复」),0 引用
- docs/embedding-comparison.md ....... 一次性选型报告,仅被 agent 产物引用
- docs/zh/plan.md ............ 描述的旧 nav 布局已重写、死配置已清,全部完成
- docs/zh/plugin-migration-plan.md ... 迁移已上生产,纯过程稿(Part 0~6 全完成)
## 更新(按当前源码核对)
- assets/docs/{zh,en}/ARCHITECTURE.md(README 指向的用户文档,最重要):
把只讲 cancel/intercept 的旧「中断机制」章节重写为「输入调度器与中断机制」——
补上两类别 + 四级中断(L1~L4,默认 L1、外部插件 L4 夹到 L3)+ 抢占/挂起/中断栈
+ 饥饿防护(PreemptCount 提升,封顶 L4)+ 抢占冷却(2s)+ 停止语义(cancelBudget)
+ 驻留子/分诊助手/残余任务;新增「上下文预算」章节(窗口 ≠ 工作面,600K 封顶,
预算是上限非填充目标)。中英章节数现已对齐(各 13 节)。
- assets/docs/{zh,en}/PLUGIN_DEV.md:插件示例表补 6 个缺失项
(acp/deepsearch/plugindev/recoverydiag/vanblog/vikunja);qq 工具数 17 → 20(实测)。
- README.md / README_EN.md:补 v1.3.x 线(此前只到 v1.2.0,而 1.3.x 已发布 12 个 patch)——
驻留式子 agent、输出通道寻址、输入调度器、轻量内核 profile、积压及时反馈。
- plan.md:开头两个「⚠️ 紧急/正在持续污染」是过期告警(实测 残留 = 0),
改为「已解决」并加文档定位说明;§13 仍是活跃路线图故保留。
- docs/zh/plugin-interface-matrix.md + 两处源码注释:清理指向已删文档的断链。
全仓 md 断链检查:仅剩 1 处,位于 third_party 的 oh_modules(第三方依赖,非本项目)。
|
2026-09-19 19:14:55 +08:00 |
|
|
|
8acd3ce1a8
|
fix(offload): 内核说明不能被再转投(自我循环)+ offload_owned 未接线
★ 线上实测两个缺陷:
1. **自我循环**:转投会在队列留一条 [系统] 说明(source=kernel),
而转投条件把这条说明也算进「积压够了」⇒ 每次转投都产生下一轮要转投的东西。
实测 5 秒内连续触发两次,分诊助手不断收到「N 条积压已转投」这类噪音。
修法:takeQueuedInputs 排除 isKernelNotice(source=kernel)。
2. **offload_owned 永远为 false**:我加了 ResidentInfo 字段、加了状态面映射,
却漏了在 rc.info() 里赋值 ⇒ 线上转投子明明存在,读出来是 null。
这类「加了字段但没接线」不会报错,只会让父的判断悄悄失效
(父据此决定回收策略,读到 false 就会把临时助手当成正式子)。
测试 +3:说明不转投 / 循环必须终止 / offload_owned 会被上报。
前两条已实测「禁用守卫会失败、恢复后通过」,是真回归测试。
|
2026-09-19 17:47:44 +08:00 |
|