601 lines
137 KiB
Markdown
601 lines
137 KiB
Markdown
# 接续入口 · 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`)、技能重组线、覆盖网络线。
|