跳过主要内容
全部文章
开发

从零构建一个 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 一批新行,字段包括 idname(模块说明符或指向入口文件的路径)和可选的 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-toolsdefineTool 注册一个工具,并通过 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 里每一个字段的完整拆解——parametersoutput.schemaoutput.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-plugindsh 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.jsonnpm 元信息 + 标记这个包是 bundle 的 dsh.bundle.patch 字段
cordis.patch.yml这个 bundle 往插件树里插入了什么——idnameconfig
index.js(或 lib/index.js插件模块本体——导出 apply,可选导出 nameinjectConfig
README.mddsh 不会读它,但任何浏览你仓库或 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(开发与运行时)分类。