Files

142 lines
11 KiB
Markdown
Raw Permalink Normal View History

# 接续包 · IM 反向通道(平台 → 插件)· 2026-09-26
> **工作区**:`E:/ProgramData/AIProject/ai1net-dsh-server`|**基线**:`D:/github/dsh_shenxian` HEAD=`e6207aa`(未 commit)
> **上游(定案依据)**:用户 2026-09-26 06:3x 原话 ——
> 「**都需要实现完整,才能让插件接入,否则两边都要返工**」
> 🔴 **实现棒已收官(06:5x)** —— 反向通道**两条都通并已真 HTTP 端到端验证 + 部署 47**。
> ⛔ 本件取代此前一切「降级三条够用 / 不必急着补」的口径。
---
## ✅ 本棒结果(实现棒 · 2026-09-26 06:4x–06:5x)
**会话**:`im-reverse-channel`(域锁:`src/im` · `src/web` · `sdk/im-plugin-host` · `test/im-plugin-bridge-callback.test.mjs`,**已释放**)
### 落了什么
| 文件 | 形态 | 说明 |
|---|---|---|
| `src/im/sdk/callback-bus.ts` | **新增** | `ImBridgeCallbackBus`(per-instance 有界队列 + pending resolve + deadline + 计数)+ `attachImBridge()` 复合接线 + `roomEventOf()` 事件形状 + 两个目标解析器 |
| `src/web/routes/im.ts` | **纯插入**(`+451/-0`) | 2 端点 `POST /api/im/plugins/bridge/pull`(长轮询)· `…/bridge/result`(回投);`register` 后 `bus.remember()`;`unregister` 后 `bus.forget()`;stats 加 `imBridge` |
| `src/im/sdk/registry.ts` | 纯插入 | 可选注入 `callbackStats`(供 `snapshot()` 带出总线读数 —— **因为观测面那行是既有行,不许改**) |
| `src/im/sdk/types.ts` | 纯插入 | `SpeakRuleContract.onTimeout: 'allow' \| 'deny'`(缺省 `allow`) |
| `src/im/sdk/binding.ts` | 微改 | `afterAppend` 的事件构造改用 `roomEventOf`(与跨进程**同一来源**,防分叉) |
| `src/im/sdk/index.ts` | 纯插入 | 门面导出 |
| `sdk/im-plugin-host/index.mjs` | 新增能力 | `onSpeakRule()` / `onEvent()` / `startCallbacks()` / `callbacks` 读数;长轮询循环(含退避、`close()` 掐断、守护超时) |
| `test/im-plugin-bridge-callback.test.mjs` | **新增 9 例** | 命中 / 超时放行 / `deny` / 队列满丢弃 / 计数 / 幂等与冒领 / 事件与私聊不外发 / 守卫不漏 / 未接线逐字不变 |
| `package.json` | 纯插入 | `test` + `verify` 各登记 1 处 |
### 关键设计(已落地,⛔ 别改)
1. **方向 = 实例侧拨出长轮询**:⛔ 平台不打进实例、⛔ 不开端口、⛔ 不新增凭据(复用 `bridgeAuth`)。
2. **`speak.check` 同步**:deadline = `min(契约 timeoutMs, maxSpeakWaitMs=1500)`;超时 ⇒ 按 `onTimeout`(缺省放行)+ `bridgeTimeouts+1`。
3. **`event.deliver` 异步**:`afterAppend` 恒同步返回;每实例队列上限 256,满则丢最旧 + `bridgeDropped+1`。
4. **长轮询服务端硬上限 20 s**(到点回空、客户端立即重拨)—— 防"常驻连接占满额度"(146 同类风险)。
5. 🔴 **`hasPlugins()` 被复合增强**:否则"只有实例内插件登记"时写路径守卫恒假 ⇒ `speak.check` **永不触发**(静默失效)。
6. 🔴 **复合不改调用点**:`attachImBridge` 增强**既有的** `pluginBinding` 对象 ⇒ `ws.ts` / `routes/im.ts` 写路径一行未动。
### 验收读数(真跑,⛔ 不是推断)
- `npm run build` **rc=0** ✅
- 新测试 **9 例 fail 0**;`im-plugin-bridge`(14) / `im-backpressure`(8) / `im-sdk` **fail 0** ✅
- **真 HTTP e2e `tmp/im-bridge-e2e-20260926.mjs` ⇒ 28 例 fail 0**,决定性四项:
- `④b speak.check 生效`:插件回 deny ⇒ `403 speak-rule` **且消息未落库** ✅
- `④c 超时 ⇒ 放行`,`bridgeTimeouts=1`(观测面可见)✅
- `④d event.deliver 送达`:订阅方真收到 `message.created` ✅
- ⑤ `bus` 读数进 `/api/im/plugins/bridge` ✅
- `npm run check:layering` **rc=0 无新增违规** ✅
- 内核四档:`store.ts` **零改动**· `routes/im.ts` **+451/-0 纯插入** ✅
- **部署 47 已做**:备份 `/opt/dsh/backups/im-callback-predeploy-20260926.tgz`(72896 B)→ tar `--owner=root --group=root` 解包 → `systemctl restart dshs` ⇒ `active`,`/api/im/stats` 与 `/api/im/plugins/bridge` 在 19100 / 25000 / 3080 等端口回 **401(路由存活)** ✅
### ⚠️ 三条如实登记(⛔ 不粉饰)
1. **`test/im-sdk.test.mjs` 仍有 3 红(35 / 36 / 78)—— 与本棒无关**:判据要求 `hub.ts` 零 diff 与 `ws.ts` 零删行,而工作区**开工前**就已带 `hub.ts +36/-0`、`ws.ts +84/-1`(第 15 棒背压修复的**在途未提交**改动)。本棒 `routes/im.ts` 实测 **-0**。
2. **09 / 08 文档未改「已通」**(原 §4-6):`dsh-server-docs` **不在本棒域锁内** ⇒ 按 R7-边界未动手,留给下一棒。
3. **`HOST_SDK_VERSION` 仍 `'1.0.0'`**:`test/im-plugin-bridge.test.mjs` 硬断言该常量(域外文件)⇒ 未升版;SDK 文档头标注"2026-09-26 增反向回调"。
---
## §0 一句话
「插件 → 平台」已通;**「平台 → 插件」现已同样通** —— 实例侧 SDK 长轮询拉取工作项,
`speakRules.check`(同步)与 `events.onEvent`(异步)两条都在真 HTTP 上验证过并已上 47。
## §1 现状与缺口(更新后)
| 面 | 同进程 | 跨进程 |
|---|---|---|
| 发言规则 `speakRules.check` | ✅ | ✅ **本棒补齐**(长轮询 + `onTimeout` 兜底 + 计数) |
| 事件 `events.onEvent` | ✅ | ✅ **本棒补齐**(有界队列 + 丢最旧计数) |
| 面板 `render` / `onAction` | ✅ 实例内渲染 | ⛔ 不需要跨进程(维持原判) |
| 登记 / 出向 | — | ✅ 已通(5 端点 + SDK) |
## §2 设计(**已定项** —— 见上「关键设计」,⛔ 不再讨论)
## §3 文件清单与内核约束(🔴 四档不得破)
`store.ts` **零改动** | `hub.ts` 纯插入 | `ws.ts` 只许动投递点 | `routes/im.ts` 纯插入。
⇒ 本项属「新增能力」⇒ **一律纯插入**;⛔ 未接线时行为必须**逐字不变**(单测 ⑨ 已覆盖)。
## §4 施工顺序(**已完成 1–7**,仅 6 的文档部分受锁所限未做)
## §5 验收(判据,⛔ 缺一不算完)—— **除文档外全部达标**
- 文档改「已通」:⚠️ 未做(域锁所限,见「如实登记 2」)
## §6 回滚
`/opt/dsh/backups/im-callback-predeploy-20260926.tgz`(推前备份,已就位);
代码回滚 `git checkout -- <本棒文件>`;服务 `systemctl restart dshs`。⚠️ 本棒未 commit ⇒ 回滚点即 `e6207aa`。
🔴 **回滚要点**:本次是**纯插入** ⇒ 回滚只需还原上述备份包并重启,⛔ 不涉及数据迁移。
## §7 边界 / 风险
- ⛔ **未接线时行为必须逐字不变**(单测 ⑨ 守着)。
- ⚠️ **长轮询连接占用**:一条实例一条常驻(服务端硬上限 20 s)—— 若日后观测到连接数异常,先看这里。
- ⚠️ `onTimeout` 缺省 `'allow'` ⇒ **回合制插件忘声明 `'deny'`,超时即破规则**;⇒ 文档与示例**必须点名**(模板见 `sdk/im-plugin-host` 的 `onSpeakRule()` 注释)。
- ⛔ **不开端口、⛔ 不要求实例可寻址、⛔ 不新增凭据类型**(本设计的立身之本)。
- ⛔ 未 commit / push;⛔ 未动覆盖网络线与投放线的任何结论。
## §8 连带待办(同一目标:**让插件能真的接进来、不返工**)
| # | 项 | 状态 |
|---|---|---|
| 1 | **可跑的最小示例工程**(`poc/business-plugins-im/` 三个示例是旧契约) | ✅ **已完成(07:3x 收尾棒)** ⇒ `poc/business-plugins-im/minimal-callbacks/`(零依赖 · `node self-check.mjs` **25 例 fail 0**) |
| 2 | 09 §4 `tables: [{` 与 08「唯一入口是 `package.json#dsh.data.schema`」**打架**(P1) | ✅ **已定口径并落文(07:4x 文档收尾棒)** ⇒ 09 §4 加「位置 vs 形状」对照表:**两者不是二选一,是同一份 schema 的两个用途**(投放/建库读 `package.json#dsh.data.schema`;运行时读 `manifest.tables`)⇒ **两处须写同一份**。依据 = 源码现读 `schema.ts` `parseDeclFromDir()` + `im/sdk/host.ts` `assertManifestShape()` |
| 3 | 09 未消 #4 移动端 / #5 错误码·自测清单·变更记录 / #8 内部编号 + **09/08 改「已通」**(本轮遗留) | ✅ **全部完成**:09 改「已通」6 处 + **新增 §13 移动端专项 / §14 错误码全表 / §15 提包前自测清单 / §16 变更记录**;§5 两档参数改自洽表述(消内部编号依赖);08 §4-补 加 09 §13 交叉指针 |
| 4 | 背压阈值**生产标定** | 未动 —— 需 admin 会话或真实流量(⛔ 不改代码,改 env) |
## §9 下一棒怎么开工(照抄)
```bash
# ⓪ 状态
"E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" "E:/ProgramData/AIProject/ai1net-dsh-server/state.py"
# ① 校验本件口径(不符即停)
md5sum "E:/ProgramData/AIProject/ai1net-dsh-server/接续包_IM反向通道实现_20260926.md"
# ② 先判可锁范围(⚠️ 文档库与 src/web 都可能被别的会话持有)
bash "D:/github/dsh_shenxian/dsh-server-docs/07-scripts/preflight-lock.sh" "IM反向通道-收尾-<日期>" \
D:/github/dsh_shenxian/dsh-server-docs/01-规范/09-IM插件SDK与扩展点契约.md
# ③ 抢锁(域按上一步结果取)
bash "D:/github/dsh_shenxian/dsh-server-docs/07-scripts/handoff-guard.sh" --claim-exec "IM反向通道-收尾-<日期>" --domains <域>
```
**下一棒建议范围**(按优先级):
1. §8-1 **可跑最小示例工程**(含 `onSpeakRule` / `onEvent` 跨进程用法)—— 这是"插件作者不再照文档猜"的唯一落点
2. §8-3 文档收尾(09 §10.4 / §10.5 / §12.5 改「已通」+ 08 变更记录 + `onTimeout` 必须点名)
3. §8-2 表声明入口口径二选一(先定口径再改文)
---
## ✅ 收尾棒结果(2026-09-26 07:3x–07:5x)
**会话**:`im-reverse-finish`(域锁:`poc/business-plugins-im` · `dsh-server-docs/01-规范`,开工 07:33)
| 落了什么 | 形态 | 判据 |
|---|---|---|
| `poc/business-plugins-im/minimal-callbacks/`(5 文件:`index.mjs` · `mock-platform.mjs` · `self-check.mjs` · `package.json` · `README.md`) | **新增目录** | `node self-check.mjs` ⇒ **25 例 · fail 0 · rc=0**(零依赖 · 自带模拟平台,不连真平台也能跑) |
| `dsh-server-docs/01-规范/09-IM插件SDK与扩展点契约.md` | 局部改 6 处 | §10.4 标题/表 · §10.5 · §12.1 · §12.3 · §12.4 · §12.5 全部改「**已通**」;残留扫描 `未通\|实现待落\|定要补\|现在先按` ⇒ **零命中** |
| `dsh-server-docs/01-规范/08-插件开发与对接规范.md` | 局部改 2 处 | 顶部 IM 指针增反向回调段(含 **`onTimeout:'deny'` 强制项**)+ §11 变更记录补 09-26 行 |
**示例覆盖的判据**(自检逐条断言,⛔ 不是推断):
① 登记成功 + 拼上实例凭据;② **送上平台的 manifest 无函数**(`check`/`onEvent` 已被 SDK 剔除);
③ **非当前回合者 ⇒ 回 `{ok:false}` 且 `detail` 可读**;④ 轮到本人 ⇒ **放行**(防规则把正常发言也卡死);
⑤ `event.deliver` 送达 ⇒ `onEvent` 真执行 ⇒ 出 1 帧;⑥ **未接线(缺 baseUrl/token)既不抛、也不启长轮询**。
**约束自查**:⛔ 内核四档**一行未动**(本棒**只动 `poc/` 与文档**,零 `src/` 改动);
⛔ 未 commit / push;⛔ 未动其它工作区文件。
⚠️ **如实登记**:① 本件(`接续包_IM反向通道实现_20260926.md`)**已被本棒改写** ⇒
**其 md5 已变**(开工时 = `935c7180b92867e0e2ba20de57e0ae5e`)⇒ 下一棒**必须重算**,⛔ 别拿旧值校验。
② §8-2(表声明入口口径)**仍未裁**。