文档库:目录改为编号制(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/;交接单不入库(政策)。
This commit is contained in:
1 parent
3d8f50e366
commit
e6207aa691
239 files changed
+34477
-14633
No files matched your search
@@ -0,0 +1,166 @@
|
||||
# `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`):
|
||||
|
||||
```json
|
||||
{ "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` 是它的可执行形态):
|
||||
|
||||
1. 客户端每 **`intervalMs`** 发一次应用层 ping;`intervalMs` 由一个空窗里发 **3** 次反推
|
||||
(实测空窗 26 s ⇒ 8,666 ms;丢一次也不至于被判死)。
|
||||
2. 🔴 **计时起点 = 该连接自己的认证完成时刻**(⛔ 不是进程启动时刻、⛔ 不是 worker 启动时刻)。
|
||||
用统一计时会一次掉成批连接 —— 本棒的回归用例 B-12 / B-13 把这两种模型**结构性区分**开。
|
||||
3. 平台的 `/api/im/stats` 在 `gateway` 模式下会回带 `keepalive` 与
|
||||
`keepaliveAnchor: 'per-connection-auth-complete'`(运维据此核对客户端是否按规则做)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 部署(示例,本棒 ⛔ 未执行)
|
||||
|
||||
```sh
|
||||
# 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 为空
|
||||
```
|
||||
|
||||
**回滚**(顺序与上线相反,两步任一即可):
|
||||
|
||||
1. 去掉边缘的 `location = /api/im/ws` → reload ⇒ 流量立刻回到平台进程内通道;
|
||||
2. 平台 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,但"换连接层后客户端要按连接回 `{}`"这层尚未做)。
|
||||
|
||||
1. **在线态取数未接线**:平台的 `presence` 需要网关的 `presence` / `join_leave` 面;
|
||||
本棒只把现状暴露在 `stats().presenceIngest = 'not-wired'`,⛔ 不假装完成。
|
||||
2. **本目录的 nginx / config 未应用**(本棒边界:⛔ 不部署到 47 / 106 生产面)。
|
||||
3. **客户端侧垫片未接进 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`。
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"_comment": "IM 线 · 第 12 棒 —— 外部连接层的参考部署配置(Centrifugo v6)。",
|
||||
"_comment2": "键位以 E-2 实测可用的 /root/im-e2/config.json 为准(v6 已把密钥挪进 client.token / http_api)。",
|
||||
"_comment3": "凭据一律用环境变量或密钥文件注入,⛔ 不要把这个文件里的占位串当真值提交到生产。",
|
||||
"_comment4": "package.json 的运行时依赖**不含**本组件 —— 它是外部进程,不在 Node 依赖树里。",
|
||||
|
||||
"client": {
|
||||
"allowed_origins": ["*"],
|
||||
"_comment_ping": "🔻 第 15 棒部署实测修正:原值 `0s` 会让客户端保活帧被判 bad request —— 没有待回的 ping 时,协议里的 `{}` 是**空命令**而非 pong。三向实测(47 就地部署,240 连接):off(从不回帧)⇒ 80/80 被断开(code 3012 `no pong`);conn(**按每连接**收到 ping 才回 `{}`)⇒ **掉 0**;global(整进程一个节拍器给所有连接发 `{}`)⇒ 80/80 被断开(code 3501 `bad request`)。⇒ 网关**自己发协议级 ping**,客户端只需按连接回 `{}`。",
|
||||
"ping_interval": "25s",
|
||||
"token": {
|
||||
"hmac_secret_key": "REPLACE_WITH_ROTATED_SECRET"
|
||||
}
|
||||
},
|
||||
|
||||
"http_api": {
|
||||
"key": "REPLACE_WITH_ROTATED_SECRET"
|
||||
},
|
||||
|
||||
"channel": {
|
||||
"_comment_proxy": "🔻 第 16 棒新增(选型单 §十六):**订阅回检** —— 客户端订某个频道时,**由平台**回答「这个用户能不能读这个房」。⛔ 不加这一段的话,连接票据只回答「你是谁」,任何登录用户都能订 `im:<任意房间>` 并收到该房实时消息 ⇒ 「成员表判读房」这条会被破掉。`endpoint` 指平台回检面(回环即可:网关与平台同机);头里的密钥必须与 `http_api.key` / `client.token.hmac_secret_key` / 平台 drop-in 的 `DSH_IM_GATEWAY_KEY` **四者同值**。⚠️ 这个共享密钥必须放 `http.static_headers`(**固定值头**),⛔ **不能**放 `http_headers` —— `http_headers` 是 `array[string]` 且元素**只是头名**(转发客户端带来的同名头);拿它塞 `\"Name: value\"` 会变成「带名无值」,平台侧取不到密钥 ⇒ 回检 401(v6.9.6 实测 `error proxying subscribe: unexpected HTTP status code: 401`)。",
|
||||
"proxy": {
|
||||
"subscribe": {
|
||||
"endpoint": "http://127.0.0.1:3080/api/im/gateway/subscribe",
|
||||
"timeout": "2s",
|
||||
"http": {
|
||||
"static_headers": { "X-DSH-IM-Gateway-Key": "REPLACE_WITH_ROTATED_SECRET" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"namespaces": [
|
||||
{
|
||||
"_comment": "命名空间 = 频道名里第一个 `:` 之前的部分 ⇒ 配合 DSH_IM_GATEWAY_CHANNEL_PREFIX=im: 使用。",
|
||||
"name": "im",
|
||||
"_comment2": "🔻 第 16 棒修正:`allow_subscribe_for_client` 只是「已认证连接可以不带 token 订阅」,**不是**权限判定;真正的授权走 `subscribe_proxy_enabled`(平台判成员表)或平台签发的订阅票据(带 token 时 token 优先,代理不参与)。两者都由**平台**裁决 ⇒ 保留 `true`。",
|
||||
"allow_subscribe_for_client": true,
|
||||
"_comment6": "🔻 第 16 棒新增:打开订阅回检。⚠️ 它不是「可选优化」—— 关掉它,任何登录用户都能订任意 `im:*` 频道。",
|
||||
"subscribe_proxy_enabled": true,
|
||||
"_comment3": "presence / join_leave 是**在线态取数**的候选来源(第 15 棒已接线平台侧;⚠️ 网关侧尚无 join/leave 回调调用方,见 §六 已知限)。",
|
||||
"presence": true,
|
||||
"join_leave": true,
|
||||
"_comment4": "history 只作断线**加速**,⛔ 不是游标权威 —— 权威在平台 GET /api/im/rooms/:id/messages。",
|
||||
"history_size": 100,
|
||||
"history_ttl": "300s",
|
||||
"_comment5": "🔻 第 15 棒修正(原文:⛔ 不启用服务端 WS ping):网关**要**发协议级 ping(见上方 `_comment_ping`);「不发 WS ping」这条实测本身没错(`wsPingRecv=0` 数的是 **WS 控制帧**),错的是由它推出的结论 —— 协议级 ping 照样存在,且客户端必须按连接回 `{}`。",
|
||||
"force_push_join_leave": false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"prometheus": {
|
||||
"enabled": true
|
||||
},
|
||||
|
||||
"log": {
|
||||
"level": "info"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
# IM 线 · 第 12 棒 —— 外部连接层的**边缘映射**示例(nginx)。
|
||||
#
|
||||
# 🔻 第 15 棒更新(2026-09-23):本文件已**落到 47** `/opt/dsh-gateway/nginx-location.conf`
|
||||
# 并用临时配置跑过 `nginx -t`(rc=0,见回报 §15.8);⛔ 但**仍未接进生效中的 vhost**
|
||||
# —— 开流量属"改线上连接层",是单独一步。
|
||||
# 落点提示:生效站点 = `ai1net.com.conf` 的 443 server 块(`location / → 127.0.0.1:3080`),
|
||||
# 把本文件的 location 插在它**之前**即可;`$connection_upgrade` 该机已存在
|
||||
# (`/www/server/panel/vhost/nginx/0.websocket.conf`)。
|
||||
#
|
||||
# 作用:客户端侧的 URL `/api/im/ws` **一个字都不变**,由边缘把它转到外部连接层的
|
||||
# WebSocket 端点;平台进程内那条 `/api/im/ws` 仍然保留(实例内凭据通道 + 回滚路径)。
|
||||
#
|
||||
# ⚠️ 前提:`$connection_upgrade` 这个 map 必须在 `http {}` 里存在一次(宝塔/常见发行版
|
||||
# 的默认配置里通常已有;没有就补下面这段):
|
||||
#
|
||||
# map $http_upgrade $connection_upgrade {
|
||||
# default upgrade;
|
||||
# '' close;
|
||||
# }
|
||||
|
||||
location = /api/im/ws {
|
||||
# 外部连接层的客户端通道(示例端口,按实际部署改)
|
||||
proxy_pass http://127.0.0.1:18081/connection/websocket;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
||||
# 长连接:⛔ 不要开缓冲,读超时要大于一次保活间隔(垫片默认 8.7 s)
|
||||
proxy_buffering off;
|
||||
proxy_read_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
|
||||
# 连接层不落平台日志;排障看连接层自己的日志与 /metrics
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# 回滚(一行):注释掉上面的 location ⇒ 请求落回平台进程内的 `/api/im/ws`。
|
||||
Reference in new issue
Block a user