build / build-and-scan (push) Waiting to run
文档库:目录改为编号制(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/;交接单不入库(政策)。
6.2 KiB
6.2 KiB
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. 三条平台保护约束
写在契约里、由宿主强制执行 —— 不是文档约定:
- 插件不直连 DB:要读写数据只能经注入的
ImDataPort(insert/update/remove/find/count)。插件侧代码里出现 SQL 即违约。 - 插件故障不拖垮房间:每个扩展点必须声明
timeoutMs+circuit(缺一注册不通过)。 回调挂起 ⇒ 超时记失败并把该次交给内核默认行为;连续失败 ⇒ 开熔断,冷却期内不再调用插件。 - 插件能力吃预算:发言判定走内核提供的
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)。若必须改内核 ⇒ 扩展点没设计对, 回头改扩展点设计,⛔ 不是改内核。