文档库:目录改为编号制(01-规范/02-架构设计/03-数据库/04-调整方案/
05-交接单/06-ops/07-scripts/08-skills/09-archive),顶层散文件归入 01-规范/;
INDEX.md 与 docs-manifest.json 重刷(档案 146 篇);旧目录名引用全量对齐。
IM 线:src/im/**(SDK / hub / store / presence / ws / gateway-token)、
src/web/routes/im.ts、src/db/plugin-data/**、src/supervisor/plugin-assembly.ts
及对应 test/**。
插件线:poc/{im-agent-bridge,im-connection-gateway,im-conversation-tabs,
business-plugins-im,carbon-mcp-probe}、src/web/routes/{sessions,overlay-device}.ts、
src/net/relay/{device-grant,instance-credential}.ts。
仓库卫生:清出 40 个历史误入库 / 已改名文件(34 个交接单归档 + 6 个旧结构,
本地均有副本);dsh-server-docs/.gitignore 补 tmp/;交接单不入库(政策)。
im-connection-gateway —— IM 连接层的参考部署(外部连接层)
本目录是 IM 线「连接层候选 B」的部署侧材料:外部连接层进程的配置样例 + 边缘映射样例 + 平台应用层帧 ↔ 网关协议的逐项映射表。
🔴 本目录里的内容只做部署:平台的连接层抽象在
src/im/connection-backend.ts(中性命名), 两个后端实现在src/im/backends/{native,gateway}.ts。⛔ 平台内核(hub.ts/ws.ts/store.ts)不认识本目录的任何概念,这是刻意的分层 —— 换掉下面这个组件不需要动内核。
参考部署用的是 Centrifugo v6.9.6(选择依据见 dsh-server-docs/交接单/IM群组-稳定性机制与框架选型.md
§十二 执行回报(IM线-第3棒)的 E-2 对照读数)。
🔻 第 15 棒部署实测修正(2026-09-23,⛔ 下方原文不改,只在此标注) —— §3 / §4 / §6 有三处被实测改写:
| 原文 | 实测结论 | 依据 |
|---|---|---|
§3 / config:ping_interval: "0s"("⛔ 不启用服务端 WS ping") |
🔴 该值会让客户端保活帧被判 bad request —— 没有待回的 ping 时,协议里的 {} 是空命令、不是 pong ⇒ 连接被断开(code 3501)。已改为 25s:网关发协议级 ping,客户端按连接回 {} |
47 三向实测:off 80/80 断(3012 no pong)· conn 掉 0 · global 80/80 断(3501 bad request) |
| §4 "本棒 ⛔ 未执行" | ✅ 已在 47 就地执行(二进制 + 配置 + systemd 单元 + 边缘片段落盘 + nginx -t);⛔ 边缘未接进 vhost(不开流量) |
见 §7 |
| §6-1 "在线态取数未接线" | ✅ 已接线(平台 presence-ingest.ts + POST /api/im/gateway/presence);stats().presenceIngest 从 not-wired 变为 gateway-join-leave |
见 §7-3 |
⚠️ 关于 §3 那句「实测 wsPingRecv = 0」:这条实测本身没错 —— 它数的是 WS 控制帧,
而网关发的是协议级 JSON ping。错的是由它推出的结论("所以网关不管保活")。
1. 拓扑
浏览器 / 实例内客户端
│ ws(s)://<平台域>/api/im/ws ← 客户端侧 URL **一个字都不变**
▼
边缘(nginx)── location = /api/im/ws ──────▶ 外部连接层(持有 10k+ 连接,做扇出)
▲
平台进程 ── POST http://<网关>/api/publish ───────────┘ (每条消息**一次**发布调用)
│
└─ 权威态:房间 / 成员 / 消息 / 游标 / 预算 —— 全在平台 `src/im/**`,一条都不外移
平台的 /api/im/ws(进程内自研 WS)不摘:
① 实例内凭据通道(x-dsh-im-instance-token)走它;② 它是一行回滚的落点。
2. 应用层帧 ↔ 网关协议:逐项映射
🔴 映射的判据是「应用层帧不变」:客户端 JSON.parse(payload.frame) 拿到的字节与
native 模式逐字相同(test/im-connection-backend.test.mjs 的 B-18 就是断这条)。
应用层(/api/im/ws 的语义) |
网关侧 | 权威在哪 |
|---|---|---|
客户端连 wss://…/api/im/ws |
边缘 location = /api/im/ws → 网关 WS 端点 |
边缘 |
鉴权(sid cookie / 实例凭据) |
网关侧的连接令牌(client.token.hmac_secret_key 签发) |
🔴 仍是平台:令牌只回答"你是谁",能不能读房由成员表判 |
{op:'subscribe', roomId} |
客户端 subscribe 频道 im:<roomId> |
平台成员表(store.isMember) |
{op:'subscribe'} 的应答 {op:'subscribed', cursor} |
⛔ 网关不产这个帧 ⇒ 由平台 REST 补(见下「游标」) | 平台 |
{op:'send', roomId, payload} |
⛔ 网关不参与:走平台 POST /api/im/rooms/:id/messages |
平台(预算 decideWrite + 落库) |
{op:'resume', roomId, since} |
平台 GET /api/im/rooms/:id/messages?since=(网关 history 只作加速) |
平台(游标权威) |
服务端 {op:'message', roomId, messages:[…]} |
平台 POST /api/publish {channel:'im:<roomId>', data:{frame:'<原字符串>'}} |
平台产生、网关投递 |
{op:'presence', roomId, members:[…]} |
网关 presence / join_leave(本轮未接线) |
⚠️ 见 §4 未完成项 |
| 保活 | 客户端按 §3 的规则自建 | 客户端侧垫片 |
负载信封(src/im/backends/gateway.ts 的 gatewayPayloadOf):
{ "channel": "im:imr_<uuid>", "data": { "frame": "{\"op\":\"message\",\"roomId\":\"imr_…\",\"messages\":[…]}" } }
3. 🔴 硬实现项 ①:必须自建保活(实测必做,⛔ 不是可选优化)
E-2 判别实验(106 /root/im-e2/hb.sh)三条读数,本棒在 E-7 里做了三向复核:
| 口径 | 结果 |
|---|---|
off:客户端完全不发协议 ping |
服务端在 26 s 处把连接判死(wsPingRecv = 0:网关不发服务端 WS ping) |
global:整进程统一计时 |
同上(这是 E-2 记录到的适配器缺陷形态) |
conn:每条连接认证完成后才起自己的 ping 计时 |
保活成立(= 本棒的修正形态) |
⇒ 规则(src/im/keepalive.ts 是它的可执行形态):
- 客户端每
intervalMs发一次应用层 ping;intervalMs由一个空窗里发 3 次反推 (实测空窗 26 s ⇒ 8,666 ms;丢一次也不至于被判死)。 - 🔴 计时起点 = 该连接自己的认证完成时刻(⛔ 不是进程启动时刻、⛔ 不是 worker 启动时刻)。 用统一计时会一次掉成批连接 —— 本棒的回归用例 B-12 / B-13 把这两种模型结构性区分开。
- 平台的
/api/im/stats在gateway模式下会回带keepalive与keepaliveAnchor: 'per-connection-auth-complete'(运维据此核对客户端是否按规则做)。
4. 部署(示例,本棒 ⛔ 未执行)
# 1) 放二进制与配置(版本与校验见 dsh-server-docs 的部署记录)
install -m 0755 centrifugo /opt/dsh-gateway/centrifugo
install -m 0644 config.json /opt/dsh-gateway/config.json # 凭据先替换 REPLACE_WITH_ROTATED_SECRET
# 2) 起进程(端口示例 18081;v6 的端口用命令行给,配置里已不再有 port 键)
cd /opt/dsh-gateway && ./centrifugo --config=./config.json --http_server.port=18081
# 3) 平台侧(drop-in,⛔ 同名键写 /etc/dshs.env 会被静默压掉)
# DSH_IM_BACKEND=gateway
# DSH_IM_GATEWAY_URL=http://127.0.0.1:18081
# DSH_IM_GATEWAY_KEY=<与 http_api.key 同值>
# DSH_IM_GATEWAY_CHANNEL_PREFIX=im:
# 4) 边缘:把 nginx-location.conf 的 location 落进站点配置 → nginx -t → reload
# 5) 就绪探测与读数
curl -s -X POST http://127.0.0.1:18081/api/info -H "Authorization: apikey <key>"
# 平台侧:GET /api/im/stats(admin)⇒ backendKind=gateway / backend.degraded 为空
回滚(顺序与上线相反,两步任一即可):
- 去掉边缘的
location = /api/im/ws→ reload ⇒ 流量立刻回到平台进程内通道; - 平台 drop-in 里
DSH_IM_BACKEND改回native(或删掉该行)→ 重启dshs。
⚠️ 两个方向的都已具备:平台的 /api/im/ws 两种后端都保留(ImConnectionBackend.dispatch),
所以第 1 步是零重启回滚。
5. 容量测算的输入(⛔ 本目录不承诺数值,数值见回报的容量表)
| 输入 | 值 | 来源 |
|---|---|---|
| 边际内存 / 连接 | 70.1 KiB(自研对照 5.9 KiB,11.9×) | E-2 实测(本棒 E-6 复核) |
| 10k 连接稳态 RSS | 750.2 MiB(自研对照 116.0 MiB) | E-2 实测(本棒 E-6 复核) |
| 客户端观测 P99(1k/5k/10k) | 28.9 / 68.1 / 111.9 ms(自研 268.6 / 337.6 / 359.4 ms) | E-2(本棒 E-6 复核) |
| 扇出离散中位 | 17.2 / 57.9 / 97.8 ms(自研 171.8 / 227.6 / 225.5 ms) | E-2(本棒 E-6 复核) |
| 平台侧 CPU | 每条消息 1 次 HTTP 发布调用;lastPublishMs 见 /api/im/stats |
本棒实现口径 |
⚠️ 内存类读数跨跑方差 ±12 MiB(E-5 教训)⇒ 任何内存判据重复跑 ≥3 次取中位。
6. 未完成项(⛔ 与 §3 的"已做"分开看)
🔻 第 15 棒状态标注(2026-09-23,⛔ 原文不改):① 已接线(平台侧
presence-ingest.ts+回调面POST /api/im/gateway/presence,读数gateway-join-leave);② 配置与边缘片段 已落 47 并通过nginx -t,仍未接进生效 vhost(不开流量);③ 客户端侧垫片仍未接进 E 单 bundle(第 13 棒已修掉帧格式 P0,但"换连接层后客户端要按连接回{}"这层尚未做)。
- 在线态取数未接线:平台的
presence需要网关的presence/join_leave面; 本棒只把现状暴露在stats().presenceIngest = 'not-wired',⛔ 不假装完成。 - 本目录的 nginx / config 未应用(本棒边界:⛔ 不部署到 47 / 106 生产面)。
- 客户端侧垫片未接进 E 单 bundle —— 见回报里的 P0 发现(客户端 bundle 与平台
/api/im/ws的帧格式当前不一致,接线前先要修那一处)。
7. 第 15 棒:47 就地部署实况(2026-09-23)
🔴 本目录只记部署落点;权威读数与判据在 dsh-server-docs/交接单/IM群组-稳定性机制与框架选型.md §15.8。
| 项 | 落点 / 值 |
|---|---|
| 二进制 | /opt/dsh-gateway/centrifugo(v6.9.6,sha256 f4869bf9c0028ed5253b74479b1dc70e3b916dae2ddad5bb03504b1eb07a848c,与 106 测试床同源) |
| 配置 | /opt/dsh-gateway/config.json(600,密钥现场 openssl rand -hex 32 生成,⛔ 不入库) |
| 单元 | dsh-gateway.service(enabled + active),--http_server.address=127.0.0.1 --http_server.port=18081,LimitNOFILE=200000 |
| 边缘片段 | /opt/dsh-gateway/nginx-location.conf(已落盘 + 临时配置 nginx -t rc=0;⛔ 未接进 vhost) |
| 切流 | DSH_IM_BACKEND / DSH_IM_GATEWAY_URL / DSH_IM_GATEWAY_KEY / DSH_IM_GATEWAY_CHANNEL_PREFIX ⇒ drop-in /etc/systemd/system/dshs.service.d/im-gateway.conf(模板 = /opt/dsh-gateway/dshs-im-gateway.conf.example) |
| 当前状态 | native(drop-in 已删,restart dshs 复验 101 ✅)⇒ 线上行为回到改动前 |
只读取证脚本(本次用的)= /opt/dsh-gateway/im15-drill.mjs:起 N 个客户端订阅一个临时频道,
三组分口径(off / conn / global)计断开数,并读平台 /api/im/stats 与网关 /api/channels。