打包与发布插件
本章目标
前面的教程用 --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 行引用的插件模块{
"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 的模块解析才能找到已安装的代码:
- 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:
dsh plugin --profile demo add ./hello-plugin
dsh --profile demo --dump-config # 能看到 "# == dsh-hello-plugin" 这一层
dsh --profile demo从全新源码 checkout 出发时,把上面的 dsh ... 换成 pnpm dsh ...。
生效配置在空根之上按以下顺序逐层组合:
- profile 的
dsh.profile.bundles所列各组合包 patch,按列表顺序(先是@deepseek-ai/dsh-base,再按加入顺序); - profile 自己的
cordis.patch.yml; - home 级
$DSH_HOME/cordis.patch.yml——各 profile 共享的机器本地偏好; - 每个
--patch <path>overlay,按 argv 顺序。
后应用的层按行胜出,而且 patch 按 id 定位条目并替换其整个 config 值,而不是深度合并各键。这给组合包作者带来两个推论:
- 你的 patch 可以按
id覆盖前面各层的行,但必须重述该行需要的每一个键,而不是只写改动的那个; - 用户可以在自己 profile 的
cordis.patch.yml里覆盖你的行而无需改动你的包,所以优先给出用户大概率会保留的配置默认值,其余交给 schema 承担。
仓库内包:目录规范与 README 门禁
如果插件随主仓库(workspace)发布,按 packages/<group>/<pkg>/ 放置。分组是纯容器(core、llm、bash、subagent、ui 等已有分组;允许新建,但分组本身没有 package.json 也没有源文件),包恰好位于其下一层。每个包四件套:package.json、tsconfig.json、src/index.ts、README.md。package.json 有一组由 pnpm run constraints 强制的不变式:private: true、type: module、main: "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 托管安装:
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:
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 包》):
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-licenses、verify-package-invariants、verify-built-package-invariants、verify-cordis-config、verify-node-next-types(声明文件在 NodeNext 消费方下编译)、verify-runtime-closure、verify-vendored-links,以及 publint——scripts/publint-all.ts 对每个包按 manifest 声明的发布视图运行 publint,检查 exports、files、types 等发布元数据是否自洽。doc-sync 里还有针对 README 与文档的 verify-package-readme-model-experience、verify-package-readme-limitations、verify-package-paths 等。发布到 npm 的独立插件包即使不在这个仓库里,也建议套用同样的检查精神:typecheck 通过、README 完整、pnpm publint 干净再发布。
小结
- bundle 是分发格式(
dsh.bundle→ patch 文件),profile 是命名组合(dsh.profile→ 有序 bundles);profile manifest 由dsh plugin维护。 - 层顺序:bundles → profile 的 patch → home 级 patch →
--patchoverlay;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 包》,了解表层组合包持有自己命令行等进阶形态。