## 补齐漏掉的单元 `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` 包含它。
17 KiB
HomeAgent 生产部署手册(homed / waiter)
适用范围:本机(
.60)的homeagent.service,以及两台设备桥宿主上的waiter-remote.service。静态站 / nginx / 证书的运维见另册
site-infra-runbook.md—— 本册不涉及。记录日期:2026-09-27。本册所有数字均与现场核对过,不是模板值。
0. 一句话拓扑
┌─────────────────── 本机 .60 ───────────────────┐
│ homeagent.service ← /usr/local/bin/homed │
│ (必须 -tags=onnxruntime 构建) │
QQ 用户 ─NapCat─► │ :9890 remotedevice 网关 │
(在 106 上) │ /home/newqqagent/ 51G 模型资产(部署不动) │
└────────┬──────────────────────┬────────────────┘
│ ws 设备桥 │ ws 设备桥
┌───────────▼──────────┐ ┌────────▼─────────┐
│ 192.168.2.106 fnnas │ │ 192.168.2.30 │
│ /opt/waiter/waiter │ │ mainnas │
│ waiter-remote.service│ │ /opt/waiter/... │
│ + NapCat(25570) │ │ │
└──────────────────────┘ └──────────────────┘
要点:
waiter与homed是两个独立部署单元,更新其一不影响其二。- GUI 不在服务端部署(见下)。
- 设备桥授权(
device_authorized)在 waiter 侧,网关地址也在 waiter 的waiter.yaml里。 - NapCat(QQ 上游)在 106 上,
http://192.168.2.106:25570。
1. 部署 homed(本机)
1.1 硬前置:必须 onnxruntime 构建
普通 go build 只有 ~28MB,缺 ONNX Runtime,会让依存句法分析与多模态
向量化失效。生产二进制是 -tags=onnxruntime(约 87MB)。
deploy/packaging/package-linux.sh:139 会显式拒绝非 onnxruntime 构建。
1.2 ★ 构建参数必须与线上一致
CGO_ENABLED=1 CC=cc go build -tags onnxruntime -o /tmp/homed-ort-new \
-ldflags "-X .../internal/meta.Version=<v> -X .../internal/meta.Commit=<c>" \
./cmd/homed
不要顺手加 -s -w。 加了会 strip 掉符号,产物从 ~86.8MB 掉到 ~78MB ——
体积差 8.8MB 会让人误判成"构建坏了",而它只是被 strip 了。
若确实要 strip,须先确认线上也是同样参数,否则两次构建不可比。
验证:
go version -m /tmp/homed-ort-new | grep onnxruntime # 必须有 build -tags=onnxruntime
ls -l /tmp/homed-ort-new # 与 /usr/local/bin/homed 同量级
1.3 部署
bash deploy-plan.sh check # 只读
NEW_BIN=/tmp/homed-ort-new bash deploy-plan.sh deploy # 需输入 yes
bash deploy-plan.sh rollback # 回滚
check:服务状态、onnxruntime 标签、libonnxruntime.so、模型资产、适配器清单deploy:备份 → 替换二进制 → 重启 → 验证- 备份落在
/var/tmp/homed-backup-<时间戳>/,含ROLLBACK.sh、旧二进制、 适配器、unit 文件 - 默认候选
/tmp/homed-ort,可用NEW_BIN=覆盖
1.4 部署后必须核对
systemctl is-active homeagent.service
journalctl -u homeagent.service --since "-3 min" | grep "multimodal space active"
journalctl -u homeagent.service --since "-3 min" | grep -c "registering tool: seq_" # 应为 7
multimodal space active: provider=chineseclip dim=512 是 ONNX 链路真的
加载起来的标志 —— 缺它说明已降级,只是没报错。
1.5 ★ 适配器升级的保护语义
/home/newqqagent/adapters/.bundled 记录上次随包带出的版本哈希:
| 盘上版本 | 判定 | 行为 |
|---|---|---|
无 .bundled(首次升级) |
未知 | 只补缺失文件,不动已有文件 |
| 盘上 == 旧内嵌 | 未被改过 | 自动覆盖为新版本 |
| 盘上 != 旧内嵌 | 用户改过 | 保留用户版本 |
2026-09-27 21:50 实测:生产 openai.lua 是 8月26日手工补过 stream_index
的版本(盘上 1a649be2… ≠ 旧内嵌 6374c596…),被正确判定为用户修改并保留。
验证方式(部署前后各跑一次,应完全一致):
md5sum /home/newqqagent/adapters/*.lua | md5sum
1.6 两次部署的真实记录(2026-09-27)
| 19:45 首次 | 21:50 修复后 | |
|---|---|---|
| 二进制 | 86,496,624 → 86,784,400 | 86,784,400 → 86,811,464 |
| onnxruntime | ✓ | ✓ |
seq_* 工具 |
0 → 7 | 7 |
| 适配器 | 无 .bundled ⇒ 一律不动 |
有清单 ⇒ 保护用户修改 |
| 备份 | homed-backup-20260927-194554 |
homed-backup-20260927-215026 |
has empty arguments 误报 |
24 次(仍在增长) | 0 次 |
首次部署前生产二进制构建于当天 06:36,而 seq 插件引入于更晚的提交 ⇒
旧实例的 strings /usr/local/bin/homed | grep -c internal/plugins/seq 为 0。
它在 QQ 上如实回答"没有编排工具"不是说谎,是确实没有。
这类"实例自述与代码状态不一致",先查二进制构建时间与内含符号, 别急着怀疑提示词或模型。
2. 部署 waiter(设备桥,106 / 30)
2.1 现状
两台配置相同(同一 device_gateway、同一 device_token、device_authorized: true),
差异只在登录方式:106 走 admin@ + sudo,30 走 root@。
2.2 更新
bash deploy-waiter.sh check # 两台一起,只读
bash deploy-waiter.sh deploy 192.168.2.106 # 需输入 yes
bash deploy-waiter.sh deploy 192.168.2.30 # 上一台验证通过后再做
bash deploy-waiter.sh rollback <ip>
逐台更新,不要并行 —— 两台都连同一网关,同时重启会同时断链。
deploy 会:备份 /opt/waiter/waiter → 替换 → 重启服务 → 验证
(服务 active、进程时长、配置 md5 未变)。
2.3 部署后确认设备已注册
journalctl -u homeagent.service --since "-2 min" | grep "device waiter-"
# 期望:device waiter-fnnas online / device waiter-mainnas online
2026-09-27 顺手解决的一个悬案:106 此前一直没有 online 日志而 30 正常,
两台配置与 token 完全相同 ⇒ 差异只可能在旧 waiter 二进制。8月27日那版落在
"未 bind 时收到 ping 会关连接"的缺陷窗口里,更新后即正常。
2.4 命令白名单(device_cmd_allowlist)
waiter 的命令白名单原本是源码里硬编码的正则(18 个命令),waiter.yaml 里
没有任何键能改它 ⇒ find/grep/sed/sort/tr 这些排查问题最常用的
只读命令一律被拒,报错:
device_ctl_cmdrun device_id:waiter-fnnas error: command not in whitelist
现改为 waiter.yaml 可配置(2026-09-27 随 7193446 部署):
device_cmd_allowlist:
- ls
- find
- grep
- sed
- 替换默认集而非追加 —— 避免"以为加了 find、结果还留着
python3 -c任意执行" - 留空 ⇒ 用内置默认集(绝不能变成"全放行",那等于静默拆掉闸门)
- 生效验证:重启后启动日志应打印
device cmd allowlist: 22 条(来自 waiter.yaml)
当前生产配置:22 条,只读为主(ls pwd cat du df free ps ip uname uptime date hostname find grep sed sort tr wc head tail stat file)。
已知局限:白名单只匹配命令名、不看参数 ⇒
find -delete、find -exec rm {} ;、sed -i、sort -o仍能放行。 这是"命令名清单",不是"只读保证"。参数级拦截是后续项。 ⇒ 文档与对话里都不要把它称作"只读白名单",那会让人以为写操作被挡住了。
追加到 waiter.yaml 的幂等做法(已用于两台):
Y=/opt/waiter/waiter.yaml
cp -a "$Y" "$Y.bak-$(date +%Y%m%d-%H%M%S)"
grep -q '^device_cmd_allowlist:' "$Y" || cat >> "$Y" <<'CFG'
device_cmd_allowlist:
- ls
- find
- grep
CFG
systemctl restart waiter-remote.service
3. 故障排查
3.1 "消息发过去没反应"
按这个顺序查,别跳步:
# 1) 消息到了吗
journalctl -u homeagent.service --since "-5 min" | grep -E "interrupt from|webhook recv"
# 群聊里没 @bot 会打 not @bot —— 那不是 bug,是设计
# 2) 任务执行了吗(tools=[] 说明模型没发 tool_call)
journalctl -u homeagent.service --since "-5 min" | grep "→ response"
# 3) LLM 通吗
journalctl -u homeagent.service --since "-10 min" | grep -i unreachable
# 4) 代理层活着吗
curl -s --max-time 8 http://127.0.0.1:8081/v1/models -H "Authorization: Bearer <key>"
# data:[] 是正常的(该网关不列模型),能返回即说明进程活着
"群聊 not @bot" 与 "私聊没回" 是两件事:前者是插件按规则丢弃, 内核压根没收到任务,自然没有"卡死"。
3.2 设备命令被拒
journalctl -u homeagent.service --since "-10 min" | grep "not in whitelist"
先确认命令第一段在不在 waiter.yaml 的 device_cmd_allowlist 里
(netstat、systemctl 不在当前的 22 条内,被拒是正确行为)。
3.3 适配器流式 tool_call 异常
journalctl -u homeagent.service --since "-10 min" | grep -E "finish_reason=length|unmarshal unified"
非法 JSON 帧 出现在启动握手期属正常(插件启动时的非 JSON 帧),
只在运行期持续出现才是问题。
3.4 有告警但不确定是不是缺陷
先看是不是内核的兜底在工作。例:重复申请 stage 锁 是 SDK 模板在每个
stage handler 入口自动加锁 + 锁不可重入所致,插件侧问题;内核的强制释放
是有意设计(避免后续插件死锁),不要当内核缺陷去修。详见
toolcall-parallel-execution-plan.md 末节。
4. 已知未修
| 项 | 性质 | 说明 |
|---|---|---|
| 白名单不看参数 | 功能限制 | find -delete/sed -i 能逃,用户已知悉并选择先下发 |
重复申请 stage 锁 |
插件缺陷 | 需改 SDK 模板并重编 9月14日的 plugins/qq/plugin.bin |
| 群聊必须 @ 才触发 | 设计 | 放宽会在群里引发 unwanted 触发 |
5. 部署站点(SDK 文档站 / introduce)
deploy-sdk-site.sh 覆盖两个静态站,与 homed / waiter 是独立部署单元。
| 站 | 本地源 | 线上位置 | 构建 |
|---|---|---|---|
| SDK 文档站 | third_party/homeagent-sdk/site_build/ |
/vol1/docker/navi-data/sites/sdk |
需 tools/apidoc/build.sh(§5.3 坑①) |
| introduce | site/(排除 README.md) |
/vol1/docker/navi-data/sites/introduce |
零构建,源即产物 |
两站部署在同一台 106 的同一 sites/ 目录下。
introduce 是零构建的 —— site/ 里就是成品,脚本部署时故意不带
README.md(那是仓库说明文档,不是站点资源)。
5.1 用法
bash deploy-sdk-site.sh --check # 只核对差异,不动线上
bash deploy-sdk-site.sh # 构建 + 部署 + 验证
bash deploy-sdk-site.sh --rollback .sdk-bak-<时间戳> # 回滚
--check 逐字节比对本地与线上,两个站都一致时可直接跳过部署。
5.2 链路(实测确认)
本机 192.168.2.60
└─ nginx stream 按 ssl_preread SNI 把 443 透传 → 192.168.2.106:3080
└─ navi 容器内的 nginx(**不是** portal-nginx,那个已 Exited 两周)
└─ server_name sdk.homeagent.jianfgit.xyz → root /app/data/sites/sdk
└─ 宿主对应 /vol1/docker/navi-data/sites/sdk
登录 106 只能用 admin@ + sudo -n,root@ 会被拒。
5.3 ★ 两个坑
① 构建必须走 tools/apidoc/build.sh,不能裸跑 mkdocs build
裸跑会丢掉整个 api/*.md 和 llms.txt —— 而 llms.txt 正是给 agent 直读的入口。
正确产物 106 个文件,裸跑只有 78 个。
② 打包不能走设备网关
106 的 waiter 白名单(device_cmd_allowlist)现有 22 条:
ls pwd cat du df free ps ip uname uptime date hostname
find grep sed sort tr wc head tail stat file
里面没有 tar(有 sed 也不能打包)⇒ device_ctl_cmdrun 打不了包,
必须走 SSH 直连。
早先这里记的是「只放行
ls/stat/find/cat」,那是 waiter 白名单 硬编码 18 条时的状况。2026-09-27 部署7193446后扩到 22 条 —— 结论(打不了包)不变但理由已变,别照旧文字理解。
5.4 新增文档后要记得更新 nav
mkdocs.yml 的 nav 没登记的页面,mkdocs 会明确警告
not included in the nav configuration —— 等于文档写完了但在站点里不可达。
改完 SDK 文档的完整流程:
cd third_party/homeagent-sdk
# 1) 在 docs/guide/ 写文档
# 2) 在 mkdocs.yml 的 nav 里登记 ← 漏了这步站点里找不到
# 3) 重新生成(含 docs/api/* 与 llms.txt 同步)
tools/apidoc/build.sh
# 4) 部署并验证
bash ../../deploy-sdk-site.sh
验证线上是否真的可访问(别只看本地产物):
curl -s -o /dev/null -m 10 -w '%{http_code}\n' -k https://sdk.homeagent.jianfgit.xyz/guide/<新文档>/
5.5 回滚点
每次部署留 .sdk-bak-<时间戳>(同 sites/ 下)。--rollback 会先把当前站
cp -a 成 .sdk-failed-<时间戳> 再恢复,失败版本不丢。
6. 部署单元清单(哪些在服务端、哪些不在)
排查"改了什么没生效"时,先确认改动落在哪个单元 —— 服务端只有前三个。
| 单元 | 入口 | 部署位置 | 服务端部署 |
|---|---|---|---|
| homed(内核) | cmd/homed |
本机 /usr/local/bin/homed |
✅ |
| waiter / waitercli | cmd/waiter |
106、30 的 /opt/waiter/waiter |
✅ |
| 站点 | site/、site_build/ |
106 的 sites/ |
✅ |
| GUI | cmd/gui |
不在服务端 | ❌ |
6.1 GUI 是客户端,不在服务端
cmd/gui 是 Electron 桌面应用(main.js / icon-tray.png /
devicebridge_dll.js),随客户端分发,不在服务端部署:
生产既无 *.service 也无对应进程。
核查时的坑:
ps -ef | grep -icE "[e]lectron|cmd/gui"会把它自己的 grep 命令行算进去而返回非 0(本机实测返回 2,实际 0)。 确认进程是否存在要看列出的内容,别只看计数。
⇒ 不要在服务端找 GUI 的产物或配置;排查 GUI 问题时,
要看用户机器上运行的客户端,而不是 homeagent.service 的日志。
它与 waiter 的关系是两端:GUI 用 devicebridge_dll.js 走设备桥协议
连接服务端 waiter,a781f8e/ff69127 修的正是 GUI 侧 bind 结果判 ok
与登记状态暴露。
6.2 ★ 本机 /usr/local/bin/waiter 与 106/30 不同步
本机也留了一份 waiter,但不是 106/30 那条设备桥:
| 位置 | 版本 | 状态 |
|---|---|---|
| 106 / 30 | 1.4.0(55906b8) |
✅ 最新 |
本机 /usr/local/bin/waiter |
v1.3.2-153-gff69127(2026-09-25 构建) |
⚠️ 落后 main |
本机这份早于 main 在 2026-09-26 合入的那批修复(设备反复掉线、 bind 判 ok、服务端发现自动链接),因此缺它们。
不过它无进程、无服务(只有 ~/.config/homeagent/waiter.yaml),
所以不影响生产设备桥。若要更新,用与 106/30 同样的构建:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags "-X .../internal/meta.Version=1.4.0 -X .../internal/meta.Commit=$(git rev-parse --short HEAD)" \
-o /tmp/waiter-new ./cmd/waiter
6.3 一次误判的记录:别只看"不在分支上"
2026-09-27 排查时,我用 git merge-base --is-ancestor ff69127 origin/main
判定"这批提交没进 main",并据此推断"存在一条未合入、只靠 reflog 撑着的
5 提交线"。该结论是错的。
真实情况:这批改动在 2026-09-26 以新 hash 重做并进入 main:
| 旧(9-25 那条线) | 新(main 上) | 内容 diff |
|---|---|---|
1acbd39 通用反向代理 |
1e58af7 |
0 行 |
3d30482 反代两处故障 |
5d278cf |
0 行 |
078517e 设备桥自动链接 |
7f5bf16 |
0 行 |
aaafaac 修复设备反复掉线 |
94c74b2 |
0 行 |
ff69127 GUI bind 判 ok |
a781f8e |
0 行 |
逐字节一致,无需合入。之所以误判,是因为只查了旧 hash 的祖先关系, 没查提交标题/内容是否已有等价副本。
⇒ 判定"某改动是否已合入"要按内容查,不能只按 hash 查。
同一改动可能被重做为新 hash;此时 merge-base 必然说不包含,
而 git merge-tree 报的 13 个"冲突"正是同源改动做两遍的必然结果。
另注:aaafaac/94c74b2(修复设备反复掉线/静默失联:ping 路径断连 +
bind 结果无人处理)正是 106 此前长期无 online 日志的成因,
2026-09-26 已进 main,今天部署的 1.4.0 包含它。