Files
dsh_shenxian/poc/business-plugins-im/minimal-callbacks

最小可跑示例 · IM 插件(含跨进程反向回调)

目标:插件作者有一个能直接跑的起点 —— 不用先啃完契约、不用先连真平台。 反向通道(平台 → 插件)已于 2026-09-26 落地并上 47;本目录是它的唯一示例落点。

一键跑

cd poc/business-plugins-im/minimal-callbacks
node self-check.mjs          # 25 例;退出码 0 = 全绿

不需要 npm install(零依赖)、不需要平台、不需要环境变量 —— 自检自带一个 mock-platform.mjs(模拟登记 / 长轮询 / 回投 / 出向四个端点),把完整协议跑一遍。

文件

文件 作用
index.mjs 主体:声明面 manifest + 拼装 start();两条回调用 onSpeakRule() / onEvent() 注册
mock-platform.mjs 极简「平台侧」模拟(⚠️ 只对齐协议形状,不替代真平台)
self-check.mjs 自检 25 例:登记 / 规则拒与放行 / 事件送达 / 未接线不变
package.json type: module;npm run self-check 同义

覆盖的两条反向回调(跨进程)

回调 语义 本示例怎么用
host.onSpeakRule() → speakRules.check 同步:平台写之前问插件,带 deadline 回合制:没轮到的人发言 ⇒ {ok:false, detail} ⇒ 拒发
host.onEvent() → events.onEvent 异步:不阻塞消息流,尽力送达 有人发言 ⇒ 广播一帧(host.frame())

🔴 三个必须记住的点

  1. 回合制 / 限速型必须显式声明 onTimeout: 'deny' —— 缺省是 'allow'(为了与平台既有口径「无插件 / 插件故障 ⇒ 放行」逐字一致)。 对回合制来说"放行"就等于规则失效:你的实例不在线(重启 / 崩溃 / 部署窗口)时, 非当前回合者的消息会照常写入。声明位在 manifest.speakRules[].onTimeout。
  2. manifest 里不许塞函数(check / validate / onEvent / render / onAction)—— 跨不了进程,SDK 会自动剔除。函数侧一律走 onSpeakRule() / onEvent() 注册; 自检里有一条专门断言"送上平台的 manifest 确实没有函数"。
  3. 一条实例只该有一条常驻长轮询(ready() 登记后自动启,幂等)——它直接占平台连接额度, ⛔ 别自己再写第二条。收尾用 await host.close()(会掐断在途长轮询,否则进程退不出去)。

⛔ 不做什么

  • 不开端口:所有调用都是插件拨出去的(HTTP 到平台);平台不打进实例、实例不需可寻址。
  • 不新增凭据:复用实例凭据(DSH_PLATFORM_IM_URL / DSH_IM_INSTANCE_TOKEN,平台起实例时注入)。
  • 不把事件当可靠消息流:事件队列有界(满则丢最旧并计数)⇒ 要可靠增量请用 host.subscribe()(游标补齐,不会丢)。
  • 判不了就拒(fail-closed)要靠"返回 {ok:false}",⛔ 不是靠抛错 —— 你的 check 抛错时,平台按"插件故障 ⇒ 放行"处理(与同进程语义逐字一致)。

接真平台

  1. 把 sdk/im-plugin-host/index.mjs 拷进你自己的包(单文件、零依赖、可 vendoring), 并把 index.mjs 顶部的 import 改成你包内的路径。
  2. 保证 pluginId(SDK 参数 = 包名)与 manifest.packageName / manifest.pluginId 自洽: @dsh-local/im-plugin-demo → 内部 id im_plugin_demo(去 scope、非字母数字 → _)。 不自洽 ⇒ plugin-id-mismatch;出向恒 plugin-unavailable。
  3. 在实例内运行:DSH_PLATFORM_IM_URL / DSH_IM_INSTANCE_TOKEN 由平台注入, ⛔ 不要手填、不要硬编码。出向发言前,bot 必须已是该房成员(进房是房主 / 管理员动作)。

与同目录另外三个示例的关系

mud-rate / office-tasks / trpg-turn 写于反向通道落地之前,把 check / onEvent 直接写在 manifest 里 —— 那是同进程形态,跨不了进程。它们仍有参考价值(声明面、 业务角色、面板),但函数侧请照本目录写。

⚠️ 本示例刻意不声明 tables(零数据面):一是保持"最小",二是 package.json#dsh.data.schema 与契约内 tables: 两种写法的口径尚未裁定(见接续包 §8-2)。 需要数据面时先定口径,别照本示例推。

依据

  • 契约:dsh-server-docs/01-规范/09-IM插件SDK与扩展点契约.md(§10.4 / §12)
  • 落地与验收:工作区 接续包_IM反向通道实现_20260926.md
  • 内核约束(⛔ 不得破):store.ts 零改动 / hub.ts 纯插入 / ws.ts 只动投递点 / routes/im.ts 纯插入