Browse Source

docs: 容器接入文档整合为单一上线手册

用户诉求:前序踩了这么多坑,后续容器部署不能再犯。

盘点发现(比格式问题更严重):
全平台 54 个容器,45 个超 24 小时无上报(死的),仅 9 个活跃。
balance 空 42 个 / channels 标准 12 个 / host.cpuPercent 标准 23 个。
→ 真正的问题不是格式分化,是八成容器根本没在用。
→ 手册第一页必须是「先确认存活」。

原 7 份重叠文档合并为《容器接入上线手册-20261020.md》:
- §0 全平台真实状态盘点
- §1 上线三步(先查存活 → 部署原版 → 配常驻)
- §2 九条验收断言
- §3 四条铁律(每条附真实踩坑记录)
- §4 故障速查表(含 Matrix 不回消息三道门)
- §5 给容器的标准提示词
- §6 上线检查表

四条铁律:
① 查运行态不查路径
② 调用失败 ≠ 凭据失效
③ 每个数字都要回读,不许自算
④ 活跃度 ≠ 健康度

反模式:「while true 循环」标注为陷阱(随会话死、重启丢失)

旧文档加「已被取代」指引,保留追溯。

验证:verify:docs 49 文件

Co-Authored-By: Claude Code <noreply@anthropic.com>
Huccct 1 tuần trước cách đây
mục cha
commit
ffb7dc6f7a

+ 14 - 0
docs/00-tracking/操作追踪.md

@@ -873,3 +873,17 @@
 | **⚠️ 另一发现:重复绑定** | `FmodeMatrixBinding` 有 **2 条 active**:`sr2WiPsDyQ`(旧主人)room `!OIm7iWR6...`、`xhVRjTMwQH`(你)room `!miyB1DxRE...`。<br>provision 按 `{userPointer, agentId}` 查 → 你拿到自己的房间(**选房逻辑正确**),但旧绑定未清理属数据问题 |
 | 验证证据 | `ng test` **503** / `build` 通过 / `verify:ui` **11/11** / `verify:docs` **48 文件** |
 | 踩坑记录 | composer 注释里写了带反引号的 `` `.input.is-overflow` `` —— 在**模板字符串内**反引号直接截断样式块,报 `Failed to resolve styles at position 1`。已改为无反引号表述 |
+
+### 2026-10-20 — 容器接入文档整合:7 份 → 1 份上线手册
+
+| 项 | 内容 |
+|---|---|
+| **用户诉求** | 「保证之前遇到这么多问题,后续给其他容器部署时不能再犯错,优化文档」|
+| **🔴 盘点发现(比格式问题更严重)** | 全平台 54 个容器实测:<br>· **45 个超过 24 小时没有任何上报(死的)**<br>· 仅 9 个 5 分钟内活跃<br>· `balance` 为空 42 个 / `channels` 标准 12 个 / `host.cpuPercent` 标准 23 个<br>**→ 问题不是"格式不统一",是"八成容器根本没在用"**。文档第一页必须是「先确认存活」|
+| **文档问题** | 03-interface/ 下已有 **7 份**互相重叠的容器文档,新容器上线不知道看哪份 |
+| **🆕 产出《容器接入上线手册-20261020.md》** | 合并为唯一入口,7 章:<br>· §0 全平台真实状态盘点(先泼冷水)<br>· §1 上线三步(**先查存活**,再部署,最后配常驻)<br>· §2 九条验收断言<br>· §3 **四条铁律**(每条附真实踩坑记录)<br>· §4 故障速查表(4 类症状 → 排查命令)<br>· §5 给容器的标准提示词<br>· §6 上线检查表 |
+| **四条铁律(本轮全部踩过)** | ① **查运行态不查路径** —— find 找不到 ≠ 进程没跑(容器报"没部署",实际在上报)<br>② **调用失败 ≠ 凭据失效** —— Bearer 401 但 body 200,误判导致排查偏一天<br>③ **每个数字都要回读** —— 两轮"已验证"分别是库里没这个键、键名值全错<br>④ **活跃度 ≠ 健康度** —— 读 state 字段假绿;改读 inbound 又变成"没人说话就红",两个极端 |
+| **新增故障速查(§4.3)** | Matrix「在线但不回消息」三道门完整排查法:IGNORE 模式 → ALLOWED_USERS → 房间白名单。<br>附配置读取假阴性警告:`.env` 走 Python 层加载,**不进 `/proc/<pid>/environ`**,用它查配置永远是空 → 必须让容器打印生效值 |
+| **新增反模式(§1 第 3 步)** | 「`while true` 循环」标注为**陷阱**:随会话死、重启就丢(lj-test 实测,gateway 重启带走上报进程,停 15 分钟无人发现)|
+| **旧文档处置** | `reporter部署方案-20261020.md`、`容器上报标准部署包-20261020.md` 顶部加「已被取代」指引,保留供追溯 |
+| 验证证据 | `verify:docs` **49 文件**(+1)/ 表格结构全部正常 |

+ 3 - 0
docs/03-interface/reporter部署方案-20261020.md

@@ -1,5 +1,8 @@
 # reporter 部署方案(给容器执行)
 
+> ⚠️ **本文档已被《容器接入上线手册-20261020.md》取代**,内容已并入该手册。
+> 保留本文件仅供历史追溯,新容器上线请看手册。
+
 > 目标:让容器具备状态上报能力,使 App 能看到它
 > 适用:`agent-node-lj-test`(或任何缺 reporter 的容器)
 > 依据:`E:\work\fmode-app\docs\03-interface\reporter-v3\status-report.py`(**v10,1100 行**)

+ 3 - 0
docs/03-interface/容器上报标准部署包-20261020.md

@@ -1,5 +1,8 @@
 # 容器状态上报 · 标准部署包(后续所有容器统一走这套)
 
+> ⚠️ **本文档已被《容器接入上线手册-20261020.md》取代**,内容已并入该手册。
+> 保留本文件仅供历史追溯,新容器上线请看手册。
+
 | 字段 | 值 |
 |---|---|
 | 文档编号 | 标准包-20261020-reporter |

+ 385 - 0
docs/03-interface/容器接入上线手册-20261020.md

@@ -0,0 +1,385 @@
+# 容器接入上线手册(唯一入口)
+
+| 字段 | 值 |
+|---|---|
+| 文档编号 | 手册-20261020-容器接入 |
+| 版本 | v2.0 |
+| 创建日期 | 2026-10-20 |
+| 取代 | `reporter部署方案-20261020.md`、`容器上报标准部署包-20261020.md`(内容已并入本文,原文件保留作历史) |
+| 配套 | `reporter字段规范-20261020.md`(字段契约细节,改脚本时查)|
+| 适用 | **所有新容器上线 / 存量容器修复 / "容器看不到"类故障排查** |
+
+> 本文每一条都来自 2026-10-17 ~ 10-20 的真实故障。
+> 写进「踩坑记录」的,都是已经真实发生过、且**会重复发生**的错误。
+
+---
+
+## 零、先看这个:当前全平台真实状态
+
+2026-10-20 全量盘点 54 个容器:
+
+| 指标 | 数量 |
+|---|---|
+| **24 小时内没有任何上报**(= 死的) | **45 个** |
+| 5 分钟内活跃上报 | 9 个 |
+| `balance` 字段为空的 | 42 个 |
+| `channels` 为标准结构的 | 12 个 |
+| `host.cpuPercent` 标准的 | 23 个 |
+| 自定义 `cny` 键(格式偏离) | 1 个 |
+
+**结论:问题不是"格式不统一",而是"八成容器根本没在用"。**
+
+新容器上线时,**先用第一章的 3 条命令确认存活**,再谈格式。格式再好、容器是死的,一样看不到。
+
+---
+
+## 一、上线三步(照做即可)
+
+### 第 1 步 · 确认存活(最重要,先做这个)
+
+新容器上来,**先不配任何东西**,跑这三条:
+
+```bash
+# ① 有没有进程在跑上报
+ps aux | grep -iE "status|report" | grep -v grep
+
+# ② 它实际跑的是哪个脚本、在哪个目录
+ls -l /proc/<上面的 pid>/cmdline
+ls -l /proc/<pid>/cwd
+
+# ③ 平台侧有没有记录
+curl -s 'https://server.fmode.cn/parse/classes/FmodeAgent?where={"agentId":"<容器名>"}' \
+  -H 'X-Parse-Application-Id: ncloudmaster' | jq '.results[0] | {agentId, updatedAt, lastField}'
+```
+
+判读:
+
+| 现象 | 结论 | 下一步 |
+|---|---|---|
+| ①② 有,③ 有且 `updatedAt` 在 5 分钟内 | ✅ 活着 | 跳到第 2 步查格式 |
+| ①② 有,③ 无记录 | 在跑但上报失败 | 查 token / agentId / 云函数 id(见 §4.1)|
+| ①② 无,③ 有记录且在更新 | ⚠️ **数据不是这个进程写的** | 查 `host.hostname` 反查真实来源(见 §4.2)|
+| ①② 无,③ 停更超过 24 小时 | ❌ 死了 | 从第 2 步开始部署 |
+
+---
+
+### 第 2 步 · 部署原版脚本
+
+**路径固定**:
+
+```
+/opt/data/git-repos/agent-status-reporter/scripts/status-report.py
+/opt/data/git-repos/agent-status-reporter/state/
+```
+
+**脚本来源**(按优先级):
+
+1. **从同宿主正常容器拷**(最快):
+   ```bash
+   docker cp <正常容器>:/opt/data/git-repos/agent-status-reporter/scripts/status-report.py \
+             <目标容器>:/opt/data/git-repos/agent-status-reporter/scripts/
+   ```
+2. 项目仓库:`E:\work\fmode-app\docs\03-interface\reporter-v3\status-report.py`(v11,49410 字节)
+
+> 🔴 **不要自己重写。** 原版 1100 行覆盖了:变化检测、失败队列、token 自愈、
+> 任务残留清理、官方 channel 探测、usage 聚合。自己复刻必然漏掉其中一部分。
+>
+> **实证**:54 个容器里唯一的格式偏离(`balance` 用了 `cny` 键)就来自一个自写脚本。
+
+---
+
+### 第 3 步 · 配置与常驻
+
+**改 `/opt/data/.env` 前先备份**:
+
+```bash
+cp /opt/data/.env /opt/data/.env.bak-$(date +%Y%m%d%H%M%S)
+```
+
+**必填 4 项**:
+
+```bash
+AGENT_ID=<容器名>                    # 缺省会退化成 hostname → agent not found
+FMODE_SESSION_TOKEN=<主人 sessionToken>   # 决定归属,谁发消息谁持有
+AGENT_FUNCTION_ID=xlosi3m46q         # 🔴 填错会走到 OBS 上传(数据进存储桶不进库)
+WECOM_BOT_ID=<企微 BotID>            # 有企微通道时必填
+```
+
+**常驻**:容器一般没有 crontab。
+
+| 方式 | 适用 | 可靠性 |
+|---|---|---|
+| **Hermes 原生 cron**(jobs.json)| 容器有 Hermes | ✅ 推荐,gateway 重启后自动继续 |
+| s6 服务 | 有 root 权限 | ✅ 最稳,但普通用户建不了 |
+| `while true` 循环 | 兜底 | ❌ **随会话死,重启就丢** |
+
+> 🔴 **`while true` 循环是陷阱。** lj-test 就是用它,gateway 重启时上报进程一起被带走,
+> 停了 15 分钟没人发现(表现为平台 `updatedAt` 冻住)。
+
+**Hermes cron 写法**:
+
+```json
+{
+  "job_id": "<生成>",
+  "name": "fmode-status-reporter",
+  "schedule": "*/2 * * * *",
+  "no_agent": true,
+  "script": "fmode-status-report.sh"
+}
+```
+
+验证:
+
+```bash
+# 看最近几次执行
+sqlite3 <hermes>/executions.db "SELECT * FROM executions ORDER BY started_at DESC LIMIT 5;"
+```
+
+---
+
+## 二、验收清单(9 条,缺一不可)
+
+```bash
+curl -s 'https://server.fmode.cn/parse/classes/FmodeAgent?where={"agentId":"<容器名>"}' \
+  -H 'X-Parse-Application-Id: ncloudmaster'
+```
+
+| # | 断言 | 通过标准 |
+|---|---|---|
+| 1 | `updatedAt` | **5 分钟内** |
+| 2 | `host.cpuPercent` | 数字,0~100 |
+| 3 | `host.memPercent` / `diskPercent` | 数字 |
+| 4 | `channels` **是对象** | `{...}`,不是 `[...]` |
+| 5 | `channels.matrix` | 与真实通道状态一致 |
+| 6 | `channels.wecomBotId` | 等于 `WECOM_BOT_ID` |
+| 7 | `heartbeatAt` | 60 秒内 |
+| 8 | `usage.totalTokens` | > 0 |
+| 9 | `balance.remainCny` | 有数字(或 `source:"none"` 且有原因)|
+
+---
+
+## 三、🔴 四条铁律(本轮全部踩过)
+
+### 铁律 1 · 查运行态,不查路径
+
+```
+❌ find / -name "status-report.py"    → 找不到就说"没部署"
+✅ ps aux | grep -i status            → 拿 pid
+   cat /proc/<pid>/cmdline            → 看实际跑的是哪个文件
+```
+
+**踩坑记录**:容器按预设路径 `find` 找不到脚本,报「本机根本没部署 reporter」。
+实际它一直在上报(`host.hostname` 是它自己的 Docker 短 ID,44 秒前刚更新)。
+
+**「找不到文件」≠「进程没在跑」。**
+
+---
+
+### 铁律 2 · 调用失败 ≠ 凭据失效
+
+| 现象 | 容易误判 | 实际 |
+|---|---|---|
+| `Bearer` 头 → 401 | token 过期了 | 调用姿势不对 |
+| body `{"sessionToken":...}` → 200 | — | ✅ 正确姿势 |
+
+**踩坑记录**:容器断定 token 已过期,据此说 balance 拿不到。实际 token 一直是好的,
+只是它用了 Bearer 头。这个误判让排查方向整个偏了一整天。
+
+---
+
+### 铁律 3 · 每个数字都要回读,不许自算
+
+```
+❌ 自己算一遍 cpuPercent=16.4,然后报"已验证"
+✅ 上报后 op=get 读回原文,把字面值贴进报告
+```
+
+**踩坑记录**:
+- 第一轮报「cpuPercent 已验证 16.4」→ 库里**根本没这个键**
+- 第二轮报「balance.remainCny=63.69」→ 实际 `{"cny":91.46}`,键名和值**都不对**
+
+**报告里的每个数字,都必须是回读原文里的字面值。**
+
+---
+
+### 铁律 4 · 活跃度 ≠ 健康度
+
+判断「通道是否正常」,不能用「最近有没有收到消息」。
+
+| 指标 | 语义 | 对不对 |
+|---|---|---|
+| 最近收到消息的时间 | **业务活跃度** | ❌ 没人说话就变红,误报 |
+| `channelState` 字段 | **自称状态** | ❌ 进程活着就绿,假绿 |
+| TCP 连接是否存在 | **连接层** | ⚠️ 覆盖不了「连上但不消费」|
+| 真实消费过事件 | **健康度** | ✅ 正确,但难测 |
+
+**踩坑记录**:容器报 `listenerOk: true`(读 `channelState`),实际 15 小时 0 条入站消息。
+翻成读「最近 inbound 时间」后,又变成「20 分钟没人说话就报 false」—— **另一个极端**。
+
+**正确做法**:分开报两个字段,各自标明语义:
+
+```
+listenerOk          = 连接层健康(并如实注明判据来源与局限)
+lastInboundAgeSecs  = 业务活跃度(只展示,不参与健康判断)
+```
+
+---
+
+## 四、故障速查表
+
+### 4.1 上报返回 `agent not found` / 数据不落库
+
+| 可能 | 检查 |
+|---|---|
+| `AGENT_ID` 没生效 | `ps auxww \| grep status-report` 看进程的完整命令行有没有带上 |
+| `AGENT_FUNCTION_ID` 填错 | 返回值带 `"key":"user/.../log/...json"` = 走了 OBS 上传(`AlP56LCKFm`),应为 `xlosi3m46q` |
+| token 失效 | 换姿势重试(见铁律 2);仍 401 才考虑重登 |
+
+### 4.2 数据在更新,但容器里找不到写入方
+
+**症状**:平台 `updatedAt` 在跳,但 `ps` 里没有 reporter 进程。
+
+**排查顺序**:
+
+```bash
+# ① 用 host.hostname 反查(容器自证字段,外部推不出来)
+#    平台读 host.hostname,和容器内 hostname 对比
+hostname
+
+# ② 按 agentId 查(比按字段名查更容易命中配置)
+grep -rl "<agentId>" /opt /etc /root /home 2>/dev/null
+
+# ③ 看是否有别的 uid 在跑
+ps -eo pid,ppid,user,cmd --forest | head -60
+```
+
+**踩坑记录**:容器连续三轮说「本机没有 reporter」「找不到 balance 代码」。
+但全平台 54 个容器里**只有它一个**用 `cny` 键 → 排除平台代写 → 确实是它专属代码在写。
+
+**判定技巧**:如果某个字段格式**全平台独一份**,那写入方一定在它自己相关的某处,不是平台统一逻辑。
+
+### 4.3 容器"在线"但不回消息(Matrix)
+
+这是本轮最复杂的故障,**三道门依次拦截**:
+
+| 门 | 症状 | 修复 |
+|---|---|---|
+| ① `MATRIX_IGNORE_USER_PATTERNS` | 消息进了 sync,但被静默丢弃 | 从忽略名单移除该用户 |
+| ② `MATRIX_ALLOWED_USERS` | 抛 `Unauthorized user (ignored)` | 把用户加进允许名单 |
+| ③ 房间白名单 | DM 房间被当非白名单 | 一般 DM 豁免,未触发 |
+
+**排查命令**:
+
+```bash
+# ① 有没有进 sync
+grep -n "<event_id>" /opt/data/logs/gateway.log*
+
+# ② 有没有被忽略
+grep -iE "ignoring sender|ignore pattern" /opt/data/logs/gateway.log*
+
+# ③ 有没有被授权门挡
+grep -iE "Unauthorized user|unrecognized" /opt/data/logs/gateway.log*
+
+# ④ 是否有 inbound 记录
+grep -iE "inbound message" /opt/data/logs/gateway.log | tail -10
+```
+
+**⚠️ 配置读取的假阴性**:`.env` 是 Python 层用 `load_hermes_dotenv()` 加载的,
+**不会出现在 `/proc/<pid>/environ`** 里。用它查「有没有配某变量」永远是空。
+
+正确做法:**让容器打印它自己读到的生效值**:
+
+```
+[env] IGNORE = '...'
+[env] ALLOWED = [...]
+```
+
+### 4.4 上报进程随重启消失
+
+**症状**:gateway 重启后,平台 `updatedAt` 冻住不动。
+
+**原因**:上报是 `while true` shell 循环起的,进程组随 gateway 一起死。
+
+**修复**:改用 Hermes 原生 cron 或 s6(见 §1 第 3 步)。
+
+---
+
+## 五、给容器的标准操作提示词
+
+```markdown
+【容器状态上报 · 标准接入】
+
+一、先查存活(不要跳过)
+  ps aux | grep -iE "status|report" | grep -v grep
+  ls -l /proc/<pid>/cmdline
+  → 有进程:先确认它在报什么、报给谁,别急着重装
+  → 无进程:按下面从零部署
+
+二、部署(不要自己写脚本)
+  mkdir -p /opt/data/git-repos/agent-status-reporter/{scripts,state}
+  从同宿主正常容器 docker cp 原版 status-report.py
+
+三、配置 /opt/data/.env(先备份)
+  AGENT_ID=<容器名>
+  FMODE_SESSION_TOKEN=<主人 token>
+  AGENT_FUNCTION_ID=xlosi3m46q
+  WECOM_BOT_ID=<企微 BotID>
+
+四、常驻(不要用 while true 循环 —— 重启会丢)
+  用 Hermes 原生 cron 或 s6
+
+五、验收(9 条,回读平台值,不许自算)
+  □ updatedAt 5 分钟内
+  □ host.cpuPercent 0~100
+  □ host.memPercent / diskPercent
+  □ channels 是对象不是数组
+  □ channels.matrix 与真实状态一致
+  □ channels.wecomBotId 匹配
+  □ heartbeatAt 60 秒内
+  □ usage.totalTokens > 0
+  □ balance.remainCny 有值
+
+六、纪律
+  · 查运行态(ps/proc),不查路径(find)
+  · 调用失败先换姿势,再怀疑 token
+  · 报告里每个数字必须是回读原文的字面值
+  · 配置要用「打印自己读到的值」验证,不要从 /proc 或文件推测
+
+报告格式:
+REPORTER-OK agentId=<...>
+updatedAt=<平台回读>
+cpuPercent=<回读> memPercent=<回读> diskPercent=<回读>
+channels=<对象/数组> matrix=<true/false>
+heartbeatAt=<回读> usage.totalTokens=<回读> balance.remainCny=<回读>
+常驻方式=<cron/s6/其他>
+```
+
+---
+
+## 六、上线检查表(复制给执行人)
+
+```markdown
+□ 存活三查(ps / proc / 平台回读)已做
+□ 脚本是原版(不是自写复刻)
+□ .env 已备份
+□ AGENT_ID / FMODE_SESSION_TOKEN / AGENT_FUNCTION_ID / WECOM_BOT_ID 四项已配
+□ 上报走 xlosi3m46q(返回不是 "key":"user/.../log/...json")
+□ 常驻用 cron 或 s6(不是 while true)
+□ 平台回读 updatedAt 在 5 分钟内
+□ 9 条验收断言全过
+□ 机器人在 App 会话里能实际回复消息
+□ 重启 gateway 后上报能自动恢复
+```
+
+---
+
+## 七、关联文档
+
+- `docs/03-interface/reporter字段规范-20261020.md` —— 逐字段契约(改脚本时查)
+- `docs/03-interface/容器初始化流程优化建议-20261020.md` —— workshop 流程优化 + 方法论
+- `docs/02-design/architecture/账号与账套体系设计.md` —— 归属模型
+- 云函数 `xlosi3m46q` v0.1.3(`.local-backup/xlosi3m46q-v0.1.3.js`)
+- 原版脚本:`E:\work\fmode-app\docs\03-interface\reporter-v3\status-report.py`(v11)
+
+> **历史文档**(已被本文取代,保留供追溯):
+> `reporter部署方案-20261020.md`、`容器上报标准部署包-20261020.md`