DSH plugin 开发规范:三种插件形态、命名导出、依赖声明、服务注册与生命周期清理自检清单
DSH plugin 开发规范的主线只有三条:所有能力通过 ctx 注册、所有必需依赖用 inject 声明、所有需要手动释放的资源交给 ctx.effect 回收。 这三条不是风格偏好,而是框架生命周期机制的前提——违反它们的写法通常不会立刻报错,而是在插件卸载、服务被替换或热更新时留下幽灵注册与资源泄漏。本文按官方文档把规范拆成可逐条核对的条目。无论你称它 DSH插件 还是 DeepSeek插件,规范条目完全一致。
DSH plugin 三种形态与命名规范:函数、对象、类怎么选
DSH plugin 支持函数、对象、类三种形态,多数场景用函数形态即可。 官方文档的原话是「Function form is sufficient in most cases」,只有在插件要对外提供服务时才用类形态(来源)。
函数形态使用具名导出,这也是最常用的一种:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-plugin'
export function apply(ctx: Context) {
// 在这里注册能力。
}
对象形态使用默认导出,把 name、inject、apply 收进一个对象:
export default {
name: 'my-plugin',
inject: ['tools'],
apply(ctx: Context) {
// ...
},
}
类形态同样默认导出,继承 Service:
import { Service, type Context } from '@deepseek-ai/cordis'
export default class MetricsService extends Service {
static inject = ['llm']
constructor(ctx: Context) {
super(ctx, 'metrics')
}
}
规范要点:函数形态的具名导出与对象 / 类形态的默认导出不能混写。 不要既写 export default 又写具名 apply——框架只看一种形态,混写会让另一半声明被静默忽略。
命名上还有两条:name 是插件在配置树里的唯一标识,类形态则把服务名交给 super()。 函数与对象形态导出的 name 用于 cordis.yml 里的条目引用与故障定位;类形态的服务名是 super(ctx, 'metrics') 的第一个参数,消费方据此用 ctx.metrics 访问。
两条容易踩的规范:
name要唯一且可读,不要用plugin、main这类通用词,否则配置树里定位不到具体来源。- 服务名决定消费方的访问路径,一经发布再改名就是破坏性变更;发布前先确定再上。
DSH plugin 依赖声明规范:inject 与 ctx.get 的分工
必需依赖写进 inject,可选依赖用 ctx.get() 在调用点查询,不要用可选依赖绕过 inject。 官方文档把两者的边界写得很清楚(来源):
// 必需:服务缺失时插件不加载。
export const inject = ['tools']
// 可选:不写 inject,在调用点用 ctx.get() 查询。
export function apply(ctx: Context) {
const metrics = ctx.get('metrics')
metrics?.record('plugin_loaded', 1)
}
规范含义是:「服务不在就干不了活」的依赖属于必需,写进 inject;「服务在就多做一步」的属于可选,用 ctx.get() 判断。 把必需依赖写成 ctx.get() 会让插件在依赖缺失时带着半残状态进入 ACTIVE,这正是很多「装了但功能不生效」的根源。
DSH plugin 生命周期与资源回收规范:Fiber 状态机与自动清理
每个 DSH plugin 都拥有一个 Fiber 作用域,它的状态是排查插件的唯一权威依据。 官方给出的状态机如下(来源):
PENDING → LOADING → ACTIVE
↘ FAILED
ACTIVE → UNLOADING → DISPOSED
| 状态 | 含义 | 开发者该做什么 |
|---|---|---|
| PENDING | 已声明,但所需依赖未就绪 | 检查 inject 声明的服务是否存在 |
| LOADING | 依赖就绪,正在执行 apply | apply 里不要做长阻塞 |
| ACTIVE | 插件运行中 | 正常态 |
| FAILED | apply 抛出异常 | 看启动日志里的异常栈 |
| UNLOADING / DISPOSED | 正在卸载 / 已完全卸载 | 确认清理逻辑已交回框架 |
依赖驱动的加载是规范的一部分:声明了 inject 的插件会等待所有必需服务就绪;如果依赖的服务消失(例如提供方被替换),插件会被自动卸载,待服务恢复后重新加载。所以插件的 apply 必须可重复执行,不要在 apply 里做只允许跑一次的全局副作用。
资源回收是同一套生命周期机制的另一半:通过 ctx 做的注册都会被框架自动追踪并在卸载时撤销,你不需要手写 removeListener 或 clearInterval。 官方列出被自动清理的四类操作:
ctx.on(event, handler)— 事件监听ctx.tools.register(tool)— 工具注册ctx.llm.registerAdapter(names, adapter)— LLM 适配器注册ctx.effect(() => cleanup)— 自定义资源
export function apply(ctx: Context) {
ctx.on('some-event', handler)
ctx.effect(() => {
const connection = createConnection()
return () => connection.close()
})
}
最容易被忽略的规范在这里:处置器按注册顺序的逆序开始调用,但多个异步处置器会并发执行,不保证逐个完成。 所以存在顺序依赖的清理步骤(例如「先断流、再关连接」)必须放进同一个 ctx.effect() 返回的处置器里,由该处置器负责串行等待;拆成两个 ctx.effect 就失去了顺序保证。
DSH plugin 服务规范:用类形态对外提供能力
当插件要向其他插件提供能力时,必须用类形态,并做三件事:继承 Service、在构造函数里 super(ctx, '<服务名>')、用 static inject 声明它自己依赖的服务。 服务同样是插件,因此也默认导出(来源):
import { Service, type Context } from '@deepseek-ai/cordis'
export default class MetricsService extends Service {
static inject = ['llm']
constructor(ctx: Context) {
super(ctx, 'metrics')
}
record(event: string, value: number) {
// 公开的服务方法。
}
}
消费方声明 export const inject = ['metrics'] 后即可调用 ctx.metrics.record(...)。配套规范是补类型声明:用 TypeScript 的声明合并把新服务挂到 Context 接口上,这样消费方才有类型提示;声明合并只提供类型,运行时的提供与消费仍然分别由类插件和 inject 完成。
服务名冲突与隔离属于另一类问题:cordis.yml 支持用 isolate 让不同插件组各持一份服务实例,写成分组插件时按需使用,不要靠改服务名绕开。
DSH plugin 工程与发布规范
分发的 DSH plugin 以「组合包」(bundle)形式交付,由 package.json 声明 dsh.bundle.patch,再由 cordis.patch.yml 用 insert 把自己的插件条目插进配置树。 最小结构是:
hello-plugin/
├── package.json # 声明 dsh.bundle
├── cordis.patch.yml # 该组合包贡献的配置层
└── index.js # patch 行引用的插件模块
{
"name": "dsh-hello-plugin",
"version": "0.1.0",
"type": "module",
"main": "index.js",
"files": ["index.js", "cordis.patch.yml"],
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
}
装进 profile 验证,两步:
- 装进 profile —— 执行
dsh plugin --profile demo add ./hello-plugin。预期:包写进 profile 依赖,dsh.bundle指向的 patch 被追加进dsh.profile.bundles。 - 核对配置层 —— 执行
dsh --profile demo --dump-config。预期:打印的配置树里能看到你的插件条目,确认 patch 确实生效。
规范要求:发布前必须声明 files,把 cordis.patch.yml 一并打进产物——只发 index.js 会让安装方拿不到配置层,插件装了也不会被挂载。想看社区插件的真实目录与 bundle 声明,可以在 DSH Plugin Hub 里找同类插件对照。
DSH plugin 开发规范自检清单
发布前逐条核对:
- 形态与导出匹配:函数形态用
export const name+export function apply;对象 / 类形态用export default,不混写。 name唯一可读,服务名发布前定死,避免通用词。- 必需依赖全部写进
inject,可选依赖才用ctx.get()。 apply可重复执行,不放只允许跑一次的全局副作用。- 需要手动释放的资源走
ctx.effect(),有顺序依赖的清理合并进同一个处置器。 - 对外提供服务时用类形态:继承
Service+super(ctx, '<name>')+static inject,并补声明合并的类型。 - 配置用
Configschema 校验,非法值在加载时响亮报错,不静默兜底(详见《DSH plugin 配置项怎么定义》)。 - bundle 声明完整:
dsh.bundle.patch+files含cordis.patch.yml。
写第一个插件的最小流程见《DSH 插件怎么开发》;装好后一直不激活,按《DSH plugin 装了不生效》对照 Fiber 状态定位;服务重复注册报错则见《DSH plugin 服务已被注册》。
常见问题
DSH plugin 开发规范的主线只有三条:**所有能力通过 ctx 注册、所有必需依赖用 inject 声明、所有需要手动释放的资源交给 ctx.effect 回收**。三条都遵守时,插件卸载、服务被替换、热更新这三种场景才是干净的;违反它们通常不会立刻报错,而是在上述场景留下幽灵注册或泄漏(来源:官方「插件与生命周期」)。
DSH plugin 的形态与导出方式必须匹配:**函数形态用具名导出(export const name + export function apply),对象形态与类形态用 export default**。官方文档明确「Function form is sufficient in most cases」,只有插件要对外提供服务时才需要类形态。混写会出问题——不要既写 export default 又写具名 apply(来源:官方「你的第一个插件」)。
DSH plugin 停在 PENDING 说明它「已声明,但所需依赖未就绪」——这正是 inject 声明的服务还没出现。Fiber 状态机是 PENDING → LOADING → ACTIVE,声明了 inject 的插件会等待所有必需服务就绪后才执行 apply;如果依赖的服务消失(例如提供方被替换),插件会被自动卸载,待服务恢复后重新加载(来源:官方「插件与生命周期」)。
DSH plugin 的清理要交回框架:通过 ctx 做的注册会在插件卸载时自动撤销,包括 ctx.on、ctx.tools.register、ctx.llm.registerAdapter 和 ctx.effect。需要特别注意的是**处置器按注册顺序逆序开始调用,但多个异步处置器会并发执行、不保证逐个完成**——所以存在顺序依赖的清理步骤必须放进同一个 ctx.effect() 返回的处置器里,由它负责串行等待(来源:官方「插件与生命周期」)。
DSH plugin 在**要向其他插件提供能力时必须用类形态**:类继承 Service,构造函数里 super(ctx, 'metrics') 注册服务名,依赖其他服务时用 static inject。类本身也是插件,所以同样默认导出。消费方用 export const inject = ['metrics'] 声明后,即可通过 ctx.metrics 调用它的公开方法(来源:官方「服务与依赖」)。
相关术语
- Fiber
- Fiber 是 DSH plugin 的作用域单位,也是 Cordis 管理插件生命周期的依据。状态机为 PENDING → LOADING → ACTIVE / FAILED,以及 ACTIVE → UNLOADING → DISPOSED;依赖未就绪时停在 PENDING,异常时进入 FAILED。— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/framework/index.zh.md
- inject
- inject 是 DSH plugin 声明必需服务的导出字段。声明了 inject 的插件会等待所有必需服务就绪后才执行 apply;服务消失时插件自动卸载,服务恢复后重新加载。可选依赖不写 inject,改在用点调用 ctx.get()。— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/framework/service.zh.md
- ctx.effect
- ctx.effect 是 DSH plugin 登记需要显式释放的资源的方式,传入的函数返回一个处置器,插件卸载时执行。顺序依赖的清理步骤必须合并进同一个 ctx.effect,因为不同处置器之间会并发执行。— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/framework/index.zh.md
- Service(服务)
- Service 是 DSH plugin 向其他插件暴露能力的基类,以类形态插件实现。构造时用 super(ctx, '<name>') 声明服务名,其他插件通过 inject 声明依赖后以 ctx.<name> 访问其公开方法。— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/framework/service.zh.md
来源
- DeepSeek Harness 官方文档 - 插件与生命周期(Fiber 状态机与自动清理)· deepseek-ai
- DeepSeek Harness 官方文档 - 你的第一个插件(三种插件形态)· deepseek-ai
- DeepSeek Harness 官方文档 - 服务与依赖(inject 与 Service)· deepseek-ai