DeepSeek Harness中插件开发新手指南
面向已有 Node.js / TypeScript 基础的开发者。读完本文,你可以独立完成:开发一个 DSH 插件 → 本地调试挂载 → 发布到社区并被他人安装。

版本说明:DeepSeek Harness 目前处于开发者预览(Developer Preview)阶段,迭代很快,官方明确声明会有兼容性破坏变更。文中机制基于 @deepseek-ai/dsh 0.1.0-rc.x 时代的公开资料整理,动手前请以官方仓库文档(GitHub:deepseek-ai/deepseek-harness)和 dsh --dump-config 的实际输出为准。
一、为什么现在是风口
DeepSeek Harness(命令行简称 dsh)是 DeepSeek 开源的 Agent 框架(agent harness),架构上「一切皆插件」:模型适配器、工具注册表、会话日志、甚至 Agent 主循环本身都是插件,整个产品就是启动时从若干配置层组合出来的一棵插件树。
对开发者友好的几点现状:
- 核心仓库暂不接受外部 PR,官方把贡献路径明确指向生态:发布插件、写教程、答社区问题、报 issue。
- 插件就是普通 npm 包,没有专门的注册中心,发布门槛极低。
- 官方指定的发现渠道只是给 GitHub 仓库打一个
dsh-plugin话题(Topic),就会被社区聚合目录收录。 - 社区内测期间已出现数百个公开插件(awesome 目录统计 900+),但大量细分领域仍是空白。
二、核心概念:一切皆插件
2.1 没有「内核 + 插件」的分层
安装目录下的约 195 个 @deepseek-ai/* 包全部是 Cordis 插件——工具、LLM 适配器、会话持久化、Web 服务器、前端 UI、沙箱策略都不例外。你写的插件和官方的 dsh-tool-bash 地位完全相同,没有「插件 API」和「内核 API」之分。
- 加能力 = 往组合里加一行
- 改行为 = 用 patch 覆盖已有的行
2.2 底层框架 Cordis:五个必须记住的想法
| 想法 | 含义 |
|---|---|
| 插件 | 一个实现了 Service 的对象:最常见是带 apply(ctx) 的函数,也可以是带 inject 的对象或 Service 子类 |
| 上下文(Context) | 服务的仓库。服务挂到稳定的 ctx.(如 ctx.tools、ctx.llm、ctx.sessions),插件之间通过 key 找服务,不 import 具体实现 |
| inject | 声明插件需要的必需服务。loader 会等待这些服务存在后再执行插件,加载顺序由依赖决定而不是文件顺序 |
| 类型化事件 | 服务通过声明合并定义事件,用 emit / waterfall / parallel / serial 分发给监听者 |
| 可逆的注册 | 工具 schema、监听器等都通过 ctx.effect() / ctx.on() 注册;插件卸载(HMR、热重载、关停)时一切自动回滚 |
扩展点(事件 / 服务)就是 dsh 的「API」。改行为时优先挂在扩展点上,不要去改主循环。
2.3 Profile 与 Bundle:两个关键概念
| 概念 | manifest | 回答的问题 |
|---|---|---|
| bundle(插件分发单元) | dsh.bundle(指向 patch 文件) | 「这个包贡献什么」——一个配置层(cordis.patch.yml),由 npm 包分发 |
| profile(可运行组合) | dsh.profile(bundles 列表) | 「哪些 bundle 按什么顺序组成这个运行实例」 |
bundle 是作者分发的单元,profile 是用户启动的单元,dsh plugin 命令负责维护 profile。
启动时配置层的叠加顺序(后层覆盖前层):
- profile 清单里列出的各 bundle(按顺序)
- profile 自己的
cordis.patch.yml - 家目录级
$DSH_HOME/cordis.patch.yml(对本机所有 profile 生效) - 命令行
--patch覆盖(按 argv 顺序)
查看你的机器实际组合出的插件树:
dsh --profile web --dump-config
打印出来的任何一行,都可以用你自己的 patch 替换。 patch 按行的 id 定位:要么整行替换其 config(不是深合并),要么插入新行。
三、开发环境准备
3.1 前置要求
Node.js:官方声明范围 ^22.19.0 || >=24.0.0,不确定时直接用 Node 24。
pnpm:dsh plugin 子命令会把参数原样转发给 profile 目录里的 pnpm,没有 pnpm 会直接报错:
npm install -g pnpm
DeepSeek API Key(运行真实模型时需要):把 DEEPSEEK_API_KEY 放进根目录 .env,pnpm dsh 会自动加载。没有 Key 也可以先写代码、跑单元测试和 --dump-config 验证。
3.2 安装 dsh
# 方式一:直接从 npm 运行(推荐普通开发者)npx @deepseek-ai/dsh web # 默认在 http://127.0.0.1:3080 启动 Web UI# 方式二:克隆源码开发(推荐要深度调试的开发者)git clone https://github.com/deepseek-ai/deepseek-harness.gitcd deepseek-harnesspnpm installpnpm run build # 不要省!只装依赖不构建会导致 Web 页面缺产物pnpm dsh web
3.3 建议建一个隔离的调试 profile
开发期间用一个独立 profile(如 --profile dev)安装开发中的插件,日常使用的 web profile 保持稳定,两者互不干扰。
四、编写你的第一个插件
4.1 最小函数插件
创建 hello.ts:
import type { Context } from '@deepseek-ai/cordis'export const name = 'hello'export function apply(ctx: Context) { ctx.logger.info('hello from my first plugin')}再创建 cordis.yml:
- name: './hello.ts'
在仓库内可以用 vendored 的 Cordis 启动器直接跑通最小挂载链路(不需要 API Key):
node --import tsx ../../vendor/cordis/bin.js
4.2 插件的三种形态
import { Service, type Context } from '@deepseek-ai/cordis'// 1. 函数插件(最常见,推荐默认用它)export function apply(ctx: Context) {}// 2. 对象插件:带 apply 方法的对象export const objectPlugin = { name: 'object-plugin', apply(ctx: Context) {},}// 3. 类插件:Service 子类(适合对外提供一个 ctx. 服务)export class MyService extends Service { constructor(ctx: Context) { super(ctx, 'myService') }} 4.3 正式插件的四个导出
一个正式的函数插件通常导出四个东西:
import type { Context } from '@deepseek-ai/cordis'import z from '@deepseek-ai/schemastery'/** 插件显示名,仅用于诊断。 */export const name = 'my-plugin'/** 声明依赖的必需服务;loader 会等它们存在再执行 apply。 */export const inject = ['tools']/** 部署期配置的 schemastery 校验 schema(可省略)。 */export interface Config { greeting: string}export const Config: z = z.object({ greeting: z.string(),})/** 插件主体:注册一切贡献,并只注册为可逆 effect。 */export function apply(ctx: Context, config: Config) { ctx.logger.info(config.greeting)} 要点:
inject只声明必需服务;可选服务用ctx.get(name)读取。- 函数插件必须命名导出,不要混用默认导出,否则 Loader 会丢掉
inject元数据。 apply签名:有Config导出时是(ctx, config),没有时是(ctx)。- 配置错误要 fail loud:加载失败会明确报错,不会静默跳过。
五、插件开发的硬性规则
这一节汇总官方文档与社区实践中反复强调的规则,违反任何一条都可能导致插件加载失败或行为异常。
5.1 注册可逆性
- 一切注册(工具、监听器、prompt 段)必须通过
ctx.effect()/ctx.on()等机制完成,插件卸载时自动回滚。ctx.effect()中注册的东西必须有 teardown,否则重载或切换 profile 时会留下重复监听器或资源。 - 工具只注册一次:注册借用的是只读 definition,不要事后改 schema;想换工具就释放所属 effect 再注册。
5.2 模型可见性规则
- 模型能看到的任何东西都必须能从会话日志重建(模型可见 ⟺ 已记录)。要给模型加新的可见输入,就扩展
SessionEventMap加一种新事件类型、从日志渲染,而不是绕过日志。 - durable 会话事件(
turn/*、step/*、tool/*等)追加进会话日志,重启后可重建;live 事件(agent/*、tools/*)只做运行期协调。两者分工不能乱。
5.3 配置规则
- 「两个部署环境可能需要不同的值」都必须做成 Config 字段,不能写死在代码里。
cordis.yml的!!js只允许出现在插件config和条目disabled下;按环境选插件要用 overlay,不要滥用!!js。- patch 覆盖是整行替换(不是深合并),覆盖时必须保留行的
id。
5.4 工具的 execute() 契约
- args 自动校验:
defineTool会在execute前校验模型生成的参数。 - 只返回一个规范 JSON 值:
output.schema定义返回值;抛异常 =isError;领域内的失败结果(如非零退出码)也要放进规范值返回。 - 遵守
exec.signal:取消信号触发时必须中止进行中的工作。 - UI 卡片与模型看到的内容分离:模型看到的由
output.render决定,UI 卡片由presentCall/presentResult返回渲染意图(generic/terminal/diff)。 - 后台长任务通过
ctx.jobs.start()注册,模型侧返回带jobId的规范句柄,且开关必须由部署配置控制。
5.5 waterfall 事件最易踩的坑
tools/pre-execute 等 waterfall 事件的监听器收到 (...args, next):调用 next() 才把结果传给下一个监听器;不调 next() 直接 return 就是短路,截断整条链。这是写钩子插件时最容易犯的错。
六、实战一:模型可见的工具插件
工具是插件最常见的用途。工具注册在 ctx.tools 上,schema 会自动进入 prompt 组装,模型就能「看到」它。
import { readFile } from 'node:fs/promises'import type { Context } from '@deepseek-ai/cordis'import { defineTool } from '@deepseek-ai/dsh-tools'export const name = 'demo-tool'export const inject = ['tools']export function apply(ctx: Context) { ctx.tools.register(defineTool({ name: 'read_file', description: 'Read a file from disk.', // 模型看到的能力描述,要写清前置条件与副作用 parameters: { path: { type: 'string', required: true, description: 'Absolute path' }, limit: { type: 'number' }, // 可选参数 }, output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }], }, async execute(args, exec) { // args 已被 defineTool 按 schema 校验并推导类型 return readFile(args.path, { encoding: 'utf8', signal: exec.signal }) }, }))}工具描述(description)的写作要求:说明何时调用、必要前置条件、失败语义与副作用。
七、实战二:拦截事件的钩子插件
不需要新工具、只想在某个环节插一脚时,用事件监听器。主循环是事件驱动的,钩子插件就是在这些事件上挂监听器。
权限门示例——在 tools/pre-execute 上拦截每一次工具调用:
import type { Context } from '@deepseek-ai/cordis'import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'declare function isAllowed(exec: ToolExecution): Promiseexport const name = 'permission-gate'export function apply(ctx: Context) { ctx.on('tools/pre-execute', async (exec, next): Promise => { if (!(await isAllowed(exec))) { return { kind: 'deny', reason: 'Denied by policy.' } } return next() })} 常用扩展点速查:
| 你要做的 | 用哪个 |
|---|---|
| 允许 / 拒绝 / 询问工具调用 | tools/pre-execute,返回 {kind:'deny'} / {kind:'ask'} |
| 工具调用必须被最终否决、不可撤销 | ctx.tools.guard() |
| 包裹工具执行生命周期(超时/重试/指标) | tools/execute |
| 显式改写工具结果或呈现内容 | tools/post-execute |
| 只观察最终结果(审计/捕获) | tools/result |
| 改写模型请求配置 | agent/request(waterfall) |
| 改写/拒绝进入 step 的消息 | agent/pre-step(waterfall) |
八、声明式配置(Config)
8.1 定义 schema
用 @deepseek-ai/schemastery(它也是 Cordis 的校验器),类型和运行时校验合一:
import z from '@deepseek-ai/schemastery'export interface Config { allowParallelInProgress: boolean}export const Config: z = z.object({ allowParallelInProgress: z.boolean().required(),}) 8.2 在 cordis.yml 里装配
- id: todo name: '@deepseek-ai/dsh-tool-todo' config: allowParallelInProgress: true
九、把插件挂载到 dsh(三条路径)
| 路径 | 适用场景 | 做法 |
|---|---|---|
| 外置插件(推荐大多数场景) | 自研、开源、单独发布 | 独立 npm 包,用 dsh plugin add 安装进 profile;package.json 声明 dsh.bundle 可自动进 bundle 层 |
| 临时 overlay | 调试、演示 | dsh --profile |
| 仓库内包 | 给 dsh 本身贡献代码 | 放 packages/(预览期核心仓库暂不接受外部 PR) |
9.1 方式一:正式安装(需要 pnpm)
dsh plugin 把参数原样转发给 profile 目录里的 pnpm,动词在最后:
dsh plugin --profile web add /path/to/my-plugin # 本地路径dsh plugin --profile web add github:you/my-plugin # Git 仓库dsh plugin --profile web add my-plugin # npm 包dsh plugin --profile web add ./my-plugin-0.1.0.tgz # tarballdsh plugin --profile web remove my-plugin # 卸载
- 相对路径锚定到命令行所在目录。
- 包声明了
dsh.bundle的,会自动追加进该 profile 的dsh.profile.bundles层栈;没声明的包只会作为普通依赖安装并收到警告。
9.2 方式二:临时 overlay(本地开发,不需要 pnpm)
# my-overlay.yml- insert: - id: my-plugin name: '/绝对路径/my-plugin/index.js'
dsh --profile headless --patch ./my-overlay.yml "任务"
9.3 验证插件已挂载
dsh --profile web --dump-config # 应看到 # == your-plugin 层和对应 id 行dsh plugin --profile web why# 确认依赖关系
十、如何上传 / 发布插件
DSH 没有专门的插件注册中心——发布 DSH 插件 ≈ 发布一个 npm 包,只是包内容遵循插件约定。官方提供三种分发途径,核心区别在于是否分发预构建产物:
| 方式 | 用户安装命令 | 安装到的是什么 | 是否需要构建授权 |
|---|---|---|---|
| npm 发布 | dsh plugin add your-package | 预构建的 lib/ 代码 | 不需要 |
| tarball 交付 | dsh plugin add ./hello-0.1.0.tgz | pnpm pack 打出的包 | 不需要 |
| Git 安装 | dsh plugin add github:you/repo | 源码(不是构建产物) | 需要(pnpm ≥ 10) |
10.1 发布到 npm(推荐给普通用户分发的首选)
# 1. 准备 npm 账户并登录npm login# 2. 先构建再发布(prepublishOnly 里做构建也行)pnpm buildnpm publish # 或 pnpm publish# 3. 验证:在某个 profile 里安装,确认能挂载dsh plugin --profile dev add your-plugindsh --profile dev --dump-config
发布前检查:入口正确导出 name / inject / apply;inject 里依赖的服务提供方要声明进 package.json;版本从 0.x 起步并遵循语义化版本;选择明确的开源协议(MIT / Apache-2.0 常见)。
10.2 交付 tarball
# 作者侧:打出 tgzpnpm pack# 用户侧:直接安装 tarball 文件,零授权dsh plugin add ./hello-plugin-0.1.0.tgz
10.3 Git 安装(最灵活,但有一道坎)
Git 安装拉取的是源码,没有任何环节替你运行 build 脚本——TypeScript 包到手没有 lib/ 输出,加载会失败。所以:
- 作者侧:必须提供自包含的
prepare脚本(pnpm 在 git 安装后运行它完成构建)。它不能假设仅开发环境存在的上下文(比如旁边有一份 monorepo checkout)。 - 用户侧:pnpm ≥ 10 首次安装会拒绝运行 git 依赖的构建脚本,需要在该 profile 的
pnpm-workspace.yaml里添加allowBuilds授权(按报错提示复制 key 即可)。这等于允许该包的代码在你机器上执行。 - 安全建议:git 安装时锁定 commit——
dsh plugin add github:you/repo#<完整commit-sha>,避免后续推送改变实际运行的代码。
10.4 让别人发现你的插件
官方指定的发现渠道非常简单——给插件仓库加上 GitHub 话题 dsh-plugin。加了话题的仓库会被社区聚合目录(awesome 清单、各类插件索引站)自动收录。
其他渠道:
- GitHub Discussions 社区板块分享插件与反馈
- DeepSeek Harness Discord 社区
- 第三方插件聚合站(会被
dsh-plugin话题自动索引)
十一、打包规范与发布前检查清单
一个标准 DSH 社区插件包(bundle)的结构:
your-plugin/ package.json # 声明 "dsh": { "bundle": { "patch": "./cordis.patch.yml" } } # main/types/exports 指向真实生成的 lib/ # files 只收录运行入口/声明/许可证/README/组合层 # Cordis 与 Service Definition 包放 peer + dev deps,自有实现放 dependencies cordis.patch.yml # bundle 的 patch 层:按行 id 插入插件行,插件按包名解析 src/index.ts # 函数插件:命名导出 name/inject/Config/apply README.md # 服务 API、事件、扩展点、安装命令、Known Limitations LICENSEpackage.json 关键片段:
{ "name": "dsh-your-plugin", "version": "0.1.0", "type": "module", "main": "./lib/index.js", "types": "./lib/index.d.ts", "exports": { ".": "./lib/index.js" }, "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }}cordis.patch.yml:
- insert: - id: your-plugin name: 'dsh-your-plugin' # 用包名,不要用 checkout 相对路径
发布前检查清单(可直接复制进 PR 描述):
- 架构:能力缝三角色(Service Definition / Provider / Consumer)是否设计完整
- 导出与依赖:
name/inject/Config/apply命名导出完整;inject的服务提供方已声明依赖 - 生命周期:所有注册可逆,HMR 下释放 fiber 后注册消失
- Config 与错误:部署期可变项全部做成配置字段;误配置 fail loud
- 工具与 UI:
execute()契约遵守;output.render与 UI 卡片分离 - 测试与文档:行为测试、真实组合测试、README 含 Model Experience 段
- 构建打包安装:
pnpm pack产物在干净 profile 里能dsh plugin add成功并出现在--dump-config中
十二、测试与质量门
在仓库内开发时,新增/修改包后逐级往上跑(本地只跑受影响的,CI 才全量):
pnpm run constraints # workspace 约束pnpm run typecheck # strict 类型检查,无 any 逃逸pnpm run lint # oxlintpnpm run buildpnpm run hygiene # knip + publint + NodeNext 消费检查pnpm run test # vitest 单元测试
测试方针要点:
- 行为测试描述行为;改行为要同步改测试并说明原因。
- 产品可见的插件要有一个真实组合测试:通过 Loader 启动
cordis.yml,而不是只用手搭的ctx.plugin(...)单测。 - 注册可逆性用 HMR 安全测试验证。
十三、常见问题排查(FAQ)
Q:dsh plugin报错找不到 pnpm?
dsh plugin 是把参数转发给 profile 目录里的 pnpm 执行的。先 npm install -g pnpm。
Q:Git 安装的 TypeScript 插件加载失败?
Git 安装拉的是源码,没人替你跑 build。插件作者要提供自包含 prepare 脚本;用户侧 pnpm ≥ 10 还需要在 profile 的 pnpm-workspace.yaml 里加 allowBuilds 授权。不想折腾就改用 npm 包或 tarball。
Q:怎么确认插件真的挂载了?
dsh --profile ,应该能看到 # == your-plugin 层和你的插件行 id。
Q:插件异常导致 dsh 无法启动,如何临时禁用?
在 profile 的 cordis.patch.yml 里加一行即可,无需卸载:
- id: your-plugin disabled: true
Q:patch 覆盖了配置但没生效?
patch 是整行替换而非深合并——覆盖时必须保留行的 id,且被替换字段要全部重述。
Q:bundle 插件安装后还要手动 insert 吗?
不要。声明了 dsh.bundle 的包会被自动注册进 bundle 层栈;再往 profile 的 cordis.patch.yml 手动 insert 同 id 会报 duplicate loader entry id 导致无法启动。
Q:核心 API 会变吗?
会。开发者预览阶段官方明确声明有兼容性破坏变更。建议:pin 住你实验用的仓库 commit 或包版本;以官方文档和 --dump-config 实际输出为准。
-
08.25
科大讯飞与麒麟软件达成战略合作 多行业落地智能体联合创新
-
08.25
Anthropic CEO只直接管一个人
-
08.25
我国牵头制定国际标准 为磁性元件提供权威技术标尺讲了什么-主要信息和内容重点
-
08.25
地鼠传奇限时活动如何玩-地鼠传奇限时活动玩法介绍
-
08.25
人民教育音像数字出版社与小猿达成合作讲了什么-主要信息和内容重点
-
08.25
AI如何进行数据收集助力企业决策与创新的关键策略
-
-
下载
- |
-
-
下载
- 《行尸走肉第一章》免安装中文汉化硬盘版下载
- 单机|436 MB
- 一款以动作冒险为主题的游戏
-
-
下载
- 《街头霸王X铁拳》免安装中文汉化硬盘版下载
- 单机|111MB
- 一款非常好玩的格斗游戏
-
-
下载
- |
-
-
下载
- 《暗黑破坏神3》免安装繁体中文正式版下载
- 单机|7630 MB
- 一款以角色扮演为主题的游戏
-
-
下载
- 《马克思佩恩3》免安装硬盘版下载
- 单机|27033 MB
- 一款以第三人称射击为主题的游戏