DSH plugin 开发规范:三种插件形态、命名导出、依赖声明、服务注册与生命周期清理自检清单

插件开发发布于 2026-09-12作者: DeepSeek Plugin 插件市场
DSH pluginDeepSeek Harness插件开发规范Cordis生命周期
DeepSeek Harness(DSH)插件开发规范:三种插件形态怎么选、name 与 inject 怎么写、哪些注册会被框架自动清理、类形态服务怎么定义。附 Fiber 状态机与发布前自检清单。

DSH plugin 开发规范的主线只有三条:所有能力通过 ctx 注册、所有必需依赖用 inject 声明、所有需要手动释放的资源交给 ctx.effect 回收。 这三条不是风格偏好,而是框架生命周期机制的前提——违反它们的写法通常不会立刻报错,而是在插件卸载、服务被替换或热更新时留下幽灵注册与资源泄漏。本文按官方文档把规范拆成可逐条核对的条目。无论你称它 DSH插件 还是 DeepSeek插件,规范条目完全一致。

DSH plugin 三种形态与命名规范:函数、对象、类怎么选

DSH plugin 支持函数、对象、类三种形态,多数场景用函数形态即可。 官方文档的原话是「Function form is sufficient in most cases」,只有在插件要对外提供服务时才用类形态(来源)。

函数形态使用具名导出,这也是最常用的一种:

ts
import type { Context } from '@deepseek-ai/cordis'

export const name = 'my-plugin'

export function apply(ctx: Context) {
  // 在这里注册能力。
}

对象形态使用默认导出,把 name、inject、apply 收进一个对象:

ts
export default {
  name: 'my-plugin',
  inject: ['tools'],
  apply(ctx: Context) {
    // ...
  },
}

类形态同样默认导出,继承 Service

ts
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 要唯一且可读,不要用 pluginmain 这类通用词,否则配置树里定位不到具体来源。
  • 服务名决定消费方的访问路径,一经发布再改名就是破坏性变更;发布前先确定再上。

DSH plugin 依赖声明规范:inject 与 ctx.get 的分工

必需依赖写进 inject,可选依赖用 ctx.get() 在调用点查询,不要用可选依赖绕过 inject 官方文档把两者的边界写得很清楚(来源):

ts
// 必需:服务缺失时插件不加载。
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依赖就绪,正在执行 applyapply 里不要做长阻塞
ACTIVE插件运行中正常态
FAILEDapply 抛出异常看启动日志里的异常栈
UNLOADING / DISPOSED正在卸载 / 已完全卸载确认清理逻辑已交回框架

依赖驱动的加载是规范的一部分:声明了 inject 的插件会等待所有必需服务就绪;如果依赖的服务消失(例如提供方被替换),插件会被自动卸载,待服务恢复后重新加载。所以插件的 apply 必须可重复执行,不要在 apply 里做只允许跑一次的全局副作用。

资源回收是同一套生命周期机制的另一半:通过 ctx 做的注册都会被框架自动追踪并在卸载时撤销,你不需要手写 removeListenerclearInterval 官方列出被自动清理的四类操作:

  • ctx.on(event, handler) — 事件监听
  • ctx.tools.register(tool) — 工具注册
  • ctx.llm.registerAdapter(names, adapter) — LLM 适配器注册
  • ctx.effect(() => cleanup) — 自定义资源
ts
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 声明它自己依赖的服务。 服务同样是插件,因此也默认导出(来源):

ts
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.ymlinsert 把自己的插件条目插进配置树。 最小结构是:

hello-plugin/
├── package.json       # 声明 dsh.bundle
├── cordis.patch.yml   # 该组合包贡献的配置层
└── index.js           # patch 行引用的插件模块
json
{
  "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 验证,两步:

  1. 装进 profile —— 执行 dsh plugin --profile demo add ./hello-plugin预期:包写进 profile 依赖,dsh.bundle 指向的 patch 被追加进 dsh.profile.bundles
  2. 核对配置层 —— 执行 dsh --profile demo --dump-config预期:打印的配置树里能看到你的插件条目,确认 patch 确实生效。

规范要求:发布前必须声明 files,把 cordis.patch.yml 一并打进产物——只发 index.js 会让安装方拿不到配置层,插件装了也不会被挂载。想看社区插件的真实目录与 bundle 声明,可以在 DSH Plugin Hub 里找同类插件对照。

DSH plugin 开发规范自检清单

发布前逐条核对:

  1. 形态与导出匹配:函数形态用 export const name + export function apply;对象 / 类形态用 export default,不混写。
  2. name 唯一可读,服务名发布前定死,避免通用词。
  3. 必需依赖全部写进 inject,可选依赖才用 ctx.get()
  4. apply 可重复执行,不放只允许跑一次的全局副作用。
  5. 需要手动释放的资源走 ctx.effect(),有顺序依赖的清理合并进同一个处置器。
  6. 对外提供服务时用类形态:继承 Service + super(ctx, '<name>') + static inject,并补声明合并的类型。
  7. 配置用 Config schema 校验,非法值在加载时响亮报错,不静默兜底(详见《DSH plugin 配置项怎么定义》)。
  8. bundle 声明完整dsh.bundle.patch + filescordis.patch.yml

写第一个插件的最小流程见《DSH 插件怎么开发》;装好后一直不激活,按《DSH plugin 装了不生效》对照 Fiber 状态定位;服务重复注册报错则见《DSH plugin 服务已被注册》。

来源:官方「插件与生命周期」官方「你的第一个插件」官方「服务与依赖」

常见问题

DSH plugin 开发规范里,插件必须遵守的硬性要求到底有哪几条?

DSH plugin 开发规范的主线只有三条:**所有能力通过 ctx 注册、所有必需依赖用 inject 声明、所有需要手动释放的资源交给 ctx.effect 回收**。三条都遵守时,插件卸载、服务被替换、热更新这三种场景才是干净的;违反它们通常不会立刻报错,而是在上述场景留下幽灵注册或泄漏(来源:官方「插件与生命周期」)。

DSH plugin 用 export default 还是具名导出,两种形态有什么区别?

DSH plugin 的形态与导出方式必须匹配:**函数形态用具名导出(export const name + export function apply),对象形态与类形态用 export default**。官方文档明确「Function form is sufficient in most cases」,只有插件要对外提供服务时才需要类形态。混写会出问题——不要既写 export default 又写具名 apply(来源:官方「你的第一个插件」)。

为什么我的 DSH plugin 一直停在 PENDING 状态不激活,和 inject 有关吗?

DSH plugin 停在 PENDING 说明它「已声明,但所需依赖未就绪」——这正是 inject 声明的服务还没出现。Fiber 状态机是 PENDING → LOADING → ACTIVE,声明了 inject 的插件会等待所有必需服务就绪后才执行 apply;如果依赖的服务消失(例如提供方被替换),插件会被自动卸载,待服务恢复后重新加载(来源:官方「插件与生命周期」)。

DSH plugin 卸载时资源没释放,规范上应该怎么写清理逻辑?

DSH plugin 的清理要交回框架:通过 ctx 做的注册会在插件卸载时自动撤销,包括 ctx.onctx.tools.registerctx.llm.registerAdapterctx.effect。需要特别注意的是**处置器按注册顺序逆序开始调用,但多个异步处置器会并发执行、不保证逐个完成**——所以存在顺序依赖的清理步骤必须放进同一个 ctx.effect() 返回的处置器里,由它负责串行等待(来源:官方「插件与生命周期」)。

DSH plugin 什么时候必须用 class 形态,服务名和 inject 该怎么写?

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

来源