
字节笔记本
2026年10月6日 · 约 9 分钟读完
dsh 插件分发:打包 bundle,装进 profile
DeepSeek 开源的 agent 框架 deepseek-harness 把理念写在了仓库简介里:Everything is a Plugin。这个 GitHub 星标超过 24 万的项目建立在 Cordis 插件框架之上,dsh 命令行负责装载一切。本地调试阶段,一份 --patch 覆盖层就能把插件临时挂进配置,但这条路走不出自己的机器:要让别人也用上你的插件,得回答分发问题,包怎么组织、装到哪里、与既有配置如何叠加。官方打包安装教程给出了一套完整方案,本文整理其中的关键机制。

两个概念,两份清单
安装机制建立在两个概念上,都用 package.json 描述,但在 dsh 键下携带的清单不同,回答的问题也不同。
bundle(组合包)是附带一个配置层的 npm 包,清单声明 dsh.bundle,回答的是这个包贡献什么:一份 patch 文件,负责插入或覆盖插件行。profile 则是 $DSH_HOME/profiles/ 下的一个目录,描述一份可启动的组合,清单声明 dsh.profile,回答的是这套配置由哪些 bundle 按什么顺序组成。一句话概括:组合包是作者编写并分发的东西,profile 是用户用 dsh --profile 启动的东西,没有东西同时是两者。
没有 dsh.bundle 声明的包同样可以安装,但只会被当作普通依赖,dsh plugin 会打印警告且不激活任何层。纯供其他插件 import 的库,就应该用这种普通包形态。
profile 目录里有两份文件:package.json 管理树外插件依赖,并在 dsh.profile 清单里记录有序的 bundles 列表;cordis.patch.yml 是用户自己的配置层。这两份清单都不需要手写,dsh plugin 负责创建和维护。
五步装进 profile
以最小插件为例。先建目录 hello-plugin,写三个文件。package.json 声明 bundle 清单:
{
"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" } }
}index.js 是插件入口,导出 name 与 apply 函数:
export const name = 'hello-plugin'
export function apply() {
console.log('[hello-plugin] plugin loaded!')
}cordis.patch.yml 定义这个包贡献的配置层:
- insert:
- id: hello
name: dsh-hello-plugin注意这里的插件行写的是包名而不是相对源码路径,这样 Node 的模块解析才能找到安装后的代码,这是与本地 --patch 覆盖层写法的关键差别。
接着在包含 hello-plugin 的目录里执行安装:
dsh plugin --profile demo add ./hello-plugindsh plugin --profile 的原理是在 profile 目录内把参数转发给 pnpm,所以 pnpm 的全部子命令都可用。首次执行会初始化 profile,@deepseek-ai/dsh-base 成为第一个 bundle;随后 pnpm 把该目录链接为依赖,dsh 检测到包声明了 dsh.bundle,把它追加进 dsh.profile.bundles 列表。装完先用 dsh --profile demo --dump-config 验证,输出里会出现以 # == dsh-hello-plugin 标记的层,确认无误再正式启动。remove 子命令则会同时移除依赖与对应的层。
四层叠加,后层按行胜出
生效配置在一个空根之上按固定顺序逐层叠加:

- profile 的 bundles 列表所列的各 bundle patch,按列表顺序应用,dsh-base 最先,已安装的组合包按加入顺序逐个跟进;
- profile 自己的 cordis.patch.yml;
- home 级的 $DSH_HOME/cordis.patch.yml,是该机器上所有 profile 共享的本地偏好;
- 每个 --patch 覆盖层,按命令行参数顺序应用。
后应用的层按行胜出,而且覆盖会整体替换目标行的 config 值,不做键级深度合并。这给组合包作者两个推论:想覆盖前面各层的行,必须重述该行需要的每一个键,不能只写改动的那一项;用户随时可以在自己 profile 的 cordis.patch.yml 里按 id 覆盖你的行,不用改你的包,所以默认值要挑用户大概率会保留的给,其余交给 schema 承担。另一个省心点是内置包名始终从 dsh 安装本身解析,pnpm 只管理树外的包,你的 bundle 可以放心依赖 @deepseek-ai/dsh-base 存在且与安装版本一致。
补充一点:应用参数不是第五层 patch。定义可运行应用的 bundle 会挂载一个普通提供方插件,导出 inject = ['cmdlineArgs'],用 @deepseek-ai/dsh-cmdline 的 parseCmdline 配自己的 commander program,在 action 里把应用自有服务提供出去。启动器把自身 flag 之后的同一份不可变参数交给每个插件,应用加专属 flag 无需改启动器,多个插件也都能解析这份快照。受参数配置的行注入该服务,在 !!js 选项里读取取值,并把部署值写在旁边作回退;遇到 --help 时提供方不发布服务,这些行就不会激活。
从 GitHub 安装:prepare 脚本这道坎
发布到注册表不是必须的,用户可以直接从 git 托管安装:
dsh plugin --profile demo add github:<你的用户名>/hello-plugin坑在这里:git 安装拉取的是源码而不是构建产物,没有任何环节会替你跑 build 脚本,TypeScript 包到手时没有 lib/ 输出,加载直接失败。解法要两侧各做一件事。
作者侧提供 prepare 脚本,pnpm 在 git 安装完成后执行它,从源码构建出发布入口。脚本必须自包含,不能假设仅开发环境才有的上下文,比如旁边正好有一份 monorepo 检出;官方文档给出的范例做法是用一份专用的 tsdown 配置直接转译 src/,不走项目引用,也不做类型检查。
用户侧要为构建授权。pnpm 10 起拒绝运行 git 依赖的 prepare 脚本,除非显式允许,所以第一次 add 会失败;dsh 会指出修法,把 pnpm 打印的包键抄进该 profile 的 pnpm-workspace.yaml 再重试:
allowBuilds:
dsh-hello-plugin: true要如实看待这项授权:它允许该包的代码在安装时于你的机器上执行,且不在 agent 运行的任何沙箱之内。只对源码可信的包授权,并锁定 commit,写成 github:<你的用户名>/hello-plugin# 的形式,让后续推送无法悄悄改变实际运行的代码。
不想让用户做授权也有办法,改为分发构建产物,两种形式都不需要任何构建权限:发布到 npm,在 pnpm publish 时构建好 lib/,用户直接 add 包名即可;或者用 pnpm pack 打出 tarball,用户执行 add 加本地文件路径就能安装。
从 --patch 调试到 bundle 分发,dsh 把插件生命周期的最后一公里补齐了:作者只管把配置层打成 npm 包,组合与叠加交给 profile,安装体验与装一个普通依赖没有区别。想给 deepseek-harness 造轮子的开发者,这一套机制值得动手过一遍。



