几周时间,200k+ star。
这个数字放在 agent 框架这个已经挤满选手的赛道里,有点反常。模型能力各家大差不差,一个 harness 想靠功能差异化出圈很难。DeepSeek Harness 的差异化不在功能列表上,而在它的一句设计口号里:Everything is a plugin。
这篇文章不聊怎么用它,只拆源码:插件长什么样、按下启动命令后发生了什么、一堆插件之间靠什么互相通信。
一、DeepSeek Harness 是什么
DeepSeek Harness(简称 dsh)是 DeepSeek AI 开源的 agent harness(智能体框架),底层由 Cordis 驱动。
Cordis 是一套以插件为中心的运行时。dsh 把从 UI 界面到最底层 agent loop 的所有能力,全部拆成了独立的插件。换个说法:它没有不可替换的核心,每个环节都可以被你的插件接管。
跑起来有两种方式。
第一种,直接跑:
npx @deepseek-ai/dsh web
第二种,从源码启动:
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web
启动后配置好 key 就能用,界面和交互跟 codex 桌面版很像。但真正值得看的是源码里的这套插件机制。下面从目录结构开始。
二、源码分析
目录结构
deepseek-harness/
├── apps/ # 最终可运行的应用
├── packages/ # dsh 的插件包
├── vendor/ # Vendored Cordis 源码
├── python/ # Python SDK 和运行时
├── native/ # 原生扩展
├── examples/ # 用户可运行的 cordis.yml 示例
├── docs/ # 架构、子系统和开发文档
├── website/ # VitePress 文档站
├── scripts/ # 构建、检查、生成器
└── .agents/ # Agent 工作流和 Agent Notes
packages/ 是仓库的主体。每个子目录是一个独立的 npm workspace package,命名统一为 @deepseek-ai/dsh-<name>,并按能力分组:
packages/
├── core/ # 核心 agent 能力
├── api/
├── llm/
├── shell/
├── client/
└── ...
注意 vendor/cordis:Cordis 不是当依赖装的,而是直接把源码 vendored 进仓库。所以下面讲的 Context、Fiber、Loader 这些机制,都在你本地仓库里能翻到。
插件写法
插件本质上就是一个导出 apply 函数的 TypeScript 模块。框架加载它时调用 apply,传入一个上下文对象 ctx,插件通过 ctx 注册自己的能力。
有三种写法。
函数形式,最常见,够用:
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
// 必需的依赖在 apply 执行前就已经就绪
console.log('[hello-plugin] plugin loaded!')
}
对象形式,需要声明依赖时更方便:
import type { Context } from '@deepseek-ai/cordis'
export default {
name: 'my-plugin',
inject: ['tools'],
apply(ctx: Context) {
// ...
},
}
类形式,当这个插件要对外提供服务时使用:
import { Service, type Context } from '@deepseek-ai/cordis'
export default class MyService extends Service {
static inject = ['tools']
constructor(ctx: Context) {
super(ctx, 'myService')
// 同步初始化放在构造函数里
}
}
写法本身没什么门槛。真正的问题在后面:这些插件是怎么被组织成一棵树、又是怎么互相调用的。
启动流程:从一条命令到一棵插件树
从启动命令 pnpm dsh --profile web 反查入口。package.json 里:
"scripts": {
"dsh": "node --import tsx/esm apps/cli/src/bin.ts"
}
入口是 apps/cli/src/bin.ts。里面按 mode 分发:
switch (invocation.mode) {
case 'profile': {
const { runProfile } = await import('./profile-boot.ts')
await runProfile({
environment: loadLayeredEnv('dsh'),
profile: invocation.profile,
patchFiles: invocation.patches,
args: invocation.args,
})
break
}
}
runProfile 本身很薄,核心就是调 boot:
import { boot } from '@deepseek-ai/dsh-app-boot'
export async function runProfile() {
const ctx = await boot(/* ... */)
}
boot 在 packages/boot/app-boot 里,去掉中间过程后只剩三步:
export async function boot(...) {
const ctx = new Context()
await ctx.plugin(Loader)
await mountRootInclude(ctx, absoluteConfigPath, patches)
return ctx
}
三步分别对应:
new Context():Cordis 的初始化,负责管理上下文与插件。ctx.plugin(Loader):把 Loader 挂到上下文上。mountRootInclude(...):用 Loader 加载packages下的插件,组装成插件树。
启动 = 建 Context → 挂 Loader → 按配置长出插件树。 没有更长的主流程,也没有硬编码的能力清单。
Fiber:插件的生命周期单元
插件树上的每个节点,在 Cordis 里对应一个 Fiber。它跟 React Fiber 的思路很像:Fiber 是插件运行实例的生命周期管理单元,所有 Fiber 组成一棵可暂停、可更新、可销毁的树。
每个 Fiber 负责四件事:
- 保存插件自己的 Context 和配置
- 等待
inject声明的依赖满足 - 执行插件代码
- 卸载时清理插件资源
插件通过 ctx.plugin() 创建 Fiber,源码在 vendor/cordis/src/registry.ts:
const fiber = new Fiber(
this.ctx,
config,
Inject.resolve(plugin.inject),
runtime,
getOuterStack,
)
这一步也解释了 inject 的意义:它不是加载顺序的提示,而是 Fiber 在真正执行 apply 之前必须等到的前置条件。依赖没满足,这个 Fiber 就停在等待态,不会执行。
插件通信
插件拆散之后,下一个问题就是它们怎么协作。dsh 里主要有三种方式。
1. 服务注入 / 服务调用:一个插件提供服务,另一个通过 inject 拿到并调用。
Provider 注册服务:
ctx.provide('llm', llmService)
Consumer 声明依赖:
export default {
inject: ['llm'],
apply(ctx) {
ctx.llm.stream(request)
},
}
2. 事件总线:发送方广播,任何插件都能监听,属于一对多。
发送方:
ctx.emit('session/event', session, event)
接收方:
ctx.on('session/event', (session, event) => {
// 处理事件
})
3. Waterfall 扩展链:多个插件按顺序处理同一个事件,监听器必须调用 next() 才能把处理权交给下一个。
ctx.on('tools/execute', async (exec, next) => {
const result = await next()
return transform(result)
})
它的调用顺序是洋葱式的:每个插件都能在真正的执行器前后插入逻辑。
Plugin A
↓ next()
Plugin B
↓ next()
实际执行器
↓
Plugin B 后处理
↓
Plugin A 后处理
这三种方式覆盖了绝大多数协作场景:需要强依赖就走服务注入,需要广播就走事件,需要拦截或改写一条处理链就用 Waterfall。前面两个负责「连起来」,Waterfall 负责「插进去」——想在工具真正执行前做校验、执行后加工结果,靠的就是它。
三、总结
这篇文章没有展开 dsh 作为 agent 本身的能力,因为那部分各家已经趋同。dsh 能出圈,靠的是它把整个系统拆成插件后带来的组合能力:主流程只有「建 Context、挂 Loader、长插件树」三步,剩下的全交给插件;插件之间用服务注入、事件、Waterfall 三种方式协作,任何一环都能被替换或拦截。
换句话说,它的价值不在于替你写好了多少能力,而在于把改写的成本降到了写一个插件。