作者:李天生
前言
最近 Deepseek Harness 在 Github 很受追捧,短短几周就拥有了 200k+ 的 star,它跟其他的 agent 框架有何不同,为什么还能在这个拥挤的赛道出圈?这篇文章主要分析一下它的“Everything is a plugin”的设计和具体应用。
Deepseek Harness 是什么?
DeepSeek Harness(简称dsh)是由 DeepSeek AI 开发的开源 agent harness(智能体框架)。
它采用一切皆插件的架构,由 Cordis 驱动, 由于这种一切皆是插件化的设计,我们可以高度定制我们自己需要能力,十分灵活。
运行的话有两种方式
- 通过
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 桌面版很像。那么 dsh 源码里面是怎么实现插件化能力的呢?接下来带着这个问题我们来分析一下他的源码。
源码分析
目录结构
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>
从 UI 界面到底层 agent loop 全部抽离成了一个个的插件,目录按能力分组:
packages/
├── core/ 核心agent能力相关
├── api/
├── llm/
├── shell/
├── client/
├── ....../
插件写法
插件是一个导出 apply 函数的 TypeScript 模块。框架在加载时调用 apply,传入一个 ctx(上下文对象),通过 ctx 注册能力。插件有三种写法:函数形式、对象形式、类形式。大多数情况下,函数形式足够了。当插件需要向其他插件提供服务时,可使用类形式(见 服务与依赖)。
函数形式
import type { Context } from '@deepseek-ai/cordis'
export const name = 'hello-plugin'
export function apply(ctx: Context) {
// Required dependencies are ready before apply runs.
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')
// Perform synchronous initialization in the constructor.
}
}
知道了插件写法之后,那么这一个个的插件之间是怎么运行起来、互相通信的呢?接下来从源码里面分析一下具体实现。
Dsh 架构分析:
我们从服务的启动出发,看看都执行了什么:
首先从启动命令 pnpm dsh --profile web 这个命令开始找到入口文件:
package.json:
"scripts": {
"dsh": "node --import tsx/esm apps/cli/src/bin.ts",
}
可以看到是入口文件是 apps/cli/src/bin.ts,然后找到如下逻辑:
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
}
}
入口文件的逻辑比较简单就是根据不同的 mode 走不同的方法,接下来我们重点去看 runProfile 的逻辑。
核心代码:
// 简化中间过程文件 apps/cli/src/profile-boot.ts runProfile 中调用 boot 方法
import {
boot,
} from '@deepseek-ai/dsh-app-boot'
export async function runProfile() {
const ctx = await boot(xxx)
}
// /packages/boot/app-boot
export async function boot(...) {
const ctx = new Context()
await ctx.plugin(Loader)
await mountRootInclude(ctx, absoluteConfigPath, patches)
return ctx
}
Boot 就是我们这次启动的核心方法,简化后剩三个方法,分别对应:
- new Context 就是 Cordis 的初始化,管理上下文、插件等
- 然后就是把 loader 挂载到 Cordis 上下文中
- 之后就是使用 loader 处理我们 packages 下的各个插件,组成我们的插件树
核心源码位置:github.com/deepseek-ai…
这三个方法能力如下图所示:
在 Cordis 里会有个 fiber 的概念跟 react 的 fiber 很像: Cordis 的 fiber 是一个插件运行实例的生命周期管理单元,然后组成一棵可暂停、可更新、可销毁”的树 源码位置
它主要负责四件事:
- 保存插件自己的 Context 和配置
- 等待 inject 依赖满足
- 执行插件代码
- 卸载时清理插件资源
插件通过 ctx.plugin() 创建 Fiber vendor/cordis/src/registry.ts:330
const fiber = new Fiber(
this.ctx,
config,
Inject.resolve(plugin.inject),
runtime,
getOuterStack,
)
插件通信
现在插件之间主要有三种通信方式:
- 服务注入 / 服务调用:一个插件提供服务,另一个插件通过
inject获取并调用。 - 事件总线:插件通过
ctx.emit()广播事件,其他插件通过ctx.on()监听。 - Waterfall 扩展链:多个插件按顺序处理同一个事件,监听器必须调用
next()才能继续传递。
服务注入:插件之间的直接通信
Provider 插件负责注册服务:
ctx.provide('llm', llmService)
Consumer 插件声明依赖:
export default {
inject: ['llm'],
apply(ctx) {
ctx.llm.stream(request)
},
}
事件通信:插件之间的广播
发送方:
ctx.emit('session/event', session, event)
接收方:
ctx.on('session/event', (session, event) => {
// 处理事件
})
Waterfall:可修改的扩展链
Waterfall 事件和普通事件不同,它允许前一个插件把处理权交给下一个插件:
ctx.on('tools/execute', async (exec, next) => {
const result = await next()
return transform(result)
})
调用关系类似:
Plugin A
↓ next()
Plugin B
↓ next()
实际执行器
↓
Plugin B 后处理
↓
Plugin A 后处理
大致了解了 dsh 架构之后可能会有疑问,那到底我们能用 dsh 做哪些 codex 做不到的事情?
Dsh 应用
续接上文:《AI 乱改代码?试试这套 SDD 规范驱动工作流》
我们尝试了基于 openspec 的 SDD 的研发工作流。但是实操过上一篇文章的朋友应该能发现,这套流程是在现有的 code agent 工具中结合的,使用起来的融合度会差一些,尤其是在对话中我们可能会忘记使用,导致 spec 文档出现偏移,反而会污染我们的上下文。刚好我们可以利用 dsh 的插件组合的能力,把 SDD 融入到 dsh 中。
流程设计
根据之前的痛点总结出如下几点,我们 dsh 需要具备如下几种能力:
- 负责spec 、agents.md 文件的初始化、并且提供流程强制校验
- UI界面spec 流程进度跟踪、归档提醒
- 输出过程跟踪,对话提醒
- 强制执行 spec 流程,防止 spec 文档出现偏移
- Coding 完成后的 spec 自检和代码的自检
在导入项目时,会初始化 spec 、agents.md 文件,然后对话面板中 prompt 携带 sdd、 spec、openspec 等词汇时会自动触发 SDD 流程,并且会强制走着一套流程,如果没有走则会进行拦截,并且在右侧面板是实时显示 spec 进度,完成情况,在 agent 输出完成之后会自动对 spec 文档和代码进行验证。
插件设计
这组能力拆分为两个插件层:SDD 流程服务插件 和 SDD 界面插件。流程服务插件维护状态并执行约束,界面插件通过 Host 事件流读取状态并渲染。
SDD 流程服务插件
项目初始化
private async initializeOnce(path: string): Promise<SddWorkflowResult> {
if (!this.config.enabled) return { path, openspec: 'disabled', agents: 'disabled' }
const openspec = await this.initializeOpenSpec(path)
const agents = await this.initializeAgents(path)
return { path, openspec, agents }
}
缺少 openspec/ 时执行 openspec init --tools none --force;缺少根目录 AGENTS.md 时,根据有界项目快照请求模型生成。快照排除依赖、构建产物、.env、凭据、私钥和证书,并限制文件数量与字节数。已有文件不覆盖,同一路径的并发初始化共享一个 Promise。
Change 状态跟踪
插件执行 openspec list --json,结合 Change 工件推导阶段:
const phase = totalTasks > 0 && completedTasks === totalTasks
? 'verifying'
: completedTasks > 0
? 'implementing'
: await has('design.md')
? 'approved'
: await has('proposal.md')
? 'proposed'
: 'draft'
归档目录 openspec/changes/archive 中的项目标记为 archived。完整状态通过 Cordis 事件发布:
ctx.emit('sdd-workflow/status', status)
状态包含项目路径、初始化状态、Change 阶段、任务计数和验证结果。
写入前流程强制
该拦截器通过监听 tools/pre-execute 事件,在工具执行前进行校验和拦截。
if (!isMutatingTool(exec) || exec.agent === undefined) return next()
await this.refreshChanges(path)
if (hasActiveChange(this.latest.get(path)) || isOpenSpecCommand(exec)) return next()
return {
kind: 'deny',
reason: 'OpenSpec workflow required: create or select an active Change before modifying project files.',
}
没有活动 Change 时,write、edit、修改型 bash 和 run_code 被拒绝;openspec ... 命令保留用于创建和推进 Change。skill、ask_user_question 等非写入工具显式放行。
Web bundle 配置:
- id: sdd-workflow
name: '@deepseek-ai/dsh-sdd-workflow'
config:
enforceWorkflow: true
validateOnStop: true
codeChecks:
- pnpm run typecheck
- pnpm run lint
- pnpm run test
完成后的 Spec 和代码自检
Change 进入 verifying 后,Host 执行:
await this.runValidationCommand(command, path, timeout, [
'validate', '--all', '--no-interactive', '--strict',
])
OpenSpec 通过后执行 codeChecks。结果写入 ProjectVerificationStatus;失败时通过litiaagent.steer() 返回诊断,全部通过时返回归档提醒。
SDD 界面插件
ui-workspace:工作区树状态
实现位置:packages/client/ui-workspace/src/client/tree.ts、packages/client/ui-workspace/src/client/rows/Rows.tsx。
Host 事件消息在 WorkspaceManager 中按项目路径缓存:
} else if (envelope.payload.type === 'host/sdd-workflow-status') {
const { status } = envelope.payload
this.sddWorkflow = { ...this.sddWorkflow, [status.path]: status }
this.notifier.markDirty()
}
deriveGroups() 将状态挂到 Workspace 节点,Rows.tsx 把 pending、loading、completed、failed 映射为状态点和文本,显示初始化结果摘要。
ui-project-details:详情进度与归档提醒
组件先从 useSessions 读取当前会话,再从 useWorkspaces 找到会话所属的 Workspace;如果会话还没有归属 Workspace,则回退到会话自身的 cwd。这个路径是状态表的唯一索引,避免详情栏根据自己的文件扫描结果重新计算项目状态:
const status = workspaceState.sddWorkflow?.[workspace?.path ?? cwd ?? '']
const changes = status?.changes ?? []
没有匹配状态时,组件仍显示项目标题和路径,但不渲染初始化、Change 或验证区域。
匹配到 SddWorkflowStatus 后,详情栏显示 AGENTS.md 和 OpenSpec 的初始化状态,并列出每个 Change 的名称、阶段、已完成任务数和总任务数。
存在 Change 时,详情栏还会汇总任务进度,并显示所有 Change 中最新的 lastModified 值。
验证信息也来自同一份状态快照。组件显示验证总体状态,以及每项 codeChecks 的通过或失败结果;失败项附带服务端返回的错误文本。
只有当验证状态为 passed 且至少一个 Change 仍处于 verifying 阶段时,才显示归档提醒:
{status.verification.state === 'passed' && changes.some(change => change.phase === 'verifying') &&
<div className={css.reminder}>Ready to archive this Change.</div>}
状态由插件 dsh-sdd-workflow 生成。它执行 openspec list --json,读取 Change 工件,发布 sdd-workflow/status。
host-apiproxy 将事件转换为 host/sdd-workflow-status 事件消息,并在新连接建立时发送 statuses() 返回的完整快照。 Client 的 WorkspaceManager 按项目路径更新 sddWorkflow,再把它提供给两个 UI 插件。ui-workspace 显示初始化状态,ui-project-details 显示 Change 和验证详情;两者都不执行 OpenSpec 命令,也不自行推导阶段,重连后会从同一份快照恢复状态。
如下图所示,左侧增加了项目初始化的展示,中间 AI 输出区增加了 sdd 流程拦截校验,右侧详情区域新增了SDD流程进度的展示:
扩展
除此之外我们还能利用 dsh 做些什么?现在有很多公司在做 AI 研发需求的闭环,需求提出后 AI 就能自动完成需求的开发、审查、发布。从 0 - 1 开发的话,成本无疑是巨大的,但是通过 dsh 我们可以低成本完成半自动化的流程,如下图所示:
把需求和 coding 建立在同一个工作流中,人工只需要维护关键的节点介入。
总结
本文其实并没有讲 dsh agent 本身的能力,因为在现在其实各家 agent 能力都大同小异,dsh 出圈真正的原因是灵活的插件组合能力,赋予未来更多的想象。