Skip to content

03 · dsh 总体架构:一棵启动出来的插件树

这一章回答:运行 dsh 时,从命令行参数到一棵活的插件树,中间发生了什么?我们会用真实机器的 --dump-config 输出来观察这棵树。

3.1 运行中的 dsh 是什么

官方架构文档一句话定义:

A running dsh is 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

这就是"一切皆插件"的直接证据:llmsessionagentagent-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 文件

webheadless 是两个随包附带的模板 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 上按序应用:

两个必须记住的语义:

  1. 同 id 后写覆盖先写:谁在最上层谁说了算。
  2. 替换是整行 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.tspackages/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/agentAgent 接口、活跃注册表、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,依赖满足的依次启动,一棵活的插件树就出现了。运行中谁都不特殊,谁都可替换。

小练习:读自己的插件树

  1. 运行 npx dsh --profile headless --dump-config,数一数树上有多少行。
  2. 找到 id: agent-loop 的行,对照 3.5 的表,说出它背后是哪个包、提供什么服务。
  3. 找到带 !!js 的 config 值(如 task: !!js ctx.headlessStartup.task),猜猜这个语法是什么意思。答案在第 8 章

基于 DeepSeek Harness(开发者预览版 0.1.0-rc.6)与 Cordis 撰写