深色模式
03 · dsh 总体架构:一棵启动出来的插件树
这一章回答:运行
dsh时,从命令行参数到一棵活的插件树,中间发生了什么?我们会用真实机器的--dump-config输出来观察这棵树。
3.1 运行中的 dsh 是什么
官方架构文档一句话定义:
A running
dshis a plugin tree composed at boot from ordered layers.一个运行中的 dsh 是一棵在启动时由有序分层组合出来的插件树。
关键词有两个:插件树(运行时的一切都是树上挂载的插件)和有序分层(这棵树不是代码写死的,而是若干层配置叠出来的)。
让我们亲手把这棵树打印出来。装好 dsh 后(npm i -D @deepseek-ai/dsh),执行:
sh
npx dsh --profile headless --dump-config你得到的是 300 多行的 YAML,每条代表树上的一个插件行。节选开头:
yaml
# == @deepseek-ai/dsh-base
- id: llm
name: '@deepseek-ai/dsh-llm'
- id: session
name: '@deepseek-ai/dsh-session'
- id: typert
name: '@deepseek-ai/dsh-typert-registry'
- id: agent
name: '@deepseek-ai/dsh-agent'
- id: agent-default-model
name: '@deepseek-ai/dsh-agent-default-model'
config:
provider: deepseek-official
model: deepseek-v4-flash
# …中间省略 20 行…
- id: agent-loop
name: '@deepseek-ai/dsh-agent-loop'
config:
agents: []
# == @deepseek-ai/dsh-headless
- id: code-runtime
name: '@deepseek-ai/dsh-code-runtime-worker-thread'
- id: headless-startup
name: '@deepseek-ai/dsh-headless/startup'
- id: headless-runner
name: '@deepseek-ai/dsh-headless'
inject:
- headlessStartup
config:
task: !!js ctx.headlessStartup.task这就是"一切皆插件"的直接证据:llm、session、agent、agent-loop——我们上一章说的那些"核心模块"——在这里只是若干行可替换的配置。任何一行都能被你自己的 patch 替换掉。
3.2 三层结构:Profile / Bundle / Patch
这棵树的"配方"由三层结构描述:
| 层 | 是什么 | 存储在哪 |
|---|---|---|
| Profile | 命名的产品组合:列出它叠加哪些 bundle、安装了哪些树外插件、保存用户的 cordis.patch.yml | $DSH_HOME/profiles/<name>/ |
| Bundle | 插件行的分发格式:一个 npm 包,在自己的 package.json 里声明 "dsh": { "bundle": { "patch": "./cordis.patch.yml" } } | npm 包 |
| Patch | 对插件行的覆盖:按 id 替换整行 config,或 insert 新行 | 各层 yml 文件 |
web 和 headless 是两个随包附带的模板 profile:
text
web = [dsh-base, dsh-web-app] # 浏览器界面
headless = [dsh-base, dsh-headless] # 一次性任务,无 HTTP 服务器dsh-base 是每个 profile 的第一层,装着所有形态共享的插件:模型适配器、工具、持久化、沙箱与审批策略、设置、凭据、遥测。dsh-web-app 在它上面加浏览器应用;dsh-headless 加一次性任务运行器——并且一个 HTTP 服务器插件都不挂。产品形态的差异就是这两份清单的差异。
3.3 组合顺序:一棵树是怎么叠出来的
这是理解 dsh 的关键机制。启动时,所有 patch 层在一个空的 entry list 上按序应用:
两个必须记住的语义:
- 同 id 后写覆盖先写:谁在最上层谁说了算。
- 替换是整行 config 替换,没有深合并:你的 patch 覆盖某行的
config时,必须复述你想保留的所有字段。这是官方文档反复强调的已知限制("A user patch replaces the whole matched config — no deep-merge")。
home 层为什么比 profile 层高?
profile 层是"这个产品形态怎么配",home 层($DSH_HOME/cordis.patch.yml)是"我这台机器怎么配"。机器级偏好(比如所有 profile 都关闭遥测)应该赢过 profile 级设置,所以 home 层排在上面。--patch 是临时的实验性覆盖,排最高。
3.4 启动链路全景
组合出配置树只是第一步。完整启动链路(对应源码 apps/cli/src/profile-boot.ts 与 packages/boot/app-boot):
启动器与应用的边界也值得一提:启动器只解析自己的 flag(--profile、--patch、--dump-config),第一个未识别 token 之后的参数原样交给 profile 的应用插件。所以:
sh
dsh --profile web --help # web 应用自己的帮助(--host/--port 等)
dsh --help # 启动器自己的帮助
dsh --profile headless "任务" # 位置参数是 headless 的任务文本3.5 核心包地图:树的骨架来自哪里
--dump-config 里那些行,背后是仓库里的核心包。官方架构文档给了这张表——这是整个教程最重要的表格之一:
| 包 | 拥有什么 | ctx 键 |
|---|---|---|
core/session | 追加式的 SessionEvent 日志与内存仓库 | ctx.sessions |
core/system-prompt | 提示词片段与工具 schema 的装配 | ctx.systemPrompt |
core/tools | 作用域化的工具注册表与守卫式执行管线 | ctx.tools |
core/agent | Agent 接口、活跃注册表、agent/* 事件 | ctx.agents |
core/agent-loop | 实现该接口的默认驱动引擎 | ctx.agentLoop |
core/scope | 每个 agent 的作用域化注册原语 | 库,无键 |
llm/llm | 消息与流式词汇表 + 适配器接缝 | ctx.llm |
它们的运行时关系:
注意箭头的方向:没有任何一行是"核心调用插件",全是"插件向服务注册"。 agent-loop 虽然叫"驱动引擎",它也只是一个消费其他五个服务的普通插件——你可以写一个自己的 agent-loop 替换它,这就是第 1 章那句话的字面兑现。
3.6 接缝(Seam):可替换能力的标准形状
官方文档还有一个重要概念:seam(接缝)。一个可替换的能力由三个角色构成:
| 角色 | 职责 | 例子(文件系统) |
|---|---|---|
| Service Definition | 声明接口 | ctx.fs 的接口定义 |
| Service Provider | 实现接口 | 本地文件系统 / E2B 远程沙箱 |
| Consumer | 使用接口 | 面向模型的读写文件工具 |
三者缺一不可,一个包可以兼任多个角色。接缝的价值在于:换一个提供方,整个产品跟着变。官方文档举的例子很直观——把文件系统和子进程提供方一起指向远程沙箱,Bash、终端、LSP 工具全部跟着搬进沙箱,一行面向模型的工具代码都不用改。
dsh 里的接缝:ctx.llm(模型适配器接缝)、ctx.fs(文件系统)、ctx.shell / ctx.subprocess(进程执行)、ctx.sandbox(沙箱)、ctx.sessionTitle(标题生成)、subagent provider 等。第 1 章那张"你想做的事 → 做法"对照表,每一行本质上都是在某个接缝上挂一个提供方或消费者。
3.7 本章小结与练习
dsh 没有 main 函数式的启动流程,只有配置层的叠加:空 entry list → bundle 层 → profile 层 → home 层 → overlay 层。叠加出配置树,Loader 把每行变成一个插件 fiber,依赖满足的依次启动,一棵活的插件树就出现了。运行中谁都不特殊,谁都可替换。
小练习:读自己的插件树
- 运行
npx dsh --profile headless --dump-config,数一数树上有多少行。 - 找到
id: agent-loop的行,对照 3.5 的表,说出它背后是哪个包、提供什么服务。 - 找到带
!!js的 config 值(如task: !!js ctx.headlessStartup.task),猜猜这个语法是什么意思。答案在第 8 章。