摘要:「万物皆插件」不是修辞——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 包)。
真正担当「内核」的,是一个被内嵌进仓库的通用插件框架 cordis(vendor/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 | ||
packages/ |
一个 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 行):PATH、NODE_OPTIONS、GIT_、一切 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。有收获就点个关注,下一篇见。
夜雨聆风