|
|
@@ -0,0 +1,300 @@
|
|
|
+# 新容器接入 Fmode App · 标准操作流程(SOP v3.0)
|
|
|
+
|
|
|
+| 字段 | 值 |
|
|
|
+|---|---|
|
|
|
+| 版本 | v3.0(2026-11-12) |
|
|
|
+| 参考实现 | `agent-node-lj-test`(`@zgdcmgnqcd:fmode.cn`)—— 全部项已实测通过 |
|
|
|
+| 适用范围 | 任何要接入 Fmode App 的容器(Hermes 架构) |
|
|
|
+| 配套 | `容器接入完整指南-v2`(原理)、`提示词-容器自我优化-v2`(发给容器) |
|
|
|
+
|
|
|
+> **本 SOP 分五步**:① 体检 → ② 修改 → ③ 自审 → ④ 复验 → ⑤ 交付。
|
|
|
+> **每一步都有可执行的命令和判定标准**,不靠感觉。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 第 0 步:先做这一项(10 秒,决定要不要继续)
|
|
|
+
|
|
|
+```bash
|
|
|
+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 | ✅ 正常 | 继续第 1 步 |
|
|
|
+| **> 1000 ms** | ❌ **DNS 卡顿** | **先修这个**,见 §1.1 |
|
|
|
+
|
|
|
+> 这个问题会让**每一轮响应慢 5 秒**,且**新会话也一样慢**。
|
|
|
+> 极易被误判成「模型慢」,实际是网络层。lj-test 实测 5023ms,修好后 21ms。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 第 1 步:体检(只读,不改任何东西)
|
|
|
+
|
|
|
+### 1.1 DNS 解析(最优先)
|
|
|
+
|
|
|
+**根因**:glibc 默认把 A 和 AAAA 查询**并发塞进同一个 UDP socket**,
|
|
|
+Docker DNS 转发器处理不了,**丢掉一个**,只能等 5 秒超时。
|
|
|
+
|
|
|
+**对照实测**(lj-test):
|
|
|
+
|
|
|
+| 方式 | 耗时 |
|
|
|
+|---|---|
|
|
|
+| `AF_UNSPEC`(默认) | 5023 ms |
|
|
|
+| `AF_INET`(强制 IPv4) | 16 ms |
|
|
|
+
|
|
|
+**修法(按可行性排序)**:
|
|
|
+
|
|
|
+| 方案 | 覆盖 | 持久 | lj-test 可行 |
|
|
|
+|---|---|---|---|
|
|
|
+| ① compose 加 `dns_opt: [single-request]` | 全部进程 | ✅ | 需平台侧 |
|
|
|
+| ② `/etc/resolv.conf` 加 `options single-request` | 全部进程 | ⚠️ 重启覆盖 | ❌ root 属主 |
|
|
|
+| ③ s6 run 脚本 `export RES_OPTIONS=single-request` | 该服务及子进程 | ✅ | ✅ **已采用** |
|
|
|
+| ④ `usercustomize.py` 注入 | 仅 Python | ✅ | ✅ 备选 |
|
|
|
+
|
|
|
+> ⚠️ 方案 ③ 用 `${RES_OPTIONS:-single-request}` 会被外部值覆盖,
|
|
|
+> 想锁死写成 `export RES_OPTIONS=single-request`。
|
|
|
+
|
|
|
+### 1.2 结束信号(✅ reaction)
|
|
|
+
|
|
|
+```bash
|
|
|
+grep -n "MATRIX_REACTIONS" /opt/data/.env || echo "(未设置,默认 true)"
|
|
|
+```
|
|
|
+
|
|
|
+**实测正常表现**(`/sync` 回读):
|
|
|
+
|
|
|
+```
|
|
|
++0.9s 👀
|
|
|
++4.6s 正文
|
|
|
++4.7s ✅ ← 晚于正文 0.1 秒
|
|
|
+```
|
|
|
+
|
|
|
+| 检查 | 期望 |
|
|
|
+|---|---|
|
|
|
+| `MATRIX_REACTIONS` | `true`(默认就是,别关) |
|
|
|
+| 每轮结束是否打 ✅ | 是(Hermes 内建,`adapter.py:2331`) |
|
|
|
+| 失败是否打 ❌ | 是 |
|
|
|
+
|
|
|
+> ❌ **不要用 `m.typing=false`**:实测早于正文 0.6 秒,会让 App「还没回就说完了」。
|
|
|
+> 如果装过发 typing 的插件,**禁用它**。
|
|
|
+
|
|
|
+### 1.3 状态上报(`extra.currentSessions`)
|
|
|
+
|
|
|
+**云函数**:`xlosi3m46q`,`field: "extra"`
|
|
|
+
|
|
|
+> 🔴 **必须放在 `extra` 里面**。直接当顶层字段会 **HTTP 400**(白名单没有它)。
|
|
|
+> 实测:`{field:"currentSessions"}` → 400;`{field:"extra", value:'{"currentSessions":[...]}'}` → 200。
|
|
|
+
|
|
|
+**每条记录的 6 个字段**:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "sessionKey": "!roomId 或 $approot_xxx",
|
|
|
+ "userId": "@用户的mxid:fmode.cn",
|
|
|
+ "status": "running",
|
|
|
+ "task": "本轮提问首行,60 字内",
|
|
|
+ "turnStartAt": 1794405380428,
|
|
|
+ "updatedAt": 1794405433842,
|
|
|
+ "ttlMs": 600000
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+| 字段 | 数据来源 | 不做的后果 |
|
|
|
+|---|---|---|
|
|
|
+| `sessionKey` | 优先 `thread_id`,退 `chat_id`(roomId) | App 匹配不上,状态永远不显示 |
|
|
|
+| `userId` | `state.db.sessions.user_id` | 多人共用时 A 跑步影响 B |
|
|
|
+| `task` | `pre_llm_call` 的 `user_message` 首行 | 顶栏只能显示「正在处理」 |
|
|
|
+| `updatedAt` | 写入时的 epoch ms | App 无法判断 running 是否过期 |
|
|
|
+
|
|
|
+**三条硬性要求**:
|
|
|
+
|
|
|
+1. 每个会话**单独一条**,不要只报容器级 `status`
|
|
|
+2. **心跳也要带上**(不能只在 turn 首尾写)
|
|
|
+3. `idle` 记录有 TTL 清理(建议 10 分钟)
|
|
|
+
|
|
|
+### 1.4 文件交付
|
|
|
+
|
|
|
+```bash
|
|
|
+ls /opt/data/plugins/plugin-deliverable-inject/ 2>/dev/null || echo "(未安装)"
|
|
|
+```
|
|
|
+
|
|
|
+| 组件 | 作用 | 必需 |
|
|
|
+|---|---|---|
|
|
|
+| `plugin-deliverable-inject` | 把「已上云」链接写进回复正文 | ✅ |
|
|
|
+| `fmode-deliverable-hook.py`(shell hook) | 上传 + 登记台账 | ✅ |
|
|
|
+| 云函数 `AlP56LCKFm` | 取 OBS 预签名 URL | ✅ |
|
|
|
+| 云函数 `THXAOGKiPK` | 登记文件台账(`registerFile`) | ✅ |
|
|
|
+
|
|
|
+**链接格式**(App 正则识别):
|
|
|
+
|
|
|
+```
|
|
|
+正文末尾空一行
|
|
|
+**已上云(可直接下载)**
|
|
|
+- https://s3.fmode.cn/...
|
|
|
+```
|
|
|
+
|
|
|
+域名必须是 `s3.fmode.cn` 或 `file-cloud.fmode.cn`。
|
|
|
+
|
|
|
+### 1.5 图文合并
|
|
|
+
|
|
|
+```bash
|
|
|
+ls /opt/data/plugins/plugin-media-text-merge/ 2>/dev/null
|
|
|
+grep -n "plugin-media-text-merge" /opt/data/config.yaml
|
|
|
+```
|
|
|
+
|
|
|
+| 检查 | 期望 |
|
|
|
+|---|---|
|
|
|
+| 目录存在 | ✅ |
|
|
|
+| 在 `plugins.enabled` 里 | **当前 lj-test 不在** → 图文被拆两轮 |
|
|
|
+| `plugin.yaml` 的 `name` | 与目录名一致(加载器按**目录名**匹配,不一致不影响) |
|
|
|
+
|
|
|
+> 启用后有 **2.5 秒合并窗口**(设计上的等待,不是性能损耗)。
|
|
|
+
|
|
|
+### 1.6 能力边界
|
|
|
+
|
|
|
+```bash
|
|
|
+ls -ld /opt/hermes /opt/data
|
|
|
+test -w /opt/data/plugins && echo "plugins 可写" || echo "不可写"
|
|
|
+```
|
|
|
+
|
|
|
+**lj-test 实测**:
|
|
|
+
|
|
|
+| 路径 | 权限 | 能做什么 |
|
|
|
+|---|---|---|
|
|
|
+| `/opt/hermes` | `root:root`,**不可写** | 只能读 |
|
|
|
+| `/opt/data` | `hermes` 属主 | 插件、配置、脚本 |
|
|
|
+| `/etc/hosts`、`/etc/resolv.conf` | root 只读 | 改不了 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 第 2 步:修改(按体检结果逐项做)
|
|
|
+
|
|
|
+### 2.1 修改清单
|
|
|
+
|
|
|
+| # | 改什么 | 在哪改 | 需要重启 |
|
|
|
+|---|---|---|---|
|
|
|
+| 1 | DNS | `/run/service/gateway-default/run` 加 `export RES_OPTIONS=single-request` | ✅ |
|
|
|
+| 2 | `currentSessions` 带 `userId`/`task`/`updatedAt` | `plugin-active-reporter/__init__.py` | ✅ |
|
|
|
+| 3 | 心跳带 `currentSessions` | 同上 | ✅ |
|
|
|
+| 4 | 启用图文合并 | `config.yaml` 的 `plugins.enabled` | ✅ |
|
|
|
+| 5 | 交付链接 | `plugin-deliverable-inject`(已装则核对) | ❌ |
|
|
|
+| 6 | 排版规范 | 提示词约束,见 §2.3 | ❌ |
|
|
|
+
|
|
|
+### 2.2 重启命令
|
|
|
+
|
|
|
+```bash
|
|
|
+s6-svc -r /run/service/gateway-default
|
|
|
+# 或
|
|
|
+/opt/hermes/.venv/bin/hermes gateway run --replace
|
|
|
+```
|
|
|
+
|
|
|
+> ⚠️ 重启会**打断正在进行的对话**。批量改完一次重启,不要每改一项重启一次。
|
|
|
+
|
|
|
+### 2.3 排版规范(不改代码,改行为)
|
|
|
+
|
|
|
+| 规则 | 错误 | 正确 |
|
|
|
+|---|---|---|
|
|
|
+| `~` 范围 | `8~16℃`(两对 ~ 被解析成 `~~删除线~~`) | `8~16℃` 或 `8 至 16℃` |
|
|
|
+| 表格 | 「客户端不支持表格,改用列表」 | **App 完整支持**,直接写标准 Markdown 表格 |
|
|
|
+| 过程行 | 工具行与结论混排 | 工具行**独占一行**,固定 emoji 开头 |
|
|
|
+| 回执 | 复述用户原文当过渡语 | 短回执(一句「收到」),正文单独一条 |
|
|
|
+| 图片 | `question` 参数抄用户原话 | 简短、去人称(如「读出截图中的文件名」) |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 第 3 步:自审(改完自己先验一遍)
|
|
|
+
|
|
|
+**每一项都要贴实测输出,不许只说「已完成」。**
|
|
|
+
|
|
|
+### 3.1 检查表
|
|
|
+
|
|
|
+| # | 项 | 命令 | 通过标准 |
|
|
|
+|---|---|---|---|
|
|
|
+| 1 | DNS | `python3 -c "...getaddrinfo..."` | **< 100 ms** |
|
|
|
+| 2 | ✅ reaction | 发一条消息,`/sync` 读事件 | ✅ 晚于正文 |
|
|
|
+| 3 | `currentSessions` | 调 `xlosi3m46q` 读 `extra` | 每条有 `sessionKey`/`userId`/`status`/`task`/`updatedAt` |
|
|
|
+| 4 | 心跳带会话 | 等一次心跳后回读 | `currentSessions` 仍在 |
|
|
|
+| 5 | 图文合并 | 发图+文字 | **只回一轮** |
|
|
|
+| 6 | 交付链接 | 让它产出一个文件 | 正文有 `**已上云(可直接下载)**` |
|
|
|
+| 7 | 表格 | 让它用表格回答 | 正常渲染成表格 |
|
|
|
+
|
|
|
+### 3.2 自审脚本(可直接发给容器)
|
|
|
+
|
|
|
+见 `提示词-容器自我优化-v2-20261111.md` 的「交付格式」一节。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 第 4 步:复验(App 侧独立回读,**不采信容器自述**)
|
|
|
+
|
|
|
+### 4.1 为什么必须独立复验
|
|
|
+
|
|
|
+实测教训:有容器口头说「改完了」,实际改的是**只读文件**,一个字没生效。
|
|
|
+另有容器说「`currentSessions` 是错的字段名」,实际是**放错了位置**(没放进 `extra`)。
|
|
|
+
|
|
|
+### 4.2 复验命令(App 侧跑)
|
|
|
+
|
|
|
+```bash
|
|
|
+# 会话级状态
|
|
|
+node scripts/verify-session-task.mjs
|
|
|
+
|
|
|
+# 状态裁决(需要容器有活跃会话时最准)
|
|
|
+node scripts/verify-unified-state.mjs
|
|
|
+
|
|
|
+# reaction 时序
|
|
|
+node scripts/verify-reaction-coverage.mjs
|
|
|
+```
|
|
|
+
|
|
|
+### 4.3 App 端专项复验
|
|
|
+
|
|
|
+```bash
|
|
|
+# 新建会话首条消息(NG0600 / 卡顿回归)
|
|
|
+node scripts/repro-new-session-send.mjs <agentId>
|
|
|
+```
|
|
|
+
|
|
|
+**通过标准**:输入框清空 < 500ms、消息出现 < 500ms、**页面错误为空**。
|
|
|
+
|
|
|
+> 🔴 这条最关键:单元测试测不出「模板渲染期写 signal」这类框架级错误,
|
|
|
+> 必须用真实浏览器。曾因此排查了 8 轮。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 第 5 步:交付确认
|
|
|
+
|
|
|
+### 5.1 最终验收(App 里人工过一遍)
|
|
|
+
|
|
|
+- [ ] 新建会话 → 立刻发消息,**消息立即出现**
|
|
|
+- [ ] 容器回复完,**卡片 / 右上角 / 左侧同时**切换状态
|
|
|
+- [ ] 发图 + 文字,**只回一轮**
|
|
|
+- [ ] 让它出文件,右侧「成果」有卡片且刷新后仍在
|
|
|
+- [ ] 让它用表格回答,**表格正常渲染**
|
|
|
+- [ ] 长任务(20+ 工具调用)期间,状态**不提前**变「已完成」
|
|
|
+- [ ] 打开历史会话,**无打字动画**
|
|
|
+- [ ] 两个会话并发,状态互不干扰
|
|
|
+
|
|
|
+### 5.2 交付物
|
|
|
+
|
|
|
+| 物 | 位置 |
|
|
|
+|---|---|
|
|
|
+| 本 SOP | `docs/03-interface/容器接入标准流程-SOP-v3-20261112.md` |
|
|
|
+| 接入原理 | `docs/03-interface/容器接入完整指南-v2-20261111.md` |
|
|
|
+| 发给容器的提示词 | `docs/03-interface/提示词-容器自我优化-v2-20261111.md` |
|
|
|
+| 复现/验证脚本 | `scripts/repro-new-session-send.mjs`、`verify-*.mjs` |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 附:lj-test 的实测基线(供新容器对照)
|
|
|
+
|
|
|
+| 项 | lj-test 实测值 |
|
|
|
+|---|---|
|
|
|
+| DNS 解析(修复后) | 21 ms |
|
|
|
+| 首字延迟 | 2~9 秒(上游模型 6~7s 为主) |
|
|
|
+| ✅ reaction 时序 | 晚于正文 **0.1 秒** |
|
|
|
+| `currentSessions` | 6 字段齐全,`sessionKey` 为 `!roomId` 或 `$approot_xxx` |
|
|
|
+| `thread_id` 覆盖率 | matrix 会话 54% 有值 |
|
|
|
+| 插件开销 | 4 个每轮执行,合计 **2~3 ms**(不是慢的原因) |
|
|
|
+| 模型 | `deepseek-v4-flash`,未开 reasoning |
|
|
|
+| `/opt/hermes` | root 属主,**不可写** |
|
|
|
+| 会话规模 | 242 会话 / 40 MB / 10601 消息(**不影响**新会话速度) |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+*本 SOP 所有判定标准均来自 2026-11-11/12 的实测,非推断。*
|