Files
dsh_shenxian/dsh-server-docs/01-规范/09-IM插件SDK与扩展点契约.md
admin e6207aa691
build / build-and-scan (push) Waiting to run
chore(仓库对齐): 文档库结构治理 + IM/插件线落地
文档库:目录改为编号制(01-规范/02-架构设计/03-数据库/04-调整方案/
05-交接单/06-ops/07-scripts/08-skills/09-archive),顶层散文件归入 01-规范/;
INDEX.md 与 docs-manifest.json 重刷(档案 146 篇);旧目录名引用全量对齐。

IM 线:src/im/**(SDK / hub / store / presence / ws / gateway-token)、
src/web/routes/im.ts、src/db/plugin-data/**、src/supervisor/plugin-assembly.ts
及对应 test/**。

插件线:poc/{im-agent-bridge,im-connection-gateway,im-conversation-tabs,
business-plugins-im,carbon-mcp-probe}、src/web/routes/{sessions,overlay-device}.ts、
src/net/relay/{device-grant,instance-credential}.ts。

仓库卫生:清出 40 个历史误入库 / 已改名文件(34 个交接单归档 + 6 个旧结构,
本地均有副本);dsh-server-docs/.gitignore 补 tmp/;交接单不入库(政策)。
2026-09-24 07:25:16 +08:00

6.2 KiB
Raw Permalink Blame History

IM 插件 SDK 与扩展点契约

面向要给 IM 房间加场景能力的插件作者。内核只提供通用地基,场景差异全部下沉到这里。 依据:05-交接单/IM群组-04-插件SDK与扩展点契约.md|需求基线:04-调整方案/142-IM群组对话-需求基线与方案.md §3.6

1. 这是什么

IM 房间内核(src/im/{types,db,store,clock,hub,ws}.ts)只定义消息信封(id / seq / ts / 作者 / via / visibility)与房间生命周期。办公群的任务卡、MUD 的指令流、跑团的回合制, 都不写进内核 —— 写法是注册一个插件,声明它要用到哪些扩展点。

SDK = src/im/sdk/:

文件 内容
types.ts 七个扩展点的契约类型 + 两档传输 + 数据面声明
host.ts 宿主实现:注册面、超时熔断、预算继承、声明式建表、审计
index.ts 对外 Facade(插件 import 这一个就够)

2. 七个扩展点

# 扩展点 契约 一句话
1 房间类型注册 RoomTypeContract 声明 room_type + 默认配置 + 走哪一档传输
2 消息载荷 PayloadContract 插件自己定义 payload 结构与校验
3 成员角色 RoleContract 业务角色(DM / 玩家 / 负责人);⛔ 不提升内核权限
4 发言规则 hook SpeakRuleContract 自由并发 / 回合制 / 限速
5 能力机器人 BotContract 注册 bot + @ 路由(形态 A 的来源)
6 事件订阅 EventSubscriber 拨出式:宿主调插件,⛔ 插件不开监听端口
7 房间内 UI PanelContract 声明面板并挂 dsh 既有 slots

无插件 ⇒ 默认自由并发:不注册发言规则时,房间行为与改造前一致,⛔ 不报错。

3. 三条平台保护约束

写在契约里、由宿主强制执行 —— 不是文档约定:

  1. 插件不直连 DB:要读写数据只能经注入的 ImDataPort(insert / update / remove / find / count)。插件侧代码里出现 SQL 即违约。
  2. 插件故障不拖垮房间:每个扩展点必须声明 timeoutMs + circuit(缺一注册不通过)。 回调挂起 ⇒ 超时记失败并把该次交给内核默认行为;连续失败 ⇒ 开熔断,冷却期内不再调用插件。
  3. 插件能力吃预算:发言判定走内核提供的 ImBudgetPort。插件只能申请,⛔ 不得自带一套限速。

4. 写一个插件

最小形状 = 一个 manifest 对象 + 一个 apply(ctx, host):

export const manifest = {
  pluginId: 'im_plugin_demo',            // = 包名去 scope、小写、连字符转下划线
  label: '示例',
  packageName: '@dsh-local/im-plugin-demo',
  version: '0.1.0',

  roomTypes: [{
    type: 'demo', label: '示例房',
    transport: 'chat',                    // 或 'realtime'
    defaultConfig: { topic: '' },
    timeoutMs: 50, circuit: { threshold: 5, cooldownMs: 30_000 },   // 🔴 必填
  }],

  payloads: [{
    kind: 'ping',
    validate: (p) => typeof p?.text === 'string',
    timeoutMs: 50, circuit: { threshold: 5, cooldownMs: 30_000 },
  }],

  tables: [{
    name: 'pings', scope: 'room', schemaVersion: 1,
    columns: [{ name: 'text', type: 'text' }],   // 归属列 room_id / user_id 由内核自动补
  }],
}

export function apply(ctx, host) {
  host.register(manifest)
  ctx?.on?.('dispose', () => host.unregister(manifest.pluginId))
}

打包与投递复用既有通道(cordis.patch.yml + package.json#dsh → 门户候选池 → 实例「能力管理」自助启用)。⛔ SDK 不新造分发机制。

5. 两档传输

档 fanoutBatch heartbeatMs maxJitterMs latencyBudgetMs
chat(档案 107 口径) 200 25 000 1 000 1 000
realtime(档案 108 游戏口径) 50 5 000 20 100

房间类型声明自己走哪一档;两档参数不同源,⛔ 不是同一份参数改个名。 未声明 ⇒ 回落 chat(⛔ 不默认给实时档)。

6. 数据面

判落点,再谈建表(03-数据库/DB-03-插件数据面规范.md):

数据种类 落点 归属列
跟着房间走的(角色卡 / 地图 / 任务卡) 该插件自己的库 dshs_pl_<pluginId>,表 p_<pluginId>_* room_id
只属于某用户自己的 同一个库 user_id
缓存 / 临时(丢了可重建) 实例本地 — ⛔ 不得当权威

规则:

  • 建表 = 声明式:字段只用中性类型(text / integer / bigint / real / boolean / json / timestamp / uuid),内核翻译成 sqlite + pg 两套 DDL。⛔ 禁裸 CREATE TABLE、 厂商专有类型、触发器、存储过程、跨前缀 JOIN。
  • 迁移只增不减:schemaVersion 必填;只允许加列(带默认值)/ 加表 / 加索引。 任一步失败 ⇒ 拒绝启用(⛔ 不做半迁移)。
  • 访问必走内核 API:房间维度自动注入 ACL(复用 canSee);跨前缀 / 内核表 看不到也查不了;事务仅限同前缀多表。
  • 卸载数据默认保留 30 天;DROP TABLE 不可逆 ⇒ 需 admin 显式确认 + 先出受影响清单。

7. 三个示例插件

poc/business-plugins-im/ 下三类场景各一个,可直接照抄:

目录 包名 类型 演示的扩展点
office-tasks/ @dsh-local/im-plugin-office office(聊天档) 任务卡载荷 + 面板 + 事件落库
mud-rate/ @dsh-local/im-plugin-mud mud(实时档) 限速发言规则 + 指令流载荷
trpg-turn/ @dsh-local/im-plugin-trpg trpg(实时档) 回合制发言规则 + 业务角色 + 骰子载荷

8. 怎么验

export PATH="E:/ProgramData/.workbuddy/binaries/node/versions/22.22.2-3:$PATH"
npm run build
node --test test/im-sdk.test.mjs      # 期望:退出码 0(66 用例全过)
git diff -- src/im/store.ts src/im/hub.ts src/web/routes/im.ts   # 期望:无输出

最后一条是硬判据:非聊天场景跑通不改内核(142 §五-6)。若必须改内核 ⇒ 扩展点没设计对, 回头改扩展点设计,⛔ 不是改内核。