# 派生插件开发指南 派生插件(derived plugin)是依赖 `dsh-notifacation-frame` 的普通 DSH 插件:它不接触通知的配置持久化、通道分发或 UI,只声明**通知什么**和**配置哪些选项**。本文说明注册契约,重点解释**派生插件配置项如何被解析**。 ## 1. 三步接入 ```ts // ① 获得类型(包括 ctx.notificationFrame 的 Context 增广) import type { NotifierDefinition, NotificationFrameService } from 'dsh-notifacation-frame' export const name = 'my-notifier' // ② 等待框架服务挂载(组合行顺序无所谓,cordis 注入解析会等待) export const inject = ['notificationFrame'] as string[] // ③ apply 里注册(ctx.notificationFrame 来自框架的 ctx.provide) export function apply(ctx: MyCtx): void { const dispose = ctx.notificationFrame.register(myDefinition) ctx.effect(() => dispose) // 插件卸载时反注册 } ``` > 注意:DSH 的 cordis `Context.on` 按 Events 表做键控泛型,第三方事件名不在表内。 > 派生插件应像 `dsh-pref-kit` 一样自声明窄面结构类型(见 > [examples/dsh-notif-demo/src/plugin.ts](../examples/dsh-notif-demo/src/plugin.ts) 的 `DemoCtx`)。 ## 2. 通知项定义(NotifierDefinition) | 字段 | 必填 | 说明 | |---|---|---| | `id` | ✅ | 全局唯一(设置文档以它为 key)。重复注册会先 dispose 旧实例。 | | `title` / `description` | ✅ | 设置卡片标题与说明。 | | `severity` | ✅ | `info` / `success` / `warning` / `error`(toast 与系统通知的样式)。 | | `channels` | ✅ | 该项允许的通道子集(卡片只渲染这些复选项)。 | | `defaultChannels` | ✅ | 用户未配置时的默认通道。 | | `defaultEnabled` | ❌ | 缺省 `true`;`false` = 默认关闭(参考内置 `tool-error`)。 | | `defaultSound` | ❌ | 用户未配置时的默认音效预设(`none`/`ding`/`pop`/`chime`/`alert`/`custom`,缺省静音;非法值回落静音)。 | | `fields` | ❌ | **派生插件的配置项**(见下)。 | | `setup(env)` | ✅ | 激活回调:注册事件监听,`env.notify(...)` 投递通知。 | ```ts const myDefinition: NotifierDefinition = { id: 'my-event', title: '我的事件', description: '…', severity: 'info', channels: ['web', 'system', 'log'], defaultChannels: ['web'], fields: [ { key: 'minValue', label: '最小阈值', type: 'number', default: 10, min: 0, max: 100 }, { key: 'greet', label: '问候语', type: 'string', default: 'hello' }, { key: 'mode', label: '模式', type: 'select', default: 'a', options: [{ value: 'a', label: 'A' }, { value: 'b', label: 'B' }], }, { key: 'enabledExtra', label: '附加提醒', type: 'boolean', default: false }, ], setup(env) { const ctx = env.ctx as MyCtx return ctx.on('some/event', (payload) => { env.notify({ title: 'Something happened', body: `value=${payload.v},当前阈值 ${String(env.config.minValue)}`, sessionId: payload.sessionId, // 可选:toast 提供“跳转会话” meta: { tool: 'some-tool' }, // 可选:标量元数据 }) }) }, } ``` ## 3. 配置项如何被解析(核心契约) 一条完整的解析链路: ``` 用户设置文档 blob { enabled, channels, options: { minValue: 99, greet: 42, mode: 'zzz' } } │ ▼ resolveBlob(def, blob) (src/shared.ts) enabled —— 非 boolean → def.defaultEnabled ?? true channels —— 白名单过滤(web/system/log)+ 去重;非数组 → defaultChannels options —— parseOptions(def, blob.options) │ ▼ parseOptions 逐字段执行 coerceField (src/shared.ts) boolean —— 非 boolean → 字段 default(无 default → false) number —— 非有限数 → 字段 default;随后钳制到 [min, max] string —— 非 string → 字段 default select —— 值必须在 options 白名单内,否则回落第一个选项 │ ▼ 框架重激活该通知项 dispose 旧 setup → env.config = 解析后的 options → setup(新 env) ``` 要点: 1. **settings 文档里存的是原始 JSON**。框架自己的 settings schema 只声明 `{ items: { [id]: { enabled, channels, options } } }`,其中 `options` 是 完全开放的 `z.dict(z.any())`——派生插件的字段**不进框架 schema**,框架 无法也不会替派生插件理解它们。 2. **解析发生在框架侧,时机是“激活前”**:注册时、用户改卡片时、外部编辑 settings 文档被 watch 到时,框架都会重新 `resolveBlob`。因此 `setup(env)` 拿到的 `env.config` 永远是合法值——类型正确、范围已钳制、 缺失项已补默认——**派生插件无需任何校验代码**。 3. **配置修改热生效**:`updateItem` 先把解析后的干净值写回设置文档 (持久化),再 dispose 旧 setup、用新配置重跑 setup。派生插件不用订阅 任何 settings 事件。 4. **字段元数据驱动卡片**:`fields` 同时是设置卡片的渲染元数据 (boolean→开关、number→数字输入带 min/max、string→文本、select→下拉) 和解析规则。一个字段一处声明,两端一致。 5. **setup 抛错不击穿**:异常被框架捕获、标记在卡片上(`setupError`), 通知项仍在目录中,其他通知项不受影响。 ## 4. setup 的生命周期与纪律 - `setup(env)` 在以下时机被调用:注册后、用户修改配置后、外部文档变化被 reconcile 后。返回的 disposer 由框架保管并在下一次激活前调用。 - `env.ctx` 是框架插件的 cordis 上下文:`ctx.on` 的事件监听会随框架 fiber 清理,但**不要**在 setup 里用 `ctx.provide` 或注册全局服务。 - 事件监听用 `ctx.on(...)` 返回 disposer;`process.on` 之类全局监听必须 自己配对 `process.off`(参考内置 `process-crash` 的写法)。 - `env.notify` 遵守当前生效配置:项被禁用时静默丢弃,通道按卡片选择分发。 需要旁路开关的场合用服务面的 `dispatch()`(如其他宿主插件的直接调用)。 ## 5. 服务面(ctx.notificationFrame) ```ts interface NotificationFrameService { register(def: NotifierDefinition): () => void // 注册(或覆盖),返回 disposer list(): NotifierItemView[] // 全部通知项 + 生效配置 dispatch(payload): void // 直接投递(旁路开关) history(limit?): NotificationRecord[] // 最近通知(新在前) updateItem(id, blob): Promise // 改配置并热生效 test(id): void // 经该项通道发测试通知 } ``` ## 6. 参考 - 完整示例(可独立安装运行):[examples/dsh-notif-demo](../examples/dsh-notif-demo) - 内置项写法(含 process 级监听、工具名过滤):[src/host/builtins.ts](../src/host/builtins.ts) - 解析实现的单测:[tests/shared.test.ts](../tests/shared.test.ts) 与 [tests/registry.test.ts](../tests/registry.test.ts)