ByteNoteByteNote
DeepSeek Harness 插件打包与安装指南
字

字节笔记本

2026年10月6日 · 约 13 分钟读完

DeepSeek Harness 插件打包与安装指南

API中转
¥120

在本地用 --patch overlay 挂载插件,适合验证一个想法;等插件稳定下来,下一步就是把它变成别人一条命令就能装的东西。DeepSeek Harness(dsh,DeepSeek 开源的 agent 框架)为此提供了一套完整的分发机制:把插件打包成可安装的组合包(bundle),用 dsh plugin add 安装进一个 profile,再由层顺序决定最终生效的配置。本文假设 dsh CLI 已经装好;如果你是从源码 checkout 跑起来的,需要先完成仓库的构建准备,把文中的 hello-plugin 目录放在仓库根目录,并把下文的 dsh 命令相应换成 pnpm dsh。

组合包与 profile:两份 manifest 的分工

两个概念,两种 manifest

安装机制建立在两个概念之上。二者都由一份 package.json 描述,但它们在 dsh 键下携带的 manifest(元数据清单)种类不同,回答的问题也不同:

  • 组合包(bundle)是附带一个配置层的 npm 包。它的 manifest 声明 dsh.bundle,回答的是"这个包贡献什么?":一个插入或覆盖插件行的 patch 文件。
  • profile 是位于 $DSH_HOME/profiles/<name> 之下、描述一份可启动组合的目录。它的 manifest 声明 dsh.profile,回答的是"这套配置由哪些组合包按什么顺序组成?"。

组合包是你编写并分发的东西;profile 是用户用 dsh --profile <name> 启动的东西。没有东西同时是两者。

组合包 manifest

创建包目录 hello-plugin,它包含三个文件:package.json 声明 dsh.bundle,cordis.patch.yml 是 profile 列出该组合包时应用的配置层,index.js 则是 patch 条目引用的插件模块。

package.json 在 dsh 键下声明 bundle manifest:

json
{
  "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 写插件入口:

js
export const name = 'hello-plugin'

export function apply() {
  console.log('[hello-plugin] plugin loaded!')
}

cordis.patch.yml 与 --patch overlay 的格式完全一致,是一个 patch 条目的 YAML 数组;区别在于插件行按包名而不是相对源码路径引用代码,这样 Node 的模块解析才能找到已安装的依赖:

yaml
- insert:
    - id: hello
      name: dsh-hello-plugin

没有 dsh.bundle 声明的包仍然可以安装,但只作为普通依赖存在:dsh plugin 会打印警告,且不激活任何层。如果一个库是供插件包 import 的工具库,而不是供用户启用的扩展,就用这种包格式。

profile manifest

profile 目录包含两个文件:

  • package.json:记录 profile 的树外插件依赖(由 pnpm 管理),外加 dsh.profile manifest 及其有序的 bundles 列表。
  • cordis.patch.yml:用户自己的 patch 层,在每个组合包层之后应用。

profile manifest 从不需要手写:dsh plugin 负责创建和维护它,下一节展示它的结果。

安装进 profile

dsh plugin --profile <name> 会把参数转发给 profile 目录内的 pnpm,因此所有 pnpm 子命令都可用。在包含 hello-plugin 的目录里执行:

sh
dsh plugin --profile demo add ./hello-plugin

首次使用会初始化 profile,@deepseek-ai/dsh-base 成为它的第一个组合包;pnpm 链接该 checkout,而 dsh 发现这个包声明了 dsh.bundle,把它追加进 dsh.profile.bundles:

json
{
  "name": "dsh-profile-demo",
  "private": true,
  "dependencies": {
    "dsh-hello-plugin": "link:/path/to/hello-plugin"
  },
  "dsh": {
    "profile": {
      "bundles": [
        "@deepseek-ai/dsh-base",
        "dsh-hello-plugin"
      ]
    }
  }
}

可以先不启动、只验证层是否就位,再正式启动:

sh
dsh --profile demo --dump-config   # 能看到 "# == dsh-hello-plugin" 层
dsh --profile demo

dsh plugin --profile demo remove dsh-hello-plugin 会同时移除依赖和对应的层。

加载顺序:四层叠加,按行胜出

生效配置的四层叠加与按行胜出规则

生效配置在空根之上按以下顺序逐层组合:

  1. profile 的 dsh.profile.bundles 列表所列的各个组合包 patch,按列表顺序:先是 @deepseek-ai/dsh-base,然后是每个已安装组合包,按其加入顺序。
  2. profile 自己的 cordis.patch.yml。
  3. home 级的 $DSH_HOME/cordis.patch.yml,即各 profile 共享的机器本地偏好。
  4. 每个 --patch <path> overlay,按 argv 顺序。

应用参数不是另一层 patch。表层组合包可以通过普通应用自有服务来解析它们,见下一节的做法。

后应用的层按行胜出,且 patch 会替换目标行的整个 config 值,而不是对各键做深度合并。这给组合包作者带来两个推论:

  • 你的 patch 可以按 id 覆盖前面各层的行,官方仓库里的 dsh-web-app 组合包覆盖 dsh-base 的行就是现成的例子;但必须重述该行需要的每一个键,而不是只写改动的那个。
  • 用户可以在自己 profile 的 cordis.patch.yml 中覆盖你的行,无需改动你的包。所以优先给出用户大概率会保留的配置默认值,其余交给 schema 承担。

内置组合包名称始终从 dsh 安装目录本身解析;pnpm 只管理树外的包,所以你的组合包可以放心依赖 @deepseek-ai/dsh-base 存在且与安装保持一致。

让表层组合包持有自己的命令行

定义了可运行应用的组合包,可以挂载一个普通提供方插件来接住命令行参数:

yaml
- id: hello-startup
  name: 'dsh-hello-plugin/startup'

该插件导出 inject = ['cmdlineArgs'],用自己的 commander program 调用 @deepseek-ai/dsh-cmdline 包里的 parseCmdline,再在 program 自己的 action 中把应用自有服务提供出去。启动器会把自身 flag 之后的同一份不可变参数交给每个插件,因此添加应用专属 flag 无需修改启动器,多个插件也可以解析同一份参数快照。Loader 行不需要启动器标记或特殊类型。

受这些参数配置的行会注入提供方服务,并在自己的 !!js 选项中读取它,同时把部署取值写在旁边作为回退:

yaml
- id: my-app
  name: '@example/my-app'
  inject: [myAppStartup]
  config:
    port: !!js ctx.myAppStartup.port ?? 8080

遇到 --help 时,提供方不会发布该服务,这些行也就不会激活。Loader 只挂载一次组合,等待每一行的普通注入完成,再基于已注入的上下文求值该行的 !!js 配置。

从 GitHub 安装:构建脚本这道坎

发布到 npm 注册表不是必须的,用户可以直接从 git 托管安装:

sh
dsh plugin --profile demo add github:you/hello-plugin

但 git 安装拉取的是源码,不是构建产物:没有任何环节运行你的 build 脚本,TypeScript 包到手时没有 lib/ 输出,加载会直接失败。解决的办法是作者与用户两边各做一件事。

作者一侧,提供一个 prepare 脚本,pnpm 在 git 安装完成后会运行它,从源码构建出发布入口。这个脚本必须自包含:不能假设仅有开发环境才有的上下文,比如旁边恰好躺着一份 monorepo checkout。官方生态里的 turtle-ui 是一个可用的例子:它的 prepare 运行一份专用的 tsdown 配置,直接转译 src/,不用项目引用,也不做类型检查。

用户一侧,为构建授权。pnpm 10 及以上版本在得到显式允许之前,拒绝运行 git 依赖的 prepare 脚本,所以第一次 add 会失败;dsh 会指出修法:把 pnpm 打印的确切包键复制进该 profile 的 pnpm-workspace.yaml:

yaml
allowBuilds:
  dsh-hello-plugin: true

然后重新执行 add 即可。

这项授权值得如实看待:它允许该包的代码在安装时于你的机器上执行,而且不在 agent 运行的任何沙箱之内。只对源码可信的包授权,并锁定 commit(github:you/hello-plugin#<sha>),让后续推送无法悄悄改变实际运行的内容。

如果不想让用户做这项授权,就改为分发构建产物,以下两种形式都不需要任何构建权限:

  • 发布到 npm,在 pnpm publish 时构建好 lib/;用户执行 dsh plugin add your-package,装到的就是预构建代码。
  • 交付 tarball:用 pnpm pack 打包;用户执行 dsh plugin add ./hello-plugin-0.1.0.tgz。

小结

组合包与 profile 分工明确:前者回答"这个包贡献什么",后者回答"这套配置由谁组成"。安装、验证、卸载都收敛在 dsh plugin 一个入口,配置的最终形态由"四层叠加、按行胜出"两条规则决定。把插件从 --patch 调试状态推进到可安装的组合包,再按信任与交付条件选一条合适的分发路线:发 npm、走 git、或直接给 tarball,插件就能从个人实验变成别人一条命令可用的扩展。

相关文章

分享: