最小可跑示例 · 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()) |
🔴 三个必须记住的点
- 回合制 / 限速型必须显式声明
onTimeout: 'deny'—— 缺省是'allow'(为了与平台既有口径「无插件 / 插件故障 ⇒ 放行」逐字一致)。 对回合制来说"放行"就等于规则失效:你的实例不在线(重启 / 崩溃 / 部署窗口)时, 非当前回合者的消息会照常写入。声明位在manifest.speakRules[].onTimeout。 manifest里不许塞函数(check/validate/onEvent/render/onAction)—— 跨不了进程,SDK 会自动剔除。函数侧一律走onSpeakRule()/onEvent()注册; 自检里有一条专门断言"送上平台的 manifest 确实没有函数"。- 一条实例只该有一条常驻长轮询(
ready()登记后自动启,幂等)——它直接占平台连接额度, ⛔ 别自己再写第二条。收尾用await host.close()(会掐断在途长轮询,否则进程退不出去)。
⛔ 不做什么
- 不开端口:所有调用都是插件拨出去的(HTTP 到平台);平台不打进实例、实例不需可寻址。
- 不新增凭据:复用实例凭据(
DSH_PLATFORM_IM_URL/DSH_IM_INSTANCE_TOKEN,平台起实例时注入)。 - 不把事件当可靠消息流:事件队列有界(满则丢最旧并计数)⇒ 要可靠增量请用
host.subscribe()(游标补齐,不会丢)。 - 判不了就拒(fail-closed)要靠"返回
{ok:false}",⛔ 不是靠抛错 —— 你的check抛错时,平台按"插件故障 ⇒ 放行"处理(与同进程语义逐字一致)。
接真平台
- 把
sdk/im-plugin-host/index.mjs拷进你自己的包(单文件、零依赖、可 vendoring), 并把index.mjs顶部的 import 改成你包内的路径。 - 保证
pluginId(SDK 参数 = 包名)与manifest.packageName/manifest.pluginId自洽:@dsh-local/im-plugin-demo→ 内部 idim_plugin_demo(去 scope、非字母数字 →_)。 不自洽 ⇒plugin-id-mismatch;出向恒plugin-unavailable。 - 在实例内运行:
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纯插入