生命不息,折腾不止。上次把 DeepSeek Harness 接上中转站只是「会用」,这篇带你走进它的灵魂——自己动手写插件,30 行代码给 Agent 加一个会干活的工具。

一、插件到底是个啥:一切皆插件

DeepSeek Harness(命令行叫 dsh)是 DeepSeek 官方开源的 Agent 框架,它的核心设计就一句话:一切皆插件。这句话不是营销,是字面意思——模型适配器、工具注册表、会话日志、甚至 agent 主循环本身,全都是插件。整个产品就是启动时从若干层配置里组合出来的一棵插件树。

底层驱动是一个叫 Cordis 的插件框架(DeepSeek 魔改了一份 vendored 版本)。在 dsh 眼里,插件就是一个 TypeScript 模块,导出 apply(ctx) 函数,框架加载插件时会调用它,把 ctx 上下文对象传进来,你就在里面注册各种能力。

1
2
3
4
5
6
7
import type { Context } from '@deepseek-ai/cordis'

export const name = 'my-plugin'

export function apply(ctx: Context) {
// 在这里注册工具、监听事件、挂服务……
}

这就是一个插件的全部骨架。好处显而易见:想加什么能力不用改主程序,写个插件挂上去就行;插件卸载时,通过 ctx 注册的一切(事件监听、工具、定时器)会自动清理,不用担心残留。

插件有三种形态:函数插件(最常见,上面的就是)、对象插件(带 apply 方法的对象)、类插件Service 子类,适合对外提供服务)。在你需要公开服务之前,一直用函数形态就够了。

二、环境准备:把源码仓库拉下来

写 dsh 插件,官方推荐在源码仓库里开发,这样能用仓库自带的命令行和脚本(typecheck / lint / test / build),还能用 vendored 启动器做不需要 API Key 的纯链路验证

环境要求:Node.js ^22.19 或 >=24,包管理器用 pnpm(仓库用 Corepack 锁了版本)。

1
2
3
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install

装完先跑一把确认环境 OK:

1
2
3
pnpm run typecheck   # 类型检查(strict)
pnpm run lint # oxlint
pnpm run build # tsc + tsdown 产出 lib/

注意:dsh 目前是 developer preview(v0.1 预览版),README 里大写加粗写着「THERE WILL BE COMPATIBILITY-BREAKING CHANGES」——接口会变,插件开发时留意版本,后续文章会讲怎么 pin 版本。

三、30 行写第一个插件:hello 走一个

在仓库根目录建个临时目录,创建 tmp/hello-plugin/hello.ts

1
2
3
4
5
6
7
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(Cordis 配置清单,loader 按它挂插件):

1
- name: './hello.ts'

然后从 tmp/hello-plugin 目录用仓库自带的 vendored 启动器跑起来(这一步不需要 API Key):

1
node --import tsx ../../vendor/cordis/bin.js

预期输出:

1
[info] hello from my first plugin

发生了什么?启动器创建根 Context 并挂上 Loader 插件 → Loader 读取 cordis.yml → 把 ./hello.ts 作为子插件挂载 → Cordis 调用你的 apply(ctx)。链路通了,插件的「最小闭环」就成立了。

一个正式的函数插件通常导出四样东西,认识一下:

导出 作用
name 插件显示名,仅用于诊断日志
inject 声明依赖的服务(如 ['tools']),loader 会等它们就绪才执行 apply
Config 可选,部署期配置的校验 schema
apply(ctx, config) 插件主体,注册一切能力

两个细节:函数插件必须用命名导出export function apply),别配默认导出,否则 loader 会丢掉 inject 元数据;有 Configapply 签名是 (ctx, config),没有时是 (ctx)

四、把插件挂到 dsh 上:三条路

写好的插件怎么让 dsh 真正加载?三条路,按场景选:

路 1:临时 overlay(调试最快)。写一个 patch 文件,用 --patch 参数启动:

1
2
3
4
# scratch-plugin/cordis.yml
- insert:
- id: hello
name: '/绝对路径/to/deepseek-harness/scratch-plugin/src/hello.ts'
1
pnpm dsh web --patch ./scratch-plugin/cordis.yml

打开 http://127.0.0.1:3080(dsh Web UI 默认端口),终端里能看到 [hello-plugin] plugin loaded!。注意插件路径必须写绝对路径

路 2:外置插件(正式安装,推荐)。把插件做成独立 npm 包(package.json 里声明 "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }),然后用 dsh plugin 子命令装进 profile——它本质是在 profile 目录里调 pnpm,所以 pnpm 的子命令全能透传:

1
2
3
4
dsh plugin --profile web add ./hello-plugin          # 本地包
dsh plugin --profile web add github:你的账号/你的仓库 # Git 源,可加 #commit 锁版本
dsh plugin --profile web remove hello-plugin # 卸载
dsh plugin --profile web update # 更新全部

profile 是 dsh 的「可运行环境」,目录在 $DSH_HOME/profiles/<名字>/,web、headless 是内置模板。装完插件记得重启 dsh web,宿主代码在启动时加载,只刷新页面不够。

路 3:看插件树 debug。插件装了却没生效,别对着界面猜,直接打印最终组合出来的配置树:

1
dsh --profile web --dump-config

任何打印出来的行,都可以用你自己的 patch 按行 id 替换。记住 dsh 配置的四层加载顺序:profile bundles → profile 的 cordis.patch.yml → 家目录 $DSH_HOME/cordis.patch.yml → 每个 --patch overlay。后层按行胜出,patch 替换的是整行 config(不是深度合并),改一个字段也得把整行键重述一遍——这是新手最常踩的坑。

五、实战:给模型加一个会干活的工具

插件最常见的用途就是给模型加工具。工具注册在 ctx.tools 上,schema 会自动进入 prompt 组装,模型就能「看到」它并主动调用。

下面是一个完整可运行的最小工具插件(30 行上下,名副其实):让 Agent 能读文件。核心 API 是 defineTool(来自 @deepseek-ai/dsh-tools)+ ctx.tools.register()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 })
},
}))
}

拆开看 execute() 契约的几条硬规则:

  • args 自动校验defineTool 会在 execute 前校验模型生成的参数(类型、必填、枚举、嵌套),你拿到的 args 类型和 schema 一致;
  • 只返回一个规范 JSON 值output.schema 定义返回值,execute 只返回它;抛异常 = 出错(isError),业务上的非理想状态(比如非零退出码)也要放进规范值返回;
  • 遵守 exec.signal:信号触发时要取消进行中的工作,别硬扛;
  • 只注册一次:注册借用的是只读定义,事后别改 schema;想换工具就释放它所属的 effect 再注册新的。

这样一个插件装好后,你在 Web UI 里跟 Agent 说「读一下 /opt/xxx/config.yaml 的前 50 行」,模型就会自己调 read_file 工具,把内容读回来再回答你——这就是给 Agent 装「手」的过程

六、进阶玩法:inject 依赖与事件钩子

不想加新工具,只想在某个环节「插一脚」?用事件钩子。dsh 主循环是事件驱动的,钩子插件就是往这些事件上挂监听器。比如下面这个权限门插件,在每次工具调用前拦截,按规则允许或拒绝:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import type { Context } from '@deepseek-ai/cordis'
import type { PreToolDecision, ToolExecution } from '@deepseek-ai/dsh-tools'

declare function isAllowed(exec: ToolExecution): Promise<boolean>

export const name = 'permission-gate'

export function apply(ctx: Context) {
ctx.on('tools/pre-execute', async (exec, next): Promise<PreToolDecision> => {
if (!(await isAllowed(exec))) {
return { kind: 'deny', reason: 'Denied by policy.' }
}
return next()
})
}

tools/pre-executewaterfall 事件:监听器收到 (...args, next),必须调用 next() 把结果传给下一个监听器;不调 next() 直接 return 就是短路(截断整条链)。这是写监听器最容易踩的坑——忘了 next()

常用扩展点速查:

你想做什么 用哪个
允许 / 拒绝 / 询问工具调用 tools/pre-execute,返回 { kind: 'deny' } / { kind: 'ask' }
工具调用必须被最终否决、不可撤销 ctx.tools.guard()
包裹工具执行生命周期(超时/重试/指标) tools/execute
改写工具结果或呈现内容 tools/post-execute
只观察最终结果(审计/记录) tools/result

七、避坑提醒

  • 安全第一:插件跑在 dsh 宿主进程里,属于可信代码——装第三方插件前,先看它仓库是否公开、许可证和维护者是否清楚、要什么权限,别因为一行安装命令就跳过检查(官方文档自己都反复强调);
  • 预览版接口会变:dsh 还是 v0.1 preview,插件契约可能随版本变动,开发时把 harness 版本记进 README,跟 GitHub Discussions 的变更公告;
  • 函数插件别配默认导出:会丢 inject 元数据,导致依赖的服务没就绪就执行 apply
  • patch 替换整行:不是深度合并,改一个键也要重述整行;
  • 跑真实模型要 API Key:工具插件要真正被模型调用,得有模型可跑。官方 key 直接填 DEEPSEEK_API_KEY 就行;想一个 Key 玩遍 Claude、GPT、Gemini、DeepSeek 全系模型,可以接 ai.aklibk.com 中转站——OpenAI 兼容接口、人民币按量付费,DeepSeek 系模型价格还便宜,settings 里改个 base URL 就换底层模型,插件的价值直接翻倍。

生命不息,折腾不止。下一篇《DeepSeek Harness 多 Agent 协作实战》,教你把一个大任务拆给多个 Agent 并行干活,让 dsh 从「单打独斗」升级成「团队作战」。