DSH plugin 开发指南:插件形态怎么选、能力注册到哪里、本地怎么调试,DSH 插件开发完整路线图
DSH plugin 开发指南解决的是「选路线」问题:先用三种插件形态里的哪一种、把能力注册到 tools 还是 service 还是事件、本地用 --patch 覆盖层还是装进 profile 调试;把这三条线定下来,再照教程动手就不会反复返工。 DSH插件 与 DeepSeek插件 都走同一套插件框架,形态与能力落点的判断标准完全一致。
DSH plugin 开发全景:三条线一次说清
一个 DSH plugin 从想法到跑通,只需要依次回答三个问题。 这三个问题对应官方文档里的三块内容(来源):
- 形态——这个插件是纯注册能力的函数,还是要带依赖和配置的对象,还是要对外提供服务的类?
- 能力落点——这个能力是给模型用(tool)、给别的插件用(service),还是只在某个时机插入(事件)?
- 调试方式——改源码期间用
--patch覆盖层热加载,还是打包后装进 profile 验证?
下面三节按这个顺序展开,每节给判断标准而不是罗列 API。
DSH plugin 形态怎么选:函数、对象、类
函数形态覆盖绝大多数插件,只有「对外提供服务」才需要类形态。 官方原文是 「Function form is sufficient in most cases」,并明确指向服务场景才用类形态(来源)。
| 形态 | 导出方式 | 该选它的信号 |
|---|---|---|
| 函数 | 具名导出 name + apply | 只注册能力,不需要自己对外提供接口 |
| 对象 | export default { name, inject, apply } | 想把 name / inject / apply 收拢成一个默认导出对象 |
| 类 | export default class ... extends Service | 要对外提供服务,其他插件通过 inject 消费你 |
类形态的最小写法如下,构造函数里做同步初始化,服务名(这里是 myService)就是其他插件 inject 时用的名字:
import { Service, type Context } from '@deepseek-ai/cordis'
export default class MyService extends Service {
static inject = ['tools']
constructor(ctx: Context) {
super(ctx, 'myService')
// 在这里做同步初始化。
}
}
注意具名导出与默认导出不能混写:函数形态用具名导出,对象形态和类形态用默认导出——这是加载器识别插件形态的依据,具体自检项见 开发规范。
DSH plugin 能力注册到哪里:tools、service、事件
能力落点由「谁来调用」决定,而不是由「你想写什么」决定。 DSH plugin 的能力有三类消费方,对应三种注册方式(来源):
- 模型调用 → 注册 tool:用
ctx.tools.register暴露一个模型可调用的工具,先inject: ['tools']。 - 其他插件调用 → 注册 service:用类形态把自己注册成服务,消费方通过
inject拿到实例。 - 框架时机 → 注册事件监听:用
ctx.on在插件加载、卸载等时机插入逻辑。
事件本身还有五种触发模式,选错模式会导致逻辑不执行或顺序错乱:emit 是广播、bail 短路返回第一个结果、serial 有序执行、waterfall 是管道且必须调用 next() 才会继续往下传。写事件监听前先确认该事件属于哪种模式。
对外暴露工具时用 defineTool 声明参数与输出,让模型知道怎么调用、让框架知道怎么渲染结果:
import { defineTool } from '@deepseek-ai/dsh-tools'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'my_cap',
description: 'Execute my capability.',
parameters: {
input: { type: 'string', required: true },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return args.input.toUpperCase()
},
}))
}
DSH plugin 需要可替换提供方时:三角色设计
只有当能力需要「换一个实现而不动调用方」时才拆包,否则不要提前拆。 官方把这类能力拆成三个角色,并用 Bash 执行能力举例:dsh-shell(Service Definition)定义契约、dsh-bash-local(Service Provider)实现本地执行、dsh-tool-bash(Consumer)暴露成模型可调用的工具(来源)。
三条依赖关系是设计的核心:
- Service Provider 依赖 Service Definition;
- Consumer 依赖 Service Definition;
- Provider 与 Consumer 互不依赖。
于是换提供方只需改 cordis.yml 里的一行,Definition 和 tool 都不用动:
# 本地执行
- name: '@deepseek-ai/dsh-bash-local'
# 换一行同样提供该服务的包即可替换实现。
官方给的三条设计要点值得抄进评审清单:不要预防性拆包(简单工具插件不拆);Request/Result 类型归 Service Definition 所有;显式优于隐式——把默认值放在明确的 resolve(request) 步骤里,而不是藏在 run() 内部的 ?? default。
DSH plugin 调试方式:--patch 覆盖层与安装验证
改源码期间用 --patch 覆盖层,验证分发产物才装进 profile——两者验证的目标不同。 官方教程用覆盖层把本地插件挂进 Web UI,三步完成(来源):
- 拿绝对路径 — 在插件仓库根执行
pwd。预期:得到仓库根绝对路径,下一步的name字段要用它。 - 写覆盖层配置 — 新建
cordis.yml,用insert把本地插件插进配置树:
- insert:
- id: hello
name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
- 带 patch 启动 — 执行下面这条命令。预期:Web UI 启动后插件已挂载,之后改源码重启即生效:
pnpm dsh web --patch ./scratch-plugin/cordis.yml
两条硬规则:插件路径必须写绝对路径;patch 文件只贡献配置、不改变 loader 解析模块路径的 profile 目录——所以「patch 里写了却没加载」通常不是语法问题,而是路径解析问题。
要验证打包产物,就换成 profile 安装:dsh plugin --profile <name> add <包>,装完先 dsh --profile demo --dump-config 核对配置层再启动。这两种方式的取舍与更多调试手法见 本地调试。
从 DSH plugin 开发指南到发布
指南定完路线,接下来按「教程 → 规范 → 发布」推进。 建议顺序:
- 照 开发教程 六步跑通第一个可安装插件;
- 用 开发规范 的自检清单过一遍导出、依赖、清理、配置;
- 打包与分发看 打包成 bundle 与 发布到插件中心;
- 插件装好后在 DSH Plugin Hub 里核对是否出现在已安装列表,并确认配置项展示正常。
如果插件加载后行为不对(没生效、服务报重复注册),先查 插件没激活 与 服务重复注册 两篇排查文,多数问题出在形态选错或 inject 写漏。
常见问题
**先决定形态再写代码——DSH plugin 开发指南的判断顺序是:只注册能力 → 函数形态;需要带 inject、Config 一起配置 → 对象形态;要对外提供可被其他插件消费的服务 → 类形态。** 官方明确「Function form is sufficient in most cases」,绝大多数插件用函数形态就够,只有提供服务的插件才需要类形态(来源:官方「你的第一个插件」)。
DSH plugin 开发时,能力落点按「谁调用它」区分:给模型用的能力注册成 **tool**(ctx.tools.register);给其他插件用的能力注册成 **service**;只想在某个时机插入逻辑就注册**事件监听**。同一个插件可以同时注册多种:先声明 inject,再在 apply 里分别注册。判断标准是调用方是模型、是插件、还是框架的生命周期(来源:官方「三角色能力设计」)。
DSH plugin 只是在能力需要**可替换提供方**时才拆成三个包。三角色是 Service Definition(定义契约与 Request/Result 类型)、Service Provider(实现)、Consumer(暴露给模型)。官方设计要点第一条就是 **Do not split preemptively**:一个简单工具插件不需要拆包,拆包的代价只在「提供方要能独立演进或替换」时才划算(来源:官方「三角色能力设计」)。
DSH plugin 本地调试按「验证什么」选:验证**插件能不能被加载**用 --patch 覆盖层,改完源码重启即生效;验证**打包产物能不能装**才用 dsh plugin --profile <name> add。--patch 只贡献配置、不改变 loader 解析模块路径的 profile 目录,所以本地调试的插件路径必须写绝对路径(来源:官方「你的第一个插件」)。
相关术语
- Service Definition / Provider / Consumer
- DSH plugin 把「需要可替换提供方」的能力拆成三个角色:Service Definition 定义服务与 Request/Result 类型,Service Provider 实现具体行为,Consumer 把它暴露成模型可调用的 tool。三角色合起来才是完整的接缝,单独任一角色都不是。— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/practice/index.md
- apply
- apply 是 DSH plugin 的入口函数,框架加载插件时调用它并传入 ctx 上下文对象,插件通过 ctx 注册工具、服务、事件等能力。— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/index.md
- inject
- inject 是 DSH plugin 声明的依赖服务列表(如 ['tools', 'llm']),框架会等这些服务全部就绪后才加载插件,因此 apply 里可以直接使用 ctx.tools 等。— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/framework/service.md
- --patch 覆盖层
- --patch 是 DSH plugin 的本地调试开关,让 dsh 在启动时把一份额外的 cordis.yml 叠加到现有 profile 上,用于插入尚未发布的本地插件;它只贡献配置,不改变 loader 解析模块路径所用的 profile 目录。— https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/user/develop/basic/index.md
来源
- DeepSeek Harness 官方文档 - 你的第一个插件(插件形态)· deepseek-ai
- DeepSeek Harness 官方文档 - 服务与依赖(service 与 inject)· deepseek-ai
- DeepSeek Harness 官方文档 - 三角色能力设计· deepseek-ai
- DeepSeek Harness 官方文档 - 事件(五种触发模式)· deepseek-ai