|
|
@@ -0,0 +1,886 @@
|
|
|
+# 新容器接入优化 · 对齐 lj-test 基线(2026-11-12)
|
|
|
+
|
|
|
+| 字段 | 值 |
|
|
|
+|---|---|
|
|
|
+| 用途 | 让**任意新容器**按本文步骤,达到与 `agent-node-lj-test` 一致的 App 回复效果 |
|
|
|
+| 参考实现 | `agent-node-lj-test`(`@zgdcmgnqcd:fmode.cn`) |
|
|
|
+| 依据 | 2026-11-12 逐项实测(真实接口调用 + 容器自述 + 代码取证),非推断 |
|
|
|
+| 配套 | `容器接入标准流程-SOP-v3-20261112.md`(体检流程)、`多用户共用容器-完整规范-20261112.md`(准入) |
|
|
|
+
|
|
|
+> **本文与 SOP v3 的关系**:SOP v3 讲「怎么体检、怎么改」,本文讲
|
|
|
+> **「改完怎么才算达标」**、**「为什么改完还是不生效」**、**「多人合用的坑」**,
|
|
|
+> 以及**一份可直接交给新容器执行的四阶段提示词**(第六节)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 一、目标状态:lj-test 的行为基线
|
|
|
+
|
|
|
+接入完成后,**以下 7 项必须全部满足**。任何一项不满足,App 里就会有相应的显示异常。
|
|
|
+
|
|
|
+| # | 能力 | 期望表现 | 实测方法 |
|
|
|
+|---|---|---|---|
|
|
|
+| 1 | **回复** | 收到消息后回正文 | App 里直接可见 |
|
|
|
+| 2 | **✅ reaction** | 每条消息发出后打 ✅ | `/sync` 读到 `m.reaction` |
|
|
|
+| 3 | **状态上报(事件驱动)** | turn 开始/结束**立即**发 HTTP | 轮询 `xlosi3m46q`,**5~10 秒内** status 变化 |
|
|
|
+| 4 | **心跳(保活)** | **3600s + 0~900s 抖动** | 10 分钟内 ≤1 次上报 |
|
|
|
+| 5 | **`currentSessions`** | 每会话 6 字段齐全 | 回读 `extra` |
|
|
|
+| 6 | **文件交付** | 正文末尾带「已上云」链接 | 让它产出一个文件 |
|
|
|
+| 7 | **自动入房** | 收到 invite 自动加入 | `joined_members` 里有它 |
|
|
|
+
|
|
|
+### 1.1 ⚠️ 最重要的一条:✅ ≠ 回合结束
|
|
|
+
|
|
|
+lj-test 的**原话**:
|
|
|
+
|
|
|
+> 「Matrix ✅(`m.reaction`):由 adapter 在 `send()` 完成后立即添加,
|
|
|
+> 表示**这条消息**已经处理完,**不代表对话轮次结束**。」
|
|
|
+
|
|
|
+| 信号 | 真实含义 | 谁能用 |
|
|
|
+|---|---|---|
|
|
|
+| `✅` reaction | **单条消息**投递完成 | 仅用于「消息已发出」 |
|
|
|
+| `extra.currentSessions[].status` 变 `idle` | **本轮**处理完成 | App 用这个判「回复结束」 |
|
|
|
+
|
|
|
+**如果一个长任务分多条发送,每条都会有一个 ✅。**
|
|
|
+把第一个 ✅ 当回合结束,App 会提前停止转圈,用户以为完成了、正文却还在输出。
|
|
|
+
|
|
|
+### 1.2 实测时序(lj-test,可作为新容器的对照)
|
|
|
+
|
|
|
+```
|
|
|
+T+0s 用户发送
|
|
|
+T+5s 容器上报 status=running ← 事件驱动
|
|
|
+T+7s 容器上报 status=idle ← 本轮结束
|
|
|
+```
|
|
|
+
|
|
|
+**结论:容器的状态上报足够快(5~7 秒)。** 如果新容器实测超过 30 秒,
|
|
|
+说明是上报机制有问题,不是"正常延迟"。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 二、Matrix 配置(达到 lj-test 效果的必需项)
|
|
|
+
|
|
|
+### 2.1 配置清单(`/opt/data/.env`)
|
|
|
+
|
|
|
+```bash
|
|
|
+# ---- 身份与连接 ----
|
|
|
+MATRIX_HOMESERVER=https://matrix.fmode.cn
|
|
|
+MATRIX_USER_ID=@xxx:fmode.cn
|
|
|
+MATRIX_ACCESS_TOKEN=xxx
|
|
|
+MATRIX_DEVICE_ID=xxx
|
|
|
+
|
|
|
+# ---- 必须开启 ----
|
|
|
+MATRIX_REACTIONS=true # ✅ 每条消息的完成标记(App 用它显示"消息已到")
|
|
|
+MATRIX_AUTO_THREAD=true # 建议开启
|
|
|
+
|
|
|
+# ---- 多人共用(关键)----
|
|
|
+GATEWAY_ALLOW_ALL_USERS=true # 全放开
|
|
|
+MATRIX_ALLOWED_USERS= # 🔴 必须留空,见 §5
|
|
|
+
|
|
|
+# ---- 调云函数用 ----
|
|
|
+FMODE_SESSION_TOKEN=r:xxx # 主人 token
|
|
|
+AGENT_ID=agent-xxx
|
|
|
+```
|
|
|
+
|
|
|
+### 2.2 准入判定顺序(实测,与直觉相反)
|
|
|
+
|
|
|
+```
|
|
|
+① MATRIX_ALLOWED_USERS 非空?
|
|
|
+ 是 → 只认名单,GATEWAY_ALLOW_ALL_USERS 被完全忽略
|
|
|
+ 否 → 看 ②
|
|
|
+② GATEWAY_ALLOW_ALL_USERS = true?
|
|
|
+ 是 → 全部放行
|
|
|
+ 否 → 拒绝
|
|
|
+```
|
|
|
+
|
|
|
+**只要白名单非空,ALLOW_ALL 就等于没开。** 这是最容易配错的地方。
|
|
|
+
|
|
|
+### 2.3 🔴 改完必须重启 gateway
|
|
|
+
|
|
|
+**这是本会话排查最久的一个坑,新容器必读。**
|
|
|
+
|
|
|
+实际案例(lj-test,2026-11-12):
|
|
|
+
|
|
|
+```
|
|
|
+插件文件 __init__.py 修改时间:13:36:15
|
|
|
+gateway 进程启动时间: 01:07:19 ← 早 12.5 小时
|
|
|
+```
|
|
|
+
|
|
|
+结果:**新代码一行都没生效**。日志里既没有成功记录、也没有失败记录 ——
|
|
|
+因为旧代码里根本没有那个函数,自然不会打任何日志。
|
|
|
+
|
|
|
+**判断方法(改完立刻做)**:
|
|
|
+
|
|
|
+```bash
|
|
|
+# 1. 看文件和进程谁新
|
|
|
+stat -c '%y %n' /opt/data/plugins/plugin-active-reporter/__init__.py
|
|
|
+ps -o lstart= -p $(pgrep -f "gateway run" | head -1)
|
|
|
+```
|
|
|
+
|
|
|
+**文件修改时间晚于进程启动时间 → 改动未加载,必须重启。**
|
|
|
+
|
|
|
+```bash
|
|
|
+# 2. 重启(二选一)
|
|
|
+s6-svc -r /run/service/gateway-default
|
|
|
+# 或
|
|
|
+/opt/hermes/.venv/bin/hermes gateway run --replace
|
|
|
+
|
|
|
+# 3. 回读进程环境,确认配置真的进去了
|
|
|
+PID=$(pgrep -f "gateway run" | tail -1)
|
|
|
+tr '\0' '\n' < /proc/$PID/environ | grep -E "MATRIX_ALLOWED|ALLOW_ALL|REACTIONS"
|
|
|
+```
|
|
|
+
|
|
|
+> ⚠️ 重启会**打断正在进行的对话**。批量改完一次重启,不要改一项重启一次。
|
|
|
+
|
|
|
+### 2.4 能力边界
|
|
|
+
|
|
|
+| 路径 | 权限 | 能做什么 |
|
|
|
+|---|---|---|
|
|
|
+| `/opt/hermes` | `root:root`,**不可写** | 只能读 |
|
|
|
+| `/opt/data` | 容器用户属主 | 插件、配置、脚本 |
|
|
|
+| `/etc/hosts`、`/etc/resolv.conf` | root 只读 | 改不了 |
|
|
|
+| `/run/s6/container_environment/` | root:root | **改不了**(见 §5.2) |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 三、状态上报协议
|
|
|
+
|
|
|
+### 3.1 🔴 两类上报必须分离(用户明确要求)
|
|
|
+
|
|
|
+| 类型 | 触发 | 间隔 | 内容 |
|
|
|
+|---|---|---|---|
|
|
|
+| **心跳(保活)** | 定时 | **3600s + 0~900s 抖动** | `heartbeatAt` + `extra.currentSessions` |
|
|
|
+| **消息状态** | **事件驱动** | turn 开始/结束**立即** | `status`、`currentTask`、`extra.currentSessions` |
|
|
|
+
|
|
|
+**为什么必须分开**:心跳是为了让 App 知道「容器还活着」,频率越低服务器开销越小;
|
|
|
+消息状态是用户体验,必须实时。**把两者合并会导致要么心跳太频繁、要么状态太慢。**
|
|
|
+
|
|
|
+> lj-test 实测:抖动在**进程启动时随机一次并保持**,不是每轮都变。
|
|
|
+
|
|
|
+### 3.2 `currentSessions` 结构
|
|
|
+
|
|
|
+放入 `extra` 字段(**不能作顶层字段,否则 HTTP 400**):
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "currentSessions": [
|
|
|
+ {
|
|
|
+ "sessionKey": "$approot_xxx 或 !roomId",
|
|
|
+ "userId": "@用户:fmode.cn",
|
|
|
+ "status": "running | idle",
|
|
|
+ "task": "本轮提问首行,60 字内",
|
|
|
+ "turnStartAt": 1794405380428,
|
|
|
+ "updatedAt": 1794405433842
|
|
|
+ }
|
|
|
+ ]
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+| 字段 | 来源 | 缺失后果 |
|
|
|
+|---|---|---|
|
|
|
+| `sessionKey` | thread_id 优先,退 chat_id | App 匹配不上,状态永远不显示 |
|
|
|
+| `userId` | `state.db.sessions.user_id` | 多人共用时 A 跑步影响 B |
|
|
|
+| `task` | 本轮用户消息首行 | 顶栏只显示「正在处理」 |
|
|
|
+| `updatedAt` | 写入时 epoch ms | App 无法判断是否过期 |
|
|
|
+
|
|
|
+### 3.3 新增:`com.fmode.turn_status` 事件(App 已支持)
|
|
|
+
|
|
|
+**目的**:App 拉云函数是 15 秒轮询,容器上报再快,用户也要等最多 15 秒才看到「转圈停止」。
|
|
|
+让容器在 turn 结束时**直接往房间推一个 Matrix 事件**,App 收到即刷新。
|
|
|
+
|
|
|
+**事件 A(首选)自定义类型 `com.fmode.turn_status`**:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "sessionKey": "$approot_xxx 或 !roomId",
|
|
|
+ "status": "idle"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**事件 B(兜底)** 若 adapter 不允许自定义类型,用 `m.room.message` 携带:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "msgtype": "m.text",
|
|
|
+ "body": "",
|
|
|
+ "fmode.turn_status": { "sessionKey": "$approot_xxx", "status": "idle" }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**发送时机**:`post_llm_call` 钩子里,`_session_mark_idle()` **之后**。
|
|
|
+
|
|
|
+**要求**:
|
|
|
+- 失败**只记日志**,不影响 turn 收尾与上报
|
|
|
+- 与 ✅ reaction **完全独立**,不得互相替代
|
|
|
+- `sessionKey` 用与 `currentSessions` 相同的值
|
|
|
+
|
|
|
+> ⚠️ App 侧已实现监听(`matrix-client.service.ts::onTurnStatus`),
|
|
|
+> **监听所有房间**,不限于当前打开的会话。
|
|
|
+> 但容器侧改动**必须重启 gateway 才生效**(见 §2.3)。
|
|
|
+
|
|
|
+### 3.4 `_thread_root()` 必须有兜底
|
|
|
+
|
|
|
+文件登记要求 `threadRoot` **必填**(`if (!threadRoot) return _fail(400, 'threadRoot required')`)。
|
|
|
+
|
|
|
+lj-test 的实现从 `state.db` 查 `thread_id`,**取不到就返回 None → 跳过登记 → 文件丢失**。
|
|
|
+
|
|
|
+```python
|
|
|
+def _thread_root(sid=None):
|
|
|
+ ...
|
|
|
+ row = con.execute("SELECT thread_id FROM sessions WHERE id=?", (sid,)).fetchone()
|
|
|
+ if not row or not row[0]:
|
|
|
+ return None # ← 这里会让文件永远无法归属到会话
|
|
|
+ return str(row[0])
|
|
|
+```
|
|
|
+
|
|
|
+**建议改为**:
|
|
|
+
|
|
|
+```python
|
|
|
+ thread_id = row[0] if row else None
|
|
|
+ if not thread_id:
|
|
|
+ # 一房间一会话下,房间号即会话标识,可安全兜底
|
|
|
+ return _chat_id_of(sid) or None
|
|
|
+ return str(thread_id)
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 四、文件与成果交付链路
|
|
|
+
|
|
|
+```
|
|
|
+容器产出文件
|
|
|
+ ↓
|
|
|
+① 调云函数 AlP56LCKFm 取 OBS 预签名 URL
|
|
|
+ ↓
|
|
|
+② PUT 上传到 OBS
|
|
|
+ ↓
|
|
|
+③ 调云函数 THXAOGKiPK { action: "registerFile", threadRoot, ... } 登记台账
|
|
|
+ ↓
|
|
|
+④ 正文末尾追加「已上云(可直接下载)」+ 链接(由 plugin-deliverable-inject 完成)
|
|
|
+ ↓
|
|
|
+App 右侧「成果」栏按会话显示
|
|
|
+```
|
|
|
+
|
|
|
+**登记参数**(`registerFile`):
|
|
|
+
|
|
|
+| 参数 | 必填 | 说明 |
|
|
|
+|---|---|---|
|
|
|
+| `threadRoot` | ✅ | **缺失直接 400** |
|
|
|
+| `agentId` | ✅ | 容器标识 |
|
|
|
+| `url` | ✅ | 公网可访问地址(`s3.fmode.cn` / `file-cloud.fmode.cn`) |
|
|
|
+| `fileName` / `fileSize` / `mimeType` | 建议 | 展示用 |
|
|
|
+| `uploadedBy` | 建议 | `agent`(容器产出)/ `user`(用户上传) |
|
|
|
+| `roomId` | 建议 | 仅作附带字段,**归属用的是 threadRoot** |
|
|
|
+
|
|
|
+**正文链接格式**(App 用正则识别):
|
|
|
+
|
|
|
+```
|
|
|
+(正文末尾空一行)
|
|
|
+**已上云(可直接下载)**
|
|
|
+- https://s3.fmode.cn/...
|
|
|
+```
|
|
|
+
|
|
|
+> 🔴 `threadRoot` 是**唯一归属键**,`roomId` 只是附带信息。
|
|
|
+> 用房间号去查 `listBySession` 会得到 0 条(实测)。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 五、多人共用:三个真实反面案例
|
|
|
+
|
|
|
+### 案例 A · 往 `.env` 加白名单不生效
|
|
|
+
|
|
|
+| 项 | 内容 |
|
|
|
+|---|---|
|
|
|
+| 现象 | 同事能进会话、能发消息,但容器不回复 |
|
|
|
+| 日志 | `WARNING gateway.run: Unauthorized user (ignored): Dropped a message from unrecognized matrix user (@xxx:fmode.cn)` |
|
|
|
+| 根因 1 | 改了 `.env` 但**没重启** gateway |
|
|
|
+| 根因 2 | 进程里的 `MATRIX_ALLOWED_USERS` 是空串 → 走了 ALLOW_ALL 分支,白名单是「死配置」 |
|
|
|
+| 解法 | 重启后**必须**回读 `/proc/<pid>/environ` 确认 |
|
|
|
+
|
|
|
+### 案例 B · s6 注入压过 `.env`(容器内无解)
|
|
|
+
|
|
|
+| 项 | 内容 |
|
|
|
+|---|---|
|
|
|
+| 现象 | 改了 `.env`、重启了,**仍然**被拒 |
|
|
|
+| 根因 | 平台在容器启动时把 `MATRIX_ALLOWED_USERS` 注入到 `/run/s6/container_environment/`,**优先级高于 `.env`** |
|
|
|
+| 证据 | 该文件 `root:root`,属主是 root,容器用户**无权删除** |
|
|
|
+| 已试失败的解法 | ① `.env` 追加用户 ② `ALLOW_ALL=true` ③ `.env` 置空串 ④ `.env` 整行删除 ⑤ 删 s6 文件 |
|
|
|
+| 解法 | **只能平台侧处理**:删除 `/run/s6/container_environment/MATRIX_ALLOWED_USERS` 后重启 |
|
|
|
+| 判断方法 | ```ls -l /run/s6/container_environment/ \| grep -i matrix``` 有输出即中招 |
|
|
|
+
|
|
|
+> 实测对照:`lj-test`(2026-10-17 建)**没有**这个注入;
|
|
|
+> `agent-lj`(2026-10-21 建)**有**。推测平台在 10-21 后改了创建流程。
|
|
|
+
|
|
|
+### 案例 C · ALLOW_ALL 开了但白名单非空
|
|
|
+
|
|
|
+| 项 | 内容 |
|
|
|
+|---|---|
|
|
|
+| 现象 | 配了 `GATEWAY_ALLOW_ALL_USERS=true`,陌生用户仍被拒 |
|
|
|
+| 根因 | `MATRIX_ALLOWED_USERS` 有值,判定短路在第一层,根本走不到 ALLOW_ALL |
|
|
|
+| 解法 | **把白名单置空**(不是清成空串,是确保进程里读到空) |
|
|
|
+
|
|
|
+### 5.1 多人共用的正确配置
|
|
|
+
|
|
|
+```bash
|
|
|
+GATEWAY_ALLOW_ALL_USERS=true
|
|
|
+MATRIX_ALLOWED_USERS= # ← 留空
|
|
|
+```
|
|
|
+
|
|
|
+**效果**:主人、同公司成员、未来新成员、甚至其他公司的人都能用。
|
|
|
+**代价**:跨公司用户只要知道容器 mxid 也能用(用户已明确接受)。
|
|
|
+
|
|
|
+### 5.2 多房间与多人共用
|
|
|
+
|
|
|
+- 每个用户 × 每个容器 = **一个 DM 房间**(provision 负责建)
|
|
|
+- 容器 **单条 sync 连接**监听全部房间,**多房间开销几乎为零**
|
|
|
+- 真正的瓶颈是**并发 turn**:N 人同时问 = N 倍 token
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 五之二、插件移植:从 lj-test 复制整套能力
|
|
|
+
|
|
|
+> 🔴 **前置判断(2026-11-12 实测,务必先做)**
|
|
|
+>
|
|
|
+> 新容器很可能**根本没有装任何 Fmode 插件**。实测 `agent-node-app-lj-test-v1`:
|
|
|
+>
|
|
|
+> ```
|
|
|
+> /opt/data/plugins/ → 目录不存在
|
|
|
+> plugin-active-reporter → 全盘零结果
|
|
|
+> currentSessions / turnStartAt / sessionKey → 全盘零结果
|
|
|
+> com.fmode.turn_status → 零结果
|
|
|
+> ```
|
|
|
+>
|
|
|
+> 这种容器只有 Hermes 原生 `platforms/matrix` 能力:能收发消息,
|
|
|
+> 但**不上报状态、没有会话表、没有文件交付**。
|
|
|
+> 此时要做的不是「改配置」,而是**整套插件移植**。
|
|
|
+
|
|
|
+### 5.2.1 lj-test 的插件清单(实测导出)
|
|
|
+
|
|
|
+| 插件 | 文件 | 必需 | 作用 |
|
|
|
+|---|---|---|---|
|
|
|
+| `plugin-active-reporter` | `__init__.py`(32,277 B)+ `plugin.yaml` | ✅ **核心** | 状态上报、`currentSessions`、turn_status |
|
|
|
+| `plugin-deliverable-inject` | `__init__.py`(5,410 B)+ `plugin.yaml` | ✅ | 文件交付链接注入正文 |
|
|
|
+| `plugin-turn-end` | `__init__.py`(13,202 B) | ⭕ 可选 | 回合结束信号(lj-test 中处于 `disabled`) |
|
|
|
+| `plugin-media-text-merge` | `__init__.py`(9,549 B) | ⭕ 可选 | 图文合并(lj-test 中未启用) |
|
|
|
+| `plugin-fmode/plugin` | 整套目录 | ⭕ 可选 | 通用能力(含 `poor_guard.py` 钩子) |
|
|
|
+| `plugin-tips-cn` | 整套目录 | ⭕ 可选 | 中文提示词增强 |
|
|
|
+| `plugin-topup-guard` | `__init__.py` | ⭕ 可选 | 充值保护 |
|
|
|
+
|
|
|
+> ⚠️ `plugin-active-reporter` 目录里还有多个 `.bak-*` 备份文件(累计 100+ KB),
|
|
|
+> **复制时排除**,只取 `__init__.py` 与 `plugin.yaml`。
|
|
|
+
|
|
|
+### 5.2.2 `config.yaml` 的注册方式
|
|
|
+
|
|
|
+插件**不是放进目录就生效**,必须在 `config.yaml` 的 `plugins.enabled` 里登记。
|
|
|
+lj-test 的实际配置(节选):
|
|
|
+
|
|
|
+```yaml
|
|
|
+plugins:
|
|
|
+ enabled:
|
|
|
+ - plugin-active-reporter
|
|
|
+ - plugin-deliverable-inject
|
|
|
+ - plugin-fmode/plugin
|
|
|
+ - plugin-tips-cn
|
|
|
+ - plugin-topup-guard
|
|
|
+ - wecom-platform
|
|
|
+ entries:
|
|
|
+ plugin-fmode/plugin:
|
|
|
+ allow_tool_override: true
|
|
|
+ plugin-deliverable-inject:
|
|
|
+ allow_tool_override: false
|
|
|
+ disabled:
|
|
|
+ - plugin-turn-end
|
|
|
+hooks:
|
|
|
+ on_session_end:
|
|
|
+ - command: python3 /opt/data/plugins/plugin-fmode/plugin/hooks/poor_guard.py
|
|
|
+ timeout: 30
|
|
|
+```
|
|
|
+
|
|
|
+**最小可用组合**:只启用 `plugin-active-reporter` + `plugin-deliverable-inject`。
|
|
|
+
|
|
|
+### 5.2.3 移植步骤
|
|
|
+
|
|
|
+```bash
|
|
|
+# 1. 在源容器(lj-test)打包(排除备份)
|
|
|
+cd /opt/data/plugins
|
|
|
+tar czf /tmp/fmode-plugins.tgz \
|
|
|
+ --exclude='*.bak-*' \
|
|
|
+ plugin-active-reporter plugin-deliverable-inject
|
|
|
+
|
|
|
+# 2. 传到目标容器(用容器间可用的通道,或经 OBS)
|
|
|
+# 目标容器落地后:
|
|
|
+cd /opt/data
|
|
|
+tar xzf fmode-plugins.tgz -C /opt/data/plugins/ # 确保 plugins 目录已存在
|
|
|
+ls -l /opt/data/plugins/plugin-active-reporter/
|
|
|
+
|
|
|
+# 3. 编辑 config.yaml,把插件名加进 plugins.enabled
|
|
|
+# 步骤见 §5.2.2
|
|
|
+
|
|
|
+# 4. 重启(必须!见 §2.3)
|
|
|
+s6-svc -r /run/service/gateway-default
|
|
|
+
|
|
|
+# 5. 验证插件真的加载了
|
|
|
+PID=$(pgrep -f "gateway run" | tail -1)
|
|
|
+ps -o lstart= -p $PID # 进程启动时间应晚于文件 mtime
|
|
|
+grep -iE "registered:|plugin-active-reporter" /opt/data/logs/gateway.log | tail -10
|
|
|
+```
|
|
|
+
|
|
|
+> 🔴 **第 5 步是关键**:日志里必须出现 `registered: plugin-active-reporter`。
|
|
|
+> 没有这行 = 没加载成功,后面所有验证都不必做。
|
|
|
+
|
|
|
+### 5.2.4 依赖与环境变量
|
|
|
+
|
|
|
+`plugin-active-reporter` 需要:
|
|
|
+
|
|
|
+| 依赖 | 说明 |
|
|
|
+|---|---|
|
|
|
+| `FMODE_SESSION_TOKEN` | 调云函数 `xlosi3m46q` 的身份(主人 token) |
|
|
|
+| `AGENT_ID` | 容器自己的 agentId |
|
|
|
+| `MATRIX_ACCESS_TOKEN` / `MATRIX_HOMESERVER` | 推 `turn_status` 事件用(新代码会从 `.env` 读) |
|
|
|
+| Python 标准库 | `sqlite3` / `urllib` / `json`(**无需 pip 安装**) |
|
|
|
+
|
|
|
+**在目标容器 `.env` 里补齐**:
|
|
|
+
|
|
|
+```bash
|
|
|
+FMODE_SESSION_TOKEN=r:xxxx # 主人的平台 token
|
|
|
+AGENT_ID=agent-node-app-lj-test-v1 # 目标容器自己的 agentId
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 五之三、🔴 先判形态:五类容器,走五条不同的路
|
|
|
+
|
|
|
+> **这是本文最重要的一节。** 接入前**必须先判定容器属于哪一类**,
|
|
|
+> 否则会按错误的路径操作,白折腾一轮(2026-11-12 在 v1 上实际踩过)。
|
|
|
+
|
|
|
+| 形态 | 特征(怎么判) | 走哪条路 |
|
|
|
+|---|---|---|
|
|
|
+| **A · 完整**(lj-test) | 有插件、`capability_check decision=allow`、上报正常 | 只核对配置 |
|
|
|
+| **B · 有插件无授权** | 插件名在日志里出现,但 `decision=deny` | **补 `granted_capabilities`**(见 §六 阶段 2 场景 0.5) |
|
|
|
+| **C · 无插件** | `ls /opt/data/plugins/` 报 `No such file or directory`;全盘搜 `plugin-active-reporter` 零结果 | **插件移植**(见 §五之二) |
|
|
|
+| **D · 有插件但版本旧** | 有 `plugin-active-reporter`,但 grep 不到 `com.fmode.turn_status` / `userId` | 覆盖为新版(重新解包) |
|
|
|
+| **E · 准入被 s6 锁死** | `ls /run/s6/container_environment/ \| grep MATRIX_ALLOWED` 有输出 | **只能平台侧处理**(见 §5.2) |
|
|
|
+
|
|
|
+### 判定命令(一次跑完,30 秒)
|
|
|
+
|
|
|
+```bash
|
|
|
+echo "=== 形态判定 ==="
|
|
|
+# C 的判断:插件目录
|
|
|
+ls -d /opt/data/plugins 2>/dev/null && echo "有 plugins 目录" || echo "C: 无 plugins 目录"
|
|
|
+find /opt/data/plugins -maxdepth 1 -name 'plugin-*' -type d 2>/dev/null | head -5
|
|
|
+
|
|
|
+# B 的判断:能力授权
|
|
|
+grep -h "capability_check" /opt/data/logs/*.log 2>/dev/null | tail -4
|
|
|
+
|
|
|
+# D 的判断:插件版本
|
|
|
+grep -c "com.fmode.turn_status" /opt/data/plugins/plugin-active-reporter/__init__.py 2>/dev/null
|
|
|
+
|
|
|
+# E 的判断:s6 注入
|
|
|
+ls -l /run/s6/container_environment/ 2>/dev/null | grep -i matrix
|
|
|
+```
|
|
|
+
|
|
|
+**六类坑的对照(本文档全部覆盖)**:
|
|
|
+
|
|
|
+| 坑 | 现象 | 在哪解决 |
|
|
|
+|---|---|---|
|
|
|
+| K1 无插件 | 目录都不存在 | §五之三 形态 C |
|
|
|
+| **K2 缺能力授权** | `decision=deny`,插件装了等于没装 | §六 阶段 2 场景 0.5 |
|
|
|
+| K3 改配置没重启 | 授权不生效 | §六 阶段 2「四步法」第 ④ 步 |
|
|
|
+| K4 `yaml.safe_dump` 重写全文件 | 无关缩进被改 | §六 阶段 2「四步法」注意事项 |
|
|
|
+| K5 `s6-svc` 不在 PATH | `command not found` | 用 `/command/s6-svc` |
|
|
|
+| K6 系统 python 无 yaml | `ModuleNotFoundError` | 用 `/opt/hermes/.venv/bin/python3` |
|
|
|
+| **K7 自我升级污染对话** | 回复里出现「💾 修行成果」,甚至顶替答案 | `auxiliary.background_review.enabled: false` |
|
|
|
+| K8 无 `MATRIX_ACCESS_TOKEN` | 推 `turn_status` 可能失败 | §六 阶段 2 场景 0.6 |
|
|
|
+| K9 插件包 URL 拿不到 | 回复里被截断 | 让对方传固定路径,或用户浏览器取 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 六、完整提示词(交给新容器执行)
|
|
|
+
|
|
|
+> **使用方法**:把下面四段**按顺序**发给新容器,每段等它**交齐证据**后再发下一段。
|
|
|
+> 不要在阶段 1 之前就让它改代码。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 阶段 1 · 自查(只读,禁止修改)
|
|
|
+
|
|
|
+```text
|
|
|
+你现在要对「接入 Fmode App」做一次**只读自查**。
|
|
|
+
|
|
|
+🔴 硬性约束:
|
|
|
+- 只读:不许改任何文件、不许重启、不许写库、不许上报
|
|
|
+- 每条都必须贴**实际命令 + 原始输出**,不许写「应该」「大概」「可能」
|
|
|
+- 查不到就明确写「不存在 / 没在跑 / 无法确认」,不许推测
|
|
|
+
|
|
|
+请逐条回答,最后汇总成一张表:
|
|
|
+
|
|
|
+【A. 进程与代码】
|
|
|
+1. gateway 进程在跑吗?PID 多少?启动时间是什么?
|
|
|
+2. 插件目录 /opt/data/plugins/ 下有哪些插件?
|
|
|
+3. plugin-active-reporter/__init__.py 的修改时间(mtime)是什么?
|
|
|
+ → 对比 A1 的进程启动时间:**文件比进程新就说明改动没生效**
|
|
|
+
|
|
|
+【A0. 🔴 先判断:你装了 Fmode 插件吗?(决定后面走哪条路)】
|
|
|
+ ls -la /opt/data/plugins/ 2>&1 | head -20
|
|
|
+ find / -name "plugin-active-reporter" -maxdepth 6 2>/dev/null | head -3
|
|
|
+ grep -rn "currentSessions" /opt/data 2>/dev/null | head -3
|
|
|
+ grep -rn "com.fmode.turn_status" /opt/data 2>/dev/null | head -3
|
|
|
+
|
|
|
+ 明确回答:
|
|
|
+ ① /opt/data/plugins/ 目录存在吗?
|
|
|
+ ② 有没有 plugin-active-reporter / plugin-deliverable-inject?
|
|
|
+ ③ 全盘搜索 currentSessions、turn_status 有没有命中?
|
|
|
+
|
|
|
+ → 若三条都是「不存在/零结果」:你**没有装 Fmode 插件**,
|
|
|
+ 后续不是"改配置"的问题,而要**从 lj-test 移植整套插件**(见文档 §五之二)。
|
|
|
+ 请在报告里明确标注「类型:无插件容器,需移植」。
|
|
|
+ → 若有插件:继续往下查具体配置差异。
|
|
|
+
|
|
|
+【B. Matrix 配置】
|
|
|
+4. 回读进程环境:
|
|
|
+ PID=$(pgrep -f "gateway run" | tail -1)
|
|
|
+ tr '\0' '\n' < /proc/$PID/environ | grep -E "MATRIX_|GATEWAY_ALLOW"
|
|
|
+ 明确回答:MATRIX_ALLOWED_USERS 是空 / 有值 / 不存在?ALLOW_ALL 是什么值?
|
|
|
+5. /opt/data/.env 里这几行的值(值可脱敏):
|
|
|
+ MATRIX_HOMESERVER / MATRIX_USER_ID / MATRIX_REACTIONS /
|
|
|
+ MATRIX_ALLOWED_USERS / GATEWAY_ALLOW_ALL_USERS
|
|
|
+6. ls -l /run/s6/container_environment/ | grep -i matrix
|
|
|
+ → 有 MATRIX_ALLOWED_USERS 吗?(有 = 平台注入,容器内改不动)
|
|
|
+
|
|
|
+【C. 上报机制】
|
|
|
+7. 心跳间隔配置是多少秒?有无抖动?代码在哪一行?
|
|
|
+8. turn 开始/结束的上报是**事件驱动**还是等心跳?
|
|
|
+ 给出触发函数名 + 文件 + 行号
|
|
|
+9. extra.currentSessions 里每条记录的 6 个字段(sessionKey/userId/status/task/
|
|
|
+ turnStartAt/updatedAt)是否齐全?缺哪个?
|
|
|
+10. 有没有发送 com.fmode.turn_status 事件?代码在哪?
|
|
|
+
|
|
|
+【D. 文件交付】
|
|
|
+11. plugin-deliverable-inject 存在吗?
|
|
|
+12. _thread_root() 函数在哪?取不到 thread 时返回什么?
|
|
|
+13. 最近一次成功登记文件是什么时候?日志贴出来
|
|
|
+
|
|
|
+【E. 准入】
|
|
|
+14. 日志里最近 24 小时有没有 "Unauthorized user"?出现几次?涉及哪些 mxid?
|
|
|
+
|
|
|
+输出格式(必须用表格):
|
|
|
+| 检查项 | 当前值 | 达标? | 依据(命令输出) |
|
|
|
+达标 = ✅ / 未达标 = ❌ / 无法确认 = ❓
|
|
|
+```
|
|
|
+
|
|
|
+**通过条件**:14 条全部有实际输出,表格完整,没有「应该/大概」。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 阶段 2 · 按差距修改
|
|
|
+
|
|
|
+```text
|
|
|
+基于阶段 1 的自查表,**只修改标为 ❌ 的项**。✅ 的不要动。
|
|
|
+
|
|
|
+🔴 硬性约束:
|
|
|
+- 改之前先备份:cp <文件> <文件>.bak-$(date +%Y%m%d-%H%M%S)
|
|
|
+- /opt/hermes 是只读的,改不了;只能改 /opt/data 下的
|
|
|
+- 每改一项,说明:改了哪个文件、哪一行、改前改后是什么
|
|
|
+- 不许改与目标无关的代码
|
|
|
+
|
|
|
+需要修改的场景与对应做法:
|
|
|
+
|
|
|
+【场景 0】🔴 阶段 1 判定为「无插件容器」(最常见)
|
|
|
+ 你没有 plugin-active-reporter,状态上报整条链路都不存在。
|
|
|
+
|
|
|
+ **做法:从 lj-test 移植整套插件**(详见文档 §五之二)
|
|
|
+
|
|
|
+ ① 让 lj-test 打包并导出(把下面这段发给 lj-test):
|
|
|
+ cd /opt/data/plugins && tar czf /tmp/fmode-plugins.tgz \
|
|
|
+ --exclude='*.bak-*' plugin-active-reporter plugin-deliverable-inject
|
|
|
+ 然后把 tgz 用 OBS 或容器间通道传到本容器。
|
|
|
+
|
|
|
+ ② 本容器解包:
|
|
|
+ mkdir -p /opt/data/plugins
|
|
|
+ tar xzf <路径>/fmode-plugins.tgz -C /opt/data/plugins/
|
|
|
+ ls -l /opt/data/plugins/plugin-active-reporter/__init__.py
|
|
|
+
|
|
|
+ ③ 在 config.yaml 的 plugins.enabled 里登记:
|
|
|
+ - plugin-active-reporter
|
|
|
+ - plugin-deliverable-inject
|
|
|
+
|
|
|
+ ④ 补齐 .env 环境变量:
|
|
|
+ FMODE_SESSION_TOKEN=r:xxx # 主人的平台 token
|
|
|
+ AGENT_ID=<本容器自己的 agentId>
|
|
|
+
|
|
|
+ ⑤ 重启并确认加载:
|
|
|
+ s6-svc -r /run/service/gateway-default
|
|
|
+ sleep 10
|
|
|
+ grep -iE "registered:.*(reporter|deliverable)" /opt/data/logs/gateway.log | tail -5
|
|
|
+ → **必须看到 registered 行**,否则插件没加载
|
|
|
+
|
|
|
+【场景 0.5】🔴 插件已装但被拒绝授权(v1 实测踩过,最容易漏)
|
|
|
+ 现象:日志里能查到插件名,但每次 turn 都被拒:
|
|
|
+ capability_check plugin=plugin-active-reporter capability=tools.override decision=deny
|
|
|
+ evidence=not granted
|
|
|
+ → **插件装了等于没装**,状态照样上报不出去。
|
|
|
+
|
|
|
+ 根因:Hermes 是「默认拒绝」设计,必须在 config.yaml 显式授权。
|
|
|
+ `plugins.enabled` 只负责「加载」,`plugins.entries.<名>.granted_capabilities` 才负责「放权」。
|
|
|
+
|
|
|
+ 修法(config.yaml):
|
|
|
+ plugins:
|
|
|
+ entries:
|
|
|
+ plugin-active-reporter:
|
|
|
+ granted_capabilities:
|
|
|
+ - tools.override
|
|
|
+ plugin-deliverable-inject:
|
|
|
+ granted_capabilities:
|
|
|
+ - tools.override
|
|
|
+
|
|
|
+【场景 0.6】🔴 关闭 background_review(解决「自我升级日志污染对话」)
|
|
|
+ 现象(用户实测截图):问「把完整下载地址贴出来」,
|
|
|
+ 回复却是「这一轮的修行成果:🔧 给「multi-artifact-delivery」上了个补丁」,
|
|
|
+ 用户的问题完全没被回答;有时还重复发送上一轮发过的文件。
|
|
|
+
|
|
|
+ 根因:Hermes 每轮结束会跑 background review,
|
|
|
+ 通过 `background_review_callback` 把摘要**发到对话里**
|
|
|
+ (`/opt/hermes/agent/background_review.py:1148-1153`)。
|
|
|
+
|
|
|
+ 修法(config.yaml,**官方开关**,不改只读源码):
|
|
|
+ auxiliary:
|
|
|
+ background_review:
|
|
|
+ enabled: false
|
|
|
+ 关掉后复盘不会启动 → 那条 💾 消息从源头就不产生。
|
|
|
+
|
|
|
+ 验证:发一句闲聊,回复里**不得出现** 💾 / 修行 / 经验值 / 复盘。
|
|
|
+
|
|
|
+【config.yaml 修改四步法】(上面几个场景都按这个执行)
|
|
|
+
|
|
|
+ 🔴 四个必踩的坑,全在这四步里避掉:
|
|
|
+
|
|
|
+ ① 备份
|
|
|
+ cp /opt/data/config.yaml /opt/data/config.yaml.bak-$(date +%Y%m%d-%H%M%S)
|
|
|
+
|
|
|
+ ② 用 **venv 的 python** 改(系统 python 没有 yaml 模块 → ModuleNotFoundError)
|
|
|
+ /opt/hermes/.venv/bin/python3 - <<'PY'
|
|
|
+ import io, yaml
|
|
|
+ p = '/opt/data/config.yaml'
|
|
|
+ d = yaml.safe_load(io.open(p, encoding='utf-8'))
|
|
|
+
|
|
|
+ # --- 按实际需要选一段 ---
|
|
|
+ # (a) 插件登记
|
|
|
+ pl = d.setdefault('plugins', {})
|
|
|
+ en = pl.setdefault('enabled', [])
|
|
|
+ for n in ['plugin-active-reporter', 'plugin-deliverable-inject']:
|
|
|
+ if n not in en: en.append(n)
|
|
|
+ # (b) 能力授权(关键!)
|
|
|
+ ent = pl.setdefault('entries', {})
|
|
|
+ for n in ['plugin-active-reporter', 'plugin-deliverable-inject']:
|
|
|
+ gc = ent.setdefault(n, {}).setdefault('granted_capabilities', [])
|
|
|
+ if 'tools.override' not in gc: gc.append('tools.override')
|
|
|
+ # (c) 关复盘
|
|
|
+ d.setdefault('auxiliary', {}).setdefault('background_review', {})['enabled'] = False
|
|
|
+
|
|
|
+ io.open(p, 'w', encoding='utf-8').write(
|
|
|
+ yaml.safe_dump(d, allow_unicode=True, sort_keys=False, default_flow_style=False))
|
|
|
+ PY
|
|
|
+
|
|
|
+ ③ 校验 + 核对 diff
|
|
|
+ /opt/hermes/.venv/bin/python3 -c "import yaml,io;d=yaml.safe_load(io.open('/opt/data/config.yaml',encoding='utf-8'));print('top keys',len(d));print('entries',d.get('plugins',{}).get('entries'));print('bg',d.get('auxiliary',{}).get('background_review'))"
|
|
|
+ diff /opt/data/config.yaml.bak-* /opt/data/config.yaml | head -40
|
|
|
+
|
|
|
+ ⚠️ **K4 注意**:`yaml.safe_dump` 会**重写整个文件**,
|
|
|
+ 可能把 `custom_providers` 之类的缩进也规范化掉。
|
|
|
+ 结构等价但格式变了 —— 用 diff 确认「只出现预期的新增行」。
|
|
|
+ 若不能接受,改用备份还原后**手工**加那几行。
|
|
|
+
|
|
|
+ ④ 重启(**必须**,改 config 不重启等于没改)
|
|
|
+ 🔴 **K5**:`s6-svc` 不在 PATH,必须用绝对路径
|
|
|
+ /command/s6-svc -r /run/service/gateway-default
|
|
|
+ sleep 12
|
|
|
+ pgrep -af "gateway run"
|
|
|
+
|
|
|
+【场景 1】MATRIX_ALLOWED_USERS 有值(多人共用打不通)
|
|
|
+ 目标:GATEWAY_ALLOW_ALL_USERS=true 且 MATRIX_ALLOWED_USERS 为空
|
|
|
+ 步骤:
|
|
|
+ sed -i 's|^MATRIX_ALLOWED_USERS=.*|MATRIX_ALLOWED_USERS=|' /opt/data/.env
|
|
|
+ # 先尝试替换,没有这一行就追加(用 if 判断,避免行首的 OR 运算符)
|
|
|
+ if grep -q "^GATEWAY_ALLOW_ALL_USERS=" /opt/data/.env; then
|
|
|
+ sed -i 's|^GATEWAY_ALLOW_ALL_USERS=.*|GATEWAY_ALLOW_ALL_USERS=true|' /opt/data/.env
|
|
|
+ else
|
|
|
+ echo "GATEWAY_ALLOW_ALL_USERS=true" >> /opt/data/.env
|
|
|
+ fi
|
|
|
+ ⚠️ 若 /run/s6/container_environment/MATRIX_ALLOWED_USERS 存在:
|
|
|
+ 容器内无解,**停止**,报告「需要平台侧删除该文件」
|
|
|
+
|
|
|
+【场景 2】turn 状态不是事件驱动(状态更新慢)
|
|
|
+ 找到 pre_llm_call / post_llm_call 钩子,确认里面有立即发 HTTP 的调用
|
|
|
+ 若没有,补上;若只有心跳,说明上报机制需要改
|
|
|
+
|
|
|
+【场景 3】currentSessions 字段不全
|
|
|
+ 补齐 6 字段,见文档 §3.2
|
|
|
+
|
|
|
+【场景 4】没有 turn_status 事件
|
|
|
+ 在 post_llm_call 里、_session_mark_idle() 之后,加:
|
|
|
+ try:
|
|
|
+ _push_turn_status(session_key, "idle")
|
|
|
+ except Exception as e:
|
|
|
+ log("turn_status failed: %s" % e) # 失败不影响主流程
|
|
|
+ 事件类型 com.fmode.turn_status,内容 {"sessionKey": key, "status": "idle"}
|
|
|
+
|
|
|
+【场景 5】_thread_root 取不到就返回 None(文件会丢)
|
|
|
+ 改为取不到时退回 _chat_id_of(sid),见文档 §3.4
|
|
|
+
|
|
|
+【改完之后必须做(缺一不可)】
|
|
|
+ ① 重启:s6-svc -r /run/service/gateway-default
|
|
|
+ ② 等 10 秒,确认进程起来了:pgrep -f "gateway run"
|
|
|
+ ③ 回读新进程环境:
|
|
|
+ PID=$(pgrep -f "gateway run" | tail -1)
|
|
|
+ ps -o lstart= -p $PID
|
|
|
+ tr '\0' '\n' < /proc/$PID/environ | grep -E "MATRIX_ALLOWED|ALLOW_ALL"
|
|
|
+ ④ 确认 A1 的「文件比进程新」问题已消除(进程启动时间现在应晚于文件 mtime)
|
|
|
+
|
|
|
+贴出以上四步的原始输出。不许只写「已重启」。
|
|
|
+```
|
|
|
+
|
|
|
+**通过条件**:每项修改都有 diff 说明,四步验证都有原始输出。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 阶段 3 · 自测审核
|
|
|
+
|
|
|
+```text
|
|
|
+现在做**自测**。每项都要贴实测输出,全绿才能进入联调。
|
|
|
+
|
|
|
+🔴 不许说「应该成功了」「理论上没问题」。
|
|
|
+
|
|
|
+【测试 1】DNS 解析(影响每轮响应速度)
|
|
|
+python3 -c "import socket,time;t=time.time();socket.getaddrinfo('api.fmode.cn',443,socket.AF_UNSPEC);print('解析耗时=%.0f ms' % ((time.time()-t)*1000))"
|
|
|
+通过标准:< 100 ms(> 1000ms 说明有 DNS 卡顿,需先修)
|
|
|
+
|
|
|
+【测试 2】准入门槛
|
|
|
+回读进程环境,确认 MATRIX_ALLOWED_USERS 为空 且 ALLOW_ALL=true
|
|
|
+再用一个**不存在的** mxid 验证逻辑:
|
|
|
+grep -n "_principal_authorized" -A 20 <authz 文件>
|
|
|
+贴出判定分支代码,说明「陌生用户会走到哪一支」
|
|
|
+
|
|
|
+【测试 3】✅ reaction
|
|
|
+让它回一条消息,然后用 /sync 或 /messages 读该房间事件
|
|
|
+通过标准:能看到 m.reaction,key 为 ✅
|
|
|
+
|
|
|
+【测试 4】状态上报时效(最关键)
|
|
|
+让一个外部脚本每秒轮询 xlosi3m46q 读你的 currentSessions,
|
|
|
+同时发给容器一条消息。
|
|
|
+通过标准:status 从 none → running → idle 的整个过程在 **10 秒内**完成
|
|
|
+(lj-test 基线是 5s 报 running、7s 报 idle)
|
|
|
+
|
|
|
+【测试 5】心跳频率
|
|
|
+观察 10 分钟(无 turn 活动)
|
|
|
+通过标准:上报请求 ≤ 1 次
|
|
|
+
|
|
|
+【测试 6】currentSessions 字段
|
|
|
+发一条消息,回读 extra.currentSessions
|
|
|
+通过标准:6 字段齐全,userId 是你自己的 mxid
|
|
|
+
|
|
|
+【测试 7】turn_status 事件
|
|
|
+发一条消息,读该房间事件
|
|
|
+通过标准:能看到 com.fmode.turn_status,内容含 sessionKey 和 status=idle
|
|
|
+若看不到 → 回到阶段 2 检查「是否真的重启了」
|
|
|
+
|
|
|
+【测试 8】文件交付
|
|
|
+让它产出一个文件
|
|
|
+通过标准:正文末尾有「已上云(可直接下载)」+ https 链接
|
|
|
+且 listBySession(threadRoot=你的会话id) 能查到这条
|
|
|
+
|
|
|
+【测试 9】🔴 插件能力授权(v1 实测踩过:装了不生效)
|
|
|
+grep -h "capability_check" /opt/data/logs/*.log | tail -10
|
|
|
+通过标准:
|
|
|
+ - 最近一次 capability_check 必须是 `decision=allow`,
|
|
|
+ 且 evidence=`granted_capabilities`
|
|
|
+ (🔴 日志里的关键字是 **allow**,不是 grant —— 源码写的是
|
|
|
+ "allow" if allowed else "deny",按 grant 去 grep 会永远是 0)
|
|
|
+ - **只判定最近一次**:历史日志里会累计大量重启前的 `deny`,
|
|
|
+ 不要用 `grep -c deny` 的总数判断(v1 实测:185 条 deny 全是重启前的历史)
|
|
|
+若最近一次仍是 deny → 回到阶段 2【场景 0.5】补 granted_capabilities,
|
|
|
+ 并确认`/command/s6-svc -r` 真的执行了(改配置不重启不生效)
|
|
|
+
|
|
|
+【测试 10】🔴 对话纯净度(不许自我升级日志污染回答)
|
|
|
+发一句闲聊,例如:「今天天气不错」
|
|
|
+通过标准:回复里**不得出现**以下任一:
|
|
|
+ 💾、修行、经验值、复盘、自我进化、闭关、修为
|
|
|
+若出现 → 回到阶段 2【场景 0.6】关闭 background_review,并确认已重启
|
|
|
+
|
|
|
+输出格式:
|
|
|
+| 测试 | 实测输出 | 通过? |
|
|
|
+全部 ✅ 才能说「自测通过」。
|
|
|
+```
|
|
|
+
|
|
|
+**通过条件**:**10 项**全绿,每项附原始输出。有任一项 ❌ 则回到阶段 2。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 阶段 4 · 联调实测(App 侧验收)
|
|
|
+
|
|
|
+```text
|
|
|
+容器侧自测已通过。现在进行 App 侧联调。
|
|
|
+
|
|
|
+🔴 这一阶段由 App 侧用固定脚本回读验证,**不采信容器自述**。
|
|
|
+
|
|
|
+请在 App 里(或让用户在 App 里)完成以下操作,同时自己贴出对应的容器侧证据:
|
|
|
+
|
|
|
+【联调 1】基本收发
|
|
|
+在 App 新建会话 → 发一句「请只回复两个字:收到」
|
|
|
+容器侧证据:贴出这一轮的 turn 开始/结束日志、上报日志
|
|
|
+
|
|
|
+【联调 2】左侧转圈时效
|
|
|
+观察 App 左侧会话列表的加载圆点
|
|
|
+通过标准:容器回复完成后 **几秒内**停止(不是 30 秒以上)
|
|
|
+容器侧证据:turn_status 事件的发送日志 + 事件 id
|
|
|
+
|
|
|
+【联调 3】会话隔离
|
|
|
+在 App 里两个不同会话先后发消息
|
|
|
+通过标准:只有发送的那条会话转圈,其他不转
|
|
|
+容器侧证据:currentSessions 里两条会话各自的 sessionKey 不同
|
|
|
+
|
|
|
+【联调 4】文件交付
|
|
|
+让它做一个文件并出示
|
|
|
+通过标准:App 右侧「成果」栏能看到,刷新后仍在
|
|
|
+
|
|
|
+【联调 5】多人共用(如适用)
|
|
|
+让另一个账号(同公司)发消息
|
|
|
+通过标准:容器正常回复,日志无 Unauthorized
|
|
|
+
|
|
|
+全部通过后,输出一份「接入完成报告」,包含:
|
|
|
+- 容器 mxid / agentId
|
|
|
+- 阶段 1 表格的最终版本(全部 ✅)
|
|
|
+- 阶段 3 八项测试结果
|
|
|
+- 本次修改的文件清单与备份路径
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 七、故障速查
|
|
|
+
|
|
|
+| 你看到的 | 最可能原因 | 怎么确认 |
|
|
|
+|---|---|---|
|
|
|
+| 同事发消息不回复 | 准入被拦 | 日志找 `Unauthorized user` |
|
|
|
+| 改了配置没效果 | **没重启** / s6 注入覆盖 | 比对文件 mtime 与进程启动时间 |
|
|
|
+| App 左侧转圈停不下来 | 容器没发 `turn_status` 或没上报 idle | 读房间事件 + 轮询 `xlosi3m46q` |
|
|
|
+| 状态显示「正在执行」但实际空闲 | `currentSessions` 有残留 running | 回读 `extra`,看 updatedAt 多旧 |
|
|
|
+| 文件回复里没有下载链接 | `_thread_root()` 返回 None 跳过了登记 | grep 日志 `SKIP register` |
|
|
|
+| 所有历史会话同时转圈 | 容器级 status=running 被当会话级用 | App 侧问题,非容器 |
|
|
|
+| 回复很慢(7~9 秒) | DNS 卡顿 | §阶段3 测试 1 |
|
|
|
+| **插件装了没反应** | **缺 `granted_capabilities` 授权**(K2) | `grep capability_check` 看是否 `deny` |
|
|
|
+| **回复里出现「💾 修行成果」** | background_review 把摘要发进了对话(K7) | `auxiliary.background_review.enabled: false` |
|
|
|
+| **自我升级日志顶替了对问题的回答** | 同上(K7) | 同上,并检查是否重启 |
|
|
|
+| `s6-svc: command not found` | 不在 PATH(K5) | 用 `/command/s6-svc` |
|
|
|
+| `ModuleNotFoundError: No module named 'yaml'` | 系统 python 没有 yaml(K6) | 用 `/opt/hermes/.venv/bin/python3` |
|
|
|
+| 改 YAML 后无关缩进也变了 | `yaml.safe_dump` 重写全文(K4) | `diff` 核对;必要时手工改 |
|
|
|
+| 容器回复里的 URL 总是被截断 | 输出长度限制(K9) | 让它传到固定路径,或用户浏览器取 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 八、本次会话中发现的 lj-test 未决项
|
|
|
+
|
|
|
+| # | 项 | 状态 |
|
|
|
+|---|---|---|
|
|
|
+| 1 | `_push_turn_status()` 已写入代码,**但 gateway 尚未重启** | ⏳ 重启后生效 |
|
|
|
+| 2 | `currentSessions` 保留历史记录长达 13 小时(不清理) | ⚠️ 建议 `idle` 超 5 分钟清理 |
|
|
|
+| 3 | `_thread_root()` 取不到 thread 时返回 None | ⚠️ 建议加房间号兜底 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+*本文所有数据为 2026-11-12 实测,非推断。*
|