在之前的章节中,我们一直通过 --patch 参数来加载本地插件。这种方式非常适合开发调试——修改代码后重启服务就能看到效果,非常方便。
但问题是:这种开发模式仅限于你自己的机器。如果你想把自己的插件分享给朋友、同事,或者发布到社区供其他人使用,总不能让他们也克隆整个 Harness 源码仓库、再把你的插件目录放在特定位置吧?
这一章,我们将解决这个问题。你会学到如何把插件打包成一个组合包(Bundle)——一个标准的 npm 包,可以通过 dsh plugin add 命令一键安装到任何 Harness 环境中。
你还会接触到 Harness 中两个最核心的配置概念:组合包(Bundle)和 Profile。理解它们之间的关系,是掌握 Harness 插件生态的关键。
两个核心概念
在开始动手之前,先花几分钟理解两个概念。它们贯穿了整个安装机制。
组合包(Bundle)——"这个包贡献什么?"
组合包就是一个声明了 dsh.bundle 字段的 npm 包。它回答的问题是:"这个包贡献了什么?"
一个组合包通常包含:
一个或多个插件(也就是我们之前写的那些 .ts 文件)。
一个 cordis.patch.yml 文件,告诉框架如何加载这些插件。
一个 package.json,其中 dsh.bundle 字段指向那个 patch 文件。
组合包是你编写并分发的东西。你可以把它发布到 npm,也可以打成压缩包发给别人,甚至直接从 GitHub 安装。
Profile——"这套配置由哪些组合包组成?"
Profile 是一个目录,位于 $DSH_HOME/profiles/<name>/ 下,它描述了一套可启动的配置组合。它回答的问题是:"这套配置由哪些组合包按什么顺序组成?"
一个 Profile 目录包含:
一个 package.json,其中的 dsh.profile.bundles 列表列出了所有已安装的组合包。
一个 cordis.patch.yml,用户可以在这里覆盖组合包的配置。
node_modules/ 目录,存放所有安装的依赖。
Profile 是用户启动时使用的东西。你可以有多个 Profile——比如一个 web Profile 用于 Web UI 环境,一个 headless Profile 用于命令行环境——它们相互独立,互不干扰。
给初中级开发者的比喻:把组合包想象成“手机 App”,Profile 想象成“手机桌面”。你下载各种 App(组合包)安装到你的手机(Profile)上,然后通过桌面(启动命令)来使用它们。不同的手机(不同的 Profile)可以安装不同的 App 组合。
对比:从开发模式到分发模式
先回顾一下我们之前是怎么加载插件的:
pnpm dsh web --patch ./scratch-plugin/cordis.yml这种方式的特征是:
插件代码在本地目录中。
通过 --patch 参数直接指向 patch 文件。
适合开发阶段快速迭代。
而打包安装后的使用方式是:
dsh plugin --profile web add dsh-hello-plugin
dsh --profile web这种方式的特征是:
插件被打包成 npm 包,安装到 Profile 中。
Profile 管理所有已安装的组合包。
适合分发和部署阶段。
两种方式可以混合使用——你甚至可以在一个已经安装了多个组合包的 Profile 上,再用 --patch 叠加一个本地开发中的插件进行调试。
bundle完整示例
第一步:准备打包目录
我们将在仓库根目录下创建一个独立的包目录,用来存放将要打包的插件代码。
首先,回到仓库根目录:
cd /path/to/deepseek-harness然后创建一个新的目录 hello-plugin/(注意,这次和之前的 scratch-plugin 是平级的):
mkdir -p hello-plugin
cd hello-plugin这个目录将成为一个完整的 npm 包——有自己的 package.json、源代码和 patch 文件。
第二步:创建 package.json
在 hello-plugin/ 目录下创建 package.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"
}
}
}逐字段来看:
字段 | 说明 |
|---|---|
name | 包的名称。建议以 dsh- 开头,表明这是一个 Harness 组合包 |
version | 遵循语义化版本规范 |
type | 声明这是一个 ES Module |
main | 包的入口文件 |
files | 发布到 npm 时包含哪些文件 |
dsh | 最关键的部分:声明这个包是一个组合包,并指定 patch 文件的位置 |
重要提示:dsh.bundle 是组合包的身份标识。如果一个包没有声明 dsh.bundle,即使被安装到 Profile 中,也只会作为普通依赖存在,不会激活任何配置层。
第三步:编写插件入口文件
创建 hello-plugin/index.js:
export const name = 'hello-plugin'
export function apply() {
console.log('[hello-plugin] plugin loaded!')
}等等,为什么这里用的是 .js 而不是 .ts?
因为我们打包的是编译后的 JavaScript 代码,而不是 TypeScript 源码。用户安装你的组合包时,直接运行的是 JavaScript,不需要在本地配置 TypeScript 编译环境。
给初中级开发者的提示:在开发阶段,我们用 TypeScript 写插件,享受类型检查带来的安全感。但在分发阶段,我们需要把 TypeScript 编译成 JavaScript,让用户开箱即用。这是大多数 npm 包的通用做法。
第四步:创建 patch 文件
创建 hello-plugin/cordis.patch.yml:
- insert:
- id: hello
name: dsh-hello-plugin这个 patch 文件和我们之前在 scratch-plugin/cordis.yml 中写的几乎一样,唯一的区别是:name 字段现在引用的是包名 dsh-hello-plugin,而不是一个文件路径。
为什么要有这个区别?因为当组合包被安装到 Profile 中后,它的代码位于 node_modules/dsh-hello-plugin/ 目录下。通过包名引用,Node.js 的模块解析机制会自动找到正确的位置。如果用文件路径,就可能因为路径不对而找不到代码。
第五步:安装到 Profile
现在组合包已经准备好了。我们来把它安装到一个 Profile 中。
首先,确保你在 hello-plugin/ 目录的上一层(即仓库根目录),然后运行:
pnpm dsh plugin --profile demo add ./hello-plugindsh, DeepSeek Harness的首字母组合
dsh 是 DeepSeek Harness 的命令行工具,同时也是 npm 包 @deepseek-ai/dsh 提供的可执行文件名。
pnpm dsh plugin --profile demo add <插件名> 的意思是:调用 DeepSeek Harness 的 CLI,向 demo 这个 profile 安装指定插件。
这条命令做了什么?
首次使用会初始化 Profile:如果 demo Profile 还不存在,dsh plugin 会先创建它,并自动安装 @deepseek-ai/dsh-base 作为第一个组合包。
安装组合包:dsh plugin 会在 Profile 目录内调用 pnpm,把 ./hello-plugin 作为依赖安装进去。
激活配置层:因为 hello-plugin 声明了 dsh.bundle,dsh plugin 会自动把它追加到 dsh.profile.bundles 列表中。
安装完成后,demo Profile 的 package.json 大致如下:
demo profile目录:C:\Users\Administrator\.dsh\profiles\demo
{
"name": "dsh-profile-demo",
"private": true,
"dependencies": {
"dsh-hello-plugin": "link:E:/ProjectsAI/deepseek-harness/hello-plugin"
},
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base",
"dsh-hello-plugin"
]
}
}
}注意: bundles 列表中的顺序:@deepseek-ai/dsh-base 在前,dsh-hello-plugin 在后。这个顺序决定了配置的加载优先级——后面的层可以覆盖前面的层。
第六步:启动并验证
先用 --dump-config 查看最终生效的配置,确认组合包已经被正确加载:
pnpm dsh --profile demo --dump-config你应该能在输出中看到一个标记为 # == dsh-hello-plugin 的配置层。
# == dsh-hello-plugin
- id: hello
name: dsh-hello-plugin然后正式启动:
pnpm dsh --profile demo在启动日志中,你应该能看到:
[hello-plugin] plugin loaded!到此,恭喜你!你的插件现在是以组合包的形式运行的。
加载顺序:谁覆盖谁?
当多个组合包被安装到同一个 Profile 中时,它们之间的配置是如何叠加和覆盖的?
Harness 按照以下顺序逐层组合配置:
Profile 的 dsh.profile.bundles 列表:按列表顺序依次应用每个组合包的 patch——先是 @deepseek-ai/dsh-base,然后是每个已安装的组合包,按它们加入的顺序。
Profile 自己的 cordis.patch.yml:用户可以在 Profile 目录下手动编辑这个文件,覆盖组合包的配置。
Home 级的 $DSH_HOME/cordis.patch.yml:各 Profile 共享的机器本地偏好。
每个 --patch overlay:按命令行参数顺序。
Profile 自己的 cordis.patch.yml
# Your patch layer for this dsh profile, applied after every bundle layer:
# a top-level YAML array of loader patch entries (id-targeted config
# overrides, disables, and insert lists; `!!js` expressions allowed).
[]后应用的层覆盖先应用的层。如果两个 patch 都定义了同一个 id 的行,后面的会完全替换前面的——注意是整个 config 值被替换,而不是深度合并各键。
这意味着:
你的 patch 可以覆盖前面各层中相同 id 的行。
但如果你想覆盖某个字段,必须重述整行的所有键,而不能只写改动的那个。
用户可以在自己的 Profile cordis.patch.yml 中覆盖你的配置,无需改动你的包。
给初中级开发者的提示:这个设计保证了配置的可预测性——你始终清楚最终生效的配置是什么,不会出现"A 改了字段 X,B 改了字段 Y,一合并却莫名其妙"这种难以调试的情况。
从 GitHub 安装
发布到 npm 不是唯一的分发方式。用户可以直接从 GitHub 安装你的组合包:
dsh plugin --profile demo add github:you/hello-plugin但这里有一个坑需要留意:Git 安装拉取的是源码,不是构建产物。如果你的插件是用 TypeScript 写的,Git 安装后只有 .ts 文件,没有编译后的 .js 文件,加载会失败。
解决方案有两种:
方案一:提供 prepare 脚本
在你的 package.json 中添加 prepare 脚本——pnpm 在 Git 安装后会运行它,从源码构建出发布入口:
{
"scripts": {
"prepare": "tsdown"
}
}这个脚本必须自包含,不能假设有 monorepo 的上下文。
用户在使用 pnpm(pnpm -v 大于等于 10) 时,若无显式授权,Git 依赖的 prepare 脚本将被拒绝执行。首次 add 操作会失败,并提示将特定的包键复制到 Profile 的 pnpm-workspace.yaml 中:
allowBuilds:
dsh-hello-plugin: true然后重新执行 add 即可。
安全提示:授权 allowBuilds 意味着允许该包的代码在安装时于你的机器上执行。请只对源码可信的包授权,并锁定 commit(github:you/hello-plugin#commit-hash),防止后续推送悄悄改变实际运行的内容。
方案二:分发构建产物(推荐)
如果你不想让用户做构建授权,就只分发构建产物:
发布到 npm:在 pnpm publish 时构建好 lib/,用户安装的就是预构建代码(本文最后有完整的发布实操示例)。
交付 tarball:用 pnpm pack 打包,用户执行 dsh plugin add ./hello-plugin-0.1.0.tgz。
方案二对用户更友好,推荐作为主要分发方式。
卸载组合包
卸载和安装一样简单:
pnpm dsh plugin --profile demo remove dsh-hello-plugin这个命令会同时移除依赖和对应的配置层。
卸载后的package.json (C:\Users\Administrator\.dsh\profiles\demo)
{
"name": "dsh-profile-demo",
"private": true,
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base"
]
}
}
}安装后的package.json (C:\Users\Administrator\.dsh\profiles\demo)
{
"name": "dsh-profile-demo",
"private": true,
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base",
"dsh-hello-plugin"
]
}
},
"dependencies": {
"dsh-hello-plugin": "link:E:/ProjectsAI/deepseek-harness/hello-plugin"
}
}完整目录结构
完成本章所有步骤后,你的 hello-plugin/ 目录结构如下:
hello-plugin/
├── package.json # 声明 dsh.bundle
├── cordis.patch.yml # 配置层,引用包名而非路径
└── index.js # 编译后的插件入口总结
本章我们完成了从“本地开发”到“可分发组合包”的转变。核心收获如下:
概念 | 说明 |
|---|---|
组合包(Bundle) | 声明了 dsh.bundle 的 npm 包,回答“这个包贡献什么?” |
Profile | 位于 $DSH_HOME/profiles/下的目录,回答“这套配置由哪些组合包组成? |
dsh.bundle.patch | 组合包的 package.json 中指向 patch 文件的字段 |
dsh.profile.bundles | Profile 的 package.json 中列出所有组合包的数组 |
pnpm dsh plugin add | 安装组合包到指定 Profile |
加载顺序 | bundles 列表顺序 → Profile patch → Home patch → --patch overlay |
后层覆盖前层 | 同 id 的行,后面的完全替换前面的 config |
你还学到了两种分发方式——npm 发布和 Git 安装——以及它们各自的注意事项。现在,你的插件已经不再是只属于你自己的代码片段,而是一个可以被任何人一键安装的、标准化的组合包。
下一章,我们将进入框架进阶篇,深入探讨 Harness 底层的 Cordis 框架,包括插件的完整生命周期、服务机制与事件系统。
发布示例实操
下面,我们将基于上一章的 hello-plugin 示例,逐步演示将 Harness 组合包发布到 npm 的完整流程。
发布前准备
在发布之前,需要确保你的开发环境已经准备就绪。
1. 注册 npm 账号
发布包到 npm 公共仓库需要一个账号。
访问 npm 官网 注册一个账号。
注册完成后,请前往你的注册邮箱完成邮箱验证,这是发布包的必要前提。
2. 本地登录 npm
在命令行中执行以下命令进行登录:
npm login按系统提示完成登录操作,登录成功后,你的认证信息会保存在本地,后续 npm publish 命令会使用这些信息。
3. 检查包名是否可用
npm 上的包名是全局唯一的。在发布前,建议先确认你打算使用的包名是否已经被占用。你可以在 npm 官网 直接搜索包名,或者使用以下命令检查:
npm view <你的包名>如果返回 404 错误,说明该包名目前可用。
配置与构建
发布到 npm 的不是 TypeScript 源码,而是编译后的 JavaScript 代码,以便用户可以直接运行。
1. 准备 package.json
确保你的 package.json 包含了发布所必需的关键字段:
{
"name": "dsh-hello-plugin",
"version": "0.1.0",
"type": "module",
"main": "index.js",
"description": "A hello plugin for deepseek-harness",
"license": "MIT",
"keywords": [
"deepseek-harness",
"plugin",
"hello"
],
"files": ["index.js", "cordis.patch.yml"],
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
}
}
}关键字段说明:
name:如果包名以 @your-username/ 开头,则称为 作用域包 (Scoped Package)。作用域包首次发布时需要额外添加 --access public 参数。
files:指定发布到 npm 的文件列表。不要把 src/ 目录或 node_modules/ 包含在内。如果你需要排除更多文件,可以创建一个 .npmignore 文件。
dsh.bundle.patch:这是 Harness 组合包的身份标识。如果缺少这个字段,包安装后不会被 Harness 识别为组合包。
2. 编译 TypeScript(如果使用 TS)
如果你的插件是用 TypeScript 编写的,需要先编译成 JavaScript。
在 package.json 中添加 build 脚本,例如使用 tsc 或 tsdown:
{
"scripts": {
"build": "tsc" // 或 "tsdown"
}
}配置好 tsconfig.json 后,执行编译:
pnpm build编译产物(如 index.js)应输出到 files 字段指定的目录中。
3. 添加 README.md
一个好的 README.md 文件能帮助用户快速了解你的插件。它通常包含:
插件名称与简介。
安装方法(pnpm dsh plugin add dsh-hello-plugin)。
使用示例或配置说明。
发布前检查
在实际发布之前,有几个命令可以帮助你验证一切是否就绪。
1. 检查登录状态
npm whoami如果返回你的用户名,说明已成功登录。
2. 模拟发布(dry-run)
cd ./hello-plugin/
npm publish --dry-run仔细检查输出的文件列表,确保没有意外包含敏感信息(如 src/ 等)。
npm notice
npm notice package: dsh-hello-plugin@0.1.0
npm notice Tarball Contents
npm notice 117B README.md
npm notice 54B cordis.patch.yml
npm notice 115B index.js
npm notice 430B package.json
npm notice Tarball Details
npm notice name: dsh-hello-plugin
npm notice version: 0.1.0
npm notice filename: dsh-hello-plugin-0.1.0.tgz
npm notice package size: 511 B
npm notice unpacked size: 716 B
npm notice shasum: a2cdaf9a84e698fdd9b0475bf7f26a47d71444b2
npm notice integrity: sha512-tUOkj7bDjBmyT[...]W4BPIcJD3a+2A==
npm notice total files: 4
npm notice
npm notice Publishing to https://registry.npmjs.org/ with tag latest and default access (dry-run)3. 本地打包测试
npm pack这会在当前目录生成一个 .tgz 文件(如 dsh-hello-plugin-0.1.0.tgz)。你可以将这个文件安装到其他项目中测试:
pnpm dsh plugin add ./dsh-hello-plugin-0.1.0.tgz发布到 npm
npm 安全策略的要求——发布包需要 两步验证(2FA) 或 支持绕过 2FA 的细粒度访问令牌。
这里使用创建支持绕过 2FA 的细粒度令牌进行操作,操作步骤:
登录 npmjs.com 后,点击右上角头像,在弹出的面板中选择 Access Tokens
点击右上角的 Create new token,进行token创建。
注意:需要勾选 "Allow bypassing two-factor authentication"
创建后复制令牌,然后在本地执行:
npm config set //registry.npmjs.org/:_authToken token令牌标准发布
npm publish作用域包首次发布
如果你的包名是作用域包(如 @your-username/dsh-hello-plugin),首次发布时需要显式声明为公共包:
npm publish --access public发布成功后,你会在 npm 官网上看到你的包页面,地址为:
https://www.npmjs.com/package/<你的包名>版本更新与重新发布
npm 上的包版本是不可变的 (Immutable)——你不能覆盖已经发布的同一个版本。如果需要更新包,必须遵循以下步骤:
修改代码。
更新 package.json 中的 version 字段,遵循语义化版本规范:
Patch (1.0.0 → 1.0.1):修复 Bug,不改变 API。
Minor (1.0.0 → 1.1.0):新增功能,向下兼容。
Major (1.0.0 → 2.0.0):引入不兼容的 API 变更。
重新执行 npm publish。
重要提示:pnpm publish 与 npm publish 的行为略有不同。在 pnpm 项目中,推荐使用 pnpm publish,它会自动执行 prepublishOnly 等生命周期脚本。
常见问题排查
错误信息 | 可能原因 | 解决方案 |
|---|---|---|
403 Forbidden | 包名已被占用,或版本号已存在 | 修改 package.json 中的 name 或 version 后重新发布 |
403 Forbidden | 未登录或认证失败 | 执行 npm login 重新登录 |
403 Forbidden | 作用域包未设置公开访问权限 | 使用 npm publish --access public |
401 Unauthorized | 未授权 | 检查 npm token 是否有效,或重新登录 |
Version not changed | 版本号未更新 | 更新 package.json 中的 version 字段 |
发布后 dsh plugin add 无法安装 | dsh.bundle 字段缺失 | 检查 package.json 中是否正确配置了 dsh.bundle.patch |
使用自动化发布(CI/CD)
对于团队协作或频繁更新的项目,建议配置 GitHub Actions 等 CI/CD 工具实现自动化发布。基本思路是:
在 npm 官网生成一个 Granular Access Token。
将 Token 添加到 GitHub 仓库的 Secrets 中(如 NPM_TOKEN)。
创建 GitHub Actions 工作流(.github/workflows/publish.yml),在打 Tag 或发布 Release 时自动执行构建和 npm publish。
至此,你已经完成了将 Harness 插件发布到 npm 的完整流程:
准备:注册 npm 账号并完成本地登录。
配置:确保 package.json 包含正确的 name、files 和 dsh.bundle 字段。
构建:将 TypeScript 源码编译为 JavaScript。
检查:使用 npm publish --dry-run 和 npm pack 进行发布前验证。
发布:执行 npm publish(作用域包加 --access public)。
更新:修改版本号后重新发布。
现在,任何人都可以通过
pnpm dsh plugin --profile <profile名称> add dsh-hello-plugin一键安装你的插件了。
安装示例
运行命令:
cd E:\ProjectsAI\deepseek-harness
pnpm dsh plugin --profile scratch add dsh-hello-plugin后,在目录
// 笔者真实测试目录
C:\Users\Administrator\.dsh\profiles中,出现 stratch 目录,内容列表包括:
└─node_modules
├─.pnpm
└─dsh-hello-plugin
└─cordis.patch.yml
└─index.js
└─package.json
└─README.md
└─.modules.yaml
└─.package-map.json
└─.pnpm-workspace-state-v1.json
└─cordis.patch.yml
└─package.json
└─pnpm-lock.yaml
└─pnpm-workspace.yaml