派生插件开发指南

August 16, 2026 · View on GitHub

派生插件(derived plugin)是依赖 dsh-notifacation-frame 的普通 DSH 插件:它不接触通知的配置持久化、通道分发或 UI,只声明通知什么和配置哪些选项。本文说明注册契约,重点解释派生插件配置项如何被解析。

1. 三步接入

// ① 获得类型(包括 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 的 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(...) 投递通知。
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)

interface NotificationFrameService {
  register(def: NotifierDefinition): () => void       // 注册(或覆盖),返回 disposer
  list(): NotifierItemView[]                          // 全部通知项 + 生效配置
  dispatch(payload): void                             // 直接投递(旁路开关)
  history(limit?): NotificationRecord[]               // 最近通知(新在前)
  updateItem(id, blob): Promise<void>                 // 改配置并热生效
  test(id): void                                      // 经该项通道发测试通知
}

6. 参考