DeepSeek Harness 插件开发指南
系列实战篇,基于 DeepSeek Harness(dsh)的 Cordis 插件系统,手把手带你写第一个插件:从
apply(ctx)函数开始,到服务、事件、配置校验,最后注册一个模型可调用的真实工具。
目录
- 插件开发前必须搞懂的三个概念
- 环境搭建:clone 仓库 + 安装依赖
- 第一个插件:apply 函数 + cordis.yml
- 生命周期管理:ctx.effect 与 Fiber 状态机
- 服务(Service):插件间如何互相调用
- 事件(Events):5 种分发模式
- 配置校验:Schema 让插件更健壮
- 实战:注册一个模型可调用的真实工具
- 完整案例:做一个代码仓库分析工具
- 调试与热更新
- 发布与安装插件
- 本篇总结
1. 插件开发前必须搞懂的三个概念
1.1 插件就是一个函数
在 Cordis 的世界里,插件本质是一个函数:
export function apply(ctx: Context) {
// 在这里描述这个插件贡献了什么能力
}
ctx是上下文(Context),插件通过它访问其他插件、注册服务、监听事件- 插件没有框架启动代码——启动、加载、销毁全部由 Cordis 内核管理
- 多个插件由
cordis.yml组合成一个应用,加载顺序由依赖关系决定,与文件位置无关
1.2 三种插件形态
import { Service, type Context } from '@deepseek-ai/cordis'
// 形态 1:函数插件(最常见)
export const name = 'hello'
export function apply(ctx: Context) {
console.log('hello from my first plugin')
}
// 形态 2:对象插件(带 apply 方法的对象)
export const objectPlugin = {
name: 'object-plugin',
apply(ctx: Context) {},
}
// 形态 3:类插件(Service 子类,用于对外公开服务)
export class MyService extends Service {
constructor(ctx: Context) {
super(ctx, 'myService')
}
}
1.3 插件树:整个 Harness 就是一株插件树
根 Context
├── Loader 插件(读入 cordis.yml)
├── hello 插件
├── tools 服务
├── llm 适配器
└── agent loop
模型、工具、会话、沙箱、UI……所有能力都是挂在这棵树上的一枚插件。
2. 环境搭建:clone 仓库 + 安装依赖
插件开发需要拿到 Harness 的源码和类型定义:
# 1. 克隆官方仓库
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
# 2. 安装依赖(仓库使用 pnpm 管理)
pnpm install
# 3. 创建你的插件开发目录
mkdir -p tmp/cordis-tutorial
cd tmp/cordis-tutorial
开发插件不需要 API 密钥——插件本身是纯代码逻辑,只有真正调用模型时才需要密钥。
验证环境是否可用(不调用任何模型):
node --import tsx ../../vendor/cordis/bin.js
3. 第一个插件:apply 函数 + cordis.yml
3.1 创建插件文件 hello.ts
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello'
export function apply(ctx: Context) {
console.log('hello from my first plugin')
}
3.2 创建组合文件 cordis.yml
- name: './hello.ts'
3.3 运行
node --import tsx ../../vendor/cordis/bin.js
# 输出: hello from my first plugin
就这么简单——一个插件跑起来了。
关键认知:
cordis.yml里各个配置项并发启动- 加载顺序由**服务依赖(
inject)**决定,不是文件写的先后顺序 - 插件文件里不需要任何启动逻辑,
apply被调用即插件加载
4. 生命周期管理:ctx.effect 与 Fiber 状态机
插件有时会创建定时器、打开连接、启动 watcher 等"外部资源"。这些资源不受 Cordis 管理,必须包装进 ctx.effect(),并提供销毁函数(disposer)。
4.1 正确管理定时器的例子
// lifecycle.ts
import type { Context } from '@deepseek-ai/cordis'
export const name = 'lifecycle-demo'
function heartbeat(ctx: Context) {
console.log('heartbeat plugin loading')
ctx.effect(() => {
const timer = setInterval(() => console.log('tick'), 200)
// 返回 disposer:插件卸载时自动清理
return () => {
clearInterval(timer)
console.log('heartbeat cleaned up')
}
})
}
export function apply(ctx: Context) {
const fiber = ctx.plugin(heartbeat) // 子插件
ctx.effect(() => {
const timer = setTimeout(async () => {
await fiber.dispose()
console.log('disposed')
process.exit(0)
}, 700)
return () => clearTimeout(timer)
})
}
4.2 Fiber 状态机
每个插件加载后都是一个 Fiber,状态流转:
PENDING → LOADING → ACTIVE → UNLOADING → DISPOSED
↘ FAILED(加载或运行出错)
4.3 已内建 effect 的 API(不用手动销毁)
| API | 说明 |
|---|---|
ctx.on(event, listener) | 事件监听,插件卸载时自动移除 |
ctx.plugin(child) | 子插件,随父插件一起销毁 |
ctx.tools.register(...) | 工具注册,自动附着到插件生命周期 |
注意:disposer 按注册逆序启动,但多个异步 disposer 是并发运行的。需要严格按顺序拆除时,应放在同一个 disposer 中依次
await。
5. 服务(Service):插件间如何互相调用
服务是插件对外暴露能力的方式。提供服务和消费服务解耦,两个插件可以互相不认识,却能协作。
5.1 提供服务:greeter.ts
import { Service, type Context } from '@deepseek-ai/cordis'
// 声明合并:给 Context 加上类型提示
declare module '@deepseek-ai/cordis' {
interface Context {
greeter: GreeterService
}
}
export class GreeterService extends Service {
constructor(ctx: Context) {
super(ctx, 'greeter') // 注册到 ctx.greeter
}
greet(who: string) {
return `Hello, ${who}!`
}
}
export const name = 'greeter'
export function apply(ctx: Context) {
ctx.plugin(GreeterService)
}
5.2 消费服务:consumer.ts
import type { Context } from '@deepseek-ai/cordis'
export const name = 'consumer'
export const inject = ['greeter'] // 声明硬依赖
export function apply(ctx: Context) {
console.log(ctx.greeter.greet('world'))
}
5.3 组合运行
- name: './greeter.ts'
- name: './consumer.ts'
输出:Hello, world!——哪怕把顺序交换,结果也一样。
5.4 依赖要点
| 场景 | 做法 |
|---|---|
| 服务必须存在 | export const inject = ['greeter'](硬依赖) |
| 服务缺失时 | 插件保持 PENDING,不崩溃、不部分运行 |
| 服务运行中消失 | 依赖它的插件被卸载,恢复后重新加载(支持热替换) |
| 服务可有可无 | 不用 inject,用 ctx.get('greeter') 探测 |
| 命名冲突 | 服务名是扁平命名空间,建议加前缀,如 my-tutorial-greeter |
6. 事件(Events):5 种分发模式
插件之间除了服务调用,还能通过事件松耦合通信。
6.1 声明、发出、监听
// stats.ts —— 声明事件并发出
import { Service, type Context } from '@deepseek-ai/cordis'
declare module '@deepseek-ai/cordis' {
interface Context {
stats: StatsService
}
interface Events {
'stats/report'(name: string, count: number): void
}
}
export class StatsService extends Service {
private counts = new Map<string, number>()
constructor(ctx: Context) {
super(ctx, 'stats')
}
bump(name: string) {
const next = (this.counts.get(name) ?? 0) + 1
this.counts.set(name, next)
this.ctx.emit('stats/report', name, next) // 发出事件
}
}
export const name = 'stats'
export function apply(ctx: Context) {
ctx.plugin(StatsService)
}
// reporter.ts —— 监听
import type { Context } from '@deepseek-ai/cordis'
import type {} from './stats.ts' // 仅为类型,无运行时导入
export const name = 'reporter'
export const inject = ['stats']
export function apply(ctx: Context) {
ctx.on('stats/report', (name, count) => {
console.log(`[stats] ${name} -> ${count}`)
})
ctx.stats.bump('tool_call')
ctx.stats.bump('prompt')
}
6.2 五种分发模式对比
| 模式 | 调用方式 | 行为 |
|---|---|---|
| emit | ctx.emit(name, ...args) | 同步广播,不等待返回值 |
| parallel | await ctx.parallel(name, ...args) | 所有监听器并发运行并一起等待 |
| serial | await ctx.serial(name, ...args) | 顺序运行,第一个非空返回值胜出并短路 |
| bail | ctx.bail(name, ...args) | serial 的同步版本 |
| waterfall | ctx.waterfall(name, ...args, next) | 环绕中间件,可转换或短路 |
6.3 Waterfall:拦截/变体模式
declare module '@deepseek-ai/cordis' {
interface Events {
'demo/transform'(input: string, next: () => Promise<string>): Promise<string>
}
}
export const name = 'waterfall-demo'
export function apply(ctx: Context) {
// 监听器 1:把下游结果转大写
ctx.on('demo/transform', async (input, next) => {
const downstream = await next()
return downstream.toUpperCase()
})
// 监听器 2:命中关键词直接短路(veto)
ctx.on('demo/transform', async (input, next) => {
if (input.includes('blocked')) return '** blocked **'
return next()
})
void (async () => {
console.log(await ctx.waterfall('demo/transform', 'hello', async () => 'hello'))
console.log(await ctx.waterfall('demo/transform', 'blocked words', async () => 'blocked words'))
})()
}
输出:
HELLO
** BLOCKED **
纪律:只做观察/修改的 waterfall 监听器必须调用
next();不调用即代表有意短路(否决)。
7. 配置校验:Schema 让插件更健壮
插件可以声明配置项,用 Schema 做运行时校验。
// config-demo.ts
import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'
export const name = 'config-demo'
export interface Config {
greeting: string
targets: string[]
}
export const Config: Schema<Config> = Schema.object({
greeting: Schema.string().default('Hello'),
targets: Schema.array(String).default(['world']),
})
export function apply(ctx: Context, config: Config) {
for (const target of config.targets) {
console.log(`${config.greeting}, ${target}!`)
}
}
- name: './config-demo.ts'
config:
targets: ['alpha', 'beta']
输出:
Hello, alpha!
Hello, beta!
校验失败的后果:
- 配置无效 → 抛出
ValidationError→ fiber 进入FAILED→ 进程以码 1 退出 - 支持
!!js标签计算配置值:greeting: !!js process.env.DEMO_GREETING ?? 'Hello'
8. 实战:注册一个模型可调用的真实工具
前面都是地基,现在进入真正的 Harness 开发:给 Agent 注册一个模型可以调用的工具。全程无需 API 密钥。
8.1 工具插件 greet-tool.ts
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { CallId } from '@deepseek-ai/dsh-llm'
export const name = 'greet-tool'
export const inject = ['tools']
export function apply(ctx: Context) {
// 1. 注册工具:模型在对话中看到 name/description/parameters 后决定调用
ctx.tools.register(defineTool({
name: 'greet',
description: 'Greet the named person.',
parameters: {
name: { type: 'string', required: true, description: 'Who to greet' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
return `Hello, ${args.name}!`
},
}))
// 2. 手动触发一次工具执行,验证链路
void (async () => {
const result = await ctx.tools.execute({
callId: CallId('demo-1'),
name: 'greet',
arguments: { name: 'Cordis' },
signal: new AbortController().signal,
})
console.log('tool replied:', JSON.stringify(result.content))
})()
}
8.2 观察插件:监听工具执行事件
// tool-logger.ts
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-tools'
export const name = 'tool-logger'
export const inject = ['tools']
export function apply(ctx: Context) {
ctx.on('tools/result', (exec, result) => {
const text = result.content
.map(block => (block.type === 'text' ? block.text : ''))
.join('')
console.log(`[tool-logger] ${exec.name} -> ${text}`)
})
}
8.3 组合运行(注意 dsh-tools 依赖 systemPrompt 服务)
- name: '@deepseek-ai/dsh-system-prompt'
- name: '@deepseek-ai/dsh-tools'
- name: './tool-logger.ts'
- name: './greet-tool.ts'
输出:
[tool-logger] greet -> Hello, Cordis!
tool replied: [{"type":"text","text":"Hello, Cordis!"}]
这就是 Harness 插件开发的精髓:你的工具被注册进 ctx.tools 后,模型在对话中只要看到合适的场景就会自动调用它——你写的 execute 函数就是工具的"身体"。
9. 完整案例:做一个代码仓库分析工具
把前面的知识点串起来,写一个实用工具:统计项目代码量,让模型通过它了解一个仓库的规模。
// repo-stats-tool.ts
import { readdir, stat } from 'node:fs/promises'
import { join } from 'node:path'
import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'
export const name = 'repo-stats-tool'
export const inject = ['tools']
const IGNORED = new Set(['node_modules', '.git', 'dist', '.next'])
async function countLines(dir: string): Promise<{ files: number; lines: number }> {
let files = 0
let lines = 0
for (const entry of await readdir(dir, { withFileTypes: true })) {
if (IGNORED.has(entry.name)) continue
const full = join(dir, entry.name)
if (entry.isDirectory()) {
const sub = await countLines(full)
files += sub.files
lines += sub.lines
} else if (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) {
files += 1
const content = await stat(full)
lines += Math.max(content.size / 30, 1) // 估算行数
}
}
return { files, lines }
}
export function apply(ctx: Context) {
ctx.tools.register(defineTool({
name: 'repo_stats',
description: 'Count total TypeScript files and estimated lines in a directory.',
parameters: {
directory: { type: 'string', required: true, description: 'Absolute path to analyze' },
},
output: {
schema: { type: 'string' },
render: (_args, value) => [{ type: 'text', text: value }],
},
async execute(args) {
const { files, lines } = await countLines(args.directory)
return `Found ${files} TS files, ~${lines} lines`
},
}))
}
工具设计的三个原则:
- 描述要具体:
description直接决定模型何时调用它,写清楚参数含义 - 参数要少而清晰:模型靠 JSON Schema 决定填什么
- 执行要快、可失败:工具返回错误文本比抛异常更友好(模型能读懂并换策略)
10. 调试与热更新
10.1 常见错误处理行为
| 场景 | 行为 |
|---|---|
apply 抛异常 | 进程终止,不会只跳过该配置项 |
| 模块路径/包名拼写错误 | 通过 logger 报告,不崩溃(但可能被吞掉,注意日志) |
| 配置校验失败 | fiber 进入 FAILED,进程码 1 退出 |
10.2 热更新(HMR)
Cordis 支持配置热重载。当你修改 cordis.yml 或插件源码后:
- 已加载的插件按需卸载、重新加载
- 服务消失时依赖插件先卸载,服务恢复后自动重新加载
- 插件停在 PENDING 时,可通过诊断工具查看等待哪个依赖
11. 发布与安装插件
插件开发完,可以发布成 npm 包让别人直接引用:
# 发布(用你项目的包管理器)
npm publish
安装方在 cordis.yml 里一行引入:
- name: 'my-company/hello-plugin' # 已发布到 npm 的包名
Harness 官方也提供社区插件生态,开发者可以从官方插件里 fork 学习,再按自己的场景裁剪。
12. 本篇总结
| 要点 | 内容 |
|---|---|
| 插件本质 | 一个 apply(ctx) 函数,能力由 ctx 组合 |
| 组合方式 | cordis.yml 声明插件列表,依赖决定加载顺序 |
| 生命周期 | ctx.effect() 管理外部资源,Fiber 状态机自动流转 |
| 服务 | Service 子类公开能力,inject 声明硬依赖 |
| 事件 | emit / parallel / serial / bail / waterfall 五种分发 |
| 配置 | Schema 校验,配置错误明确报错 |
| 真实工具 | defineTool + ctx.tools.register,模型可自动调用 |
| 核心思想 | 把能力拆成可组合、可替换、可热更新的插件 |
给初学者的路径建议:
- 先跑通
hello.ts,理解 apply + yml 的魔法 - 再写一个 Service + 一个 consumer,理解依赖注入
- 然后用
defineTool注册你的第一个真实工具 - 最后去看
examples/headless-agent/cordis.yml,对照理解官方所有配置项
掌握了这四步,你就拥有了扩展 DeepSeek Harness 的完整能力——一切皆插件,现在轮到你写插件了。
更多推荐

所有评论(0)