插件开发
DeepSeek Harness 里一切皆插件。本教程带你走完整条链路——从最小本地插件,到配置与工具,再到发布 npm bundle——内容以官方文档为事实来源(2026-08-13 核实)。
什么是插件?
插件是一个导出 apply 函数的 TypeScript 模块。框架在加载插件时调用 apply,传入一个 context 对象(ctx),你通过它注册能力:
export const name = "my-plugin"
export function apply(ctx: Context) {
// Register capabilities here.
}
这就是完整的契约。name 在日志与覆盖层中标识插件;apply 是你注册事件监听、工具、服务、定时器或 UI 的地方。所有通过 ctx 注册的东西都是 effect:插件卸载时会被自动撤销,因此插件可以组合而不会泄漏状态。
插件的三种形态
上面的函数形式覆盖了大多数场景。插件也可以是对象或类:
name: "my-plugin",
inject: ["tools"],
apply(ctx) { /* ... */ },
}
export default class MyService extends Service {
static inject = ["tools"]
constructor(ctx: Context) {
super(ctx, "myService")
// 在构造函数里做同步初始化。
}
}
当你的插件需要向外提供服务、供其他插件消费时,用类形式;服务名(这里是 myService)就是其他插件在 inject 里声明的名字。
第一个插件(本地)
从一个已完成从源码运行的仓库检出开始。创建临时项目:
创建 scratch-plugin/src/my-plugin.ts:
export const name = "hello-plugin"
export function apply(ctx: Context) {
console.log("[hello-plugin] plugin loaded!")
}
用覆盖层注册
在仓库根目录运行 pwd,然后创建 scratch-plugin/cordis.yml。插件路径必须是绝对路径——patch 文件只贡献配置,不会改变 loader 解析模块路径时使用的 profile 目录:
- id: hello
name: "/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts"
带覆盖层启动 Web UI:
[hello-plugin] plugin loaded!
$
打开 http://127.0.0.1:3080——启动期间终端会打印这行。--patch 覆盖层是在其他所有层之后应用的配置层,见下文加载顺序。
Context:清理与依赖
自动清理
通过 ctx 注册的任何东西——事件监听、工具、定时器——都会在插件卸载时被自动清理。你永远不需要手动 removeListener 或 clearInterval。对于需要显式释放的资源(网络连接、文件监听),用 ctx.effect() 提供清理函数:
ctx.effect(() => {
const timer = setInterval(() => console.log("heartbeat"), 5000)
// 插件卸载时执行。
return () => clearInterval(timer)
})
}
这也是热替换(HMR)能工作的原因:配置变更会卸载旧实例并加载新实例,而注册都属于 effect,旧实例不会留下任何残留。
用 inject 声明依赖
如果你的插件消费某个服务——tools、llm 或另一个插件提供的服务——在 inject 里声明它。框架会等所有必需服务就绪后才加载你的插件:
export const inject = ["tools"]
export function apply(ctx: Context) {
// 此时 ctx.tools 已就绪。
ctx.tools.register(/* ... */)
}
配置
导出一个 Config 类型和同名的 Schemastery schema。默认值直接写在 schema 字段上;Cordis 在加载插件时校验用户配置并填充默认值:
import Schema from "@deepseek-ai/schemastery"
export const name = "my-plugin"
export interface Config {
greeting: string
maxRetries: number
verbose?: boolean
}
export const Config: Schema<Config> = Schema.object({
greeting: Schema.string().default("Hello"),
maxRetries: Schema.number().default(3),
verbose: Schema.boolean().default(false),
})
export function apply(ctx: Context, config: Config) {
console.log(config.greeting) // 用户值或 schema 默认值
}
用户在 cordis.yml 的插件行中传入配置:
- id: hello
name: "./src/my-plugin.ts"
config:
greeting: "Hi there"
maxRetries: 5
Config——它必须实现 Cordis 要求的 Standard Schema 接口。用 Schemastery 就能免费获得。严格校验
Schemastery 支持更丰富的约束——必填字段、联合类型、带边界的数值。配置不合法时插件加载会失败并给出明确错误:
apiKey: Schema.string().required(),
timeout: Schema.number().default(30000),
mode: Schema.union(["fast", "accurate"]).default("fast"),
})
设计原则
- 不硬编码可调参数——任何不同部署可能取值不同的东西都必须是配置字段。检验标准:能否在
cordis.yml中改变它而不改代码? - 错误要响亮——在 schema 中表达自足的约束,让无效配置在加载时失败,而不是等到首次使用时才报错。
- 默认值选用户会保留的——后应用的配置层会替换整行
config(不做深合并),所以默认值要尽量贴合多数用户。
构建工具
工具是模型可以调用的能力。用 @deepseek-ai/dsh-tools 的 defineTool 注册——DSL 会根据 parameters 推导并校验 args:
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" },
},
output: {
schema: { type: "string" },
render: (_args, value) => [{ type: "text", text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
}
重启 pnpm dsh web --patch ./scratch-plugin/cordis.yml,打开 http://127.0.0.1:3080,输入 “Use the greet tool to greet Ada.” 模型会调用 greet 并收到 Hello, Ada!。
execute() 契约
- 参数自动校验——类型、必填键、字面量、联合类型和嵌套值都会在
execute运行前对照parameters校验。 - 一个规范值——
execute只返回output.schema声明的值;output.render(args, value)把它转换成面向模型的内容。不要在函数体里返回内容块。 - 抛出 = isError——基础设施故障用抛错;成功的领域结果要用规范值表达(例如非零的进程退出码),哪怕渲染器需要解释它。
- 响应 exec.signal——它触发时要取消进行中的工作(这是调用方拥有的中止信号)。
- 异步通知——
exec.agent.inject({ content, source })会追加持久上下文,供下一次模型请求看到;它不会唤醒空闲的 agent。
长耗时任务:把 run_in_background 用配置门控,通过 ctx.jobs.start({ kind, label, owner: exec.agent, run }) 注册——成功时后台分支返回类型化句柄,如 { kind: "background", jobId }。完整契约见工具编写参考。
打包与发布
本地 --patch 流程只用于开发。分发建立在两个概念之上,都由 package.json 描述,但在 dsh 键下携带不同的 manifest:
| Bundle(组合包) | Profile | |
|---|---|---|
| 回答的问题 | “这个包贡献什么?” | “这套配置由哪些 bundle 按什么顺序组成?” |
| Manifest | dsh.bundle → 一个 patch 文件 | dsh.profile → 有序 bundles 列表 |
| 角色 | 你编写并分发的东西 | 用户用 dsh --profile <name> 启动的东西 |
bundle 是附带一个配置层的 npm 包。创建包:
├── package.json # 声明 dsh.bundle
├── cordis.patch.yml # profile 列出此 bundle 时应用的层
└── 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" } }
}
patch 里按包名(而不是源码路径)引用插件,这样 Node 的模块解析能找到已安装的代码:
- id: hello
name: dsh-hello-plugin
dsh.bundle 声明的包仍然可以安装,但只作为普通依赖:dsh plugin 会打印警告且不激活任何层。供插件 import 的库用这种格式,用户启用的插件不要用。安装进 profile
dsh plugin --profile <name> <args> 会在 profile 目录内转发给 pnpm,所以所有 pnpm 子命令都可用。首次使用会以 @deepseek-ai/dsh-base 作为第一个 bundle 初始化 profile:
$ dsh --profile demo --dump-config # 不启动,先验证层
$ dsh --profile demo
dsh plugin --profile demo remove dsh-hello-plugin 同时移除依赖和对应的层。要分发,把 bundle 发布到 npm——用户用 dsh plugin --profile web add dsh-hello-plugin 安装。
加载顺序
生效配置在空根之上按以下顺序逐层组合:
- profile 的
dsh.profile.bundles列表所列的各个 bundle patch,按列表顺序。 - profile 自己的
cordis.patch.yml。 - home 级
$DSH_HOME/cordis.patch.yml——各 profile 共享的机器本地偏好。 - 每个
--patch <path>覆盖层,按 argv 顺序。
后应用的层按行胜出,且 patch 会替换整行 config 而不是深合并各键。作为 bundle 作者,你可以按 id 覆盖前面各层的行——但必须重述该行需要的每一个键,而不只是改动的那个。
从 GitHub 安装
发布到 npm 不是必须的——用户可以直接从 git 主机安装:dsh plugin --profile demo add github:you/hello-plugin。但 git 安装获取的是源码而非构建产物:不会运行你的 build 脚本,所以 TypeScript 包到达时没有 lib/ 输出。两件事必须做:
- 作者:提供
prepare脚本,从源码自足地构建发布的入口(pnpm 会在 git 安装后运行它)。turtle-ui 是可用范例。 - 用户:放行构建——pnpm ≥ 10 在显式允许前拒绝运行 git 依赖的
prepare。把 pnpm 打印的包键复制到 profile 的pnpm-workspace.yaml,写成allowBuilds: { dsh-hello-plugin: true },然后重新add。
github:you/hello-plugin#<sha>),防止后续 push 静默改变执行内容。更稳妥的做法是发布 npm 或用 pnpm pack 的 tarball,完全绕开放行。真实示例与资源
研究注册表里的生产插件:modlens(dsh.bundle)、dsh-taskboard(dsh.client Web UI)。官方文档——第一个插件 · 配置 · 工具 · 打包与安装 · 工具编写参考 · Cordis 教程。准备好分享了?看提交插件。
问题或建议?官方 Discussions 是项目规范的支持渠道;Discord 里能找到活跃的社区成员。dsh.so 本身欢迎通过提交插件或反馈改进。