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

11 KiB
Raw Blame 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 下一棒怎么开工(照抄)

# ⓪ 状态
"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(表声明入口口径)仍未裁。