在之前的章节中,我们一直通过 --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-plugin

dsh, DeepSeek Harness的首字母组合

  • dsh 是 DeepSeek Harness 的命令行工具,同时也是 npm 包 @deepseek-ai/dsh 提供的可执行文件名。

  • pnpm dsh plugin --profile demo add <插件名> 的意思是:调用 DeepSeek Harness 的 CLI,向 demo 这个 profile 安装指定插件。

这条命令做了什么?

  1. 首次使用会初始化 Profile:如果 demo Profile 还不存在,dsh plugin 会先创建它,并自动安装 @deepseek-ai/dsh-base 作为第一个组合包。

  2. 安装组合包:dsh plugin 会在 Profile 目录内调用 pnpm,把 ./hello-plugin 作为依赖安装进去。

  3. 激活配置层:因为 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 按照以下顺序逐层组合配置:

  1. Profile 的 dsh.profile.bundles 列表:按列表顺序依次应用每个组合包的 patch——先是 @deepseek-ai/dsh-base,然后是每个已安装的组合包,按它们加入的顺序。

  2. Profile 自己的 cordis.patch.yml:用户可以在 Profile 目录下手动编辑这个文件,覆盖组合包的配置。

  3. Home 级的 $DSH_HOME/cordis.patch.yml:各 Profile 共享的机器本地偏好。

  4. 每个 --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 公共仓库需要一个账号。

  1. 访问 npm 官网 注册一个账号。

  2. 注册完成后,请前往你的注册邮箱完成邮箱验证,这是发布包的必要前提。

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)——你不能覆盖已经发布的同一个版本。如果需要更新包,必须遵循以下步骤:

  1. 修改代码。

  2. 更新 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 变更。

  1. 重新执行 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 工具实现自动化发布。基本思路是:

  1. 在 npm 官网生成一个 Granular Access Token。

  2. 将 Token 添加到 GitHub 仓库的 Secrets 中(如 NPM_TOKEN)。

  3. 创建 GitHub Actions 工作流(.github/workflows/publish.yml),在打 Tag 或发布 Release 时自动执行构建和 npm publish。

至此,你已经完成了将 Harness 插件发布到 npm 的完整流程:

  1. 准备:注册 npm 账号并完成本地登录。

  2. 配置:确保 package.json 包含正确的 name、files 和 dsh.bundle 字段。

  3. 构建:将 TypeScript 源码编译为 JavaScript。

  4. 检查:使用 npm publish --dry-run 和 npm pack 进行发布前验证。

  5. 发布:执行 npm publish(作用域包加 --access public)。

  6. 更新:修改版本号后重新发布。

现在,任何人都可以通过

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