Files
HomeAgent/docs/zh/deploy-runbook.md
JianFeeeee 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

440 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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=<v> -X .../internal/meta.Commit=<c>" \
./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 <ip>
```
**逐台更新,不要并行** —— 两台都连同一网关,同时重启会同时断链。
`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 <key>"
# 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` 包含它。