|
|
9a00c02e59
|
test(proc): 修 grandchild 测试的三个设计缺陷(不是生产代码问题)
## 定位结论
`TestKillReturnsEvenWhenGrandchildSurvives` 曾在 `go test ./...`(600s 超时)
与 `make test`(20.4s FAIL)里失败,但**单跑 0.24s 通过**、连跑 3 次全绿
⇒ 「单跑绿、合跑红」。查下来是**三个测试设计缺陷**,
生产代码(`process.go`)没问题。
### 缺陷 1:名字说 Survives,实际测的是「被杀」
| 测试 | 源 | 孙进程 | kill(-pgid) 能杀吗 |
| --- | --- | --- | --- |
| …EvenWhenGrandchildSurvives | grandchildPluginSource | sleep 400,**不**设 Setpgid | **能** |
| …WhenGrandchildEscapesProcessGroup | escapingGrandchildSource | sleep 401 + Setsid | **不能** |
`grandchildPluginSource` 自己的注释写着「孙进程**不**设 Setpgid:它要留在
插件的进程组里」⇒ 第一个测试里孙进程不会 Survive。容易让人误以为
「脱组场景已被覆盖」,而它覆盖的是另一个场景。
**已改名** `…WhenGrandchildDiesWithProcessGroup`。
### 缺陷 2:判据数的是全系统进程
两个计数器扫 `/proc` 找 `"sleep 400"` / `"sleep 401"` 字符串,
**不区分父子关系** ⇒ 同机任何命中同样 cmdline 的进程/容器都串味。
原注释记过一次前车之鉴(「我第一版就踩了:明明单跑通过,合跑却红」),
但当时只加了 base 快照,**没解决全局匹配这个根因** —— base 救不了
「别的测试中途拉起 sleep 400」。
**已修**:新增 `procPPid()`,两个计数器都限定 PPid 属于本测试的插件。
顺带补 `e.Name()` 的 `Atoi` 校验(原来会把 /proc/self、/proc/net 也读一遍)。
### 缺陷 3:defer 清理「拿不到 pid 就整个跳过」
`if pid := pluginPid(p); pid > 0 { Kill }` 在 pid 取不到时静默跳过
⇒ 残留 sleep 400 污染后续测试 ⇒ 变成下一个测试的假失败。
**已修**:新增 `cleanupSleepMarkers(marker)`,按唯一 cmdline 标记兜底清理。
## ★ 我被推翻的一个假设
我一度认定根因是 `waitLoop` 里 `p.cmd.Wait()` **先阻塞**、拆管道在**之后**
(`process.go:359-367`)—— Go 的 `exec` 里 `Wait()` 会等 copy goroutine,
而那些要等所有管道写端关闭,孙进程持有着 ⇒ 死锁。
**实测推翻了它**:把拆管道提到 `Wait` 之前,那个测试 **5 次全 FAIL**
(改前只是偶发)。真正的根因是上面三个测试设计问题;「Wait 阻塞」只是
**被孙进程持管道放大**的效应。
⇒ 改生产代码不但没修好,还把偶发变成必现。**先证明因果再动手。**
## 判据:grandchild_design_test.go(4 条)
★ 它是**查源码文本**的,我一改源文件(改名/加 ppid 限定/加兜底清理),
锚点就全过期 ⇒ 三条判据一起红。
⇒ 判据自己被重构打断时,要改的是**判据的锚点**(认新旧两种形态),
不是回退修复。最后把判据①从「解函数体比对 spawn 参数」简化为
「只问名字是否还说 Survives」—— 少耦合一层,少失效一处。
变异测试三个都抓到:改名回 Survives / 抽掉 ppid 限定 / 去掉兜底清理。
## 门禁
- 全量 `go test ./...`:**43 包 ok、0 FAIL**
- `internal/plugin/proc` 连跑 **5 次全绿**(原来会红的地方)
- `go test -race ./internal/plugin/proc/`:ok
- 无 sleep 400/401 残留
## 顺带
`a5f6126` 之后 README 的 biome 格式化不再 churn:仓库**没有** biome 配置,
格式来自流水线默认 ⇒ 每次提交后它都会把工作区改脏。我这次会话里反复
`git checkout --` 把它丢掉,那是和流水线对抗。**提交后 biome 再跑就是
no-op**,问题根除。
|
2026-09-28 10:00:10 +08:00 |
|
|
|
79c15f0452
|
test(webui): 压测补齐另外 3 个插件的路由 —— 我第一版漏了两个
第一版只压了 webui 的 `/api/v1/*`。核实后发现 **8080 上注册 HTTP 路由的
插件有 4 个,监听端口还不止 8080**(`ss -ltnp` 实测):
| 端口 | 插件 | 路由 | 认证 |
| --- | --- | --- | --- |
| 127.0.0.1:8080 | webui | /api/v1/* | config_webui.api_key |
| | remotedevice | /api/v1/device/* | config_remotedevice.ws_token |
| | kbtree 子路径 | /api/v1/knowledge/tree/{categories,counts} | config_webui.api_key |
| 127.0.0.1:9876 | **pluginmgr** | /plugins(**无** /api/v1) | **无需认证** |
| 127.0.0.1:9892 | **kbtree** | /categories /counts(**无**前缀) | config_kbtree.token |
| 127.0.0.1:9890 | remotedevice | 设备 WS 网关(非 REST) | 不压 |
⇒ 「打 /api/v1/*」这个假设只对 8080 上的 webui 成立:`:8080/plugins`
实测 **404**。三套认证各不相同,脚本现在按分组取对应 token。
## 端点 18 → 24
新增:`/api/v1/device/online`(remotedevice)、`/plugins`(pluginmgr)、
`/categories` `/counts`(kbtree 根路由)、以及 webui 前缀内的
`/api/v1/knowledge/tree/{categories,counts}`。
收 `/api/v1/device/online` 之前逐行读过实现:仅 `GET` +
`registry.OnlineList()`,纯读。**不收** `/device/push`(向真实设备下发)、
`/device/ws`(长连接)、`/device/{id}`(语义未核实)。
## 实测(24 端点 × 3 档,2880 请求零失败)
| scale | 请求 | 吞吐 | p50 | p95 | p99 | max | 成功 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 1 | 480 | 953 req/s | 4.0ms | 23.0ms | 34.7ms | 40.6ms | 480/480 |
| 2 | 960 | 1310 req/s | 7.9ms | 32.4ms | 48.4ms | 72.3ms | 960/960 |
| 3 | 1440 | 1422 req/s | 12.0ms | 42.7ms | 60.5ms | 88.7ms | 1440/1440 |
压测期间服务端 `active`、0 个 5xx。
**p99 随并发单调上升**(34.7 → 48.4 → 60.5ms),符合排队预期。
上一轮曾出现 scale=2 的 p99 反常地高于 scale=3,当时机器上另一个 agent 的
chromium 占 766% CPU ⇒ 环境噪声,已明确不作为性能特征。
## 又踩了一次前缀的坑(两种方向都踩了)
1. 「表里存路径后半段 + 代码按 group 补前缀」⇒ remotedevice 拼成
`/api/v1/api/v1/device/online` ⇒ 落到 webui 兜底路由、
返回 **200 + 登录页 HTML**(靠 `looks_like_login_page()` 抓到)。
2. 反过来「表里已含前缀 + 代码仍补」⇒ webui 组全 404。
⇒ 最终统一成**表里写完整路径、代码不补**。两次都是靠 `--probe` 抓到的,
这正是它存在的理由。
## 顺带修掉我写错的一处
`contextlib.suppress(sqlite3.connect)` —— `connect` 是函数不是异常类,
`suppress` 会抛 `TypeError`。改回显式 `try/except sqlite3.Error`,
并把 `config.db` 的打开方式保持 `mode=ro`(压测不碰生产库写路径)。
## 文档里我自己写错又改正的一处
§7 表格里 scale=1 一行先写成 1680/2013/5.4ms,核对 JSON 后实为
**480/953/4.0ms**(24 端点 × 20 轮)。已订正,并加了提交前的
JSON 逐项比对,三档现已全部一致。
|
2026-09-28 09:33:22 +08:00 |
|
|
|
aed8f1f808
|
test(webui): WebAPI 只读端点压测 + 实测 2160 请求零失败
打的是**生产实例** 127.0.0.1:8080。
## 为什么只压只读端点
压测绝不能改状态。18 个端点**逐个探测确认**是 GET + 只读:
小(/status…/proxy/services,104B–1.8KB)、中(/plugins…/knowledge,
4.4KB–53KB)、大(/kernel 157KB、/memory/graph 338KB、/knowledge/tree 425KB)。
**排除**:/chat(真调 LLM)、/chat/interrupt(中断在跑的任务)、
/settings/* 与 /plugins/*(改配置)、/login /logout(改会话)、
knowledge/memory 写接口、/device/*(控制真实设备)。
## ★ 两条判据(都不是"看有没有报错")
### 1. 必须先 `--probe`:webui 无认证时返回 200 + 登录页 HTML
含 `THEME_PLACEHOLDER`,**状态码是 200**。只看 `http_code` 会把登录页
当成健康响应 ⇒「全部 200」是假的。脚本因此额外校验响应体。
### 2. 端点表漏了 `/api/v1` 前缀 ⇒ 18 个端点全 404
第一版把端点存成路径后半段(`"/status"`),拼出
`http://127.0.0.1:8080/status`,而真实路径是 `/api/v1/status` ⇒
**全部 404**,同一时刻 curl `/api/v1/status` 却是 200。
⇒ **压测脚本必须先 probe 再压。** 不 probe 的话那一跑的结论会是
「webui 全挂」,完全错误。这条已写进手册。
## 实测(2026-09-28)
| scale | 请求 | 吞吐 | p50 | p95 | p99 | max | 成功 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 1 | 360 | 779 req/s | 6.7ms | 26.8ms | 45.5ms | 51.6ms | 360/360 |
| 2 | 720 | 788 req/s | 12.2ms | 52.6ms | 163.0ms | 213.5ms | 720/720 |
| 3 | 1080 | 1038 req/s | 16.4ms | 60.4ms | 79.7ms | 123.0ms | 1080/1080 |
**2160 请求零失败**;压测期间服务端 `active`、0 个 5xx、0 个 webui 错误,
QQ/agent 链路未受影响,homed CPU 仅 1.2%。
### 重响应不是瓶颈(并发 1 → 16)
| 端点 | 大小 | 并发 1 | 并发 16 | 吞吐 |
| --- | --- | --- | --- | --- |
| /status | 0.2KB | 104 req/s | **1781 req/s** | 320KB/s |
| /kernel | 153KB | 126 req/s | 375 req/s | 19 → **57 MB/s** |
| /memory/graph | 330KB | 70 req/s | 410 req/s | 23 → **135 MB/s** |
| /knowledge/tree | 415KB | 79 req/s | 717 req/s | 33 → **297 MB/s** |
大 JSON 端点的 p50 **不随并发上升**(/knowledge/tree 12.3ms → 8.9ms,
排队更充分、效率更高)⇒ 瓶颈不在 JSON 序列化。
## ⚠ 一处未下结论的观察
scale=2 的 p99(163ms)反而**高于** scale=3(79.7ms)。看着反常,但当时
机器上另一个 agent 的 chromium 占 **766% CPU**(另有 cjpm/cjc 在编译)
⇒ 是**环境噪声**。
**未在可比条件下重测就不下结论** —— 不拿这一组当性能特征。要判定需先
固定负载条件再跑。这条也写进手册。
## 顺带
ruff 报的三条 blocker 都修了:`pct()` 空样本不再抛错(调用方直接拿去做
f-string 格式化)、写 JSON 失败明确报错而非静默丢报告、错误体读取用
`contextlib.suppress` 免得二次异常盖掉真正的状态码。
|
2026-09-28 09:22:16 +08:00 |
|
|
|
56023e31c4
|
docs(runbook): 记「判断线上跑哪次构建」的正确判据(我今天差点白部署)
## 线上其实已经是修好的版本
去部署 `d084137`(webui 总览 5 个 KPI 空)前核实,发现:
/api/v1/status → {"commit":"d084137", "startedAt":"2026-09-27T23:23:39"}
23:23 那次部署**不是我做的**(我只在 21:50 部署过),已经包含
`d084137`。`/api/v1/kernel` 里 5 个 KPI 依赖的字段也全部有值
(`plugins` 39 个、`llm`/`memory`/`documents`/`runtime`/`onnx` 非空)。
⇒ **不需要部署。**
## ★ 为什么 `strings` 不能用来判断
webui 等插件的静态资源是 `//go:embed` **编译进二进制**的
(`internal/plugins/webui/handler.go:27`),内容取决于**构建时**磁盘上的
文件。⇒ 已提交但未部署的改动,线上二进制的 `strings` 里**也可能**
出现新代码片段。
我据此差点白重启一次生产。同一天还撞上**字节数完全相同**的巧合
(86811464),更掩盖了这点 —— 大小相同更让人以为"没变化,不用管"。
## 正确的判据顺序
1. `/api/v1/status` 的 `commit` —— 线上在跑什么
2. `git log <commit>..HEAD` —— 差哪些提交
3. 那些提交里**有无运行时改动**(`internal/`、`cmd/`)—— 只有它需要部署
4. 文档 / 判据 / 部署脚本类提交**不需要**部署
## ⚠ 那条命令必须带认证头
无认证时 `/api/v1/status` 返回**登录页 HTML**(200 + `THEME_PLACEHOLDER`),
`grep '"commit"'` 匹配不到 —— 看起来像"命令没输出",实际是认证缺失。
与 GUI 客户端 `api()` 专门检测 `THEME_PLACEHOLDER` 是同一件事。
## 顺带:内置插件 vs 独立二进制
`internal/plugins/<name>/` 编译进 homed;`plugins/<name>/plugin.bin`
是独立插件,要单独构建部署。**目录存在不等于有独立二进制** ——
`plugins/webui/` 目录存在但 **0 个文件**,走内置。
实测(2026-09-28):`webui`/`cmd`/`seq` 内置,`qq` 独立。
改内置插件只需重编 homed。
|
2026-09-28 08:52:12 +08:00 |
|
|
|
5cc0f53df8
|
docs(runbook): 补 §6 部署单元清单,并纠正一次误判的记录
## 补齐漏掉的单元
`cmd/` 下共 7 个可构建入口,服务端只部署 3 个:
| 单元 | 入口 | 服务端部署 |
| --- | --- | --- |
| homed(内核) | `cmd/homed` | ✅ 本机 `/usr/local/bin/homed` |
| waiter / waitercli | `cmd/waiter` | ✅ 106、30 的 `/opt/waiter/waiter` |
| 站点 | `site/`、`site_build/` | ✅ 106 的 `sites/` |
| **GUI** | `cmd/gui` | ❌ **Electron 桌面应用,随客户端分发** |
**GUI 不在服务端部署**(已确认生产无 `*.service`、无进程)。它与 waiter
是两端:GUI 用 `devicebridge_dll.js` 走设备桥协议连服务端 waiter,
`a781f8e` 修的正是 GUI 侧 bind 判 ok 与登记状态暴露。
⇒ 排查 GUI 问题要看**用户机器上的客户端**,不是 `homeagent.service` 日志。
另记:核查"有没有进程"时 `ps -ef | grep -c "[e]lectron"` 会把**自己的
grep 命令行**算进去返回非 0(实测返回 2,实际 0)—— 要看列出的内容,别只看计数。
## 本机 waiter 与 106/30 不同步
本机也留了一份 `/usr/local/bin/waiter`,`v1.3.2-153-gff69127`(2026-09-25
构建),**早于** main 在 09-26 合入的那批修复(设备反复掉线、bind 判 ok、
服务端发现自动链接),因而缺它们。106/30 已是 `1.4.0`。
本机这份无进程无服务,不影响生产设备桥;更新方式已记入文档。
## 记一次误判:判定"是否已合入"要按内容查
我曾用 `git merge-base --is-ancestor ff69127 origin/main` 判定"这批提交
没进 main",并推断"存在一条未合入、只靠 reflog 撑着的 5 提交线"。**该结论是错的。**
真实情况:这批改动在 2026-09-26 以**新 hash** 重做并进入 main,五对逐字节一致
(diff 均为 0 行):`1acbd39`→`1e58af7`、`3d30482`→`5d278cf`、
`078517e`→`7f5bf16`、`aaafaac`→`94c74b2`、`ff69127`→`a781f8e`。
⇒ 只查**旧 hash 的祖先关系**会误判;同一改动被重做为新 hash 时
`merge-base` 必然说不包含,而 `git merge-tree` 报的 13 个"冲突"
正是同源改动做两遍的必然结果,**不能当冲突去解**。
顺带修正一处归因:106 此前长期无 `online` 日志的成因是
`94c74b2`(设备反复掉线/静默失联:ping 路径断连 + bind 结果无人处理),
2026-09-26 已进 main,今天部署的 `1.4.0` 包含它。
|
2026-09-27 23:10:49 +08:00 |
|
|
|
e773f156fb
|
feat(deploy): 站点部署流水线(SDK 文档站 + introduce)
`deploy-sdk-site.sh` 覆盖两个静态站,与 homed / waiter 是独立部署单元:
- SDK 文档站:本地 `third_party/homeagent-sdk/site_build/` → 106 的
`/vol1/docker/navi-data/sites/sdk`
- introduce:本地 `site/`(零构建,源即产物)→ `sites/introduce`
用法:`--check`(只核对差异)/ 默认(构建+部署+验证)/ `--rollback <备份名>`。
`--check` 逐字节比对,2026-09-27 核实两站线上与本地产物**完全一致**。
## 更新了一处会误导的注释
脚本原注释写「设备网关白名单只放行 ls/stat/find/cat,打不了包」。
那是 waiter 白名单**硬编码 18 条**时的状况;2026-09-27 部署 7193446 后
106 已扩到 **22 条**(含 find/grep/sed/sort/tr/wc/head/tail/stat/file)。
**结论(打不了包)不变,但理由已变** —— 22 条里**没有 `tar`**,
有 `sed` 也不能打包。照旧文字理解会以为白名单只有 4 条。
## docs/zh/deploy-runbook.md 补 §5 站点章节
- 链路:`.60` nginx stream 按 ssl_preread SNI → 106:3080 → navi 容器内 nginx
(**不是** portal-nginx,那个已 Exited 两周)
- 坑①:构建**必须**走 `tools/apidoc/build.sh`。裸跑 `mkdocs build` 会丢掉
整个 `api/*.md` 和 `llms.txt` —— 而 `llms.txt` 正是给 agent 直读的入口。
正确产物 106 个文件,裸跑只有 78 个
- 坑②:打包不能走设备网关(无 `tar`),必须 SSH 直连
- ★ **新增文档后必须更新 `mkdocs.yml` 的 nav** —— 没登记会被 mkdocs 明确
警告 `not included in the nav configuration`,等于写完了但站点里不可达
- 验证要打**线上**而不是只看本地产物
- 回滚点与失败版本保留策略
写文档时我一度把 introduce 的域名写成「另见 §5.2」,但 §5.2 只讲了 SDK 的
SNI 链路 —— 已改为只写"同台同目录",并补上脚本里有依据的"故意不带 README.md"。
|
2026-09-27 23:00:04 +08:00 |
|
|
|
175fa9dd62
|
docs: 新增生产部署手册(homed / waiter)
`site-infra-runbook.md` 是**静态站 / nginx / 证书**的手册,不含 homed 与
waiter —— 两次生产部署(19:45 首次、21:50 修复)因此只存在于提交信息里,
查不到。
## 内容
**§1 homed**
- 硬前置:必须 `-tags=onnxruntime`(普通 build 只有 ~28MB,缺 ONNX Runtime)
- ★ **构建参数必须与线上一致**:不要顺手加 `-s -w`。加了产物从 86.8MB 掉到
78MB,8.8MB 的差会让人误判成"构建坏了",而它只是被 strip 了
- `check` / `deploy` / `rollback` 三条命令与备份位置
- 部署后必核:`multimodal space active: provider=chineseclip` 才是 ONNX
真加载起来的标志,缺它说明已降级但**不报错**
- ★ 适配器升级的保护语义(`.bundled` 三种情形的判定表),并记 21:50 那次
实测:手工补过 `stream_index` 的 `openai.lua` 被正确判定为用户修改并保留
- 两次部署的真实数据对照表
**§2 waiter**
- 逐台更新、不可并行(两台连同一网关,同时重启会同时断链)
- 部署后确认 `device waiter-* online`
- 记 2026-09-27 顺手解决的悬案:106 此前无 `online` 而 30 正常,两台配置与
token 完全相同 ⇒ 差异只可能在旧二进制,8月27日那版落在"未 bind 时收到
ping 会关连接"的缺陷窗口
- `device_cmd_allowlist` 的替换语义、生效验证、幂等追加方法
- ★ 明确写**白名单只匹配命令名、不看参数**,`find -delete`/`sed -i` 仍能逃
⇒ **不要称它为"只读白名单"**
**§3 故障排查**:按"消息没反应 / 命令被拒 / 适配器异常 / 告警是否缺陷"
四条线各给命令;特别标注"群聊 not @bot"与"私聊没回"是两件事
**§4 已知未修**:三项目前是已知限制而非疏漏
文中数字均与现场核对:脚本子命令确实存在(`check`/`deploy`/`rollback`)、
生产白名单确为 22 条、两次二进制大小取自实际部署。
|
2026-09-27 22:25:58 +08:00 |
|