写一个自己的插件
从使用者变成贡献者:一个插件就是一个导出 apply(ctx) 的模块。写最小插件、注册模型可调用的工具、打包成 bundle 发布到 npm 或 GitHub。
约 10 分钟读完
读完这篇你会
- 理解 DSH 插件的真实形态(一个导出
apply(ctx)的模块) - 写出一个最小插件,并加载进 Web UI 验证
- 给它加上一个模型可以调用的工具
- 把它打包成可安装的插件包,发布给别人用
这一篇面向想给生态贡献插件的开发者,需要你能看懂基础的 TypeScript。普通用户可以跳过,不影响其他教程。
插件其实长这样
先破除神秘感:DSH 的"一切皆插件"不是修辞——它的每个能力(工具注册表、模型适配、界面、甚至主循环)都是一个插件,而你写的插件和官方插件地位完全相同。
一个插件就是一个模块,导出三样东西:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'my-plugin' // 插件名
export const inject = ['tools'] // 声明依赖的服务
export function apply(ctx: Context) {
// 在这里注册能力
}
name:插件身份,诊断信息里显示的名字;inject:依赖声明——框架会等这些服务就绪后才执行apply,加载顺序由依赖决定而不是文件顺序;apply(ctx):唯一入口。ctx上挂着所有服务(ctx.tools、ctx.llm……),你在里面注册的一切都会在插件卸载时自动清理,不需要手动 removeListener。
官方的开发者教程在仓库 docs/user/develop/basic/ 目录,这一篇就是它的浓缩版加中文注释。
第一步:跑起一个最小插件
最省事的开发方式是从官方仓库源码跑(能用内置的 pnpm dsh 命令):
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install && pnpm run build
在仓库根目录建一个实验目录和一个插件文件:
scratch-plugin/
└── src/
└── my-plugin.ts
my-plugin.ts 写:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
console.log('[hello-plugin] plugin loaded!')
}
再建 scratch-plugin/cordis.yml(注意插件路径必须是绝对路径,把 /absolute/path/to/ 换成你机器上仓库的真实路径):
- insert:
- id: hello
name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
带覆盖层启动:
pnpm dsh web --patch ./scratch-plugin/cordis.yml
打开 http://127.0.0.1:3080,启动日志里出现 [hello-plugin] plugin loaded!,你的第一个插件就跑起来了。
第二步:注册一个模型能调用的工具
把 my-plugin.ts 换成带工具注册的版本:
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'greet-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'greet',
description: 'Greet someone by name.',
parameters: {
name: { type: 'string', required: true, description: 'The name to greet' },
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
}
重启(还是同一条 --patch 命令),然后在对话里说:"用 greet 工具向 Ada 问好"。模型会调用 greet,拿到 Hello, Ada! 作为工具结果。
三个要点:
name是模型点名的依据,起名要表意;description决定模型会不会想到用这个工具,写清楚触发场景;parameters声明输入 schema,框架据此校验参数。
第三步(可选):接受用户配置
插件可以导出一个 Config schema,让用户在 cordis.yml 里改行为:
import Schema from '@deepseek-ai/schemastery'
export interface Config {
greeting: string
}
export const Config: Schema<Config> = Schema.object({
greeting: Schema.string().default('Hello'),
})
export function apply(ctx: Context, config: Config) {
console.log(config.greeting) // 用户值或 schema 默认值
}
设计原则:凡是不同部署可能想改的值,都应该是配置字段,不要硬编码。
第四步:打包发布
本地实验用 --patch 就够了;要分发给别人,需要打成一个 bundle(可安装的 npm 包)。目录结构:
hello-plugin/
├── package.json # 声明 dsh.bundle
├── cordis.patch.yml # 这个包贡献的配置层
└── index.js # 插件入口
package.json 的关键是 dsh 字段:
{
"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" } }
}
cordis.patch.yml 里插件行引用包名而不是路径:
- insert:
- id: hello
name: dsh-hello-plugin
本地装进 profile 验证:
dsh plugin --profile web add ./hello-plugin
dsh --profile web --dump-config # 能看到 "# == dsh-hello-plugin" 层说明挂上了
对外发布的三种方式:
| 方式 | 命令 | 特点 |
|---|---|---|
| npm | dsh plugin --profile web add your-package | 发布时预构建 lib/,用户安装无构建、无授权提示,体验最好 |
| GitHub | dsh plugin --profile web add github:you/hello-plugin | 拉源码不拉产物,用户要过 allowBuilds 授权;作者必须提供自洽的 prepare 脚本 |
| tarball | dsh plugin --profile web add ./hello-plugin-0.1.0.tgz | pnpm pack 产物,同样免构建授权,适合内网分发 |
推荐优先发 npm。走 GitHub 分发时,务必在 README 里写清楚你针对的 dsh 版本——DSH 还在 developer preview 阶段,API 会有破坏性变更。
开发纪律四条
来自官方文档与事故复盘,违反了不报错但行为诡异:
- 只用具名导出:不要
export default。Loader 会把默认导出折叠成插件本体,inject等元数据被静默丢弃,依赖声明失效; - 禁止模块级副作用:模块只会被 import 一次,但插件可能挂载/卸载多次。
setInterval这类东西必须写在apply里、用ctx.effect()包裹,否则卸载后永远清不掉; - patch 是浅覆盖不是深合并:覆盖某行
config时要写全所有要保留的字段,只写一个等于把其他字段全删了; - 加新行必须用
insert:想加行却用覆盖语法会报entry not found。
排错第一反应
任何"改了配置没生效",先 dump 最终配置树,不要猜:
dsh --profile web --dump-config
它会打印所有层叠加后的结果(bundle 层 → profile 层 → home 层 → --patch 层),你的改动在哪一层被吞了一眼可见。
下一步
写好的插件想让更多人发现?发到 GitHub 时加上 dsh-plugin 话题标签,方便社区检索——本站的插件目录正是从这里收录社区插件的。