cordis.patch.yml 详解:DeepSeek-Harness 的 bundle 如何插入和覆盖配置
DeepSeek-Harness 插件作者如何编写 cordis.patch.yml、insert、id、name、config 到底声明了什么,以及 bundle 和 profile 的 package.json 字段有何不同。
cordis.patch.yml 是 DeepSeek-Harness(dsh)的 bundle 用来声明"它往正在运行的 Cordis 插件树里贡献了什么"的 YAML 文件——一份条目列表,每一项要么 insert 新行,要么按 id 覆盖某一行已有的 config。本文从插件作者的角度来看这个文件:当你是打包一个 bundle 的那个人时,你会写什么,而不是操作者覆盖某个 bundle 时会写什么。
两种操作
cordis.patch.yml 里的每一项都是下面两种形态之一:
# insert one or more new plugin rows
- insert:
- id: my-tool
name: './index.js'
config:
greeting: hello
# override an existing row's config, by id
- id: my-tool
config:
greeting: goodbye
insert 是 bundle 添加此前不存在的行的方式——这正是别人安装你的插件时,你自己插件的 cordis.patch.yml 会做的事。裸露的 id + config 形式,是后面某一层按同一个 id 覆盖一行已经存在的配置的方式。作为编写自己 patch 文件的 bundle 作者,你几乎总是在用 insert;按 id 覆盖是操作者(或者你自己 profile 的 patch)之后才会用到的。
insert 行里的三个字段
id——连接键。它是 profile 自己的 patch、机器级 patch,或者--patchflag 之后用来定位并覆盖这一行的方式。取一个稳定且专属于你插件的名字(比如dsh-hello-plugin,而不是plugin),因为和另一个 bundle 的id撞了,谁的层加载得晚谁就直接赢。name——实际会被加载的东西:一个模块说明符(发布后是 npm 包名)或者指向你入口文件的路径(本地开发时用绝对路径)。这个字段最终解析到导出你apply函数的那个模块。config(可选)——传入你插件Configschema(如果有的话)的初始值。这个值如何流入apply(ctx, config),见《用 Schemastery 让 DeepSeek-Harness 插件可配置》。
这个文件是从哪里被引用的
cordis.patch.yml 自己什么都做不了——它必须被你包的 package.json 里的 dsh.bundle.patch 字段指向:
{
"name": "dsh-hello-plugin",
"dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
}
这个字段就是把一个 npm 包标记为可安装 dsh bundle 的全部内容。没有它的包仍然能作为普通依赖正常安装,但 dsh 会打印警告,不会激活里面的任何东西——这对于在一个真插件旁边发布一个辅助库很有用,不会被 dsh 误认成插件本身。
bundle 的 package.json 对比 profile 的 package.json:两个不同的 dsh 字段
这是最容易让第一次写插件的人踩坑的地方,因为两者都是带 dsh 键的 package.json 文件,但它们回答的是相反的问题:
dsh.bundle.patch | dsh.profile.bundles | |
|---|---|---|
| 存在于 | 你编写的插件包里 | $DSH_HOME/profiles/<name>/package.json |
| 回答的问题 | "这个包贡献了什么?" | "这个 profile 由哪些 bundle 按什么顺序组成?" |
| 值 | 指向一份 cordis.patch.yml 的路径 | 一份有序的 bundle 包名数组,@deepseek-ai/dsh-base 排第一 |
| 谁来写 | 你,在打包插件时写一次 | dsh 自己,在 bundle 增删时自动维护 |
第一个是你写并发布的。第二个你永远不需要手动编辑——执行 dsh plugin --profile <name> add <package> 会通过 pnpm 安装你的包,因为 dsh 看到了你的 dsh.bundle.patch 字段,就会自动把你包的名字追加进那个 profile 的 dsh.profile.bundles 列表。如果你是从 profile 操作者而不是插件作者的角度来看这件事,《Profiles and Bundles in DeepSeek-Harness, Explained》完整讲了那一半。
多个 bundle 的 patch 是如何真正组合起来的
一旦一个 profile 装了好几个 bundle,dsh 会按 dsh.profile.bundles 里这些 bundle 出现的顺序——@deepseek-ai/dsh-base 第一个,然后按安装顺序排后面装的——依次应用每个 bundle 的 cordis.patch.yml。在这一叠之上是 profile 自己的 cordis.patch.yml,再往上是 $DSH_HOME/cordis.patch.yml(机器级,所有 profile 共享),最后是命令行上的 --patch flag。《DeepSeek Harness 配置指南》讲了完整的四层堆叠、支配其中每一次覆盖的"整体替换而非深度合并"规则,以及如何用 --dump-config 检查组合后的结果——本文聚焦的是作为 bundle 作者你真正要写的那一层。
一个真实例子:官方 MCP client bundle 的写法
官方的 @deepseek-ai/dsh-mcp-client 插件正是用这种 insert 形态配置的,也是一个非 trivial config 块的好例子:
- id: mcp-github
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: github
transport: stdio
command: npx
args: ['-y', '@modelcontextprotocol/server-github']
env:
GITHUB_TOKEN: !!js process.env.GITHUB_TOKEN
注意这里的 name 是一个 npm 包说明符,不是本地路径——这就是一个发布出来供别人安装的 bundle,它的 insert 条目该长的样子,对比本地开发时用的绝对路径形式。
内置 bundle 都在哪里
dsh 自带三个内置 bundle,遵循的正是同一套 dsh.bundle.patch + cordis.patch.yml 写法,它们是官方仓库里真实存在的文件,如果你想看这个模式在生产规模下(而不是玩具示例里)是什么样子,值得去读一读:packages/bundle/base(每个 profile 起步都会用到的 bundle)、packages/bundle/web-app(dsh web 在其上叠加的部分),以及 dsh --profile headless 用的 headless bundle。每一个都有自己的 package.json 声明 dsh.bundle.patch,旁边配一份 cordis.patch.yml——在 deepseek-ai/deepseek-harness 仓库里分别位于 packages/bundle/base/cordis.patch.yml 和 packages/bundle/web-app/cordis.patch.yml。
FAQ
我自己 bundle 的 patch 文件里,insert 和按 id 覆盖两种都需要吗?
通常只需要 insert——你是在添加此前不存在的行。按 id 覆盖是你 bundle 的使用者(profile 自己的 patch,或者机器级 patch)之后用来调整你插入的那一行的。
name 能直接指向一个 TypeScript 文件吗?
官方示例里既展示了本地开发用的相对/绝对路径形式,也展示了发布出去的 bundle 用的包名形式;一个原始的 .ts 文件能不能不经构建步骤就被解析,还是需要先编译成 .js,取决于你的 prepare 脚本和这个包是怎么被安装的——从源码安装时构建步骤的影响,见《从 GitHub 安装 DeepSeek-Harness 插件》。
如果我忘了写 dsh.bundle.patch 字段会怎样?
你的包仍然会作为普通 npm 依赖正常安装,但 dsh 会把它当成一个纯库,而不是插件——它会打印警告,不会加载任何 cordis.patch.yml,哪怕包里确实有这个文件。
每个 insert 行都必须要有 config 吗?
不需要——它是可选的。一个没有 Config schema、或者对每个字段的默认值都满意的插件,完全可以在它的 insert 条目里省略 config。
一个 bundle 的 cordis.patch.yml 能 insert 不止一行吗?
可以——insert 接受一个列表,单个 bundle 可以在一份文件里注册好几个插件行(比如一个既提供工具、又提供它所依赖的服务的 bundle)。列表里的每一行仍然需要各自唯一的 id。
我选的 id 需要和我包的 npm 名字一致吗?
不需要——id 和 npm 包名是相互独立的。id 只需要在某个 profile 组合出的插件树内保持唯一;取一个能明显对应到你包名的名字,只是方便操作者之后查找、定位它来做覆盖。
下一步
如果你在写第一个 bundle,从《从零构建一个 DeepSeek-Harness 插件》开始,用《用 Schemastery 让插件可配置》加上可配置选项,想看上面这种 MCP 桥接 bundle 如何融入更大的图景,读《如何在 DeepSeek-Harness 中使用 MCP Server》。想看分层和调试的操作者视角,读《DeepSeek Harness 配置指南》。像上面这样的真实 MCP client bundle,可以在 FindHarness 的 MCP & Connectors(MCP 与连接器)分类里找到,更广泛的、由其他插件作者构建的 bundle 则在 Development & Runtime(开发与运行时)分类里。