# HomeAgent 生产部署手册(homed / waiter) > 适用范围:本机(`.60`)的 `homeagent.service`,以及两台设备桥宿主上的 > `waiter-remote.service`。 > > 静态站 / nginx / 证书的运维见另册 > [`site-infra-runbook.md`](./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 ★ 构建参数必须与线上一致 ```bash CGO_ENABLED=1 CC=cc go build -tags onnxruntime -o /tmp/homed-ort-new \ -ldflags "-X .../internal/meta.Version= -X .../internal/meta.Commit=" \ ./cmd/homed ``` **不要顺手加 `-s -w`。** 加了会 strip 掉符号,产物从 ~86.8MB 掉到 ~78MB —— 体积差 8.8MB 会让人误判成"构建坏了",而它只是被 strip 了。 若确实要 strip,须先确认线上也是同样参数,否则两次构建不可比。 验证: ```bash 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 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 部署后必须核对 ```bash 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…`),被**正确判定为用户修改并保留**。 验证方式(部署前后各跑一次,应完全一致): ```bash 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 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 ``` **逐台更新,不要并行** —— 两台都连同一网关,同时重启会同时断链。 `deploy` 会:备份 `/opt/waiter/waiter` → 替换 → 重启服务 → 验证 (服务 active、进程时长、**配置 md5 未变**)。 ### 2.3 部署后确认设备已注册 ```bash 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` 部署): ```yaml 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` 的幂等做法(已用于两台): ```bash 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 "消息发过去没反应" 按这个顺序查,**别跳步**: ```bash # 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 " # data:[] 是正常的(该网关不列模型),能返回即说明进程活着 ``` **"群聊 not @bot"** 与 **"私聊没回"** 是两件事:前者是插件按规则丢弃, 内核压根没收到任务,自然没有"卡死"。 ### 3.2 设备命令被拒 ```bash journalctl -u homeagent.service --since "-10 min" | grep "not in whitelist" ``` 先确认命令**第一段在不在** `waiter.yaml` 的 `device_cmd_allowlist` 里 (`netstat`、`systemctl` 不在当前的 22 条内,被拒是**正确行为**)。 ### 3.3 适配器流式 tool_call 异常 ```bash 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`](./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 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 文档的完整流程: ```bash 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 ``` 验证线上是否真的可访问(**别只看本地产物**): ```bash 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 同样的构建: ```bash 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` 包含它。