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

135 lines
6.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)`:
```js
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. 怎么验
```bash
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)。若必须改内核 ⇒ 扩展点没设计对,
回头改扩展点设计,⛔ 不是改内核。