Files
dsh_ai1net_server/接续入口_IM线_20260922.md
T

601 lines
137 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.
# 接续入口 · IM 线(2026-09-22 开线)
> 🔴 **工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server`(本入口**只此一份**,⛔ 不得复制到别的工作区)
> **开线棒**:会话 `aliyun-dsh-server` · 2026-09-22 16:4x(用户当场提要求,本会话因上下文过高收口开线)
> **上游文档**(文档库 = `D:\github\dsh_shenxian\dsh-server-docs`):`调整方案/142-IM群组对话-需求基线与方案.md`(需求基线)+ 五份交接单 `交接单/IM群组-{A,B,C,D,E}-*.md`(均已按 09-22 两份架构定稿对齐,各单 §〇)
> **代码基线**:`D:\github\dsh_shenxian`(生产口径 `dshs`)
> 🔴 **cwds 判据(2026-09-23 加;**优先级高于本文任何历史记录**)**:本线接续棒的 automation `cwds` 一律写 **小写盘符 + 反斜杠** —— `e:\ProgramData\AIProject\aliyun-dsh-server`。宿主**逐字落库、⛔ 不规范化**(连大小写也不),写法不同即裂成**两个同名会话分组**(09-22 正斜杠裂一次、**09-23 大写盘符又裂一次**)。⛔ **下文出现的 `cwds = E:\…`(大写盘符)全是误写的历史记录,登记新棒时不得照抄其大小写。**
---
## §0 状态(**最新一行 = 执行依据**)
- **2026-09-22 16:4x|开线**:用户要求「IM 具体如何实现才能在复杂环境下稳定 + 调研同类开源框架(Telegram 等海外项目),找最新性能稳定最好的」。
- 开线棒已实搜并固化 **8 条事实**(见 §3)⇒ 下一棒 ⛔ **不必重搜**。
- ✅ **下一棒 = 规划棒** —— 已于 2026-09-22 17:4x 完成(见本 §0 末行)。
- ✅ **已登记接续棒(唯一一个)**:automation `2798048a-0ac8-4515-85bd-b58375e756ac`(一次性 · `scheduledAt = 2026-09-22T17:40`)**=本次会话(已消费)**。
⚠️ 为何不是「收口 + 5~8 分钟」:`dsh-auto-handoff-chain §3.1.1` 的**唯一允许拉长情形** = 等一个外部窗口 —— 此刻**插件投放与分库线的执行棒已排定 16:52 起跑**(其规划棒刚收口),全局锁同一时刻只允许一个执行会话 ⇒ 本棒排在其后(16:52 + 执行棒实测 21~41 min + 余量 ≈ 17:40)。
⛔ 下一棒若抢不到锁 ⇒ **只报告并停**,⛔ 不删锁、不接管(R9)。
- 🔴 **2026-09-22 17:0x 口径更新(用户明令,优先级高于本文其余各处)**:用户原话「**机器可以换 不要那这个限制规划**」⇒ **资源(单实例 1024 MiB / 宿主 1870 MiB)不再作为选型否决项**。框架与架构按 **稳定性 · 性能 · 可运维性** 主导判据规划;**机器配置 = 方案里要给出的"需求测算"**(用户按需采购),⛔ **不得以"装不下 / 现有机器跑不动"为由否掉任何方案**。
- ✅ **2026-09-22 17:4x|规划棒收口(第 1 棒 ✅ 完成)**:产出 **`交接单/IM群组-稳定性机制与框架选型.md`**(T1 机制清单 **25 条** + T2 判据矩阵 **7 类候选 × 7 轴** + T3 三案选型 + 实验 **E-1~E-4**)。
**本单自决项(可推翻)**:① 判据轴取「**身份可复用 / 存储同库 / 七扩展点承载**」为决定性轴(⛔ 不按 star / 热度排序)② **建议 = 案 3 形态 + 案 1 路径**(连接层写成可替换,⛔ 不现在引整框架)③ 实验一律落 **106**(47 不动)。
**下一棒(唯一)** = **第 2 棒 · 执行棒** = automation `b656009b-ecdd-4f12-a077-d12e8294dce8`(一次性 · `scheduledAt = 2026-09-22T17:52`),动作见 **§2**。
- ✅ **2026-09-22 18:2x|第 2 棒(执行棒)✅ 完成(步 1–4)** —— 回填见该单 **§十二 执行回报** + **§十三 断言名清单**(18/18)。
**实测结论(三条)**:① **E-3 = 能** —— ejabberd 可用既有 `users` 作外部认证源,且干净 mnesia 起点下**不落地 `passwd` 账号表**;🔴 但发现 ejabberd 对 `check_password` **有进程内缓存**(改表不立刻生效、重启即生效)⇒ 案 2 「继续但需追加前置项」。② **E-1(零依赖自研 WS,106 实测)**:1k/5k/10k 连接 **upgrade 全成功、心跳超时率 0**,RSS 58.5→131.8 MiB(边际 **7.5 KiB/连接**),P99 = **45.3 / 256.3 / 421.1 ms**,且 **P99 ≈ 事件循环最大滞后** ⇒ 瓶颈是**单进程全量扇出的排队**,不是连接数供给。③ 离线检测**只能靠心跳兜底**(写失败路径 ≤3 s 生效,但观测窗口短于扇出间隔时 `close` 事件不发生)。
**前置核实**:4/5 已验(`src/im/` 不存在 · schema = **v13** ⇒ 不触发改去 A 单 · upgrade 回调 `proxy.ts:759`(规划读数 718 已漂移)· `npm test` 248/247 过 1 跳过);🔴 第 4 条(47 上 `dshs` 的 `\dt`)**未核实** —— 47 的 sshd **已从 32022 漂移到 22**,且 PG 只走 TCP 需口令(⛔ 未取用凭据);已给替代证据(`schema.ts` 内三表 DDL 计数 = 0)。
**下一棒(唯一)** = **第 3 棒 · 执行棒续** = automation `21443815-acec-4069-94ad-6be3242c9d5d`(一次性 · `scheduledAt = 2026-09-22T18:23`),动作见 **§2「⏭️ 第 3 棒」**。
- ✅ **2026-09-22 19:0x|第 3 棒(执行棒续)✅ 完成(§八 步 5–8)** —— 回填见 `交接单/IM群组-稳定性机制与框架选型.md` **§十二 执行回报(IM线-第3棒)**。
**实测结论(四条)**:① **E-2 = Centrifugo v6.9.6 跑通**(10k 连接 upgrade 全成功、**零掉线**:服务端权威 `POST /api/channels` 稳态 `num_clients` = 1000/5000/10000 = 目标值):客户端观测 **P99 = 28.9 / 68.1 / 111.9 ms**、扇出离散中位 **17.2 / 57.9 / 97.8 ms** ⇒ 均优于自研(1/2.7、1/1.9);代价 = **边际每连接 70.1 KiB vs 自研 5.9 KiB(11.9×)**,稳态总量 **750.2 vs 116.0 MiB**。② 🔴 **保活方向相反(实测)**:Centrifugo **不发服务端 WS ping**(`wsPingRecv=0`),且**客户端不发 ping ⇒ 26 s 内 10000/10000 全被断开**;判别实验证明「保活计时起点必须绑该连接自己的认证完成时刻」(用整进程统一计时 ⇒ 10k 一次掉 **2773** 条,是适配器缺陷)。③ **二次判定 = 本期不引入外部连接层组件**(维持案 3 形态 + 案 1 路径),依据 = E-1 三档 `服务端 P99 ≈ 事件循环最大滞后`(10k:340.3 vs 342.5 ms)⇒ 瓶颈是**可就地解决的扇出排队**;并写下**可证伪回退判据**(10k P99 > 150 ms 或离散中位 > 120 ms ⇒ 引入)。④ **E-4 = 可行**:E2EE 最小路径 = 三层密钥(设备身份密钥 / 房间 `DK(room_id, epoch)` / 成员公钥包裹的密钥包)+ 两张新表(`im_room_keys` / `im_agent_grants`)+「**加人不换钥、移除必换钥、轮换先于消息可见**」,出 **8 条可断言路径**;**relay 零改动**。
**步 8 回填**:§四 **25 条机制全部有归属** —— A 单 16 + B 单 5 + C 单 3 + D 单 3 + E 单 2(共 29 行;组 E 与组 A3 因跨"实现层/契约层/客户端层"两端各写一条并注明同源);⛔ 未改 A–E 任一单的既有结论。
**下一棒(唯一)** = **第 4 棒 · 执行棒** = automation `db244c60-db22-4d7d-87a9-bced514bbea4`(一次性 · `scheduledAt = 2026-09-22T19:13`),动作见 **§2「⏭️ 第 4 棒」**。
- ✅ **2026-09-22 19:3x|第 4 棒(执行棒)✅ 完成** —— 回填见 `交接单/IM群组-稳定性机制与框架选型.md` **§十二 执行回报(IM线-第4棒)**(新建独立小节)+ 第 3 棒 §十二-5「回退判据」处的 🔻 追加块(⛔ 原文未改)。
**实测结论(四条)**:① **E-5 改造 = 分片扇出 + 同片批写**(106 `/root/im-e2/server-shard.mjs`,新建 268 行;`/root/im-e1/*.mjs` 一行未改;`/metrics` 原字段逐条不变、`msgsSent` 语义核对 = 6×N ✅)。② 🔴 **回退判据被触发** —— 10k 客户端观测 P99 = **337.6 / 359.4 ms**(判据 150 ms;两次复跑)、扇出离散中位 = **227.6 / 225.5 ms**(判据 120 ms);**同会话 E-1 基线复跑** = 268.6 / 171.8 ms ⇒ 两项均变差。③ 🔴 **被证伪的是「分时」这一手法** —— 事件循环最大滞后 349.7 → **13.0–46.9 ms**(13–27×)但端到端整圈**反变长**;分片数 4/8/16 与批写开/关**四个变体均无改善** ⇒ 整圈墙钟由 `write()` **绝对总量**决定,A 候选的正确形态是**并行(多进程)**,⛔ 不是继续切细"分时"。④ ⚠️ **RSS 判定为噪声内**(10k 五变体 107.9–132.8 vs 基线 115.9,跨跑方差 ±12 MiB)⇒ 后续内存判据须重复跑 ≥3 次取中位。
**门禁**:改造判定为**负向**(1k P99 49.8>44.7、10k 337.6/359.4>268.6 有档位变差)⇒ 已按门禁停下复盘、⛔ 未硬推;🔴 **引入外部组件属影响面扩大 ⇒ ⛔ 未擅自引入**,三条候选(A 就地继续优化/B 引入 Centrifugo 类/C 混合)已各带优缺点交拍板(收在该单 §十二 第4棒 段末「待拍板」)。
**判据 6 / 7(第 3 棒欠账,本轮补跑)**:判据 6 ✅ `npm test`(Node 22)= **248 / 247 过 / 0 败 / 1 跳过**,与第 2 棒基线一致;判据 7 ⚠️ **测试集内无该用例 ⇒ 未覆盖**(`grep -rin "101" test/` 零命中;`grep -rln "proxy" test/` 零命中;最接近 = `test/relay.test.mjs:1383 rawWsProbe`,属中继侧 upgrade),⛔ 未自造用例。
**下一棒(唯一)** = **第 5 棒 · 执行棒** = automation `e1c1b3a5-0c11-4544-8b28-6fa80684d9de`(一次性 · `scheduledAt = 2026-09-22T19:33`),动作见 **§2「⏭️ 第 5 棒」**。
- ✅ **2026-09-22 19:4x|第 5 棒(执行棒)✅ 完成 —— IM 五单合流第一单(A 单)已开工并收口** —— 回填见 `交接单/IM群组-A-房间内核与DB.md` **§十 执行回报(IM线-第5棒)**(新建独立小节,⛔ 原文未改)。
**交付(本机代码面)**:`src/db/schema.ts` 追加 **v14**(`rooms` / `room_members` / `messages` 三表 + 索引,**192 行新增 / 0 行删除**,既有迁移零触碰)+ 新建 **`src/im/{types,db,store,clock}.ts`(963 行)** + 新建 **`test/im-store.test.mjs`(552 行,23 用例)** + `package.json` 的 `test`/`verify` 各登记 1 处。
**判据读数**:`npm run build` rc=0 | `node --test test/im-store.test.mjs` = **23 / 22 过 / 0 败 / 1 跳过**(跳过 = PG 段自跳过)| `npm test`(Node 22)= **271 / 269 过 / 0 败 / 2 跳过**(基线 248 ⇒ +23 全为本单)| `grep -c "^ { version:"` = **14** | 全仓改动 72 → **74**(增量恰为本单 2 个已跟踪文件)。
**五条只读前置全部满足**(v13 为最新、v14 空闲、IM 三表 DDL 计数 = 0、双后端同形、`src/im/` 原不存在)⇒ ⛔ 未触发「停手改去别的单」。
🔴 **两处口径说明(已写进该单 §十.7,⛔ 不是判据放水)**:① 判据 3 的「不同到达序 ⇒ 同一数组」在 DB 层按 §四-6 拆成「读模型恒按 seq 递增」+「规范序函数对同一集合 3 种到达序输出同一数组(含落库往返版)」;② `id = sha256(authorId ‖ canonical(payload))`(**含作者**)—— 纯内容哈希会让同房不同作者的同文消息互相吞掉(静默丢消息),改后判据 2/9 全部保持。
⚠️ **卡点**:`npm run verify` 整体 rc=**1**,卡在 **既有**脚本 `scripts/verify-platform-admin-section.mjs`(`require()` + 顶层 await ⇒ `ERR_AMBIGUOUS_MODULE_SYNTAX`;该文件**未经改动**)⇒ **与本单无关**;链路其后 6 步逐条补跑 **rc 全 0**、`check-layering` **✅ 无新增违规**。⛔ 未顺手修(超出 §三 改动面)。
⚠️ **已知单内不一致(实测确认)**:A 单 §五-1 / §六-1 写的「最新 = 12」是 09-20 旧值残留,**实测 = 14**(按 §〇-5 的 v14 判);⛔ 只报告未改。
**下一棒(唯一)** = **第 6 棒 · 执行棒** = **开工 B 单**(`交接单/IM群组-B-平台API与用户端IM-WS通道.md`),动作见 **§2「⏭️ 第 6 棒」**。
- ✅ **2026-09-22 20:2x|第 6 棒(执行棒)✅ 完成 —— IM 五单合流第二单(B 单)已落地** —— 回填见 `交接单/IM群组-B-平台API与用户端IM-WS通道.md` **§九 执行回报(IM线第6棒)**(新建独立小节,⛔ 原文未改)。
**交付(本机代码面)**:新建 **`src/im/hub.ts`(372)+ `src/im/ws.ts`(542)+ `src/web/routes/im.ts`(333)**(共 1247 行)+ 改 **`src/supervisor/proxy.ts`(+30 / −0,委托钩子 `setUpgradeDispatch`+回调首行分流)**+ **`src/web/server.ts`(本单 +7:import + 挂载)**。
**判据读数**:`npm run build` rc=0 | `check-layering` **✅ 无新增违规**(rc=0;另提示 `src/im/*` 未归类,不影响退出码)| `npm test`(Node 22)= **271 / 269 过 / 0 败 / 2 跳过**(第 5 棒基线一致)| **本机端到端验收 15 PASS / 0 FAIL / rc=0**(REST 五类 + WS 握手含 `cursor` + 投递每连接恰好 1 次 + 未订阅 0 次 + 游标补拉恰好 50 无重复 + 心跳超时 ≤2 周期判离线并落具名日志 + 单用户超限 429 具名码 + **既有子域隧道 HTTP 200 / upgrade 101 不受影响**)| `npm run smoke:subdomain` = **`OK: subdomain routing + auth flow passed`**。
**标定(§五-5,A 单初值 `2000 / 10 / 30 s` ⇒ 建议不回填)**:投递增量三档严格线性(Δdeliveries/Δmessages = 10 / 100 / 1000 / 2000),**2000 连接的 hub 内扇出 P99 = 0.023 ms**;presence 1000 成员房、每人每分钟一次变化 ⇒ 实发 **5.0 帧/秒**(C=5,比对照 16,667 低 **3333×**)与 **1000.0 帧/秒**(C=1000,低 **17×**);预算三档具名码逐条命中且**人类成员不受限频**;`decideWrite` P99 = 0.030 ms。
**§二 前置**:5 条中 3 处读数漂移(routes import 11→**14** 行但仍无 `im`;`on('upgrade')` 全仓 **2 处**(relay 自建 http server + 平台 `app.server`)⇒ 「平台 app.server 上仅 1 处」成立;`proxy.ts:718` → **759**)⇒ ⛔ 未触发停手。nginx 那条本机(Windows)无法核实、且本棒⛔ 不动服务器 ⇒ 如实标注(location 草案已写进 §九.6)。
⚠️ **未完成项**:nginx location 未应用(⛔ 不部署)|§六.1-9 的「跨进程重启」半程未实测(需部署动作)|`GET /api/im/rooms` 为 `O(房间数)` 查询(需 A 单 `store.ts` 加下推查询)|⛔ 未 commit/push/scp、⛔ 未动 47/106、⛔ 未引入新依赖。
**下一棒(唯一)** = **第 7 棒 · 执行棒** = automation `d2161e2a-bb50-47e2-9a26-50f291fc1d0b`(一次性 · `scheduledAt = 2026-09-22T20:26`)= **开工 C 单**(`交接单/IM群组-C-Agent接入插件.md`),动作见 **§2「⏭️ 第 7 棒」**。
- ✅ **2026-09-22 21:1x|第 8 棒(执行棒)✅ 完成 —— IM 五单合流第四单(D 单)已落地** —— 回填见 `交接单/IM群组-D-插件SDK与扩展点契约.md` **§十 执行回报(IM线-第8棒)**(新建独立小节,⛔ 原文未改)。
**交付(本机代码面)**:新建 **`src/im/sdk/types.ts`(458)+ `src/im/sdk/host.ts`(695)+ `src/im/sdk/index.ts`(38)**(共 1191 行)+ 新建 **三个示例插件** `poc/business-plugins-im/{office-tasks,mud-rate,trpg-turn}/`(各含 `package.json` + `cordis.patch.yml` + `lib/index.js`,共 475 行)+ 新建 **`test/im-sdk.test.mjs`(1019 行,66 用例)** + 新建说明页 `dsh-server-docs/docs/IM插件SDK与扩展点契约.md` + 改 `package.json` 2 处(`test`/`verify` 各登记 1 处)。
**判据读数**:`npm run build` rc=0 | `node --test test/im-sdk.test.mjs` = **66 / 66 过 / 0 败 / 0 跳过** | `npm test`(Node 22)= **366 / 364 过 / 0 败 / 2 跳过**(第 7 棒基线 300 ⇒ +66 全为本单)| **内核零改动**:`git diff` 对 `src/im/store.ts`/`hub.ts`/`web/routes/im.ts` **空**、`git status --short src/im` **无 M 标记** | `check-layering` rc=0 **✅ 无新增违规**。
**三条硬判据**:① **七点齐备**(`EXTENSION_KEYS` 恰为 142 §3.6 七个;三插件合起来全覆盖)② **三类场景各跑通一个**(office 聊天档 / mud 实时档+限速 / trpg 实时档+回合制,各落一行到自己的表,**内核一行未改** ⇒ 142 §五-6 通过)③ **§六.1-8 失败模式齐备**(每个扩展点条目都带 `timeoutMs` + `circuit`,缺一即不通过)。
**口径更正(本棒新增,⛔ 不改 A–E 结论)**:D 单 §九 表格与 §三 的 **`im.data`** 是 09-20 规划态命名 —— 落成的是 **`ImDataPort` 注入式端口**(真库端口属建库面,本棒不越界);§九 已就地加**只标注的落点说明块**。
**未完成项 / 已知限(5 条,逐条注明卡在哪)**:① E2EE 密文形态**只在契约面成立**(§四-6 形态档位待确认;密钥面未实现,判据落成"载荷契约不声明 envelope 字段")② 数据面端口是**注入式桩**,非真库端口 ③ `check-layering` 仍提示 `src/im/*` 未归类(改它超改动面)④ 迁移「删列/改类型 ⇒ 拒绝启用」只验到**声明面** ⑤ 三个示例插件**未打包/未投放/未启用**(属投放线面)。
⛔ 未 commit/push/scp、⛔ 未部署、⛔ 未动 47/106、⛔ 未引入新依赖。
**下一棒**:⛔ **未登记**(E 单被 D 单收口结论阻塞,见下行)。
- 🔴 **2026-09-22 21:1x|IM 线下一步 = 等用户拍板(⛔ 未登记任何接续棒)**:
**原因(硬性,非"更慢更绕")**:E 单(`交接单/IM群组-E-会话界面UI.md`)**开工前必须定落点**,而**落点三案无客观优劣**(属体验偏好 ⇒ §1 边界外第 ⑤ 类):
- **候选 A · 集成进门户**(`web/portal.html` 新面板):优点 = 与账号/成员/管理面同域同一次登录,成员与权限天然对齐平台用户;缺点 = 门户是平台自己的页,与「实例内聊天」是两套线程,房间消息要在平台侧渲染,页面变重。
- **候选 B · 独立页**(`web/im.html`,可挂子域):优点 = 与门户解耦、可按全屏消息流设计,改动面小、回滚干净;缺点 = 多一个入口要维护(登录态/导航/i18n 各一套),用户要记住「聊天在这里」。
- **候选 C · 实例内工作台面板**(走 dsh 既有 slots):优点 = **与 agent 同一屏**(代答与上下文都在自己实例里),复用既有面板与插件分发面;缺点 = 房间是跨用户平台级资源 ⇒ 成员表/消息仍走平台 API,同一房间在**每个用户实例里各渲染一份**(UI 状态不共享),且交付面落进**实例内插件链**。
- **E 单原倾向**(可被推翻):**候选 A**。
⇒ 三案各有优有劣、客观标准分不出高下 ⇒ **⛔ 不替用户拍**;拍板到手后再登记 E 单接续棒。
⚠️ **另有一项待拍板仍悬**:E-5 实测「分时扇出」无效 ⇒ 连接层三候选(A 就地继续优化(多进程并行)/B 引入 Centrifugo 类外部连接层/C 混合)—— 拍板前 ⛔ 不引入任何外部组件。
**交付(本机代码面)**:新建 **`src/im/instance-token.ts`(197)+ `src/im/agent-bridge.ts`(341)+ `poc/im-agent-bridge/{package.json,cordis.patch.yml,lib/index.js}`(368)+ `test/im-agent.test.mjs`(29 用例)**;改 **`src/web/routes/im.ts`(+约 78)+ `src/im/ws.ts`(+约 12)+ `src/web/server.ts`(+约 14)+ `src/supervisor/orchestrator.ts`(+约 55)+ `src/supervisor/remote-spawner.ts`(+约 30)+ `src/worker/agent.ts`(+3)**。
**判据读数**:`npm run build` rc=0 | `node --test test/im-agent.test.mjs` = **29 / 29 过 / 0 败** | `npm test`(Node 22)= **300 / 298 过 / 0 败 / 2 跳过**(第 6 棒基线 271 ⇒ +29 全为本单)| `check-layering` rc=0 **✅ 无新增违规**(仍提示 `src/im/*` 未归类,不影响退出码)。
**两条关键判据(§五-4,本线最硬的两条)**:① 上下文能引用**窗口内、但没有 @ 它**的消息(`ok 13`,prompt 里含未被 @ 的那条);② **越权内容不出现**(`ok 16`,他人 `visibility='private'` 的内容不在 prompt 里)。
**两处 501 预留点已换真校验**:`src/web/routes/im.ts` 的 `imAuth` 与 `src/im/ws.ts` 的 `doUpgrade`(正确 token ⇒ 通过;不认/过期/跨区 ⇒ **401 且不升级**,⛔ 不再 501)。
⚠️ **偏离已逐条写进该单 §九.6**:① `ws.ts` 改注入钩子(⛔ 不直接 import 登记簿,否则分层反向)② 改动面比 §三 大 4 个文件(集群形态必须由 Manager 签发+随 launch 投递,否则换集群「一条 IM 凭据都不产生」)③ 本地 LLM 会话对接**留成可注入接口** `opts.askLocalSession`(属 D 单契约面,⛔ 未自造)④ 凭据用**内存登记簿**(代价:平台 `dshs` 重启后已签发 token 失效,需等实例重启)⑤「标识截图」因 E 单未开工而给 JSON。
⛔ 未 commit/push/scp、⛔ 未部署、⛔ 未动 47/106、⛔ 未引入新依赖。
**下一棒(唯一)** = **第 8 棒 · 执行棒** = automation `c57c0577-f2ab-496a-91dd-24d90bb405fa`(一次性 · `scheduledAt = 2026-09-22T20:53`)= **开工 D 单**(`交接单/IM群组-D-插件SDK与扩展点契约.md`),动作见 **§2「⏭️ 第 8 棒」**。
- ✅ **2026-09-23 05:5x|E 单落点已拍板(用户三次拍板,⛔ 不再征询)** —— 本棒为**只读调研 + 落点固化**(无代码改动、未抢锁写码)。
🔴 **用户拍板(原话口径)**:① 落点 = **C 案**(实例内 dsh 会话页 tab:「dsh会话、群组会话1、群组会话2…」)② tab 保活 = **候选一**(`display:none`)③ **群组 tab 上限 = 5 个** ④ 新建群组入口 = **放在对应插件内** ⑤ 开源界面 = **借鉴**优秀群聊项目的界面功能与交互(**按 ① 方案:只借交互与组件,⛔ 不引整站**)。
**技术依据(实测已验)**:dsh 会话区 = **官方公开可寻址 DOM slot**(`[data-slot="main.conversation"]`,旧名 `conversation` 兜底);**MCN 工作台已在生产用同一机制接管**(`dsh-plugin-mcn/lib/client.js:306` 选择器、`:453-495` wrapper 接管与卸载还原、`:3025` 侧边栏注册)。⇒ **可行性已由现网代码证实,⛔ 不触 R2**(走官方扩展面)。
🔴 **新发现的前置缺口(E 单必须补)**:`src/web/routes/im.ts` **全部只有 6 条路由**,`GET /rooms` 按 `isMember()` **过滤**(非成员看不到任何房)、加人要求 `role IN (owner,admin)` ⇒ **普通用户无法「发现群 → 点按钮加入」,用户要的按钮后端不存在**。补缺口成本可控:`store.listRooms` 已有筛选签名(扩展参数即可)|`rooms.config_json` 已解析成 `room.config`(可放标志不迁移;但授权语义宜走**显式列 + v15 只增迁移**)|**在线态已内建**(`hub.ts` presence,1 s 批合并)⇒ 无需新增数据源。
🔴 **新风险(必带对策,已写进 E 单判据 11)**:接管依赖 `[data-slot]` 选择器,**官方改槽名会静默失效**(已被咬过一次:0.1.5-rc.1 改名 ⇒ 工作台点了没反应)⇒ **多选择器兜底 + 失效可见提示**。
**已固化落点**:E 单 §四-4/§四-5(拍板)+ §三 范围 + §五 步骤(**新增步 0 补缺口、步 2/3/8 新增 tab/保活/上限/加入**)+ §六 判据(**新增 8–13 条**)+ §七 回滚 + §八 回报;本入口 §0/§2/§4。
**下一棒(唯一)** = **第 9 棒 · 执行棒** = **开工 E 单**(`交接单/IM群组-E-会话界面UI.md`)= automation `b4312852-71ae-41a1-9304-a857cb1d7eba`(一次性 · `scheduledAt = 2026-09-23T06:03`),动作见 **§2「⏭️ 第 9 棒」**。⚠️ 另一项待拍板仍悬:连接层三候选(E-5 已证「分时扇出」无效)—— ⛔ 与本棒**解耦**,拍板前不引外部组件。
- ✅ **2026-09-23 06:3x|第 9 棒(执行棒)✅ 完成 —— 开工 E 单,IM 线 A–E 五单全部落地** —— 回填见 `交接单/IM群组-E-会话界面UI.md` **§执行回报(IM 线第 9 棒 · 2026-09-23 06:2x)**(新建独立小节,⛔ 原文未改)。
**交付(本机代码面)**:**步 0 前置缺口已补** —— `src/db/schema.ts` 追加 **v16**(`rooms.discoverable` + `rooms.join_policy`,**显式列路线**)|`src/im/{types,store,hub}.ts` 增量(`JoinPolicy`/`JoinRejection`/`JoinOutcome`、`toRoom` 双后端归一、`listRooms({discoverable})` 下推、新增 `joinRoom` 判序、新增 `presenceSnapshot`)|`src/web/routes/im.ts` **新增 4 条端点**(`GET /rooms?discoverable=1` · `POST /rooms/:id/join` · `GET /rooms/:id/members` · `PATCH /rooms/:id/members/:mid`)|新建 **`poc/im-conversation-tabs/`** 四件(`package.json` 零依赖 / `cordis.patch.yml` / `lib/index.js` 25 行 / **`lib/client.js` 1228 行**)|新建 **`test/im-ui.test.mjs`(29 用例)**|改 `test/im-store.test.mjs`(PG 段 ratchet `= 14` ⇒ `>= 14`)+ `package.json`(`test`/`verify` 各登记 1 处)。
**判据读数**:`npm run build` rc=0 | `node --check` 双通过(client.js / index.js)| `node --test test/im-ui.test.mjs` = **29 / 29 过 / 0 败** | `npm test`(Node 22)rc=0 = **420 / 418 过 / 0 败 / 2 跳过**(基线 366 ⇒ 本棒 **+29**,余量为并发会话同仓贡献)| `check-layering` rc=0 **✅ 无新增违规**。
**步 0 选型(自决,可推翻)**:走**路线甲「显式列 + v16 只增迁移」**,三条理由 = ① 授权语义不埋 config(可索引/可约束/可审计)② 可下推(`WHERE discoverable = 1`)③ 默认值落保守侧(`0` / `'invite'` ⇒ 存量房间零变更,⛔ 不会因迁移把私有群批量变公开)。
**判据 1–13 全部取证**:多选择器兜底(新名在前)· 卸载反向还原(`insertBefore` + `prevCss` 恢复 + `wrap.remove()`)· 保活 `display:none`(草稿在 React state、滚动位置 `listRef`)· 上限 5 + 具名 toast(⛔ 无 `.slice` 截断)· 槽位失效 `.imtabs-alert` 可见 · presence 快照同源 · 发现+加入(`not_found`/`not_discoverable` 归同一 404 不泄露存在性、`join_closed` 403、`room_full` 409)· 重连 2^n 封顶 15 s + 抖动 · 词条 zh/en 双侧齐且渲染面零硬编码中文。
⚠️ **实际读数漂移**:E 单 §四-5 与用户口径写的迁移号是 **v15**,实测 **v15 已被别的线占用 ⇒ 取 v16**(与「迁移号只增、取下一个空号」纪律一致)。
🔴 **未完成项(卡点,⛔ 非"更慢更绕")**:**生效链路第 ④ 关「线上可见面」未做** —— 本棒边界明令「⛔ 不部署到 47/106」,且实例内 client bundle 的投放属**插件投放线**(须走候选池 → 用户自助启用 → **重启实例**)。⇒ **E 单 ⛔ 不能判「已交付」**,须由投放线接力出线上截图。
⛔ 未 commit/push/scp、⛔ 未部署、⛔ 未动 47/106、⛔ 未引入新依赖、⛔ 未改 A–E 既有结论、⛔ 未替用户拍 §4 未决项。
**下一棒**:⛔ **未登记** —— A–E 五单已全部落地 ⇒ 下一步只能按 **§4 剩余待拍板项**推进,而三项(E2EE 档位 / 房间上限与预算数值 / 连接层三候选)**均需用户先拍板** ⇒ 按纪律**停手等拍板**(见下行)。
- 🔴 **2026-09-23 06:3x|IM 线下一步 = 等用户拍板(⛔ 未登记任何接续棒)**:理由 = §4 三项待拍板项**无客观优劣**,且**都不是"执行棒能自己判"的技术项** ⇒ 拍板前 ⛔ 不开新棒、⛔ 不引外部组件。**E 单的线上交付另需投放线**(见上行卡点)。
- ✅ **2026-09-23 12:1x|第 10 棒(收口棒)✅ 完成 —— 三项待拍板全部落定** —— 回填见选型单 **§十四 用户拍板落定**(新建小节)+ A 单 §四-7 / §十-4 状态块(⛔ 各单既有结论未改)。
**三项拍板**:① **E2EE 形态档位 = 强度档**(含量化性能判据:密码学开销 ≈ 55 µs/条 ⇒ 相对 1 s 事件循环预算 0.006%、50 msg/s 下占单核 ≈ 0.3%)② **房间上限与预算 = 按建议固化**(`2000` / `10` / `30 s` 转正式值 + 适用前提 = 单进程自研扇出形态,改形态须复测)③ **连接层 = 候选 B(引入 Centrifugo 类)**(含三条硬实现项:补发自建心跳 / 可替换接口 / 容量测算 70.1 KiB·连接)。
🔴 **本棒最重要的一条提醒**(⛔ 别漏):**强度档的"影响较小"只覆盖 CPU 维度**,三处结构性代价须按实现项对待 —— ① 棘轮状态每设备持久化(丢了永久断链)② **群聊必须 Megolm 化**(误用严格双棘轮 ⇒ 扇出 `O(N)`→**`O(N²)`**,**这才是会压垮连接层的那条**)③ **agent 代答模型重做**(`im_agent_grants` 在强度档下不成立 ⇒ D 单 §四-2 须重写、E4-6 断言须改写)。
⚠️ **落地顺序硬约束**:**档位重写(选型单 §十四-14.3 的 9 条清单)先于任何 E2EE 代码动工** —— E-4 的 8 条断言里 E4-4/E4-6/E4-8 三条是档位相关的,抢跑等于白写。
⏸ **本轮只拍板未落码**:三项都是"先定方向再动工"的决策项;下一棒的动工范围取决于**先做哪一项**(见下行)。
- 🔴 **2026-09-23 12:1x|本轮收口时所处状态 = 「三项已拍板 · 未排下一棒」** —— 三项拍板**已落文档**、**⛔ 尚无任何一行按新档位改动的代码**。
**下一步可选(⛔ 待用户拍板先做哪项,本入口不替排)**:
- **① 档位重写(做 E2EE 线的前置)** —— 按选型单 **§十四-14.3 的 9 条清单**重写。🔴 **顺序硬约束:此项必须先于任何 E2EE 代码动工**(E-4 八条断言里 E4-4 / E4-6 / E4-8 三条是档位相关的,抢跑等于白写)。
- **② 连接层换型(候选 B ⇒ 引入 Centrifugo 类)** —— 三条硬实现项:补发自建心跳 / 按"可替换连接层"接口落(⛔ 不把概念泄漏进 `src/im/**`)/ 出容量测算表。
🔴 **上游阻塞未解**:IM 入口 §2 序号 6 记的「`/opt/dshs` 与 `/opt/dsh` 分叉」仍在 ⇒ 换型须先定部署根(⛔ 两套根同时存在时安装位置不可判)。
🔴 **2026-09-23 A 线新查实的事实(对 ② 直接有用)**:**47 的代码/二进制根 = `/opt/dshs`;106 的 = `/opt/dshs-cluster`**(106 上**没有** `/opt/dshs`)⇒ 原「`/opt/dshs` 与 `/opt/dsh` 分叉」这个表述**要修正**:真正分叉的是**47 与 106 两台机之间**,不是 47 上两个目录之间。`/opt/dsh` = 平台数据根(state/backups/artifacts/users),**三台机同名**。
- **③ E 单线上可见面** —— 卡在**插件投放线**(实例内 client bundle 须走 候选池 → 用户自助启用 → **重启实例**),⛔ IM 线自己交付不了,须投放线接力。
✅ **2026-09-23 投放线已推进**:装配协议 `link:` 已落地并两机部署(见该线入口 §0 末行)⇒ ③ 的**载体条件已具备**,只差把 `poc/im-conversation-tabs/` 打进候选池投放。
- ✅ **2026-09-23 16:2x|第 11 棒(执行棒)✅ 完成 —— ①档位重写(纯文档面 · 零代码)** —— 用户口令「先把 a线做完,处理完成后在做b线」,A 线收口后转本线。
**交付(4 个文档面,⛔ 零代码改动)**:
① 🔴 **`交接单/IM群组-稳定性机制与框架选型.md` §四-6 整节按强度档重写** —— 密钥层级 3 层 → **4 层**(新增「发送者链 `Chain(room_id, sender_device_id, chain_index)`」);`DK(room_id, epoch)`(每 epoch 一把)→ **每消息一把 `MK_i`(棘轮推进)**;「密钥包」→ **「链头包」**(表 `im_room_keys` 的列名 `epoch` → `sender_device_id` + `chain_index`);密钥包/链头规则、消息路径上行字段、换钥三条(**加人也需分发链头** ← 与旧文相反)、**E4-4 / E4-6 / E4-8 三条断言按链轮次改写**;新增 **N-1 棘轮状态持久化** + **N-2 Megolm 化(保 `O(N)` 扇出)** 两条硬要求;旧「档位待拍板」表述作废。
② `交接单/IM群组-D-插件SDK与扩展点契约.md` **§四-6 重写**(agent **升格一等成员设备**;`im_agent_grants` 旧模型说明其为何在强度档下不成立 + 表结构重写方向)+ **新增 §十一「档位重写落地说明」**(11.1 逐条影响表 / 11.2 四条写码硬约束 / 11.3 未做项)。
③ `调整方案/142-IM群组对话-需求基线与方案.md` §六-3 更新为「档位已定 = 强度档」。
④ 选型单 **§七-2 表格加「2026-09-23 现值」列**(⛔ 原始待定态记录保留,不改历史)+ §十二-187 行 E2EE 论据加档位更新标注 + D 单 §十-1 执行回报行加标注指向重写版。
**§14.3 九条清单落位核对**:1/2/3 ✅(E-4 §6.1/6.2/6.3 已改)|4/5/6 ✅(E4-4/E4-6/E4-8 已改)|7 ✅(D 单 §四-2 已重写)|8/9 ✅ **明确保留不改**(设备身份密钥只签名 · relay 零改动 —— 已就地写「原样保留」)。
**门禁**:本棒**纯文档** ⇒ ⛔ 未跑 `npm test`(零代码漂移无需回归,且选型单既有的 Node 22 基线读数仍有效);`find` 时间戳核实**只动了 4 个 .md**、`src`/`test`/`skills` **零改动** ✓。
⛔ 未部署、⛔ 未动 47/106、⛔ 未 commit / 未 push、⛔ 未引入新依赖、⛔ 未建表未写一行密钥面代码(遵守 §14.3「重写先于代码」的顺序约束)。
🔴 **下一步(② / ③ 二选一,⛔ 均需用户拍板或投放线接管)**:
- **② 连接层换型**:**技术项、可自决**,但**体量最大**(按"可替换连接层"接口落 + 补心跳 + 容量测算表),且**必须先定部署根**(本轮已查实 = 47 `/opt/dshs` / 106 `/opt/dshs-cluster`,阻塞已解 ⇒ 可开工)。
- **③ E 单线上可见面**:**需投放线接力**(把 `poc/im-conversation-tabs/` 打进候选池 → 用户启用 → 重启实例)——投放线装配协议已就绪 ⇒ **可接力**。
🔴 **仍在等拍板的唯一一项**:**跨区可见性默认**(倾向互不可见,§4-5)—— 与本轮解耦。
- ✅ **2026-09-23 18:2x|第 11 棒(收口棒)✅ 完成 —— 优先级拍板:先做连接层 B 案** —— 用户原话「**先做b**」。
**本棒动作 = 只落优先级 + 交接动工范围**(⛔ 未写码):上下文已达 162k token(触发预算告警)⇒ 按纪律收口开新会话执行,⛔ 不在高上下文里开工写码。
**🔴 本棒最有价值的实查发现**:B 案的"可替换连接层"接缝**已经现成** —— `src/im/ws.ts:209` 的 `createImWsRuntime(options)` 工厂 + `ImWsRuntime`(`:190`)/`ImWsRuntimeOptions`(`:163`)/`ImUpgradeDispatch`(`:188`)接口 + `resolveInstance?` 注入钩子(`:179`,`:387` 调用点)⇒ **⛔ 不必从零造接口**,B 案 = 在此接缝上加 Centrifugo 后端实现(命名须中性化,⛔ 不把 Centrifugo 概念泄漏进内核)。
**动工范围已写进 §2「第 11 棒」**(含 5 步建议顺序 + **三条硬实现项**(补发自建心跳/可替换接口/容量测算表)+ **验收判据对账表**(10k P99 对账 E-2 的 28.9/68.1/111.9 ms、扇出离散中位 17.2/57.9/97.8 ms、心跳超时率 0、`/api/im/ws` 路径与帧格式语义不变)+ 环境注意(106 资产位置 / `ssh test106` / `ulimit -n 65536` / Centrifugo v6 配置键改位))。
**未决(§4-5,⛔ 未替用户拍)**:跨区可见性默认(倾向"互不可见")|强度档下 agent 代答具体形态(属 D 单重写面)。
- ✅ **2026-09-23 19:0x|automation 库已自愈(app 自动重建)+ 第 12 棒接续棒已补登** ——
**库状态(实测)**:`workbuddy.db` **magic 已恢复为 `SQLite format 3`**(mtime **18:55**);损坏库被 app 自动改名留档 = `workbuddy.db.corrupt-2026-09-23T10-55-47-845Z`(**35,377,152 B**,与既有诊断读数同尺寸)+ 同目录留 `workbuddy.db.recovery-pending`(`state=PENDING` / `walReplayed=null`)⇒ **app 自己走了「重建空库」那条路**(= DB 修复讨论里的**案 A**)。
🔴 **代价**:**历史 automation 记录全丢** —— `list` 只剩两条 2026-08-27 的旧条目 ⇒ 其余各线的接续棒**需逐线重登**。
**补登结果**:第 12 棒(动工连接层 B 案)已登记 = automation **`89e11630-4a06-41ca-bfc7-3c66ff4d8a53`**(一次性 · `scheduledAt = 2026-09-23T19:10` · `cwds = E:\ProgramData\AIProject\aliyun-dsh-server`)⇒ 下行「登记受阻」块**已解**。
🔴 **B 案零开工取证(本行实跑)**:`grep -ril "centrifugo\|ImConnectionBackend\|connection-backend" src poc test` ⇒ **零命中**;`src/im/` 仍为原 8 文件 + `sdk/` ⇒ 与「第 11 棒收口时⛔尚无一行按新档位改动的代码」一致。
🔴 **编号口径更正(本节内两处撞号)**:§2 那段标题与下行受阻块原写「第 11 棒」,与 §0 已完成的第 11 棒(①档位重写 + 收口)**撞号** ⇒ 本节起**下一棒 = 第 12 棒**(与手工接力模板的会话名 `IM线-第12棒` 一致)。
🔴 **E 单线上可见面的缺口(本行实查,⛔ 尚无人认领)**:投放线入口 `接续入口_插件投放与分库线_20260922.md` 里 **`im-conversation-tabs` 零命中** ⇒ 该投放**没有落在任何一方的待办清单上**(本线 §0/§3-③ 说"须投放线接力",投放线入口没登记它)⇒ **第 12 棒收口时须显式登记到投放线**(或本线自行排棒)。
✅ **已办(2026-09-23 19:4x,第 12 棒收口执行)** —— 已在投放线入口 **§5「本线后续」新增第 5 条**(写明"是什么 / 为何以前没人做 / **硬阻塞 = 本线 P0 协议不一致** / 投放前须先等第 13 棒修完"),并顺带更正该入口 §0 末行「automation 不可用」的**过时前提**(库已于 18:55 由 app 重建)。⚠️ **归属仍未认领** ⇒ 已列进 §2「第 13 棒」的连带待办。
⚠️ **本条未做**:`MEMORY.md` 瘦身仍欠(另轮处理)。
- ✅ **2026-09-23 19:3x|第 12 棒(执行棒)✅ 完成 —— 连接层 B 案已落地(本机代码面,⛔ 未部署)** —— 回填见选型单 **§十五 B 案实现回报(IM 线 · 第 12 棒)**(新建整节,⛔ §十四 未改)。
**交付**:新建 `src/im/connection-backend.ts`(中性接口 + 选择器 + 保活计划 + 两条自检)|`src/im/keepalive.ts`(自建保活垫片)|`src/im/backends/{native,gateway}.ts`|`test/im-connection-backend.test.mjs`(**28 用例**)|`poc/im-connection-gateway/{config.json,nginx-location.conf,README.md}`;改 `src/im/hub.ts`(**纯重构**:扇出抽成可注入 `ImFanoutPort` + 新增 `deliverFrame()`)|`src/web/routes/im.ts`(后端选择 + 迟绑定扇出槽位 + `/api/im/stats` 回带后端读数与保活要求)|`package.json`(+2 −2)。
**判据读数**:`npm run build` rc=0 | 新测试 **28 / 28 过 / 0 败** | `npm test`(Node 22)rc=0 = **478 / 476 过 / 0 败 / 2 跳过**(第 9 棒基线 420/418 ⇒ 本棒登记 **+28**,余 30 条为并发会话同仓贡献)| `check-layering` rc=0 **✅ 无新增违规** | `smoke:subdomain` = **`OK: subdomain routing + auth flow passed`**(⚠️ 脚本跑完不自退 = 既有现象,外层 timeout 兜底)。
**三条硬实现项逐条落地**:① **自建保活** = `keepalive.ts`(单扫描器 + **每连接自己的死线**)+ **E-7 三向实证** ② **可替换接口** = 中性命名 + 两后端 + 能力自述("不发服务端心跳却不声明需要垫片" ⇒ **构造即抛**)③ **容量测算表** = 见 §15.5。
**E-6 对账 E-2(同脚本同参数)**:扇出离散中位逐档重合(1k 19.07 vs 17.2|5k 51.23 vs 57.9|10k **99.12 vs 97.8** ms)⇒ **B 案依据未被推翻**;⚠️ P99 三档偏高 6–49%(10k 141.64 vs 111.9 ms,原因未定位)⇒ 只对"扇出中位 + 内存中位"作等值判定。
🔴 **E-7(本棒新做·硬实现项 ① 的判据,106 · 10k)**:`off`(不发 ping)t=26 s 掉到 **0**(全断)|`global`(整进程统一计时)掉 **2517**(E-2 记录 2773,**独立复现**)|`conn`(每连接认证后起计时)**掉 0**(10000→10000→10000,worker `closed=0`)。三组 `wsPingRecv` 恒 **0**。
🔴 **P0 发现(本棒最重要,⛔ 未修 —— 详见 §15.7)**:**E 单客户端 bundle 与平台 `/api/im/ws` 的帧协议不一致** ⇒ 客户端发 `{type:'subscribe'}` / `{type:'ping'}`、按 `msg.type` 收帧(`poc/im-conversation-tabs/lib/client.js:45/421/439/446/460-464`);平台认 `{op:…}`,其余 ⇒ `reject('bad-op')` ⇒ **`socket.destroy()`**(`src/im/ws.ts:251/264-303/347`),推的帧一律 `{op:…}` ⇒ **投放后必现「连上就断、一条消息看不到」** ⇒ **E 单线上可见面在修掉它之前不可能通过**。
⚠️ **口径更正(本棒)**:「心跳超时率 = 0」**不能**直接用 `e2sum.py` 的 `hbTimeoutRate`(它把爬坡期建立的连接也算作"应完成整轮" ⇒ 结构性非零:1k 0.0005 / 5k 0.0067 / 10k 0.042–0.052)⇒ **权威判据应改为「服务端 `num_clients` 不下降 + worker `closed=0`」**(E-7 的 `conn` 口径两条都满足)。
⚠️ **未完成项**:在线态取数**未接线**(`stats().presenceIngest='not-wired'`)|gateway **未部署**(47/106 一行未动)|**客户端侧垫片未接线**(被 P0 阻塞)|50k 以上容量为线性外推、CPU 未测。
⛔ 未 commit/push/scp|⛔ 未部署|⛔ 未动 47/106|⛔ 未引入新依赖|⛔ 未改 A–E 五单结论|⛔ 未替用户拍 §14.6 的未决项。
**下一棒(唯一)** = **第 13 棒 · 执行棒** = automation **`31f0b0d7-f652-4081-88d0-3aec69136949`**(一次性 · `scheduledAt = 2026-09-23T19:43` · `cwds = E:\ProgramData\AIProject\aliyun-dsh-server`)= **修 §15.7 的 P0(客户端协议对齐 + 保活垫片接进客户端传输层)**,动作见 **§2「⏭️ 第 13 棒」**。排它的理由 = **先修掉阻塞「已交付功能」的缺陷**(属客观可判,非业务优先级);② 部署 + ③ 在线态接线排在它之后。
✅ **收尾五件全过**:① 结果回填(选型单 §十五)② 域锁已 `--release-exec`(复核 `交接单/.locks/` 为空)③ 下一棒已登记(= 上行 automation `31f0b0d7`;automation 库本轮实测**可写**)④ 入口 §0/§2 已推进 ⑤ 当日日志 `.workbuddy/memory/2026-09-23.md` 已追加。
✅ **顺带办掉一条跨线缺口(本行第 136–137 行)**:E 单投放面此前"两不管" ⇒ 已在**投放线入口 §5「本线后续」新增第 5 条**(写明"硬阻塞 = 本棒 P0")+ 改正该入口 §0 末行「automation 不可用」的**过时前提**(库 18:55 已由 app 重建)。⚠️ **该投放的归属仍未认领** ⇒ 已列进 §2「第 13 棒」的连带待办。
⚠️ **编辑事故与修复**:插入 §2「第 13 棒」块时**误把 §3 的标题行当作了 `old_string`** ⇒ 一度删掉 `## §3 已取事实…`;本棒已复原,`grep "^## §"` 复核 §0/§1/§2/§3/§4/§5/§6 **齐全**。教训:用 Edit 插块时 ⛔ 不要把**下一节的标题行**当锚点。
- ✅ **2026-09-23 19:5x|第 13 棒(执行棒)✅ 完成 —— §15.7 的 P0 已修(本机代码面,⛔ 未部署 / 未投放)** —— 回填见 `交接单/IM群组-E-会话界面UI.md` 新追加的 **§执行回报(IM 线第 13 棒)**(⛔ E 单正文与结论一字未改)+ 选型单 **§15.7 处的 🔻 处置标注**(⛔ 原文未改)。
**改动面 = 2 个文件**(严格按本入口 §2 第 13 棒的动工范围):`poc/im-conversation-tabs/lib/client.js`(主件)+ `test/im-ui.test.mjs`。
**判据读数**:`npm run build` rc=0 | `node --check` 双通过 | `im-connection-backend` **28 / 28 过**(⛔ 未回退)| 四件 IM 用例 **148 / 147 过 / 0 败 / 1 跳过**(im-ui 29 ⇒ **30**)| `npm test`(Node 22)rc=0 = **479 / 477 过 / 0 败 / 2 跳过**(第 12 棒基线 478 / 476 ⇒ 本棒 **+1**)| `check-layering` rc=0 ✅ **无新增违规**。
**修法**:客户端对齐平台的 **op 制** —— 出站收敛到唯一入口 `sendOp()` + 白名单(`subscribe / ping / resume`);入站 `switch (msg.op)` **全量 9 分支**(`hello/subscribed/unsubscribed/message/messages/presence/sent/pong/error`,`error` 写进 UI ⛔ 不静默);**自建保活**(周期 `⌊26000/3⌋ = 8666 ms` = 垫片 `planKeepalive()` 同款口径 + **死线由本连接 `open` 时刻推算** + 半开链路检测)。⛔ **未改服务端**("兼容 `type` 别名"= 永久两套协议,本线已定不做)。
🔴 **本棒最重要的发现(入口 §2 的一条建议本身是陷阱)**:原写「保活 ⇒ `{op:'pong'}`(`pong` 是已认 op)」—— **`pong` 不是已认 op**:`src/im/ws.ts` 的客户端 op `switch` 只有 `ping / subscribe / unsubscribe / resume / send` **5 个 case**,`pong` **只出现在该文件模块头注释的表格里**(`case 'pong'` 检索 = **false**)⇒ 照原建议发 `pong` 会落 `default` ⇒ `bad-op` ⇒ `destroy()`,**与 P0 同症**。已改发 **`{op:'ping'}`**,并在 `E-14` 加**反向专测**钉死前提。⚠️ `ws.ts` 头注释与实现不一致,属内核面 ⇒ **只报告未改**。
**新增判据 `E-14`(协议一致性,两侧源码机械对账)**:① 出站 `subscribe / ping / resume` **⊆** 平台认的 5 个 op ✅ ② 无 `type:` 出站帧、无 `msg.type` 分支 ✅ ③ 客户端 `case` 集合 **=** 平台推帧集合,**恰 9 个** ✅ ④ 周期 +「每连接 `open` 起计时」两条都在 ✅。非空绿证据 = `tmp/im13-proto-proof.mjs`(只读打印)。
**三处口径偏离(已逐条写进回报 §6)**:① `resume` 的 `since` **不用** `subscribed` 回带的 `cursor`(那是**房间最新 seq** ⇒ 补拉取出 0 条),改用**本端自己的进度** `cursorRef.current` ② 发送路径**保持 REST**(入口那条以"bundle 没有发送路径"为前提,**该前提不成立** ⇒ ⛔ 未做未被要求的路径变更)③ REST `pull(cursorRef.current)` **保留不动**(E 单既有判据 `E-9c`),与 WS `resume` 并存且按 `m.id` 去重。
🔴 **未完成项**:**E 单线上可见面(生效链路第 ④ 关)仍未验收** —— 须走候选池投放 → 用户自助启用 → **重启实例** ⇒ E 单 ⛔ **仍不能判「已交付」**;本棒只解掉它的**唯一硬阻塞**。
⛔ 未改 `src/im/**` | ⛔ 未改 A–E 五单结论 | ⛔ 未部署 / 未投放 | ⛔ 未 commit/push/scp | ⛔ 未引入新依赖 | ⛔ 未动 47 / 106 | ⛔ 未碰 §14.6 未决项。
**下一棒(唯一)** = **第 14 棒 · 执行棒** = automation **`2d059ec8-0664-45de-a1c1-e8478f1686df`**(一次性 · `scheduledAt = 2026-09-23T20:15` · `cwds = E:\ProgramData\AIProject\aliyun-dsh-server`)= **认领并执行 `im-conversation-tabs` 的投放(可自动部分)+ 出线上取证清单**,动作见 **§2「⏭️ 第 14 棒」**。
- ✅ **2026-09-23 20:2x|第 14 棒(执行棒)✅ 完成 —— `im-conversation-tabs` 的投放(可自动部分)已落地 47;E 单只剩「用户点启用」这一步** —— 回填见 `交接单/IM群组-E-会话界面UI.md` 新追加的 **§执行回报(IM 线第 14 棒)**(⛔ E 单正文与结论一字未改)+ 操作单 `E:\ProgramData\AIProject\aliyun-dsh-server\交付物\IM群组-ui投放与线上取证操作单-20260923.md`。
**投放物**:`@dsh-local/im-conversation-tabs` v`0.1.0`|tgz sha256 **`c9b1184aca8eadd6c84316d2e79427546a65f59f0614a2ec9704c895a311bfb1`**(21,494 B)|`lib/client.js` md5 **`21fbc52fdedc9915238dd1c3f5c5e827`**(=第 13 棒修完 P0 的那一版,本机源 / tgz 包内 / 47 共享层**三处逐字一致**)。
**判据读数**:① 包三件齐断言 **rc=0**(`exports['./client']` / `dsh.client.platform='web'` / `dsh.bundle.patch`)② `POST /api/plugins/business` ⇒ **http=200**、池 **4 → 5**、`replaced:false`、`trustedOverride:false`、`compat.level:"unknown"`(非 incompatible)、无 P0 命中 ③ `POST /api/plugins/business/share` ⇒ **http=200 `action:"created"`**、`fileRef = link:/var/lib/dshs/bundled-plugins/_dsh-local_im-conversation-tabs`(root:root、全 world-readable)④ 审计 `audit_log` id **360**(upload)/ **361**(share)⑤ 回滚素材 `/opt/dsh/backups/plugins/_dsh-local_im-conversation-tabs/0.1.0.tgz`。⛔ 未铺 profile、⛔ 未改任何源码、⛔ 未 commit/push/scp。
🔴 **本棒自决项(可推翻)**:入口 §2 只写「打进候选池」,本棒**同时发布到共享层** —— 装配协议是 `link:`(09-23 拍板)⇒ profile 写的是软链依赖、目标是共享层目录,**只进池不发布**用户启用必撞 `plugin_bundle_missing`(=半成品投放)。⇒ 已写进投放线入口 §5-5:**本线后续给任何插件投放都要带上这一步**。
🔴 **本棒最重要的口径更正(入口 §2 的判据 ③ 取不到)**:**IM 链路服务端一条日志都不写** —— `src/im/ws.ts` / `src/web/routes/im.ts` / `src/supervisor/proxy.ts` 三处 `process.stderr` / `console.*` **零命中**;`reject()`(`ws.ts:227-234`)只回一帧 `error` 再 `destroy()`,**不落日志** ⇒ `journalctl | grep bad-op` **恒空**,而"空"会被误读成"没发生"(PB 已记同类坑)。⇒ 判据 ③ 的取证面**改到客户端**(DevTools → Network → WS → `/api/im/ws` → Messages 帧流水 + `.imtabs-conn` 连接态),已写进操作单 §三。
⚠️ **另两条实测发现(只报告未动,R7)**:① 47 上在跑的 **admin 实例**其 bwrap **没有** `--ro-bind-try /var/lib/dshs/bundled-plugins`(scope 启动于 **09-21 00:55**,早于该绑定上线;47 的 `orchestrator.js:1047-1048` 已含)⇒ **必须走一次重启**才带上——**启用动作自带重启**,无需额外动作;106 在跑的实例 cmdline 里**有**该绑定。② 47 admin profile 有一条 **09-21 的陈旧软链** `node_modules/@dsh-local/storyforge`(`dependencies`/`bundles` 里都已无),第 8 棒探针残留;③ 发现 **13 条 `user_agent='poc-curl2'` 的历史临时 admin 会话**未清(TTL 600 s ⇒ 早已过期)——本棒**只删自己那一条**(`deleted_sessions=1`),其余只报告。
⚠️ **未完成项(卡点,⛔ 非"更慢更绕")**:① **E 单「线上可见面」仍未验收** —— 卡在**用户动作**(实例「功能管理」启用),本棒把"无解"推进到"只差点一下",⛔ 不能替用户点 ② **106 共享层缺该包** ⇒ 若目标实例在 106 启用会失败(属「跨节点内容分发」立项面,操作单 §四-1 已给手工同步命令,**本棒未执行**)③ 在线态取数未接线 / gateway 未部署(与第 12 棒同一批,排在再后)。
⛔ 未改 `src/im/**` | ⛔ 未改 `poc/im-conversation-tabs/**`(打包只做**只读复制**)| ⛔ 未改 A–E 五单结论 | ⛔ 未 commit/push/scp | ⛔ 未引入新依赖 | ⛔ 未铺 profile | ⛔ 未取用 47 的 PG 凭据 | ⛔ 未碰 §14.6 未决项 | ⛔ 未替用户点启用。
**下一棒(唯一)** = **第 15 棒 · 执行棒** = automation **`4e08cc77-c222-43ee-a3f5-521baebe2d70`**(一次性 · `scheduledAt = 2026-09-23T20:45` · `cwds = E:\ProgramData\AIProject\aliyun-dsh-server`)= **47 就地部署连接层 gateway + 在线态取数接线**(第 12 棒 §十五 的两个未完成项),动作见 **§2「⏭️ 第 15 棒」**。⚠️ 若用户先反馈了操作单 §三 的六条取证结果 ⇒ **优先按反馈处置**(那是"已交付功能"的验收面)。
- ✅ **2026-09-23 21:3x|第 15 棒(执行棒)✅ 完成 —— 47 就地部署连接层 gateway + 在线态取数接线(第 12 棒 §十五 的两个未完成项**全部闭合**)** —— 回填见选型单 `交接单/IM群组-稳定性机制与框架选型.md` 新追加的 **§15.11**(⚠️ 落点**不是**本入口原写的「§15.8」—— §15.8 已被「未完成项 / 已知限」占用 ⇒ 改落 §15.11,⛔ §十五 既有小节一字未改,只在 §15.8 表前加了一段 🔻 状态标注)。
**交付(①代码面 / ②47 部署面)**:① `src/im/presence-ingest.ts` **新建**(取数端口 + fail-closed 解析器 + 常量时间 `secretEquals`)+ `src/im/connection-backend.ts`(增可选 `presenceIngest`)+ `backends/{native,gateway}.ts`(自述如实报 `local-hub` / 未接线则仍自曝 `not-wired`)+ `src/web/routes/im.ts`(**网关回调面 `POST /api/im/gateway/presence`** + 成员表/发消息的在线态改走端口 + `stats()` 增 `presenceIngest` / `presenceOnline`)+ 新测 `test/im-presence-ingest.test.mjs` **13 用例**;② 47 上 **20 个 `lib/**` 产物逐字一致**(属主 `root:root`)+ 网关进程 `/opt/dsh-gateway/centrifugo`(v6.9.6,sha256 `f4869bf9…a848c`)+ `config.json`/`gateway.env`(600)+ 单元 `dsh-gateway.service`(enabled+active、**只绑 `127.0.0.1:18081`**、公网 IP 探测 rc=7)+ 边缘片段落盘 + 旧产物备份 `/opt/dsh/backups/im-gateway-deploy-20260923-210125/`。
**判据读数**:`npm run build` **rc=0**|B 组 **28 / 28**|新测 **13 / 13**|`npm test`(Node 22)**492 用例 / 490 过 / 0 败 / 2 跳过**|`check-layering` **rc=0**(未归类 1 = 既有 `src/platform-paths.ts`)|部署后 `/api/im/ws` **101**、`systemctl is-active dshs` = **active**、门户 **200**|`presenceIngest` **`not-wired` ⇒ `local-hub`(native) / `gateway-join-leave`(gateway)**、`presenceOnline` **0 → 2**、`presenceInputs` **0 → 2**、`presenceFrames` **0 → 1**|取数 E2E 订阅者收到原文 presence 帧|回调面 **带 key 200**(`{ok:true,source:"gateway-join-leave",accepted:2,rooms:1}`)/ **不带 401** `bad-gateway-key` / native 模式 **404 fail-closed**|**回滚演练** 删 drop-in → `daemon-reload` → 重启 ⇒ 环境键 0 个、握手 101、portal 200、`backendKind=native`、回调面 404 ⇒ **线上已回改动前**。
🔴 **保活三向实测(47 就地部署的网关上,240 连接 / 45 s)**:`off`(从不回帧)**80/80 掉**(`3012 · no pong`)|`conn`(按每连接收到自己的 ping 才回 `{}`)**0/80 掉**(收/回 **480/480**)|`global`(整进程一个节拍器)**80/80 掉**(`3501 · bad request`)⇒ §15.3(E-7)在**真实部署实例**上复现;干净 conn 全程 `num_clients` **240 不降**。
🔴 **一条既有结论被实测改写**:`client.ping_interval` **`0s` ⇒ `25s`** —— 「网关不发 WS ping」那条实测**本身没错**(`wsPingRecv=0` 数的是 **WS 控制帧**),错的是由它推出的结论;网关**会**按 `ping_interval` 发**协议级 JSON ping**,客户端只需**按连接回 `{}`**(没有待回 ping 时 `{}` 是空命令 ⇒ 被判 `bad request` 断开)⇒ 仓库与 47 部署两侧已同步修正。🔴 **遗留**:`GATEWAY_CAPABILITIES.providesServerHeartbeat` 仍 `false`(与实测不符)⇒ **已排入第 16 棒**。
⚠️ **未完成项(卡点,⛔ 非"更慢更绕")**:① **边缘未接进生效 vhost**(边界明令本棒不切流;片段已落盘 + `nginx -t` rc=0、生效 vhost md5 `897c26bf89165e55a7a22a8c46932972` 未变 ⇒ 只剩"插入 `ai1net.com.conf` 443 块 + reload"一步)② **客户端侧未按网关协议接入**(E 单 bundle 传输层仍按平台协议 ⇒ **切流的硬前置**)③ **106 未部署**(本棒只做 47)④ **容量未复测**(未切流 ⇒ 复测条件未到)。
⚠️ **一条异常(原因已查明)**:抢锁约 30 分钟后 `交接单/.locks/` **被清空**(本会话域锁消失、写文档被 hook 拦下)⇒ 已**重抢同一组域**并复核 `OWNER`(会话名/session_id 吻合)后继续。🔴 **原因 = 另一条线的棒放锁跑了 `--release-exec "<会话名>"`(脚本不读位置参数、取 `_who="${ME:-}"`,其 shell `ME` 未设)⇒ 命中 `-z` 分支 ⇒ 遍历 `.locks/*` 全删**(已记当日日志 + 沉淀 PLAYBOOK §24.4)⇒ ⛔ 与本棒无关。✅ 正确姿势 = `ME="<会话名>" bash scripts/handoff-guard.sh --release-exec` + 放锁后必核 `.locks/`;本棒收口已按此执行(**只释放自己的锁**)。
⛔ 未 commit/push/scp 源码(部署产物除外)|⛔ 未动 `poc/im-conversation-tabs/**` 一行|⛔ 未改 A–E 五单结论|⛔ 未引新依赖进 `src/**`(网关是**外部进程**)|⛔ 未动 106 用户实例面|⛔ 未取用 DB 凭据|✅ 清场:`poc-curl2` 临时会话 **0**、47 `/tmp` 残留 **0**、演练连接 **0**。
**下一棒(唯一)** = **第 16 棒 · 执行棒** = automation **`e14edc3d-0ab2-4a78-84be-c93752308bb9`**(一次性 · `scheduledAt = 2026-09-23T21:43` · `cwds = e:\ProgramData\AIProject\aliyun-dsh-server` **小写盘符**)= **换连接层的剩余代码面:① 能力口径修正(`providesServerHeartbeat`)② 平台侧接入面(令牌 / `connect` 代理二选一,执行棒自决)③ E 单客户端传输层双模化(含"按连接回 `{}`")④ 本机回环 E2E**,动作见 **§2「⏭️ 第 16 棒」**。⚠️ 若用户先反馈了操作单 §三 的六条取证结果 ⇒ **优先按反馈处置**。⚠️ **本棒不切流 / 不重打包 / 不重投放 / 无需部署**(切流 + 重投放 + 目标档位复测留第 17 棒,届时再排)。
⚠️ **`cwds` 字面取证 —— 本棒一次差点写错的更正(⛔ 记下来别再犯)**:判据群体 = **`sessions.cwd`**(宿主就是按它分组)—— **小写 `e:\ProgramData\AIProject\aliyun-dsh-server` = 159** vs 大写 `E:\ProgramData\AIProject\aliyun-dsh-server` = **4** ⇒ **正解 = 小写**。🔴 **陷阱**:若只量 `automations.cwds` 会**得出反结论**(大写 3 / 小写 1)—— 那只是"前面几棒写错的人多"(棒 12/13/15 用的正是那 4 条错字面里的),⛔ **不是判据**。⇒ 第 16 棒 `cwds` 已按**小写**登记(本棒初登记时曾误用大写,已当场改正)。
- ✅ **2026-09-23 23:1x|第 16 棒(执行棒)✅ 完成 —— 换连接层的剩余代码面全部闭合** —— 回填见选型单 `交接单/IM群组-稳定性机制与框架选型.md` **新建 §十六**(⛔ §十五 既有小节一字未改)+ E 单 `交接单/IM群组-E-会话界面UI.md` **追加**「§执行回报(IM 线第 16 棒)」(⛔ 未改既有正文)。
**交付(三项代码 + 一份 E2E)**:① `src/im/backends/gateway.ts` 的 `providesServerHeartbeat` **`false`⇒`true`**(依据 = §15.11.3 三向实测)+ **连带**新增结构性判据 `fanoutOffsite && !needsKeepaliveShim ⇒ throw`(否则原"构造即抛"因该字段改真而**变成装饰**)~ 口径依据 = 「会不会自己发 ping」与「平台能不能替远端 socket 代发」**正交**;② 平台侧接入面 **选 (a) 发二层票据 + 新增订阅回检**(`POST /api/im/gateway/token`、`POST /api/im/gateway/subscribe`、`GET /api/im/transport`、`stats()` 增 `gatewayAccess`)~ 被否 (b) `connect` 代理(理由:改动落部署物、回滚面更大、鉴权依赖边缘带 cookie ⇒ **与"边缘未接 vhost"耦合**,且工作量不更少);③ `poc/im-conversation-tabs/lib/client.js` 传输层**双模化**(`native` **逐字不变**、分发共用一个 `onAppFrame`;`gateway` 走网关协议:`connect` → `subscribe` → **空对象即回 `{}`** → `push.pub.data.frame` 后照原路径分发;模式**从平台只读面取,⛔ 未硬编码**);④ 新增测试 `test/im-gateway-access.test.mjs` **15 用例**(**G-8/G-9 直接驱动真客户端件**)。
**判据读数**:`npm run build` **rc=0**|新测 **15 / 15**|B 组 **28 / 28**|presence 组 **13 / 13**|`npm test`(Node 22)**507 用例 / 505 过 / 0 败 / 2 跳过**|`check-layering` **rc=0**(未归类 1 = 既有 `src/platform-paths.ts`)|**本机回环 E2E(真 Centrifugo v6.9.6 + 真平台进程 + 真客户端件)16 / 16 PASS**:`upgrade=101/101/101 connect应答=3/3`、订阅票据 `alice=200 / bob=403 not-a-member`、带票据订阅 `{"id":2,"subscribe":{…}}`、**不带票据**非成员 `{"id":2,"error":{"code":403,"message":"not-a-member"}}`(回检兜底生效)、一条消息 `发送=200 delivered=1` 双端收到且原文含标记、presence `进=true 出=true`、**保活 `ping数=10 / alice.closed=false / carol.closed=false / num_clients=2`(掉线 0,两侧同证)**。
🔴 **七条实测踩点已写进 §16.5**(样例配置带中文注释不能直喂网关|回检密钥必须放 `http.static_headers`(`http_headers` 只是**头名**,实测两种错法)|`ping_interval` 必须 > `pong_timeout`|"回 `{}`"须常驻且只回一次|Windows `path.join` 反斜杠 ⇒ 路径转换**静默失效** ⇒ 网关**不报错退回默认配置**|就绪轮询须接住 `fetch` 抛错|判据谓词"空真"陷阱)。
⚠️ **本机载体受限(与产品无关)**:Windows 版网关二进制**不可得**(GitHub 0–19 KB/s、六镜像全败、106 亦 rc=28)⇒ 用 **106 上已有 Linux 版**(md5 `17f814b5e048d1393508d4617b104a98`,回传后逐字一致)+ 混合载体(**网关跑 WSL 回环 18099**、**平台跑 Windows**,因 `better-sqlite3` 是 Windows 版原生模块)⇒ 两条链路均已实测。
⚠️ **未完成项(卡点,⛔ 非"更慢更绕")**:① **presence 回填生产侧缺调用方**(Centrifugo OSS 无 join/leave 回调,如实记 §16.7-1)② 切流 / 106 部署 / 容量复测(按边界留第 17 棒)③ **E 单线上可见面仍未验收** —— 卡在**用户动作**(实例「功能管理」启用现版 `v0.1.0`),⛔ 不能替用户点。
⛔ 未切流|⛔ 未重打包|⛔ 未重投放|⛔ 未 commit/push/scp|⛔ 未引新依赖进 `src/**`|⛔ 未改 A–E 五单结论|⛔ 未动 §14.6|⛔ 未动 106 用户实例面|⛔ 未取用 DB 凭据|⛔ 网关密钥未打印/未进日志/未进文档|✅ 网关全程只绑回环 + 端口避开 47 的 `18081`|✅ 清场:本机与 WSL 网关进程 **0**。
**下一棒(唯一)** = **第 17 棒 · 执行棒** = automation **`19904b63-431a-4f0a-85cc-c80fcb47221f`**(一次性 · `scheduledAt = 2026-09-23T23:37` · `cwds = e:\ProgramData\AIProject\aliyun-dsh-server` **小写盘符**)= **重打包 + 重投放 + 边缘接 vhost + 切流 + 目标档位复测(`2000` / `10` / `30 s`)+ 本棒新增面的部署同步**(`lib/**` 产物 + 47 网关 `config.json` 回检块按 §16.5-2 修正形态落盘),动作见 **§2「⏭️ 第 17 棒」**。⚠️ 若用户先反馈了操作单 §三 六条取证结果 ⇒ **优先按反馈处置**。
## §1 目标(**用户原话,⛔ 不得改写**)
> 「im具体如何实 才能在复杂环境下稳定 还需要调研同类开源框架,比如telegram等海外通讯项目,找个最新性能稳定最好的」
拆成三条待答判据:
- **T1 · 稳定性**:写出「复杂环境下稳定」的**具体机制清单**(⛔ 不是口号):每条机制要配**可验证判据** + **在我们代码里的落点**。
- **T2 · 框架调研**:同类开源框架(含 Telegram 系海外项目)给出**可比较的判据矩阵**(⛔ 不是罗列产品)。
- **T3 · 选型结论**:给出**可落地的选型建议** —— 判据以 **稳定性 · 性能 · 可运维性** 为主位;⚠️ **资源不是约束**(§0 口径更新:**机器可换** ⇒ 资源只作为方案的「**需求测算**」输出,⛔ 不作否决项)。既有资产见 §3-7。
## §2 🎯 本轮动作
**第 1 棒(规划棒)✅ 已完成(2026-09-22 17:4x)** —— 产出 `D:\github\dsh_shenxian\dsh-server-docs\交接单\IM群组-稳定性机制与框架选型.md`。以下为**原要求(存档用)**:出**一份可执行交接单** → 落 `D:\github\dsh_shenxian\dsh-server-docs\交接单\`(**先原子占号**;8 段模板 = 该目录 `README.md §二`),至少覆盖:
1. **T1 稳定性机制**:连接层 / 消息层 / 存储层 / 跨区层 四组机制,各自「机制 → 判据 → 落点(`file:line` 或明确"待新建")」。
2. **T2 框架判据矩阵**:候选 = ejabberd · OpenIM · Matrix(Dendrite) · Telegram 系(Opengram / Ferrite / teamgram)· Tinode · 轻量实时层(Centrifugo 类);判据 = 自托管可行性 / 资源占用 / 与 PG 共存 / 群规模 / E2EE / 许可与生态 / 与我们既有资产的契合度。
3. **T3 选型建议**:三案(**自研薄内核** / **整框架引入**(ejabberd / OpenIM 整装接入)/ **混合 = 借机制 + 引组件**)各写优点与缺点(**竖排成段**),按 **稳定性 · 性能 · 运维复杂度 · 资源需求(测算值,机器可换)** 四维比较,给建议与**可验证的下一步实验**。⚠️ **资源不作否决项**(§0 口径更新)。
⛔ **只出单**:不改码、不部署、不动 47/106、不替用户拍 §4 的项。
**✅ 第 2 棒(执行棒)已完成(2026-09-22 18:2x)** —— 实测结论与全部读数见该单 **§十二 执行回报** + **§十三 断言名清单(18/18)**。以下为**原要求(存档用)**:按交接单 `D:\github\dsh_shenxian\dsh-server-docs\交接单\IM群组-稳定性机制与框架选型.md` 执行其 **§八 步 1–4**:
1. 逐条核实该单 §二 的 **5 条只读前置**(🔴 若 `src/im/` 已存在、或 `schema.ts` 已 ≥v14 ⇒ **停手,改去 A 单**)。
2. 把该单 §四 机制清单(组 A–C 共 **18 条**)转成**断言名清单草案**(⛔ 不实现)。
3. 跑实验 **E-3**:ejabberd 能否用既有 `users` 作外部认证源、且**不落地自己的账号表** ⇒ 出二值结论 + 配置与日志证据。
4. 跑实验 **E-1**:自研 WS 连接上限标定(1k / 5k / 10k 并发)⇒ RSS + 心跳超时率 + 消息 P99。
⛔ 本棒**不做** E-2 / E-4(留给第 3 棒)|⛔ 不做 §八 步 5–8。
🔴 实验一律落 **106**(测试床);⛔ **47(生产 Manager)不动一行**。
⛔ 不改 `src/**` 功能实现、⛔ 不改 A–E 五单结论、⛔ 不部署、⛔ 不 commit/push/scp、⛔ 不替用户拍 §4 的未决项。
⚠️ 回填 = 在该单新增「§十二 执行回报」(格式见该单 §十一)。
**⏭️ 第 3 棒(执行棒续 · 本轮动作)**:继续按同一交接单执行 **§八 步 5–8**:
1. **E-2**:同机同压测脚本跑 **Centrifugo 类**组件,给同维度读数(RSS / 心跳超时率 / 消息 P99)—— 压测脚本已在 **106 `/root/im-e1/`**(`server.mjs` / `wsworker.mjs` / `run.mjs` / `sum.py`),⛔ 不必重写。
2. **E-4**:E2EE 形态对照(确认「中继只见密文 / 控制面只见密文与身份」在自研案下的最小实现路径:群密钥分发 + 成员变更换钥)。
3. **§八 步 6 二次判定**:「连接层引入与否」,判定必须写陈述句并**引用 E-1/E-2 的具体行**;⛔ 不预设结论。
4. **§八 步 8 回填**:把 §四 机制清单回填到 A–E 各单验收段(⛔ 不改各单结论)。
5. 🔴 **本棒新发现必须带进第 3 棒**:ejabberd `check_password` 的**进程内缓存**(改表不立刻生效、重启即生效)⇒ 案 2 需追加「查该缓存开关/TTL」前置项;E-1 的 **P99 ≈ 事件循环滞后** ⇒ 二次判定要把「扇出排队」作为自研侧的主项。
⚠️ 环境注意(第 2 棒实测,⛔ 别踩):**47 的 sshd 在 22 端口**(`~/.ssh/config` 的 `bt-server` 写 32022 已失效)|47 的 PG 是本机进程(**非** `docker exec dshs-pg`)且只走 TCP 需口令|**106 上 ejabberd 已装好但已 `stop`+`disable`**:**可直接复用的外部认证配置 = `/opt/ejabberd/conf/ejabberd.yml`**(现为 `auth_method: [external]` + `extauth_program: /usr/local/bin/im-e3-extauth.py` + `default_db: mnesia` + `loglevel: debug`);`ejabberd.yml.im-e3.bak` 是**改前原件**(默认 internal auth),⛔ 别当样例用。⛔ 不必重下(GitHub 直连约 1 MB/min)。
**⏭️ 再往后**:第 4 棒收口后与 IM 五单 A–E 合流(串行建议 A → B →(C ∥ D)→ E)。
**✅ 第 3 棒(执行棒续)已完成(2026-09-22 19:0x)** —— 动作 = 本单 **§八 步 5–8**:E-2(Centrifugo v6.9.6 同机同脚本对照)+ 二次判定(连接层引入与否)+ E-4(E2EE 最小路径)+ §四 机制清单回填 A–E 五单验收段。**全部读数与判据见 `交接单/IM群组-稳定性机制与框架选型.md` §十二 执行回报(IM线-第3棒)**(含:E-2 对照表、失败口径与实测教训、二次判定陈述句、E-4 的 8 条可断言路径、五单回填行号表)。
**⏭️ 第 4 棒(执行棒 · 本轮动作)**:本单内闭环收口两件,⛔ 不碰 A–E 结论、⛔ 不开工 A 单:
1. **E-5 = 自研扇出分片对照实验(主任务)** —— 把第 3 棒写下的**可证伪回退判据判定掉**(回退判据 = 自研完成扇出改造后 10k 档客户端观测 P99 > **150 ms** 或扇出离散中位 > **120 ms** ⇒ 引入 Centrifugo 类组件)。做法:在 106 `/root/im-e2/` **新建** `server-shard.mjs`(蓝本 = `/root/im-e1/server.mjs`,`/metrics` 字段与路径 `/api/im/ws` 保持一致),改造 = **(a) 分片扇出**(连接按编号取模分 S 片,每片 `setImmediate` 分时,不把整圈放同一 tick)+ **(b) 同片批写**(拼 Buffer 一次 `write` 或 `cork/uncork`);跑 1k/5k/10k(`e2run.mjs --target=raw --server=/root/im-e2/server-shard.mjs`,其余参数与第 3 棒完全一致),与第 3 棒 E-1 行**逐格对照**。🟢 只做正向迭代(任一档变差 ⇒ 停下复盘并如实报告)。🔴 若回退判据被触发 ⇒ ⛔ 不擅自引入组件,写清三条候选(就地继续优化 / 引入外部连接层 / 混合)各带「优点 / 缺点」竖排成段,**交用户拍板**。
2. **补跑 §九 判据 6 / 7(平台回归,第 3 棒欠账)** —— 本机 `D:\github\dsh_shenxian` 跑 `npm test`(Node 22,只判「0 败」);判据 7 = 既有「用户 → 自己实例」子域 upgrade 仍 101(测试集内无该用例 ⇒ 如实说明,⛔ 不自造)。
⚠️ 环境注意(第 3 棒实测,⛔ 别踩):**106 用 `ssh test106`**(别名已配;`ssh [email protected]` 会因 `IdentitiesOnly` 失败)|106 上必带 `ulimit -n 65536`(默认 1024)|E-2 资产在 `/root/im-e2/`(`e2worker.mjs` / `e2pub.mjs` / `e2run.mjs` / `e2sum.py` / `config.json` / `centrifugo` v6.9.6 二进制)|Centrifugo v6 配置键已改位(`client.token.hmac_secret_key`、`http_api.key`、端口用 `--http_server.port`)|47 的 sshd 在 **22** 端口|🔴 实验一律落 **106**,⛔ **47 不动一行**。⚠️ 登记门禁须带 `ME="<线名>-第N棒"`,否则会因「锁被自己占用」误报不可放行。
**✅ 第 4 棒(执行棒)已完成(2026-09-22 19:3x)** —— 动作 = **E-5(自研扇出分片对照实验)** + 补跑 **§九 判据 6/7**。**全部读数、归因表与判定见 `交接单/IM群组-稳定性机制与框架选型.md` §十二 执行回报(IM线-第4棒)**(含:改造点与协议对齐表、三档六跑同会话对照表、10k 六变体归因表、回退判据判定、判据 6/7 明细)。
**✅ 第 5 棒(执行棒)已完成(2026-09-22 19:4x)** —— 动作 = **开工 A 单**(IM 五单合流第一单)。**全部读数、DDL 摘要、逐条判据落位与两处口径说明见 `交接单/IM群组-A-房间内核与DB.md` §十 执行回报(IM线-第5棒)**。以下为**原要求(存档用)**:**开工 A 单 —— `交接单/IM群组-A-房间内核与DB.md`**(IM 五单合流第一棒):
1. 先逐条核实 A 单 §二 的 5 条只读前置(⚠️ 前置 1 特例:v14 若已被占 ⇒ 取下一个空号继续,⛔ 不停手)。
2. 按 A 单 §五 步骤 1–7 执行(严格按 §三「改什么 / 不动什么」:只动 `src/db/schema.ts` 追加 v14、新建 `src/im/**`、新建 `test/im-store.test.mjs` 并登记进 `package.json` 清单)。
3. 按 A 单 §六(含 §六.1 追加判据 = 选型单 §四 组 B/C/D 共 16 条)验收。
4. 按 A 单 §八 格式在其尾部**追加**执行回报。
**✅ 第 6 棒(执行棒)已完成(2026-09-22 20:2x)** —— 动作 = **开工 B 单**(IM 五单合流第二单)。**全部读数、逐条判据表、标定数值与口径说明见 `交接单/IM群组-B-平台API与用户端IM-WS通道.md §九 执行回报(IM线第6棒)**。以下为**原要求(存档用)**:**开工 B 单 —— `交接单/IM群组-B-平台API与用户端IM-WS通道.md`**(A → B 串行,A 已收口):
1. 先读 B 单 §〇 架构对齐 + §二 只读前置,逐条核实(🔴 B 单依赖 A —— `src/im/**` 与 v14 三表**已在第 5 棒落地**,前置里若仍写「A 未做」按现状更新判断,⛔ 不改 B 单结论)。
2. 按 B 单 §五 步骤执行,改动面严格按 B 单 §三:`src/im/{hub,ws}.ts` + `src/web/routes/im.ts` + `src/web/server.ts`(挂载);**服务器 / nginx 仅新增 `/api/im/ws` location**。
3. ⚠️ B 单 §五-5 含**房间上限与发言预算的标定**(A 单只落了待标定初值 `2000 / 10 / 30 s`)⇒ 本棒需给标定命令与读数。
4. 🔴 **硬回归判据 = 既有子域隧道不受影响**(`proxy.ts` 现有 upgrade 回调按路径分流,⛔ 不改子域隧道语义);跑 `npm test` 只判「0 败」。
5. 按 B 单 §八 格式在其尾部**追加**执行回报。
⚠️ 环境注意(第 5 棒实测,⛔ 别踩):**47 的 sshd 在 22 端口**|47 的 PG 是**本机进程**(`127.0.0.1:15432`)**非容器** ⇒ 「`docker exec dshs-pg psql`」写法全失败;`su postgres` 无 unix socket、需 TCP+口令(⛔ 不取用凭据)|🔴 **`npm run verify` 必红且与本线无关** —— 卡在既有脚本 `scripts/verify-platform-admin-section.mjs`(`require()` + 顶层 await);判定看 `npm test` 的「0 败」,或其链路其后 6 步逐条补跑 | `npm test` 用 **Node 22**(`E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3`)|⚠️ `npm test` 前 `export PATH=<上面的 node 目录>:$PATH`。
⚠️ 上游已固化事实(第 5 棒实测,⛔ 别重查):**v14 已被 A 单占用**(`src/im/**` 已存在)|A 单 §五-1 / §六-1 里的「最新 = 12」是旧值残留,**真值 = 14**|内核的 `id` **含作者**(`sha256(authorId ‖ canonical(payload))`,见 A 单 §十.7-2)。
**✅ 第 7 棒(执行棒)已完成(2026-09-22 20:2x)** —— 动作 = **开工 C 单**(IM 五单合流第三单)。**全部读数、逐条判据表、判据 7–10 明细与 5 条口径说明见 `交接单/IM群组-C-Agent接入插件.md` §九 执行回报(IM线第7棒)**。以下为**原要求(存档用)**:**开工 C 单 —— `交接单/IM群组-C-Agent接入插件.md`**(B 已收口,A → B → C 串行):
1. 先读 C 单 §〇 架构对齐 + §二 只读前置,逐条核实(🔴 C 单依赖 A + B —— `src/im/**`、v14 三表、`src/web/routes/im.ts` 与 `/api/im/ws` **均已落地**;前置里若仍写「B 未做」按现状更新判断,⛔ 不改 C 单结论)。
2. 按 C 单 §五 步骤执行,改动面**严格按 C 单 §三**。
3. 🔴 **`x-dsh-im-instance-token` 的 501 预留点就是 C 单要接的地方** —— 两处:`src/web/routes/im.ts` 的 `imAuth`(会话不成且带该头 ⇒ 501)+ `src/im/ws.ts` 的 `doUpgrade`(握手前)。C 单须把它从 fail-closed 换成**真校验**,并按 §〇-4 绑定 `instanceId → userId` + 所在区(跨区本期拒绝)。
4. 判据须给「命令 + 期望输出 + 退出码」;跑 `npm test`(Node 22)只判 **0 败**。
5. 按 C 单 §八 格式在其尾部**追加**执行回报。
⚠️ 环境注意(第 6 棒实测,⛔ 别踩):**本机自写脚本 / `smoke:*` 的 Node 进程可能不自退**(伪实例子进程持句柄)⇒ 脚本显式 `process.exit()`,或外层 `timeout` 兜底并把「是否自退」如实写进回报|⚠️ **`fetch()` 带自定义 `host` 头不生效** ⇒ 测子域 / 多 Host 必须用 `http.request`(首版拿到假 404)|⚠️ 假时钟(`hub` 的可注入 `now`)**每轮必须重置**,时钟倒退会让 1 s 批次闸门整窗失效(首版标定读数假 0)|47 的 sshd 在 **22** 端口|`npm run verify` 必红且与本线无关(判定看 `npm test` 的 0 败)。
⚠️ 上游已固化事实(第 6 棒实测,⛔ 别重查):**平台 `app.server` 上的 upgrade 回调只有 1 处**(`proxy.ts` 现 **759** 行一带),IM 分流走它的**委托钩子** `setUpgradeDispatch()` ⇒ ⛔ 别再加第二个 `app.server.on('upgrade')`(Node 会把同一事件并发投给两个监听器 ⇒ IM 路径被子域判定判失败并静默 destroy)|`/api/im/` 下的非 WS upgrade **fail-closed 404**(⛔ 不落到隧道)|`GET /api/im/stats` 限 **admin**|`hello` 的 `cursor` 初值为 `{}`,权威游标在 `subscribed` / `resume` 帧|`DSH_IM_WS=0` 可整体下线、`DSH_IM_HB_MS` 可改心跳周期(测试/标定用)。
**⏭️ 第 8 棒(执行棒 · 下一轮动作)**:**开工 D 单 —— `交接单/IM群组-D-插件SDK与扩展点契约.md`**(A → B → C → D 串行,C 已收口):
1. 先读 D 单 §〇 架构对齐 + §二 只读前置,逐条核实(🔴 D 单依赖 A + B + C —— `src/im/**` 六个模块、v14 三表、`/api/im/**` 与 `/api/im/ws`、实例凭据登记簿与 `poc/im-agent-bridge/` **均已落地**;前置里若仍写「C 未做」按现状更新判断,⛔ 不改 D 单结论)。
2. 按 D 单 §五 步骤执行,改动面**严格按 D 单 §三**。
3. 🔴 **C 单留了两处接口给 D 单接**:① 本地 LLM 会话对接 = `poc/im-agent-bridge/lib/index.js` 的 `opts.askLocalSession`(现为占位 `defaultAsk`)② 代答标识的 UI 契约 = `src/im/agent-bridge.ts` 的 `replyMarkerOf` / `replyPayloadOf`(回带 `agentReply` / `agentLabel`)。⛔ 别把这两处当缺陷重做。
4. 判据须给「命令 + 期望输出 + 退出码」;跑 `npm test`(Node 22)只判 **0 败**(本棒基线 300)。
5. 按 D 单 §八 格式在其尾部**追加**执行回报。
⚠️ 环境注意(第 7 棒实测,⛔ 别踩):**往 md 文件追加长文本别用 bash heredoc**(单引号会炸,本轮踩过)⇒ 改成 Write 一个 `.py` 再跑|⚠️ 插件包 `lib/index.js` 是纯 JS,⛔ 不许带 TS 类型标注(`import { connect, type Socket }` 会让 ESM 直接 `SyntaxError`,本轮踩过)|本机自写脚本 / `smoke:*` 的 Node 进程可能不自退 ⇒ 脚本显式 `process.exit()` 或外层 `timeout` 兜底|`fetch()` 带自定义 `host` 头不生效 ⇒ 测子域 / 多 Host 必须用 `http.request`|47 的 sshd 在 **22** 端口|`npm run verify` 必红且与本线无关(判定看 `npm test` 的 0 败)。
⚠️ 上游已固化事实(第 7 棒实测,⛔ 别重查):**实例凭据 = 内存登记簿**,由 `web/server.ts` 建**唯一一份** `app.imInstanceTokens` 并同时交给两个 spawner(`LocalSpawner` 与 `RemoteSpawner`)⇒ 发放侧与校验侧同簿;⛔ 别在别处 `new ImInstanceTokenRegistry()`(那样发的 token 永远认不出)|`DSH_PLATFORM_IM_URL` / `DSH_IM_INSTANCE_TOKEN` **任一缺失 ⇒ 插件不启动**(天然开关,也是唯一回滚动作)|`doUpgrade` 的实例凭据走**注入钩子** `resolveInstance`(`im/ws.ts` ⛔ 不 import web 层,分层方向要保住)。
**✅ 第 8 棒(执行棒)已完成(2026-09-22 21:1x)** —— 动作 = **开工 D 单**(IM 五单合流第四单)。**全部读数、七个扩展点逐点表、跨场景证据、三组失败注入原文、5 条未完成项与 5 条口径说明见 `交接单/IM群组-D-插件SDK与扩展点契约.md §十 执行回报(IM线-第8棒)`**。以下为**原要求(存档用)**:**开工 D 单 —— `交接单/IM群组-D-插件SDK与扩展点契约.md`**(A → B → C → D 串行,C 已收口):
1. 先读 D 单 §〇 架构对齐 + §二 只读前置,逐条核实(🔴 D 单依赖 A + B + C —— `src/im/**` 六个模块、v14 三表、`/api/im/**` 与 `/api/im/ws`、实例凭据登记簿与 `poc/im-agent-bridge/` **均已落地**;前置里若仍写「C 未做」按现状更新判断,⛔ 不改 D 单结论)。
2. 按 D 单 §五 步骤执行,改动面**严格按 D 单 §三**。
3. 🔴 **C 单留了两处接口给 D 单接**:① 本地 LLM 会话对接 = `poc/im-agent-bridge/lib/index.js` 的 `opts.askLocalSession`(现为占位 `defaultAsk`)② 代答标识的 UI 契约 = `src/im/agent-bridge.ts` 的 `replyMarkerOf` / `replyPayloadOf`(回带 `agentReply` / `agentLabel`)。⛔ 别把这两处当缺陷重做。
4. 判据须给「命令 + 期望输出 + 退出码」;跑 `npm test`(Node 22)只判 **0 败**(本棒基线 300)。
5. 按 D 单 §八 格式在其尾部**追加**执行回报。
⚠️ 环境注意(第 7 棒实测,⛔ 别踩):**往 md 文件追加长文本别用 bash heredoc**(单引号会炸,本轮踩过)⇒ 改成 Write 一个 `.py` 再跑|⚠️ 插件包 `lib/index.js` 是纯 JS,⛔ 不许带 TS 类型标注(`import { connect, type Socket }` 会让 ESM 直接 `SyntaxError`,本轮踩过)|本机自写脚本 / `smoke:*` 的 Node 进程可能不自退 ⇒ 脚本显式 `process.exit()` 或外层 `timeout` 兜底|`fetch()` 带自定义 `host` 头不生效 ⇒ 测子域 / 多 Host 必须用 `http.request`|47 的 sshd 在 **22** 端口|`npm run verify` 必红且与本线无关(判定看 `npm test` 的 0 败)。
⚠️ 环境注意(第 8 棒实测,⛔ 别踩):🔴 **本机 `src/im/**` 与 `web/routes/im.ts` 等服务面文件在 A–D 四单落地后仍未 commit** ⇒ 它们对 `git status --short` 显示为整块 `??`(**未跟踪**,不是改动);★ **「内核零改动」必须用内容级判据**(`git diff` 对每个模块单独为空 + `git status --short src/im` 里无 `M` 标记),⛔ 不能用「`status --short` 无输出」(那样会假红,首版踩过)|🔴 **文档库的任何 `Edit`/`Write` 都要求先持有全局执行锁**(钩子会拦;追加执行回报时若锁已释放会报「无锁 ⇒ 不允许直接修改」)⇒ 追加类操作的顺序 = 抢锁 → 写文档 → 放锁 → 过门禁 → 再抢锁写入口/登记|🔴 **写库测试的桩必须做「表全名归一」**(`p_<pluginId>_<name>`,见 SDK `dataTableFullName`),否则「裸名 vs 全名」会被当成两张表导致判据 11/13 **假绿**(首版踩过)。
⚠️ 上游已固化事实(第 8 棒实测,⛔ 别重查):**SDK 契约层 = `src/im/sdk/{types,host,index}.ts`**(`ImSdkHost` 是七个扩展点的唯一注册/调用入口)|**`ImDataPort` / `ImBudgetPort` 是注入式端口**(真库端口与建库面属「插件投放与分库线」)|**超时/熔断/抛错的降级方向 = 放行**(⛔ 不是拒绝发言,否则插件挂了房间就哑了)|**限速只约束 agent**(与 A 单 `decideWrite` 同向,人类成员不受限)|**`requiresReview` 默认拒绝**(未审核的新 `room_type` 不落进可建房列表)|同名 `room_type` 冲突 ⇒ 后注册者**被拒并进 `rejected`**(⛔ 不静默覆盖)。
**✅ 第 9 棒(执行棒)已完成(2026-09-23 06:3x)** —— 动作 = **开工 E 单**(IM 五单合流第五单,本线最后一单)。**全部读数、步 0 选型理由、步骤 0–11 逐条落位、判据 1–13 取证、生效链路四关与未完成项见 `交接单/IM群组-E-会话界面UI.md` §执行回报(IM 线第 9 棒)**。以下为**原要求(存档用)**:**⏭️ 第 9 棒(执行棒)= 开工 E 单**(`交接单/IM群组-E-会话界面UI.md`)。🔴 **落点已于 2026-09-23 拍板(§0 末行),⛔ 不再征询**:
1. 先读 E 单 **§〇 架构对齐 + §二 只读前置 + §四-4/§四-5(拍板)**,逐条核实(E 单依赖 A + B + C + **D 已收口**;`src/im/**`、v14 三表、`/api/im/**` 与 `/api/im/ws`、`src/im/sdk/**` 均已落地)。
2. **按 E 单 §五 步骤执行(含新增的步 0 / 步 2 / 步 3 / 步 8)**,改动面严格按 E 单 §三。🔴 **落点 = C′**:实例内接管会话区 `[data-slot="main.conversation"]`,顶部 tab 行;**保活 = `display:none`**;**群组 tab 上限 5**;新建群组入口在业务插件内。
3. 🔴 **步 0 是前置**(不补则步 8 的「加入群聊」按钮无接口可调):新增「可发现房间列表 + 自助加入」;标志位在 **「`rooms` 显式列 + v15 只增迁移」** 与 **「`config_json`」** 间二选一,**选型自己定并写明理由**(我倾向显式列:属授权语义,按 R7 口径不宜埋 config)。
4. 🔴 **验收面**:E 单 §六 判据 **1–13**(8 tab 形态 / 9 保活 / 10 上限 / 11 槽位失效可见 / 12 在线态 / 13 发现+加入)。
5. 判据须给「命令 + 期望输出 + 退出码」;跑 `npm test`(Node 22)只判 **0 败**(基线 **366/364 过 2 跳过**)。⚠️ **client bundle 改完必须重启实例**(bundle 在实例启动时加载)。
6. 按 E 单 §八 格式在其尾部**追加**执行回报。
⚠️ 环境注意(第 9 棒新增,⛔ 别踩):🔴 **接管会话区必须多选择器兜底**(`[data-slot="main.conversation"]` → `[data-slot="conversation"]`)+ **失效时给可见提示**;官方已改过一次槽名(0.1.5-rc.1),**单一选择器 ⇒ 静默失效**(MCN 被咬过:点了没反应)|🔴 **flex 布局用 CSS 规则 + `!important` 驱动,⛔ 不用 inline style**(React 重渲染会重置 inline,MCN 的注释已写明)|🔴 **组件卸载必须反向还原**(conversation 插回原位 + 恢复 `previousCss` + 移除 wrapper),否则切走再回来 DOM 结构会坏|⚠️ 官方 slots 注册面很窄(实测只用到 `sidebar.footer.action` / `settings.section`)⇒ **会话区靠 DOM 接管,⛔ 不是 `slots.register`**。
⚠️ 上游已固化事实(第 9 棒可用,⛔ 别重查):**IM 数据走同源相对路径 `/api/im/**`**(实例内带 `sid` cookie ⇒ `imAuth` 认**用户本人**;⛔ 不硬编码主机 / 区域)|**在线态数据源已内建**(`hub.ts`:`PresenceState='online'|'offline'`、presence 按 **1 s 批合并**、只推订阅该房的连接)⇒ ⛔ 不新增数据源|**容量上限** `ROOM_LIMITS.maxMembers = 2000`|`store.listRooms({ownerId?,roomType?})` 已有筛选签名(扩参数即可)。
**🏁 本线状态(2026-09-23 第 10 棒收口后):A–E 五单全部落地;§4 三项待拍板**已于 09-23 全部拍板**(详见下文);下一步 = **动工连接层 B 案**。**
**✅ 三项拍板(2026-09-23,⛔ 不再征询)** —— 全文见选型单 **§十四 用户拍板落定**:
1. **E2EE 形态档位 = 强度档**(性能判据:密码学 ≈ 55 µs/条 ⇒ 占 1 s 事件循环预算 0.006%、50 msg/s 下占单核 ≈ 0.3% ⇒ CPU 维度可用)。
🔴 三处结构性代价(⛔ 不受"影响较小"覆盖):① 棘轮状态**每设备持久化**(丢了永久断链)② **群聊必须 Megolm 化**(误用严格双棘轮 ⇒ 扇出 `O(N)`→**`O(N²)`**)③ **agent 代答重做**(`im_agent_grants` 不成立 ⇒ D 单 §四-2 重写、E4-6 断言改写)。**9 条连带重写清单见 §十四-14.3。**
2. **房间上限与预算 = 按建议固化**:`maxMembers 2000` / `agentQuotaPer10Min 10` / `silencePeriodMs 30 s` 已转正式值(选型单 §十四-14.4 + A 单状态块)。🔴 适用前提 = **单进程自研扇出**形态下标的 ⇒ 改形态须复测。
3. **连接层 = 候选 B(引入 Centrifugo 类外部连接层)**(选型单 §十四-14.5)。
**✅ 第 12 棒(执行棒)已完成(2026-09-23 19:3x)= 动工连接层 B 案** —— 交付、判据读数、E-7/E-6 实验、容量表、P0 发现见选型单 **§十五 B 案实现回报(IM 线 · 第 12 棒)**。以下为**原要求(存档用)**:动工连接层 B 案 —— 用户拍板原话「**先做b**」(优先级已定,⛔ 不再征询)。
⚠️ **编号更正(2026-09-23 19:0x)**:本段原写「第 11 棒」,与 §0 中**已完成**的第 11 棒(①档位重写 + 收口)撞号 ⇒ 下一棒一律称 **第 12 棒**(会话名 `IM线-第12棒`)。
**🔴 落点已经现成(本棒实查,⛔ 别从零造接口)**:`src/im/ws.ts` 已有可替换接缝 ——
- `createImWsRuntime(options: ImWsRuntimeOptions): ImWsRuntime`(`:209`,工厂)
- `export interface ImWsRuntime`(`:190`)+ `ImWsRuntimeOptions`(`:163`)
- `export type ImUpgradeDispatch`(`:188`)
- 既有槽位:`resolveInstance?`(`:179`,实例凭据注入钩子)、`options.resolveInstance?.(req.headers)`(`:387`)
⇒ **B 案 = 在这个接缝上加一个 Centrifugo 后端实现**,⛔ **不改内核语义**、⛔ **不把 Centrifugo 概念泄漏进 `src/im/**`**(连命名都要中性化,例如 `ImConnectionBackend`)。
**B 案动工范围(建议顺序,可推翻)**:
1. **先落"可替换连接层"抽象** —— 把现有自研 WS 实现归到一个后端(`native`),再加 `centrifugo` 后端;两后端**同一接口、同一协议语义**(`/api/im/ws` 路径不变、帧格式不变、游标语义不变)。
2. **写 Centrifugo 适配器**(🔴 含实测必做项,见下)+ 配置样例(v6 配置键已改位:`client.token.hmac_secret_key` / `http_api.key` / 端口用 `--http_server.port`)。
3. **对账第 3 棒 E-2 实测**(复跑同脚本同参数,判据见下)。
4. **出容量测算表**(按「机器可以换」口径:边际 **70.1 KiB/连接** vs 自研 **5.9 KiB** ⇒ 算"要多少机器与内存",⛔ 不以此否决方案)。
5. **顺带复测两个固化数值**(`2000` / `10` / `30 s` 的适用前提是"单进程自研扇出" ⇒ 换形态后必须复测;⚠️ 内存类判据**须重复跑 ≥3 次取中位**)。
**🔴 三条硬实现项(⛔ 缺一即不可上线)**:
1. **必须补发自建心跳** —— 实测 Centrifugo **不发服务端 WS ping**(`wsPingRecv=0`);客户端也不发 ping ⇒ **26 s 内 10000/10000 全被断开**。
⚠️ 且「**保活计时起点必须绑该连接自己的认证完成时刻**」—— 用整进程统一计时会一次掉 **2773** 条(这是适配器缺陷,判别实验已证)。
2. **按可替换接口落**(见上,接缝已现成)。
3. **出容量测算表**(见上第 4 点)。
**验收判据(⛔ 对账式,不是重新发明)**:
| 判据 | 期望 |
|---|---|
| `npm run build` | rc=0 |
| `node --test test/im-store.test.mjs test/im-agent.test.mjs test/im-sdk.test.mjs test/im-ui.test.mjs` | **0 败**(本棒基线:im-ui 29 / 全量 `npm test` 420-418 过 2 跳过) |
| `npm test`(Node 22) | **0 败**(基线 **420 / 418 过 / 2 跳过**) |
| `check-layering` | rc=0 **✅ 无新增违规** |
| 10k P99(客户端观测) | 对账 E-2 的 **28.9 / 68.1 / 111.9 ms**(自研 E-1/E-5 = 268.6 / 337.6 / 359.4 ms) |
| 扇出离散中位 | 对账 E-2 的 **17.2 / 57.9 / 97.8 ms**(自研 = 171.8 / 227.6 / 225.5 ms) |
| 心跳超时率 | **0**(含"补发自建心跳"后的验收) |
| ⛔ 语义不变 | `/api/im/ws` 路径、帧格式、游标语义、`DSH_IM_WS` 开关**全部不变** |
**⚠️ 环境注意(第 3 棒实测,⛔ 别踩)**:**106 用 `ssh test106`**(别名已配;`ssh root@106.54.21.172` 会因 `IdentitiesOnly` 失败)|106 上**必带 `ulimit -n 65536`**(默认 1024)|E-2 资产在 **106 `/root/im-e2/`**(`e2worker.mjs` / `e2pub.mjs` / `e2run.mjs` / `e2sum.py` / `config.json` / **`centrifugo` v6.9.6 二进制**)|E-1 资产在 `/root/im-e1/`(`server.mjs` / `wsworker.mjs` / `run.mjs` / `sum.py`)|47 的 sshd 在 **22** 端口|🔴 实验一律落 **106**,⛔ **47(生产 Manager)不动一行**。
⚠️ **本机执行环境**:每条 bash 命令前置 `export PATH="/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/usr/bin:/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/bin:$PATH"`|`npm test` 用 **Node 22**(`E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3`)|`npm run verify` **必红且与本线无关**(判定看 `npm test` 的 0 败)|往 md 追加长文本⛔ 别用 bash heredoc ⇒ 用 Write 写 `.py` 再跑|文档库任何 `Edit`/`Write` 都要求**先持全局执行锁**。
**⛔ 边界**:⛔ 不改 A–E 五单既有结论|⛔ 不替用户拍未决项(见 §4-5)|⛔ 不引新依赖到 `src/im/**` 内核(Centrifugo 只作为**外部进程**,不进 `package.json` 运行时依赖)|⛔ 不部署到 47/106 生产面|⛔ 不 commit/push/scp|⛔ **不抢跑 E2EE 强度档重写**(那是另一棒,且**档位重写必须先于任何 E2EE 代码动工** —— 选型单 §十四-14.3 的 9 条清单)。
✅ **2026-09-23 19:0x 已解** —— 库由 app 自动重建(18:55),第 12 棒已补登 = automation `89e11630-4a06-41ca-bfc7-3c66ff4d8a53`(`scheduledAt = 2026-09-23T19:10`)⇒ **以下为历史记录(存档用,⛔ 不必再按"受阻"处理)**。
🔴 **⚠️ 2026-09-23 18:2x 登记受阻(当时唯一未完成项)**:**第 12 棒(动工 B 案,当时写作「第 11 棒」)未能登记 automation 接续棒** —— automation 子系统双向失败。
- **症状**:建 ⇒ `database disk image is malformed`;查 ⇒ `file is not a database`。
- **根因(只读诊断,证据在手)**:活动库 = `E:\ProgramData\.workbuddy\workbuddy.db`(`CODEBUDDY_CONFIG_DIR` 实测指向此目录;35,377,152 B)。**主库文件头 16 字节非 `SQLite format 3`(乱码)**;`-wal` 4,346,632 B;🔴 **`?mode=ro&immutable=1` 也读不出** ⇒ **不是单纯的 WAL 错位,是主库头部级损坏**。主库 mtime **09-23 18:26**、`-wal` **18:28**(app 仍在写它)。
- **无任何备份**(目录下只有三件套,无 `.bak`)。
- ⚠️ 旁证:`C:\Users\Administrator\.workbuddy\workbuddy.db`(09-12 07:02,192,512 B)**magic 正常、30 个对象可读** ⇒ 那是**旧库**(非活动),⛔ 不可当备份用。
- 🔴 **处置**:**未擅自修** —— 修法(删 `-wal`/`-shm` 或恢复备份)属**不可逆破坏性操作**,且需**停 app**(会中断当前会话与其他会话)⇒ 属红线门禁,**已报告用户等拍板**(见本轮回复)。
**✅ 手工接力方式(⛔ 现已不需要 —— 19:0x 已补登 automation,本段仅存档)**:入口 §2「第 12 棒」段**已自包含**(动工范围 + 三条硬实现项 + 验收判据对账表 + 环境注意 + 边界)⇒ 万一 automation 再次不可用,**用户手工开一个新会话**,说:
> 按 `E:\ProgramData\AIProject\aliyun-dsh-server\接续入口_IM线_20260922.md` 的 **§0 最新行 + §2「第 12 棒」**段执行(会话名 `IM线-第12棒`);开工第 0 步跑 `state.py`,第 1 步抢全局执行锁。
**✅ 回头必做 —— 已于 19:0x 完成**:库由 app 自愈后**已立即补登**第 12 棒接续棒(`89e11630-4a06-41ca-bfc7-3c66ff4d8a53`),「补登完成」已写进 §0。
🔴 **连带待办(本线 ⛔ 别忘)**:**其余各线的接续棒全部丢失**(automation 记录随重建清空)⇒ 各线开工前须先按自家入口 §2 重登。
**收尾五件(缺一算未完成)**:① 结果回填(选型单 §十四 追加"B 案实现回报"小节,或新建档案)② 释放锁 ③ 过登记门禁(`ME="IM线-第12棒"`)后登记下一棒 ④ 入口 §0 追加状态行、§2 推进 ⑤ 追加 `.workbuddy/memory/YYYY-MM-DD.md`。
**⏭️ 第 13 棒(执行棒 · 下一轮动作)= 修 §15.7 的 P0:E 单客户端协议对齐 + 保活垫片接进客户端传输层**
🔴 **为什么排它**(属客观可判,⛔ 不是业务优先级):它**阻塞「已交付功能」**——E 单的客户端 bundle 一旦投放,**首帧即被服务端判 `bad-op` 断开**、平台推的帧**无分支接住** ⇒ IM 的线上可见面在修掉它之前不可能通过(也阻塞硬实现项 ① 的客户端那一半)。② gateway 部署+复测 与 ③ 在线态接线排在它之后。
1. **只读前置**:逐条列出**两侧**的现状行号 —— 客户端 `poc/im-conversation-tabs/lib/client.js` 的出站帧与 `msg.type` 分支;平台 `src/im/ws.ts` 的 op 表(`:264-303`)与推帧集合(`:265/277/284/299/336/487/561`)。⚠️ 先确认第 12 棒之后两侧**未变**。
2. **动工范围(严格)**:`poc/im-conversation-tabs/lib/client.js`(主件)|如确需配套,`poc/im-conversation-tabs/lib/index.js`、`package.json`|`test/im-ui.test.mjs`(**只改被这次帧名变更直接打破的断言**,逐条在回报里写清"改了哪条、为什么")+ 新建/扩一个协议一致性用例。
⛔ **不许**改 `src/im/**` 的服务端协议("服务端兼容 `type` 别名" = 永久维护两套协议,本线**已定不做**)。
3. **具体修改点(建议,可推翻)**:
- **订阅**:`{type:'subscribe', roomId, since}` ⇒ `{op:'subscribe', roomId}`(⚠️ 平台的 `subscribe` **不读 `since`**);游标补齐走**独立路径** `{op:'resume', roomId, since}`,`since` 取 `subscribed` 帧回带的 `cursor`(重连时才发)。
- **保活**:`{type:'ping'}` ⇒ `{op:'pong'}`(⛔ 不要发明新 op;平台对**任意**客户端帧都刷 `lastSeenAt`,且 `pong` 是已认 op)。**计时起点 = 该连接自己的 `open` 时刻**(⛔ 不是模块加载/进程启动时刻)—— 规则与判据见 `src/im/keepalive.ts`(浏览器 bundle 是纯 JS,⛔ 不能 import TS ⇒ **照同一口径在 bundle 内实现**)。
- **发送**:若 UI 有输入框,发送必须走 `{op:'send', roomId, payload, visibility?}`(当前 bundle **没有发送路径** ⇒ 若 UI 无入口则如实记为"本期无发送面",⛔ 不硬造 UI)。
- **收帧**:`msg.type` ⇒ `msg.op`;`message` 的形状是 `{op:'message', roomId, messages:[…]}`(**数组**);`presence` 是 `{op:'presence', roomId, at, members:[{memberId,state,at}]}`(**数组**);`hello` / `subscribed` / `unsubscribed` / `messages` / `sent` / `error` 都要有分支 —— `error` ⛔ **不得静默丢弃**(要能让用户看见)。
- **重连**:保持现有指数退避(2^n 封顶 15 s + 抖动 ✅),但必须保证**每次 `open` 重置 ping 计时器且起点是本次 open**。
4. **判据(命令 + 期望 + 退出码)**:
- `npm run build` rc=0 | `node --check poc/im-conversation-tabs/lib/client.js` rc=0
- `node --test test/im-connection-backend.test.mjs` = **28 / 28 过**(⛔ 不得回退)
- `node --test test/im-ui.test.mjs` **0 败** | `npm test`(Node 22)**0 败** | `check-layering` rc=0 **✅ 无新增违规**
- 🔴 **协议一致性判据(本轮新增,必须以"两侧源码文本互相对照"的方式做机械断言,⛔ 不是"看了一遍")**:① 客户端出站帧的字面量集合 **⊆** 平台认的 op 集合;② 客户端**不存在** `type:` 形式的出站帧;③ 客户端收帧分支覆盖 `hello|subscribed|unsubscribed|message|messages|presence|sent|error`;④ ping 间隔与"每连接 `open` 时刻起计时"两条都在。
5. **环境注意(⛔ 别踩)**:本机 bash 每条命令前置 `export PATH="/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/usr/bin:/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/bin:$PATH"`|`npm test` 用 **Node 22**(`E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3`)|`npm run verify` **必红且与本线无关**(判定看 `npm test` 的 0 败)|⛔ 插件包 `lib/*.js` 是**纯 JS**,不许带 TS 类型标注(第 7 棒踩过)|⛔ 往 md 追加长文本别用 bash heredoc(改用 Write 写 `.py` 再跑)|文档库任何 `Edit`/`Write` 需**先持全局执行锁**。
🔴 **抢锁域必须含 `poc/im-conversation-tabs`**(第 12 棒只声明了 `poc/im-connection-gateway` ⇒ 本轮实查该域当时**未在锁内**,⛔ 别照抄上一棒的域清单)。
6. **边界**:⛔ 不改 `src/im/**` 的服务端协议|⛔ 不改 A–E 五单既有结论|⛔ 不部署 / ⛔ 不投放(投放属**投放线**)|⛔ 不 commit/push/scp|⛔ 不引入新依赖|⛔ 不动 47/106|⛔ 不碰 §14.6 未决项。
7. **收尾五件**同本入口惯例。**回填** = `交接单/IM群组-E-会话界面UI.md` 追加「**§执行回报(IM 线第 13 棒)**」小节 + 在选型单 §十五-15.7 处加 🔻 **处置标注**(⛔ 原文不改)。
✅ **连带待办已办一半(2026-09-23 19:4x,第 12 棒收口)**:已在投放线入口 **§5「本线后续」新增第 5 条**(写明 P0 是它的硬阻塞、投放前须先等本棒修完)+ 改正该入口「automation 不可用」的过时前提(库 18:55 已由 app 重建)。
⛔ **仍未认领的部分**:`im-conversation-tabs` 的**投放本身**没有归属 ⇒ 本棒至少要推进一步:**本线自行排一棒**,或**确认投放线已认领**(⛔ 不推进 ⇒ 又回到"两不管")。
**⏭️ 第 14 棒(执行棒 · 下一轮动作)= 认领并执行 `im-conversation-tabs` 的投放(可自动的部分)+ 出线上取证清单**
🔴 **为什么排它**:① 第 13 棒**已修掉投放的唯一硬阻塞**(§15.7 的 P0)⇒ 现在**具备投放条件**;② E 单的「线上可见面」(生效链路第 ④ 关)是 A–E 五单里**唯一没有验收过的交付面** ⇒ 投掉它才算闭环;③ 本入口 §2 第 13 棒 §8 明令「本棒至少要推进一步:**本线自行排一棒**」—— 本条即兑现(⛔ 不推进 ⇒ 又回到"两不管")。⚠️ 与 ② gateway 部署 / ③ 在线态接线 **解耦**,那两项排在再后。
1. **只读前置(先逐条核实,⛔ 别凭记忆)**:
- 投放的**唯一方式**(`CODEBUDDY.md §2`):admin 在门户**导入候选池** → **用户在实例「功能管理」自助启用/禁用**(⛔ **不铺 profile**);机制/边界/回滚见档案 **16 / 36 / 65**。
- 装配协议 = **`link:`**(2026-09-23 拍板,推翻 `file:`)⇒ 待落地 4 处里**含"重编译部署 47 / 106"** —— 先确认这 4 处是否已全部落地(投放线入口 §0 末行有记录)。
- 代码根:**47 = `/opt/dshs`**、**106 = `/opt/dshs-cluster`**(106 上**没有** `/opt/dshs`);**`/opt/dsh` = 平台数据根**,三台同名。取 `DSHS_DB_URL` **只从 drop-in `cluster.conf` grep**(⛔ 不 `source /etc/dshs.env`,那会拿到错的 5432)。
- 47 的 sshd 在 **22** 端口;47 的 PG 是**本机进程**(`127.0.0.1:15432`)**非容器** ⇒ 「`docker exec dshs-pg psql`」写法**全失败**。
2. **动工范围(严格)**:① 把 `poc/im-conversation-tabs/` 按装配协议打进**候选池**(admin 途径)② 在**投放线入口** `接续入口_插件投放与分库线_20260922.md` **§5「本线后续」第 5 条**处把归属**写成本线认领**(⛔ 不新增重复条目、⛔ 不改该入口其余内容)③ 出**「用户启用 → 重启实例 → 线上取证」操作单**(含每条判据的命令与期望)。
⛔ **不许**改 `poc/im-conversation-tabs/**` 的代码(第 13 棒刚验收过;若投放中发现**新缺陷** ⇒ **停下只报告**,⛔ 不顺手动)|⛔ **不许**改 `src/im/**`。
3. **判据(命令 + 期望 + 退出码)**:
- 插件包可被装配:`package.json` 三件齐(`exports['./client']` / `dsh.client.platform='web'` / `dsh.bundle.patch`)⇒ `node -e` 断言 rc=0。
- 候选池里**能查到** `im-conversation-tabs`(admin 只读查询;⛔ 不改任何池内他项)。
- 🔴 **线上可见面(本棒的目标终点,⚠️ 需用户动作)**:实例「功能管理」启用 → **重启实例**(bundle 在实例启动时加载)→ 取证 = ① 会话区出现 tab 行(`[data-slot="main.conversation"]` 被接管)② `/api/im/ws` 握手 **101** 且**不再出现 `bad-op` 断开**(服务端日志)③ 房间消息**真的渲染出来**。
⛔ **用户未启用 ⇒ 本项如实记为"未通过",⛔ 不伪造成功、⛔ 不代替用户点启用**。
4. **环境注意(⛔ 别踩)**:本机 bash 每条命令前置 `export PATH="/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/usr/bin:/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/bin:$PATH"`|`npm test` 用 **Node 22**(`E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3`)|`npm run verify` **必红且与本线无关**(判定看 `npm test` 的 0 败)|**106 用 `ssh test106`**(别名已配;`ssh root@106.54.21.172` 因 `IdentitiesOnly` 必失败)|非 ASCII 会话名的锁目录必带 `cksum` 后缀(属正常)|文档库任何 `Edit`/`Write` 需**先持全局执行锁**,且域锁**必须含 `poc/im-conversation-tabs`**(第 12 棒漏了它 ⇒ 第 13 棒实查该域当时**未在锁内**)。
5. **边界**:⛔ 不改 `src/im/**`|⛔ 不改 `poc/im-conversation-tabs/**` 代码|⛔ 不改 A–E 五单结论|⛔ 不 commit/push/scp|⛔ 不引入新依赖|⛔ 不铺 profile(投放只有候选池这一条路)|⛔ 不取用 47 的 PG 凭据|⛔ 不碰 §14.6 未决项。
6. **收尾五件**同本入口惯例。**回填** = `交接单/IM群组-E-会话界面UI.md` 追加「**§执行回报(IM 线第 14 棒)**」小节(⛔ 不改既有正文)+ 投放线入口 §5 第 5 条落归属。
✅ **本条已完成(2026-09-23 20:2x)** —— E 单回报已追加(该单 350 → **410 行**,章节 `## ` 复核 §二…§八 **齐全未丢**);投放线入口 §5 第 5 条已改写为「**归属 = IM 线自行认领完结**」+「本线后续给任何插件投放都要带上**共享层发布**这一步」(⛔ 未新增重复条目、⛔ 未改该入口其余内容)。
**✅ 第 15 棒(执行棒)已完成(2026-09-23 21:3x)= 47 就地部署连接层 gateway + 在线态取数接线**
✅ **完成情况(2026-09-23 21:3x,本段保留原文备查)**:§2 第 15 棒四项**全部落地并逐条取证** ⇒ 回填见选型单 **§15.11**(⚠️ 落点为 §15.11 **不是**本节原写的「§15.8」—— §15.8 已被「未完成项 / 已知限」占用;⛔ 未重排既有小节,只在 §15.8 表前加了一段 🔻 状态标注)。**两项未完成项已闭合**:②gateway **已部署到 47**(网关进程 + 配置 600 + 单元 `dsh-gateway.service` enabled+active、只绑 `127.0.0.1:18081` + 边缘片段落盘、`nginx -t` rc=0)|①在线态取数**已接线**(`presenceIngest` `not-wired` ⇒ `local-hub`(native) / `gateway-join-leave`(gateway))。🔴 **本棒实测改写了 §3 一条既有结论**(`ping_interval` `0s` ⇒ **`25s`**:网关**会**发协议级 JSON ping,「不发 WS ping」那条实测没错、由它推出的结论错了)⇒ 仓库与部署两侧已同步修正。⚠️ **优先判据未触发**(操作单 §三 六条取证用户尚未反馈)⇒ 按默认路径执行。
🔴 **为什么排它**:第 12 棒把 B 案**代码面**做完、第 13 棒解掉 P0、第 14 棒把 E 单投放做完 ⇒ **第 12 棒 §十五 剩下的两个"未完成项"成了本线唯一的可自动推进面**:① gateway 未部署(47/106 一行未动)② 在线态取数未接线(`stats().presenceIngest='not-wired'`)。
⚠️ **但优先判据是"用户的取证反馈"**(见 §0 第 14 棒行的末句):若用户已反馈操作单 §三 的六条结果 ⇒ **先按反馈处置**,本节两项顺延。
✅ **本轮上线风险已被设计按住**:`DSH_IM_BACKEND` 未声明 ⇒ **默认 `native`**(`src/im/connection-backend.ts:11/45`)⇒ **部署网关二进制 + 配置样例本身不改任何线上行为**;"切换"是另一个动作(写 `DSH_IM_BACKEND=gateway` + 重启 `dshs`),**一行可回滚**。
1. **只读前置(先逐条核实,⛔ 别凭记忆)**:
- 第 12 棒交付物现状:`src/im/connection-backend.ts` / `keepalive.ts` / `backends/{native,gateway}.ts` / `test/im-connection-backend.test.mjs` / `poc/im-connection-gateway/{README.md,config.json,nginx-location.conf}` —— 逐条 `ls` + `grep` 确认**未漂移**。
- `DSH_IM_BACKEND` 的选择器与默认值(`selectConnectionBackend`,`:119`)+ **能力自述的构造即抛**判据(`:184` `assertCapabilities`)。
- 47 上 `/opt/dshs` 的部署产物是否含 `lib/im/{connection-backend,keepalive}.js` + `lib/im/backends/`(第 12 棒**只在本机**改码,**未部署** ⇒ 大概率**没有**,若确实没有,本棒第 2 步要先 build + scp)。
- 47 的 `/etc/systemd/system/dshs.service.d/` 里 `DSH_IM_WS` / `DSH_IM_BACKEND` **是否已有值**(有 ⇒ 先读清再动)。
- 106 的 Centrifugo 二进制位置(`/root/im-e2/centrifugo`,v6.9.6)与配置键改位(`client.token.hmac_secret_key` / `http_api.key` / `--http_server.port`)。
2. **动工范围(严格)**:
- **① 部署连接层代码**:`npm run build` ⇒ 把 `lib/im/**` 新增产物 scp 到 **47 `/opt/dshs/`**(若 106 也要跑网关 ⇒ 同法到 `/opt/dshs-cluster/`);`restart dshs`;⚠️ **默认 `native` ⇒ 行为不变**,重启后必须复核 `/api/im/ws` 仍 **101**。
- **② 装外部连接层进程**(**47 就地**):Centrifugo v6.9.6 二进制 + `poc/im-connection-gateway/config.json` 落到 `/opt/dsh/*` 下的运行位;**只绑回环**;systemd 单元(或前台 nohup + 记录 PID);⛔ **⛔ 必须补发自建心跳**(Centrifugo 不发服务端 ping —— E-2/E-7 实测)。
- **③ 边缘映射**:`poc/im-connection-gateway/nginx-location.conf` 的 `location = /api/im/ws` —— **先只落配置文件并 `nginx -t`**,⛔ 本棒**不开流量**(切流属"改线上连接层"=影响在线用户 ⇒ 单独一步、单独取证)。
- **④ 在线态取数接线**:把 `stats().presenceIngest='not-wired'` 换成真源(`hub.ts` 的 presence 已内建,1 s 批合并 ⇒ **⛔ 不新增数据源**)。
- ⛔ **不许**改 `poc/im-conversation-tabs/**`(第 13/14 棒刚验收)|⛔ **不许**改 A–E 五单结论|⛔ 不引新依赖进 `src/im/**`(Centrifugo 只作**外部进程**)|⛔ 不动 106 生产面(除 ① 的同步部署)。
3. **判据(命令 + 期望 + 退出码)**:
- `npm run build` **rc=0** | `node --test test/im-connection-backend.test.mjs` = **28 / 28 过**(⛔ 不得回退)| `npm test`(Node 22)**0 败** | `check-layering` **rc=0 ✅ 无新增违规**。
- **部署后行为不变**:47 `curl` `/api/im/ws` 握手 **101**(未声明 `DSH_IM_BACKEND` 时);`systemctl is-active dshs` = `active`;门户 `http://127.0.0.1:3080` **200**(`-H 'Host: alotbuy.com'`)。
- **网关进程**:`ss -lntp` 确认**只绑 `127.0.0.1:<port>`**(⛔ 不许 `0.0.0.0`);`/health` 或等价只读口 200。
- **保活自证**(硬实现项 ①):复用第 12 棒 E-7 三向实验(`off` / `global` / `conn`)⇒ **`conn` 口径掉 0**;权威判据 = **服务端 `num_clients` 不下降 + worker `closed=0`**(⛔ **不用** `e2sum.py` 的 `hbTimeoutRate` —— 第 12 棒已实测它是结构性非零)。
- **在线态接线**:`GET /api/im/stats`(admin)里 `presenceIngest` **不再是 `not-wired`**,且 `presence` 读数随真实在线变化(给两条读数的对照)。
- **回滚演练**:`DSH_IM_BACKEND` 删掉 → `restart dshs` → 握手回 101 ⇒ 记一条。
4. **环境注意(⛔ 别踩)**:本机 bash 每条命令前置 `export PATH="/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/usr/bin:/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/bin:$PATH"`|`npm test` 用 **Node 22**(`E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3`)|`npm run verify` **必红且与本线无关**|**106 用 `ssh test106`**(别名已配)|**47 用 `ssh bt-server`**(sshd **22** 端口)|47 的 PG 是**本机进程**(⛔ 无 `docker exec dshs-pg`;**⛔ 不取用凭据**)|🔴 **改 drop-in 才是唯一载体**(同名键写 `/etc/dshs.env` 会被**静默压掉**)|🔴 **改 bwrap 参数前先看目标机 bubblewrap 版本**(47=**0.4.0**/106=**0.11.0**,`--perms` 属 0.5+)|⛔ 往 md 追加长文本别用 bash heredoc(用 Write 写 `.py` 再跑)|文档库任何 `Edit`/`Write` 需**先持全局执行锁**;域锁建议含 `src/im` · `poc/im-connection-gateway` · `交接单/IM群组-稳定性机制与框架选型.md`。
5. **边界**:⛔ 不改 `poc/im-conversation-tabs/**`|⛔ 不改 A–E 五单结论|⛔ **本棒不切流**(`location` 只落配置 + `nginx -t`,⛔ 不改生效中的 vhost 指向)|⛔ 不 commit/push/scp 源码(**部署产物除外**)|⛔ 不引新依赖|⛔ 不碰 §14.6 未决项|⛔ 不动 106 的用户实例面。
6. **收尾五件**同本入口惯例。**回填** = 选型单 `交接单/IM群组-稳定性机制与框架选型.md` **§十五 追加「§15.8 gateway 部署回报」小节**(⛔ 不改 §十五 既有内容)+ 顺手把 §十五 的"未完成项"两条标注状态。
✅ **已办(2026-09-23 21:3x)**:⚠️ 落点**改为 §15.11**(§15.8 已被「未完成项 / 已知限」占用,⛔ 不重排既有小节)|§15.8 表前加了 🔻 状态标注|`交接单/.locks/` 释放、入口 §0/§2 已推进、当日 `.workbuddy/memory/2026-09-23.md` 已追加。
**⏭️ 第 16 棒(执行棒 · 下一轮动作)= 换连接层的剩余代码面:能力口径修正 + 平台侧接入面 + E 单客户端双模传输**
✅ **已完成(2026-09-23 23:1x · 会话 `IM线-第16棒`)** —— ① ② ③ 三项 + ④ 本机回环 E2E **16 / 16 全绿(掉线 0)**;回填见选型单 **§十六** + E 单「§执行回报(IM 线第 16 棒)」。🔴 **本棒七条实测踩点务必先读 §16.5**(尤其第 2 条:回检共享密钥必须放 `http.static_headers`,`http_headers` 元素只是**头名**)。
🔴 **为什么排它**:第 15 棒把网关**部署到 47**、把在线态取数接了线,但实测同时暴露了「**切流还差哪一块**」——三件**全部可在本机完成并自验**,是本线**唯一不依赖用户动作**的可自动推进面:① 网关鉴权走 `client.token`(JWT,`config.json` 里**没有** `connect` 代理)⇒ **平台侧必须先有"发连接准入"的面**(现在没有);② E 单客户端 `poc/im-conversation-tabs` 的传输层**仍按平台 IM 协议**(`{op:'subscribe'}`…),而网关只认自己的协议(`{"connect":…}` / `{"subscribe":{"channel":"im:<roomId>"}}` / **空对象 `{}` 即 pong**)⇒ **直接切流会全量连不上**;③ `GATEWAY_CAPABILITIES.providesServerHeartbeat=false` 与第 15 棒实测(网关**会**发协议级 ping)**不符** ⇒ 能力自述错,且它参与 `assertCapabilities` 的构造即抛判据。
✅ **本轮上线风险为零**:⛔ 本棒不切流、不重打包、不重投放、不需要任何部署 ⇒ 全部改动只在本机代码面 + 本机回环 E2E。
⚠️ **第 17 棒(届时再由当棒收官时排,⛔ 不预登记)** = 重打包 + 重投放 + 边缘接 vhost + **切流** + 目标档位复测(`2000` / `10` / `30 s`)。
1. **只读前置(先逐条核实,⛔ 别凭记忆)**:
- `src/im/backends/gateway.ts` 的 `GATEWAY_CAPABILITIES` 全字段 + `src/im/connection-backend.ts` 的 `assertCapabilities`:构造即抛的判据在哪一行、断的是什么;改 `providesServerHeartbeat` 会不会让某条既有断言变成"必须提供垫片"⇒ **两者一起看再动**。
- `src/web/routes/im.ts` 现有面:`/api/im/ws`(native)+ 第 15 棒新加的 `POST /api/im/gateway/presence`(fail-closed + `secretEquals` + `IM_GATEWAY_KEY_ENV`)⇒ **复用同一套密钥与 fail-closed 风格**,⛔ 不要另造第二套。
- `poc/im-connection-gateway/config.json` 当前鉴权形态(`client.token.hmac_secret_key`;**无** `connect` 代理)+ 47 上的部署实况(`/opt/dsh-gateway/`)。
- `poc/im-conversation-tabs/lib/client.js` 的**传输层**三处:WS 建立点、出站帧构造点、入站 `switch` 分发点(第 13 棒刚把出站对齐成 `op:` 制 ⇒ ⛔ 别回退)。
- 47 上 `/opt/dsh-gateway/gateway.env`(600)密钥**只读确认存在**即可,⛔ **不取内容、不进日志、不写文档**。
2. **动工范围(严格)**:
- **① 能力口径修正**:`gateway.ts` 的 `providesServerHeartbeat` 改 **`true`**(依据 = 选型单 §15.11.3 / §15.11.4 实测)+ **连带复核** `assertCapabilities` 与 B 组既有用例;若某条断言语义因此改变 ⇒ 同步改断言**并写清为什么**。
- **② 平台侧接入面(二选一 · 执行棒自决 · 须写清优缺点与选择理由)**:
**(a) 发连接令牌** —— 新增 `POST /api/im/gateway/token`(登录态校验 ⇒ HMAC-JWT,`sub`=userId、短 `exp`、用同一份网关密钥;⛔ 密钥不进响应体/日志)⇒ 客户端用 `{"connect":{"token":…}}`;
**(b) 改网关为 `connect` 代理** —— 网关侧加 `client.connect.proxy`(或等价位)指向平台新回调面 ⇒ 客户端 `connect` 不带 token,鉴权靠**同源 cookie / 头**经边缘带到平台。
两种都要满足:**权威仍在平台**("成员表判读房"这条不许破)+ **fail-closed**(无密钥 / 未登录 ⇒ 401/403,⛔ 不静默放行)。
- **③ E 单客户端传输层双模化**(`poc/im-conversation-tabs/lib/client.js` 及同目录件):`native` 走原协议**逐字不变**;`gateway` 走网关协议 —— 建连 → `connect` → 按房间 `subscribe` `im:<roomId>` → **收到空对象 `{}` 就按连接回 `{}`** → 入站取 `push.pub.data.frame` 后**照原路径** `JSON.parse(frame)` 分发(⇒ 应用层帧**逐字不变**)。模式来源 = 平台在只读面告诉客户端(自决:随 `/api/im/stats` 或专用只读面;⛔ 不许硬编码)。
- **④ 本机回环 E2E**:本机起网关(样例配置、**回环且避开 47 的 18081**)+ 真平台进程 ⇒ 用**真客户端件**跑通四条:连接 / 订阅 / 一条消息收发 / presence 一进一出。
- ⛔ **不许**改 A–E 五单结论|⛔ **不许**改 `src/im/**` 内核语义(`hub.ts` / `ws.ts` / `store.ts` 是权威面;只许如 ① 那样改自述)|⛔ **本棒不切流、不重打包、不重投放**|⛔ 不引新依赖进 `src/**`(客户端侧若确需网关 SDK ⇒ 单独说明,并给"无依赖最小实现"作对照)。
3. **判据(命令 + 期望 + 退出码)**:
- `npm run build` **rc=0**|`node --test test/im-connection-backend.test.mjs` **28 / 28**(⛔ 不得回退)|`node --test test/im-presence-ingest.test.mjs` **13 / 13**(⛔ 不得回退)|`npm test`(Node 22)**0 败**|`check-layering` **rc=0 无新增违规**。
- **①** 有用例能断「`providesServerHeartbeat` 与实测一致」;`assertCapabilities` 的构造即抛仍可复现(给一条反例)。
- **②** 未登录 / 无密钥 ⇒ **401 / 403**;凭证正确 ⇒ **200** 且**响应体里没有密钥**;`grep -c` 密钥在 `stats()` / 日志 / 响应体中 = **0**。
- **③** `native` 路径回归**逐字相同**(给 diff 或哈希)|`gateway` 路径下"**空对象即回**"必须有**独立断言**(⛔ 不许只靠端到端间接证明)。
- **④** E2E 四条全通 + **掉线 0**(沿用第 15 棒权威口径:服务端 `num_clients` 不降 + 客户端 `closed=0`,⛔ **不用** `hbTimeoutRate`)。
- **回滚**:本棒产物一行可回(客户端切回 `native` / 撤销新增面)⇒ 各记一条。
4. **环境注意(⛔ 别踩)**:本机 bash 每条命令前置 `export PATH="/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/usr/bin:/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/bin:$PATH"`|`npm test` 用 **Node 22**(`E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3`)|`npm run verify` **必红且与本线无关**|**47 用 `ssh bt-server`**(22 端口)/**106 用 `ssh test106`**|47 的 PG 是**本机进程**(⛔ 无 `docker exec dshs-pg`;⛔ 不取凭据)|🔴 **改 drop-in 才是唯一载体**(同名键写 `/etc/dshs.env` 会被**静默压掉**)|🔴 **网关密钥只在 `/opt/dsh-gateway/{config.json,gateway.env}`(600)** —— ⛔ 不打印、不进文档、不写进任何 `stats` / 日志 / 响应|🔴 **网关只许绑回环**|⚠️ **本机起网关要避开 47 的 `18081`**|⛔ 往 md 追加长文本别用 bash heredoc(用 Write 写 `.py` 再跑;追加一律 `open(p,'ab')` 二进制,判据 = `b.count(b'\r')` **不变**)|文档库任何 `Edit`/`Write` 需**先持全局执行锁**。
5. **边界**:抢锁域**必须含** `src/im` · `src/web/routes/im.ts` · `test` · `poc/im-conversation-tabs`(客户端传输层)· `poc/im-connection-gateway` · `交接单/IM群组-稳定性机制与框架选型.md` · `交接单/IM群组-E-会话界面UI.md`。⛔ 不改 `poc/im-conversation-tabs/**` 的**应用层帧语义**(只改传输层)|⛔ 不改 A–E 五单结论|⛔ **本棒不切流**|⛔ 不 commit/push/scp 源码(本棒**不需要**任何部署)|⛔ 不引新依赖进 `src/**`|⛔ 不碰 §14.6 未决项|⛔ 不动 106 用户实例面|⚠️ **第 16 棒改完客户端也⛔ 不要重投放**(用户尚未启用现版 v0.1.0 ⇒ 现版取证仍有效;重投放属第 17 棒)。
6. **收尾五件**同本入口惯例。**回填** = 选型单 `交接单/IM群组-稳定性机制与框架选型.md` **§十六(新建小节,⛔ 不改 §十五 既有内容)** + E 单 `交接单/IM群组-E-会话界面UI.md` **追加**「§执行回报(IM 线第 16 棒)」(⛔ 不改既有正文)。
**⏭️ 第 17 棒(执行棒 · 下一轮动作)= 重打包 + 重投放 + 边缘接 vhost + 切流 + 目标档位复测**
🔴 **为什么排它**:第 16 棒把连接层的**代码面**闭合了(能力口径 / 平台接入面 / 客户端双模 / 本机回环 E2E 全绿),
但**线上仍是 native** —— 连接层 gateway 虽已在 47 就地部署(第 15 棒),边缘 vhost 尚未接入、客户端仍是旧传输层、
本棒新增的 `lib/**` 产物与网关 `config.json` 回检块也没同步过去。⇒ 本棒是"**让改动真正生效**"的一棒。
1. **只读前置(先逐条核实,⛔ 别凭记忆)**:
- 47 现状:`/opt/dsh-gateway/config.json` 的回检块**形态**(⛔ 只读确认结构,**不取密钥内容**)——
必须是 `http.static_headers`,⛔ **不是** `http_headers`(见选型单 §16.5-2)。
- 边缘生效 vhost(`ai1net.com.conf` 443 块)现况 + 第 15 棒落盘的片段;`nginx -t` 与备份。
- 用户是否已在实例「功能管理」启用 `im-conversation-tabs` 现版 `v0.1.0`(**这决定切流顺序**)。
- 选型单 §十六 的七条踩点(⛔ 至少读第 1 / 2 / 5 条)。
2. **动工范围**:**重打包**(E 单客户端含本棒的 gateway 传输层)⇒ **重投放**(候选池 v0.2.0)⇒
**47 `lib/**` 产物同步 + 网关 `config.json` 回检块修正形态落盘 + 重启网关** ⇒ **边缘接 vhost(插片段 + `nginx -t` + reload)**
⇒ **切流** ⇒ **目标档位复测**(`2000` / `10` / `30 s`,沿用第 15 棒权威口径:`num_clients` 不降 + `closed=0`)。
⚠️ **顺序铁律**:客户端新版必须**先投放且实例重启生效**,再切流;否则网关模式下旧客户端会全量连不上。
⚠️ 若用户尚未启用现版 ⇒ 先做"重打包 + 重投放 + 产物同步 + 边缘片段落盘(⛔ 不 reload)",
**切流留到用户启用后再排**(这属"卡在用户动作",⛔ 不许替用户点)。
3. **判据(命令 + 期望 + 退出码)**:`npm run build` rc=0|`npm test`(Node 22)0 败|`check-layering` rc=0 无新增违规|
切流后:`/api/im/ws` **101**、`/api/im/stats` 的 `backendKind=gateway`、`transport.protocol=gateway`、
线上一条消息收发 + presence 一进一出、**掉线 0**(`num_clients` 不降 + `closed=0`)|
**回滚**:删边缘片段 → `nginx -t` → reload ⇒ 线上回到 native(第 15 棒已演练过一次,命令可复用)。
4. **环境注意**:本机 bash 前置 `export PATH="/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/usr/bin:/e/ProgramData/.workbuddy/binaries/PortableGit/versions/1.2.0/bin:$PATH"`|
`npm test` 用 Node 22|**47 用 `ssh bt-server`**(22 端口)/**106 用 `ssh test106`**|🔴 网关**只许绑回环**|
🔴 网关密钥**只在 `/opt/dsh-gateway/{config.json,gateway.env}`(600)** —— ⛔ 不打印 / 不进日志 / 不进文档 / 不进 `stats`。
5. **边界**:抢锁域**必须含** `poc/im-conversation-tabs` · `poc/im-connection-gateway` · `src/im` · `src/web/routes/im.ts` ·
`交接单/IM群组-稳定性机制与框架选型.md` · `交接单/IM群组-E-会话界面UI.md`。⛔ 不改 A–E 五单结论|⛔ 不碰 §14.6|
⛔ 不动 106 用户实例面|⛔ 不引新依赖进 `src/**`|⚠️ 切流是**本棒唯一动线上**的动作 ⇒ 动手前一句话说明影响谁 / 断多久。
6. **收尾五件**同本入口惯例。
---
## §3 已取事实(2026-09-22 实搜|8 条|下一棒 ⛔ 不必重搜)
**框架侧**
1. **ejabberd 26.07**(2026-07-30 发布)— Erlang/OTP;官方称**单节点 200 万并发会话**(生产实测);WhatsApp / Nintendo / BBC 在用;XMPP 核心 + **400+ XEP**(MUC / MAM / PubSub / OMEMO)+ 内建 MQTT / SIP / Matrix;故障哲学 = Erlang「let-it-crash」+ 热升级;社区版 **GPLv2** 免费自托管,商业版 €16,000/年。
2. **OpenIM**(Go 微服务)— `msggateway`(WS 长连接) + RPC 服务 + **Kafka** + **MongoDB** + Redis + etcd;官方口径**单节点 10 万+ 连接 / P99 < 100 ms / 10 万 QPS**;**seq 全局序列 + 增量同步**;三级缓存(LRU → Redis → DB);支持**十万级大群**;配置口径:1K–10K 用户 4C8G、50K–100K 需 16C32G 集群。
3. **Matrix** — Synapse(Python,慢) / **Dendrite(Go,性能更好)**;**联邦 + 强制 E2EE**;运维复杂度最高(官方文档自述需懂联邦协议 / 反代 / DB)。
4. **Telegram 官方服务端 ⛔ 不开源**;2026 出现的**第三方实现**(用官方客户端改 DC 地址 + RSA 公钥接入):
- **Opengram** — C#/.NET 9,fork 自 `mytelegram`,**layer 216**,Apache 2.0;微服务 + MongoDB/Redis/RabbitMQ/MinIO;CQRS;含 Calls(TURN/STUN + mediasoup)。
- **Ferrite** — C#,**layer 214**,Cassandra/Redis/MinIO/Elasticsearch/Kafka;**494 / 732 方法实现**(其余返回 `403 METHOD_DISABLED`)。
- **teamgram-server** — Go 1.23,**layer 222**,MySQL/Redis/etcd/Kafka/MinIO + gRPC(go-zero);⚠️ **社区版仅私聊 + 基础群**,频道 / 超级群 / 机器人 / 秘密聊天在**企业版**。
5. 其他候选:**Tinode**(Go/gRPC,移动优先)· Rocket.Chat / Mattermost / Zulip(协作型,功能重)· Openfire(Java XMPP,老牌稳定)· Jackal(Go XMPP)· lhttp(Go WS 轻框架)。
**我们的约束(判据来源)**
6. **资源口径**(⚠️ 2026-09-22 17:0x 用户更正:**机器可以换**)⇒ **资源不是选型否决项**。现实基线(仅供**需求测算**)= 当前单实例**硬顶 1024 MiB** / 宿主 **1870 MiB**(`scripts/verify-mem-model.mjs` 口径)。上面第 1–4 条里,**ejabberd 是单运行时(Erlang VM)**,其余都要外挂 **3–5 个组件**(Kafka / MongoDB / etcd / MinIO / RabbitMQ)⇒ 规划时**算清"要多少机器与内存才能稳"并写进方案**,⛔ **不得以"装不下"否掉方案**。
7. **既有资产可复用**(⛔ 不要重复造):PG **13.23**(控制面库 `dshs` + 一插件一库 `dshs_pl_*`,归属列强制)· Node/TS 平台与 fastify · nginx(宝塔) · 覆盖网络(relay **只绑回环**、**跨用户隔离是结构性的**)· **联邦多区**(149:数据不动、只有请求动)· 实例内可挂 **dsh slots** 面板。
8. **可直接"借机制"**(seq 全局序列 + 游标增量同步(OpenIM)· **CQRS + 队列削峰**(Opengram/Kafka 思想 ⇒ 我方落 **PG `FOR UPDATE SKIP LOCKED` + `LISTEN/NOTIFY`**)· **无状态网关 + 可重启**(`msggateway` 那层)· 连接心跳与僵尸清理 · 背压 / 限速 / 熔断(142 §3.6 三条保护约束同向))。
⚠️ **借机制的理由 = 机制更可控、判据可自验**,⛔ **不再是"引框架塞不下"** —— §0 口径更新后,「**整框架引入**」重新成为**平等候选**(与自研、混合同台按四维比较)。
## §4 ⛔ 未拍板项(本线 ⛔ 不得替用户决定;均已在 142 §六 / 各单登记)
1. ~~**UI 落点**~~ ✅ **已拍板(2026-09-23)**:**C 案**(实例内 dsh 会话页 tab)+ 保活 = `display:none` + 群组 tab 上限 **5** + 新建群组入口在业务插件内 + 开源界面**只借鉴交互与组件(按 ① 方案,⛔ 不引整站)**。详见 E 单 §四-4 / §四-5。
⚠️ **仍悬的子项**:E 单步 0 的「可发现 / 自助加入」标志位走**显式列 + v15 迁移**还是 **`config_json`** —— 属技术选型(有客观判据)⇒ **执行棒自决**,⛔ 不占用户拍板额度。
2. ~~**E2EE 形态档位**~~ ✅ **已拍板(2026-09-23)= 强度档**(用户原话「强度档是否影响性能,影响较小就用强度档」)。**性能判据已量化**:一条消息全部密码学开销(AES-GCM 0.5–2 µs + X25519 ~50 µs + HKDF 1–2 µs ≈ **55 µs**)vs 扇出 P99 23 µs ⇒ 相对 1 s 事件循环预算 ≈ **0.006%**、50 msg/s 下占单核 ≈ **0.3%** ⇒ **CPU 维度可用**。
🔴 **两处结构性代价须按实现项对待**(⛔ 不受"影响较小"覆盖):① **棘轮状态必须每设备持久化**(丢了永久断链 ⇒ 新增持久化表 + 恢复逻辑)② **群聊必须 Megolm 化(每发送者一条链)** —— 若误用严格双棘轮做 N 人群 ⇒ 扇出 `O(N)` 变 **`O(N²)`**,**这才是会压垮连接层的那条**。③ **agent 代答模型须重做**:`im_agent_grants`「按房间逐条授权」在强度档下**不成立** ⇒ agent 升格**一等成员设备** ⇒ **D 单 §四-2 语义与表结构须重写**,E4-6 断言须改写。详见选型单 **§十四-14.1~14.3**(含 9 条连带重写清单)。
3. ~~**房间上限与发言预算数值**~~ ✅ **已拍板(2026-09-23)= 按建议固化**(用户原话「按照建议执行」):`maxMembers = 2000` / `agentQuotaPer10Min = 10` / `silencePeriodMs = 30 s` **由"待标定初值"转为正式值**(依据 = 第 6 棒实测扇出 P99 0.023 ms)。详见选型单 **§十四-14.4** + **A 单 §四-7 状态块**。
🔴 **适用前提(正式条款)**:这三个数是**单进程自研扇出**形态下标的 ⇒ 连接层改形态后**必须复测**;复测前不得当跨形态恒定值引用。⚠️ 复测要点:内存类判据**须重复跑 ≥3 次取中位**(E-5 跨跑方差 ±12 MiB)。
4. ~~**连接层三候选**~~ ✅ **已拍板(2026-09-23)= 候选 B:引入 Centrifugo 类外部连接层**(用户原话「2 选 b」)。依据 = 第 3 棒 E-2 实测(10k P99 28.9/68.1/111.9 ms vs 自研 268.6/337.6/359.4 ms;扇出离散中位 17.2/57.9/97.8 vs 171.8/227.6/225.5)。详见选型单 **§十四-14.5**。
🔴 **三条硬实现项**:① **必须补发自建心跳**(Centrifugo 实测**不发服务端 WS ping**,客户端也不发 ⇒ **26 s 内 10000/10000 全被断开**;且"保活计时起点必须绑该连接自己的认证完成时刻",整进程统一计时会一次掉 2773 条)② **按"可替换连接层"接口落**(⛔ 不把 Centrifugo 概念泄漏进 `src/im/**` 内核)③ **出容量测算表**(边际 **70.1 KiB/连接** vs 自研 5.9 KiB;按"机器可以换"口径算需求,⛔ 不以此否决方案)。
5. ✅ **下一棒先做哪一项 = 已拍板(2026-09-23)= 先做连接层 B 案**(用户原话「**先做b**」)⇒ 第 11 棒 = 动工 B 案(范围见 §2「第 11 棒」)。⚠️ **E2EE 强度档重写排在 B 案之后**,且**档位重写必须先于任何 E2EE 代码动工**(选型单 §十四-14.3 的 9 条清单)。
6. 🔴 **跨区可见性默认**(倾向"互不可见")—— 与上述各项**解耦**,**仍在等拍板**。
7. 🔴 **强度档下 agent 代答的具体形态**("一等成员设备"的撤销语义 / agent 之间是否互见)—— 属 **D 单重写面**,须单独定。
6. ~~**连接层换型的部署根阻塞**~~ ✅ **已解(2026-09-23 A 线查实)**:**47 的代码根 = `/opt/dshs`、106 的 = `/opt/dshs-cluster`**(106 上无 `/opt/dshs`);`/opt/dsh` = 平台数据根,**三台同名** ⇒ 换型安装位置可判 ⇒ **② 连接层换型已无阻塞,可开工**。
**已拍板**(⛔ 不要当未决项再问):首批场景 = **三类都要**(办公群 / MUD / 跑团 ⇒ D 单判据升级为三类各跑通一个)|插件扩展点边界 = 新 `room_type` **需 admin 审核后生效**(自定,可推翻)。
🔴 **2026-09-23 新增已拍板**:E2EE 形态档位 = **强度档**|房间上限与预算 = **`2000` / `10` / `30 s` 转正式值**|连接层 = **候选 B(引入 Centrifugo 类)**|UI 落点 = **C 案**。**四项均已落文档**,⛔ 不必再问。
## §5 开工校验
- **源仓 HEAD**:`git -C D:/github/dsh_shenxian log --oneline -1`(以实跑为准)。
- **入口漂移判据**:以本文件 **§0 最新一行** 为准;若 §0 与 §2 冲突 ⇒ 以 §0 为准。
- ⚠️ **本机 Bash 工具 shim 已坏**(`dirname: command not found` ⇒ `ls`/`cat`/`mkdir` 全 not found,`export PATH` 救不回)⇒ 抢锁 / 放锁 / 任何需要 shell 的活**一律走**:
`"E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" "E:/ProgramData/AIProject/aliyun-dsh-server/tmp/claim_lock.py" --claim-exec "<线名>-第N棒"`(该脚本用 `subprocess` 调 PortableGit 的 `bash.exe` 并给足 `usr/bin`+`bin`+`mingw64/bin`)。
⚠️ 抢锁失败时脚本 **rc=1 且中文输出乱码** ⇒ 别把「乱码」误判成「锁被占」,**解锁文件**读 `dsh-server-docs/交接单/.exec-lock/OWNER` 才准。
## §6 ⛔ 不要重做
- IM 需求基线(142)与五单 A–E 的**架构对齐**(2026-09-22 已完成,见各单 §〇)—— ⛔ 不要重做。
- §3 的 8 条框架事实 —— ⛔ 不必重搜(要补新事实另说)。
- 🔴 **E-4 的档位内容** —— **2026-09-23 第 11 棒已按强度档重写完毕**(选型单 §四-6 + D 单 §四-6 + §十一)⇒ ⛔ **不要按旧「工程务实档」重写一遍**,也不要以为它还"待重写"。
- 🔴 **§15.7 的 P0(客户端帧协议)** —— **2026-09-23 第 13 棒已修完**(客户端对齐平台的 op 制,`E-14` 机械对账守着)⇒ ⛔ **不要重做协议对齐**、⛔ **不要以为客户端还在发 `type:`**。
⚠️ **同时作废一条旧建议**:§2「第 13 棒」原文写的「保活 ⇒ `{op:'pong'}`(`pong` 是已认 op)」**是错的** —— `src/im/ws.ts` 的客户端 op `switch` **没有 `case 'pong'`**(`pong` 只在模块头注释的表格里)⇒ 发它会被 `bad-op` 断开。**客户端心跳一律发 `{op:'ping'}`**(服务端回 `pong`)。⛔ 不要照旧建议改回去。
- 🔴 **三项拍板(档位 / 预算数值 / 连接层)** —— 已定,⛔ 不要当未决项再问。
- 🔴 **`im-conversation-tabs` 的投放(可自动部分)** —— **2026-09-23 20:2x 第 14 棒已做完**(打包 + 入候选池 + 发布到 47 共享层,审计 id 360/361)⇒ ⛔ **不要重投、不要重打包**、⛔ **不要再把它登记给别人**(归属已由本线认领完结)。⚠️ 剩余**唯一**未闭合项 = **用户在实例「功能管理」启用**(用户动作,见 `交付物/IM群组-ui投放与线上取证操作单-20260923.md`)。
- 🔴 **连接层 gateway 的 47 就地部署 + 在线态取数接线** —— **2026-09-23 21:3x 第 15 棒已做完**(产物逐字一致属主 root:root|`dsh-gateway.service` enabled+active、**只绑 `127.0.0.1:18081`**|边缘片段落盘 `nginx -t` rc=0、**生效 vhost 未改**|取数 `presenceIngest` `not-wired` ⇒ `local-hub`(native) / `gateway-join-leave`(gateway),回调面 `POST /api/im/gateway/presence` 带 key 200 / 不带 401 / native 404)⇒ ⛔ **不要重部署、不要重复接线**、⛔ **不要以为 `presenceIngest` 还是 `not-wired`**。详见选型单 **§15.11**。
- 🔴 **`client.ping_interval` 口径** —— 旧值 **`0s` 是错的**(无待回 ping 时 `{}` 是空命令 ⇒ 被网关判 `bad request` 断开;240 连接实测 80/80 掉)⇒ 已改 **`25s`**。⚠️ 「网关不发服务端 WS ping」(`wsPingRecv=0`)那条实测**本身没错** —— 它数的是 **WS 控制帧**,网关发的是**协议级 JSON ping**;错的只是由它推出的结论。⇒ ⛔ **不要照 `0s` 改回去**、⛔ **不要说"网关不管保活"**;客户端保活 = **按连接**收到 ping 后回 `{}`(⛔ 整进程统一节拍器会被判 `bad request`)。
- ⚠️ **一条口径更正(第 14 棒实测,⛔ 别再按旧法取证)**:**IM 链路服务端不写任何日志**(`src/im/**` / `src/web/routes/im.ts` / `src/supervisor/proxy.ts` 全零日志点;`reject()` 只回 `error` 帧 + `destroy()`)⇒ **⛔ 不要用 `journalctl | grep bad-op` 当判据**(恒空,且"空"会被误读成"没发生");协议类取证一律走**客户端**(DevTools → WS 帧流水)。
- 本线**不碰**别人的 lane:插件投放与分库线(`接续入口_插件投放与分库线_20260922.md`)、技能重组线、覆盖网络线。