Skip to content

打包与发布插件

本章目标

前面的教程用 --patch overlay 加载本地插件。本章把插件打包成可安装的组合包(bundle),用 dsh plugin add 安装进一个 profile,解释决定组合后配置的层顺序与 patch 机制,最后给出本地安装、GitHub 安装与发布到 npm 的完整路径。

学完本章你会:

  • 分清 bundle 与 profile 两个概念,以及 package.json 里的 dsh.bundle / dsh.profile 字段;
  • 写一个组合包:package.json + cordis.patch.yml + 插件代码;
  • 理解 patch 按 id 替换整行 config(不是深合并)与四层叠加顺序;
  • 知道从 GitHub 安装时「构建脚本这道坎」及两种绕开它的分发方式;
  • 知道发布前要过哪些检查。

两个概念,两种 manifest

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

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

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

写一个组合包

组合包目录三件套:

hello-plugin/
├── package.json       # 声明 dsh.bundle
├── cordis.patch.yml   # profile 列出本 bundle 时应用的层
└── index.js           # patch 行引用的插件模块
jsonc
{
  "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" } }
}

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

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

没有 dsh.bundle 声明的包仍然可以安装,但只作为普通依赖:dsh plugin 会打印警告且不激活任何层——供其他插件 import 的库包就保持这种格式。

profile 目录本身有两个文件:package.json(pnpm 管理的树外插件依赖 + dsh.profile manifest 及其有序 bundles 列表)和 cordis.patch.yml(用户自己的 patch 层,在每个组合包层之后应用)。profile manifest 从不手写dsh plugin 负责创建和维护它。

安装进 profile 与层顺序

dsh plugin --profile <name> <args...> 在 profile 目录内转发给 pnpm,因此所有 pnpm 子命令都可用。首次安装会初始化 profile(@deepseek-ai/dsh-base 作为它的第一个组合包),pnpm 链接该 checkout,dsh 因包声明了 dsh.bundle 而把它追加进 dsh.profile.bundles

sh
dsh plugin --profile demo add ./hello-plugin
dsh --profile demo --dump-config   # 能看到 "# == dsh-hello-plugin" 这一层
dsh --profile demo

从全新源码 checkout 出发时,把上面的 dsh ... 换成 pnpm dsh ...

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

  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 按 id 定位条目并替换其整个 config 值,而不是深度合并各键。这给组合包作者带来两个推论:

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

仓库内包:目录规范与 README 门禁

如果插件随主仓库(workspace)发布,按 packages/<group>/<pkg>/ 放置。分组是纯容器(corellmbashsubagentui 等已有分组;允许新建,但分组本身没有 package.json 也没有源文件),包恰好位于其下一层。每个包四件套:package.jsontsconfig.jsonsrc/index.tsREADME.md。package.json 有一组由 pnpm run constraints 强制的不变式:private: truetype: modulemain: "lib/index.js"types: "lib/types/index.d.ts"@deepseek-ai/cordis 同时出现在 peerDependencies 与 devDependencies、@deepseek-ai/schemastery 放在 dependencies、files 精确列出运行时产物(不发布 src、map 与声明映射)。

README 以固定规范序列结尾:先是 ## Model Experience——每个模型上下文条目一个 H3,含 #### What the model sees#### Token effect#### KV Cache effect 三个有序 H4;再是 ## Known Limitations and Deferred Work——逐条列出持久的消费方缺口与维护者约束。这两节是门禁强制的:verify-package-readme-model-experience 校验章节结构与归属,verify-package-readme-limitations 要求规范标题与恰好一个顶级 bullet;与模型无关的包走审计过的白名单。

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

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

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

但 git 安装拉取的是源码,不是构建产物:没有任何环节运行你的 build 脚本,因此 TypeScript 包到手时没有 lib/ 输出,加载会失败。必须两边各做一件事:

  • 作者提供一个 prepare 脚本——pnpm 在 git 安装后运行它——从源码构建出发布入口,且必须自包含:不能假设仅开发环境才有的上下文(比如旁边有一份 monorepo checkout)。
  • 用户为构建授权。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

发布前的检查

仓库内包合并前要过的验证(见《实操手册:添加 workspace 包》):

sh
pnpm install        # 注册 workspace
pnpm run doc-sync   # 生成目录与 verify-* 门禁
pnpm run constraints && pnpm run typecheck && pnpm run lint
pnpm run build && pnpm run hygiene

其中 hygiene 聚合了一长串 verify-* 门禁:constraints(package.json 不变式)、verify-dsh-package-licensesverify-package-invariantsverify-built-package-invariantsverify-cordis-configverify-node-next-types(声明文件在 NodeNext 消费方下编译)、verify-runtime-closureverify-vendored-links,以及 publint——scripts/publint-all.ts 对每个包按 manifest 声明的发布视图运行 publint,检查 exports、files、types 等发布元数据是否自洽。doc-sync 里还有针对 README 与文档的 verify-package-readme-model-experienceverify-package-readme-limitationsverify-package-paths 等。发布到 npm 的独立插件包即使不在这个仓库里,也建议套用同样的检查精神:typecheck 通过、README 完整、pnpm publint 干净再发布。

小结

  • bundle 是分发格式(dsh.bundle → patch 文件),profile 是命名组合(dsh.profile → 有序 bundles);profile manifest 由 dsh plugin 维护。
  • 层顺序:bundles → profile 的 patch → home 级 patch → --patch overlay;patch 按 id 替换整行 config,非深合并。
  • 仓库内包放 packages/<group>/<pkg>/,README 必须带 Model Experience 与 Known Limitations 章节(有门禁强制)。
  • GitHub 安装拉源码不构建:作者给 prepare,用户给 allowBuilds 授权;不想授权就发 npm 或 tarball。
  • 发布前过 typecheck、verify-* 门禁与 publint。

下一步

  • 把本章的 hello-plugin 装进你的 profile,用 --dump-config 观察每一层;
  • 阅读官方文档《打包与安装插件》与《实操手册:添加 workspace 包》,了解表层组合包持有自己命令行等进阶形态。