乐于分享
好东西不私藏

DeepSeek-Harness 源码剖析(一):这个仓库里根本没有"产品"

DeepSeek-Harness 源码剖析(一):这个仓库里根本没有"产品"

摘要:「万物皆插件」不是修辞——DSH 仓库里真的没有内核。Agent 循环、工具、会话全是地位平等的插件,产品形态由 Bundle/Profile/Patch 三层 YAML 叠加、一次合成出来。这篇把组装层从 YAML 到插件树的全过程拆开。

先说个事:之前我零散拆过 5 篇 dsh(万物皆插件、热插拔、guard、多智能体、自举插件),那是案例集。从这个系列开始系统性重来——《DeepSeek-Harness 源码剖析》,三条线把这个仓库从底到顶读穿:教程连载 13 篇为主线(基座 cordis/组装 → 核心插件逐个拆解 → 实战与复盘),逐包深读 32 篇、横向专题 8 篇做侧翼。旧文不废,后面会作为深度案例引用。

开篇第一个反常识的结论:这个仓库里根本没有「产品」

一、纯插件应用:内核对 Agent 一无所知

DeepSeek-Harness(下称 DSH)和 Claude Code、Codex CLI 是同一形态:终端里跑一个「模型 + 工具循环」的编码代理。

但工程结构上有个根本差别。Claude Code 是「产品是内核,能力是外壳」;DSH 反过来——它不是一个单体应用加插件系统,而是一个纯插件应用

我在仓库里找不到「内核实现 Agent、插件提供外围功能」的分层。恰恰相反:Agent 循环、工具注册表、会话日志、LLM 适配、上下文压缩,全部是地位平等的内置插件(统一 @deepseek-ai/dsh-* 前缀的 npm 包)。

真正担当「内核」的,是一个被内嵌进仓库的通用插件框架 cordisvendor/cordis,版本 4.0.1)。而 cordis 本身对 Agent 一无所知——它只提供四件事:插件定义、依赖注入、事件总线、生命周期管理。

这就是《拆解 DeepSeek Harness:万物皆插件》里那句「万物皆插件」的字面意思。不是修辞,是事实。

读 DSH 源码的正确姿势因此是:先搞清「插件怎么被组装、怎么互相找到」,再逐个读插件。基座篇三篇就做第一件事。

二、仓库地图:vendor 与 packages 的边界

先建立地图。仓库根目录值得关注的只有四块:

deepseek-harness/ ├── vendor/          # 内嵌的 cordis 框架全家桶(与业务无关) │   ├── cordis/      #   微内核本体:Context/Fiber/Registry/Events/Service │   ├── loader/      #   把 YAML 配置树驱动成插件树 │   ├── include/     #   patch 层叠加算法 │   ├── group/  hmr/  schemastery/  cosmokit/  ... ├── packages/        # DSH 业务插件(几十个 dsh-* 包) │   ├── boot/app-boot/        # 启动胶水(本篇主角) │   ├── bundle/base|headless|web-app/  # 三个官方 bundle │   ├── llm/ session/ compaction/ shell/ subagent/ ... ├── apps/            # cli 与 web 两个产品入口 └── examples/        # headless-agent 等示例组合

读源码最容易犯的错,是把这两层混在一起讲。边界很清晰:

知道什么
不知道什么
vendor/cordis
 等框架代码
插件、注入、事件、生命周期
「Agent」「LLM」「会话」——找不到任何业务字符串
packages/
 业务插件
「我提供什么服务、注入什么服务、配置 schema 是什么」
插件怎么挂载、依赖怎么解析

一个 DSH 插件长什么样?看 packages/bundle/headless/src/index.ts:27-34,入口只有几行元数据加一个函数:

export const name = 'headless-runner' export const inject = ['agentDefaultModel', 'agents', 'sessions']  export const Config: z<Config> = z.object({   task: z.string().required(), })

没有 main 函数,没有注册中心调用。name/inject/Config 这些静态元数据由 cordis 的 registry 读取(vendor/cordis/src/registry.ts:100-111 的 Plugin.Base 接口)。配置文件里一行 - id: headless-runner 就能把它挂进运行时。

三、组装问题:从 YAML 到插件树

DSH 的「产品」不是编译产物,而是一份组合声明

看 examples/headless-agent/cordis.yml,一份典型的完整组合,25 条 entry:

- id: settings   name: '@deepseek-ai/dsh-settings-file'  - id: llm-deepseek   name: '@deepseek-ai/dsh-llm-deepseek'   config:     thinking: enabled     models:       - id: deepseek-v4-pro         contextWindow: 128000  - id: agent-spine   name: '@deepseek-ai/dsh-agent-spine-demo'   config:     agents:       - id: main         model: deepseek-v4-flash         cwd: !!js process.cwd()

每条 entry 声明一个插件实例:id 是组合内唯一标识(patch 靠它定位),name 是 npm 包名,config 过插件的 schema 校验。注意 !!js process.cwd()——这个 YAML 方言允许嵌 JavaScript 表达式,Loader 在配置注入时求值(packages/boot/app-boot/src/index.ts:201-207)。

关键点:entry 的书写顺序不代表加载顺序

每个插件的 inject 声明了它依赖的服务名。Loader 把整棵树并发挂起,服务出现一个、激活一批。比如 compaction-basic 声明 static inject = ['llm', 'tokenMeter', 'sessions']packages/compaction/compaction-basic/src/index.ts:104),那在这三个服务全部就位之前,它只是安静地挂在 PENDING 状态,不报错、不阻塞别人。

这是 cordis fiber 模型的核心特性,基座篇第二篇细讲。

四、三层叠加:Bundle → Profile → 启动器

一份 cordis.yml 写 25 行没问题。但「CLI 版 DSH」和「Web 版 DSH」共享 90% 的插件组合,只有少数 entry 配置不同——总不能让每份配置都复制粘贴。

DSH 的答案是三层叠加:Bundle 层 → 用户 Profile 层 → 启动器层。实现在 packages/boot/app-boot/src/profile.ts,文件头注释(1-23 行)本身就是一份准确的设计说明。

4.1 三个概念

  • Bundle
    :一个 npm 包,package.json 里声明 「dsh」: { 「bundle」: { 「patch」: 「./cordis.patch.yml」 } }——「我贡献一个 patch 层」。
  • Profile
    $DSH_HOME/profiles/ 下的目录,manifest 里的 dsh.profile.bundles 是一个有序 bundle 名列表,外加用户自己的 cordis.patch.yml
  • Patch
    :一个 { id, config | disabled | insert } 数组,叠加在已有 entry 列表之上,后写的层赢

4.2 patch 的三种动作

packages/bundle/base/cordis.patch.yml 是所有 profile 的公共底座,以一个巨大的 insert 从空列表建起整棵树——timer、hmr、llm、session……全在这里。

而 packages/bundle/headless/cordis.patch.yml 展示 patch 的三种动作:

# 1. 按 id 改写既有 entry 的 config - id: system-prompt   config:     persona: >-       You are a coding agent powered by the {{model}} model...  # 2. 按 id 直接禁用 base 层挂上的插件 - id: hmr   disabled: true  # 3. insert 追加新 entry - insert:     - id: headless-runner       name: '@deepseek-ai/dsh-headless'

base 层文件头有句重要注释:patch 替换的是目标 entry 的整个 config,不做深合并。所以「哪个 bundle 拥有某行的 config」必须唯一——共享行只留中性默认值,模式差异行下放到各模式 bundle。

4.3 加载流程里的两个细节

loadProfile()profile.ts:371-403)是组装入口,流程不复杂:解析 profile 目录 → 读 bundles 列表 → 逐个解析 bundle 的 patch 文件 → 最后读用户自己的 patch 层。

两个值得单独说的细节。

双锚点解析resolveBundleDir()profile.ts:344-355)先从 dsh 安装本体解析 bundle,再从 profile 目录解析。注释写明了契约:@deepseek-ai/dsh-base 这类内置 bundle 必须永远来自当前运行的 dsh 安装,不允许被 profile 本地副本劫持——否则你会得到一个「框架两份实例」的幽灵 bug。

fail loud。bundles 列表里的包如果没有声明 dsh.bundle,直接抛错(profile.ts:392-394)。把一个普通包列成 bundle 是配置错误,不是「没有 patch」。

4.4 一次合成:composeEntries

多层 patch 的合成发生在 composeEntries()profile.ts:413-420):

return applyEntryPatches([], structuredClone(layers.flat()), ...)

所有层摊平成一张 patch 列表,从空 entry 列表出发做一次applyEntryPatches 调用——与 boot 实际挂载用的是同一个函数同一次调用。这保证 dump 出来的配置和真实运行完全一致。

层的顺序即优先级:bundle 层按数组顺序 → 用户 profile 层 → 启动器层(--patch 文件、命令行 flag 派生的 patch)。后层赢。

Bundle 建树、Profile 微调、启动器覆盖,composeEntries 一次合成 entry 列表

五、.env 层叠与启动时序

组合声明之外,进程环境也分层。loadLayeredEnv()packages/boot/app-boot/src/index.ts:177-198):

优先级是进程继承环境 > 调用目录 .env > Harness home .env。两份文件先都解析完再应用,任何一份有错都不会出现「改了一半」的状态。应用时 process.env[name] === undefined 才写入,已存在的值不覆盖。

还有个 bootstrap-only 黑名单(93-117 行):PATHNODE_OPTIONSGIT_、一切 DSH_ 前缀变量只允许来自真实启动环境,写进 .env 直接抛错。道理很硬:这些变量决定进程怎么启动、代码从哪加载,让项目文件篡改它们等于打开供应链攻击面

启动链路收尾,把 boot() 的完整时序画出来:

dsh --profile headless 「task」   ├─ loadLayeredEnv()      # 继承环境 > 项目 .env > home .env(黑名单拒绝)   ├─ installFailLoud()     # unhandledRejection 兜底,给终端 2 秒恢复 raw mode   ├─ loadProfile()         # 解析 bundle 层 + 用户 patch 层(安装锚点优先)   ├─ composeEntries()      # applyEntryPatches 一次合成 entry 列表   └─ boot()        ├─ new Context()          # cordis 根上下文        ├─ ctx.plugin(Loader)     # vendor 的 Loader 服务        ├─ mountRootInclude()     # include entry 吃进配置 + patches        ├─ await loader.await()   # 等树稳定(inject 驱动激活)        └─ assertEntries*()       # 三道审计,失败则 dispose 半启动树

启动失败时的处理也很克制:先 ctx.fiber.dispose() 清掉半启动的树,再抛带 cause 链的聚合错误。installFailLoud()(609-649 行)还会给终端持有者至多 2 秒恢复 raw mode 再退出——细节周到。

六、设计取舍:三层叠加换来了什么

为什么是三层? Bundle 分层把「建树」(base)、「改树」(mode bundle)、「微调」(用户 profile)拆成三层,每层只关心自己的差异。没有这套分层,CLI 与 Web 版就要各维护一份完整 cordis.yml,90% 内容重复。

代价是什么? 配置溯源变难:一个 entry 的最终值可能来自 base bundle、被模式 bundle 覆写、再被用户 profile 覆盖。不知道三层各贡献了什么,就很难回答「这个配置为什么是这个值」。好在 renderConfigDump 复用同一条合成路径做逐层溯源导出,算是官方给的解法。

换个方案会怎样? 「每个 profile 一份完整配置」阅读最简单,但 base 更新一处默认、所有 profile 手动同步。「继承式合并」(extends)则有语义模糊问题:深合并还是浅合并、数组追加还是替换?三层叠加用「一次性扁平成 patch 列表 + 后写赢」规避了继承链的语义模糊,在「重复」和「可追溯」之间取了平衡。

七、结语

回到开头的结论:DSH 的「产品」确实不存在于任何一份代码里。它是 bundle/profile/patch 三层叠加、一次 applyEntryPatches 合成出来的组合声明;cordis 调度器零业务,全部能力由插件提供。

把组装层搞清楚后,下一个问题自然浮现:cordis 凭什么能「服务没到就先挂起、服务到了自动激活」?inject 驱动的并发激活是怎么实现的?

下一篇《DeepSeek-Harness 源码剖析(二)》进入 vendor/cordis/src 逐文件拆 fiber 模型。之后的插件深拆篇,会把旧 5 篇零散拆解(热插拔、guard、多智能体、自举插件)作为案例接进来。

你读一个陌生仓库时,习惯从入口函数开始追,还是先找「组装层」看地图?


本系列基于 DeepSeek Harness 源码(MIT,0.1.0-rc.5)与官方 Agent Notes 整理,仓库:github.com/deepseek-ai/deepseek-harness。有收获就点个关注,下一篇见。