从零构建一个 DeepSeek-Harness 插件:分步教程
一份动手实操的分步教程,带你构建一个真实的 DeepSeek-Harness 插件——package.json、cordis.patch.yml、apply(ctx)、注册一个工具、本地 --patch 加载、HMR,以及把它装进 profile。
构建一个 DeepSeek-Harness(dsh)插件,本质上是写一个小型 npm 包:一份带 dsh.bundle.patch 字段的 package.json、一份告诉 dsh 要加载什么的 cordis.patch.yml,以及一个导出 apply(ctx) 的 JS/TS 模块。本教程会从一个空文件夹开始,一步步构建出一个可以真正装进某个 profile 的可用工具——不需要脚手架 CLI,因为 dsh 本身也没有提供这种东西。
开始之前你需要什么
你需要一个能正常运行的 dsh 安装——如果还没装好,参考《DeepSeek Harness 快速上手》或《分平台安装指南》——外加 PATH 上可用的 Node.js 和 pnpm,因为 dsh plugin 会把命令直接转发给 pnpm。如果你想先弄清楚"插件到底是什么"这套理论再动手写代码,建议先读《DeepSeek-Harness 插件底层是如何工作的》;本文默认你已经了解这套契约,重点放在具体步骤上。
第一步:创建文件夹
mkdir hello-plugin && cd hello-plugin
npm init -y
这个文件夹既是你的工作目录,最终也会是你的 npm 包根目录。
第二步:写 package.json
让一个普通 npm 包变成 dsh 能识别的"可安装插件",唯一需要的字段就是 dsh.bundle.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" }
}
}
有几个字段值得单独说一下:
"type": "module"—— 按 ESM 写,和整个文档里的官方示例保持一致。"files"—— 决定实际发布到 npm 上的内容白名单;漏掉cordis.patch.yml是一个很常见的坑,会悄无声息地导致从 registry 安装失败。"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }—— dsh 真正会读取、用来判断"这个包是一个 bundle"的唯一字段。没有它,dsh plugin add仍然会把这个包当普通依赖装上,但会打印警告,且不会激活任何配置。
第三步:写 cordis.patch.yml
这是 dsh.bundle.patch 字段指向的那个文件。它是一个 YAML 数组;每一项要么往正在运行的 Cordis 插件树里 insert 一批新行,字段包括 id、name(模块说明符或指向入口文件的路径)和可选的 config:
- insert:
- id: hello
name: './index.js'
id 是后续层(profile 自己的 patch、机器级 patch、--patch flag)用来定位并覆盖这一行的键——如果你想搞清楚 insert 和按 id 覆盖在各层之间如何交互的完整机制,参见《cordis.patch.yml 详解》。
第四步:写入口文件
index.js 导出插件的 apply(ctx) 函数——这是契约里唯一必需的部分:
export const name = 'hello-plugin'
export function apply(ctx) {
console.log('hello-plugin loaded')
}
到这一步,它已经是一个合法的、可加载的插件了。虽然目前还没做任何有用的事,但它满足了 dsh 真正会检查的契约:一个导出了 apply 的模块。
第五步:注册一个工具
要让插件真正做点 agent 能调用的事情,用来自 @deepseek-ai/dsh-tools 的 defineTool 注册一个工具,并通过 inject 声明你依赖 tools 服务:
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'hello-plugin'
export const inject = ['tools']
export function apply(ctx) {
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}!`
},
}))
}
inject: ['tools'] 不只是语义上的说明,它有实打实的机制作用:dsh 在 tools 服务存在之前不会调用 apply,如果这个服务后来消失了,也会自动卸载这个插件。想了解 defineTool 里每一个字段的完整拆解——parameters、output.schema 与 output.render 的区别、execute 在其中的位置——参见《如何用 defineTool 给 DeepSeek-Harness 添加自定义工具》。
第六步:本地加载,不用安装任何东西
不需要发布或者 dsh plugin add 一个插件才能试用它——用 --patch flag 加一条用绝对路径写的 insert 项,让某个正在运行的 profile 直接指向这个文件:
# scratch.cordis.yml
- insert:
- id: hello
name: '/absolute/path/to/hello-plugin/index.js'
dsh --profile web --patch ./scratch.cordis.yml
如果想在不启动完整会话的情况下确认 dsh 真的加载到了它,用 --dump-config——它会打印完整组合后的配置树然后退出:
dsh --profile web --patch ./scratch.cordis.yml --dump-config
第七步:用热重载迭代
dsh 运行时编辑 index.js 不会自动生效——这需要加载的插件树里有 @deepseek-ai/cordis-plugin-hmr。开启 HMR 后,保存文件会触发 dsh 卸载旧实例、加载新代码,不需要手动重启。这和《插件底层原理》与《服务与生命周期指南》里描述的插件卸载机制是同一套底层机制——HMR 只是把这套"卸载再重新加载"的循环,从"进程关闭"这个触发条件换成了"文件变更"。
第八步:装进一个真实的 profile
对插件满意之后,用任何用户会用的方式把它装上,从本地文件夹安装:
dsh plugin --profile web add ./hello-plugin
记住路径是相对于你执行命令时所在的目录解析的,而不是相对于 profile 目录。因为 dsh plugin 只是把参数转发给 pnpm,这条命令本质上就是在 profile 目录里执行 pnpm add ./hello-plugin——等你准备好把它分发成一个 GitHub 仓库或者一个 tarball 时,dsh plugin --profile web add github:you/hello-plugin 和 dsh plugin --profile web add ./hello-plugin-0.1.0.tgz 的工作方式是一样的。所有支持的安装源的完整拆解见《如何安装 DeepSeek-Harness 插件》,GitHub 源插件第一次需要构建步骤时会发生什么,见《从 GitHub 安装 DeepSeek-Harness 插件》。
第九步:发布它
dsh.bundle.patch 是唯一让你的包变成可安装 bundle 的字段——除此之外发布就是一次完全普通的 npm publish / pnpm publish,只要 files 里包含了编译后的入口文件和 cordis.patch.yml。完整的发布清单——发布前要构建什么、让你被发现的 GitHub topic、以及 package.json 里的 dsh 字段如何让 FindHarness 这类目录站能验证你是一个真插件而不是一个只是提到 dsh 的项目——见《如何发布一个 DeepSeek-Harness 插件》。
每个文件到底是干什么用的
| 文件 | 作用 |
|---|---|
package.json | npm 元信息 + 标记这个包是 bundle 的 dsh.bundle.patch 字段 |
cordis.patch.yml | 这个 bundle 往插件树里插入了什么——id、name、config |
index.js(或 lib/index.js) | 插件模块本体——导出 apply,可选导出 name、inject、Config |
README.md | dsh 不会读它,但任何浏览你仓库或 npm 页面的人会期待它存在 |
FAQ
我需要一个脚手架工具来起一个新插件吗?
不需要——截至 2026 年 8 月,dsh 并没有提供官方的 create-dsh-plugin 这类 CLI。上面的目录结构就是文档自己的示例所使用的结构,小到完全可以手写。
不装进某个 profile 也能测试插件吗?
可以——用 --patch 配合一条指向绝对路径的 insert 项(第六步)就能加载插件,完全不需要跑 dsh plugin add。这是标准的本地开发循环;只有当你真的要让某个 profile 自己的 package.json 依赖它时,才需要用真正的 add。
我的插件必须注册一个工具才算合法吗?
不需要。一个只导出 apply 且对 ctx 什么都不做的模块,仍然是一个合法、可加载的插件——只是没什么用。工具、命令、钩子、MCP 桥接,全都是你在 apply 内部可选注册的东西,而不是独立的插件类型。
如果我的插件需要依赖另一个插件的服务怎么办?
在 inject 里声明,比如 inject: ['tools', 'llm']。dsh 会推迟调用 apply,直到每一个注入的服务都存在;如果之后某个服务消失了,插件会被自动卸载——这个状态机的具体运作方式见《服务、依赖注入与插件生命周期》。
下一步
在《如何用 defineTool 给 DeepSeek-Harness 添加自定义工具》中深入了解你刚注册的这个工具,用《用 Schemastery 让插件可配置》让它变得可配置,等它准备好给别人用时,照着《如何发布一个 DeepSeek-Harness 插件》走一遍流程。想看看一个真实、已上线的工具插件长什么样,可以浏览 Tools & Capabilities(工具与能力)分类下的 modlens,或者 FindHarness 上更完整的 Development & Runtime(开发与运行时)分类。