Files
dsh_ai1net_server/docs/交接单/接续包_IM反向通道实现_20260926.md
T
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

143 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 接续包 · IM 反向通道(平台 → 插件)· 2026-09-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(表声明入口口径)**仍未裁**。