Skip to content

入口点

dsh 命令是 apps/cli/package.json 中声明的单一 bin:

json
{ "bin": { "dsh": "lib/bin.js" } }

dsh bin(apps/cli/src/bin.ts)从自己签入的 package.json 读取应用版本,用 parseDshArgs 解析 argv,并按结果调用模式分发:

模式行为
profile启动命名 profile(runProfile
dump-config打印组合后的 profile 树并退出,不启动
plugin通过转发给 pnpm 管理 profile 的插件

每个模式都是动态导入的,互不相关的模式不会进入彼此的派发路径。

两层参数模型

一个关键设计决定把命令行一分为二:

  • 启动器(launcher)只解析自己的标志:--profile、可重复的 --patch--dump-config--dump-default-config
  • 其余所有参数原样交给启动的插件树,作为 ctx.cmdlineArgs;每个被注入的应用插件解析自己的标志族,并打印自己的 --help

这就是为什么 dsh --profile tui --resume abc 会以内部参数 ['--resume', 'abc'] 启动 tui profile,而 dsh --profile web --help 打印的是 web 应用的帮助而非启动器的。应用标志永远到不了启动器;启动器遇到的第一个不认识的 token 即内部参数的起点。

apps/cli/src/args.ts 用 commander 实现这一点(program.allowUnknownOption().passThroughOptions().enablePositionalOptions()),而 --patch 刻意做成非可变参的重复收集器,以免吞掉内部参数。

web--profile web 的硬编码别名;plugin 通过在 profile 目录内把参数转发给 pnpm 来管理其依赖。

关键标志

标志作用域效果
--profile <name>启动器启动 $DSH_HOME/profiles/<name> 下的 profile
--patch <path>启动器在 profile 层与 home 层之后应用的额外补丁覆盖(可重复)
--dump-config启动器打印组合后的树(含用户层与 --patch)并退出
--dump-default-config启动器只打印 bundle 层,不含用户层或覆盖
web子命令--profile web 的别名
--version / -V启动器打印版本并退出
--help应用所有每个应用打印自己的
--host / --port / --trusted-hostweb 应用packages/bundle/web-app/src/startup.ts 中的 web-startup 通过 parseCmdline 解析,作为 ctx.webStartup 提供

启动器层没有 --port。端口是应用所有的标志:web-startup provider 注入 ctx.cmdlineArgs,解析自己的 commander 程序并发布 webStartup。标志配置的行通过惰性配置读取它,因此在参数解析之前没有任何东西绑定端口,dsh --profile web --help 不会启动任何 server。

package.json 中的 dsh 字段模式

Profile 与 bundle 各自声明一个 dsh 段(packages/boot/app-boot/src/profile.ts):

字段属主含义
dsh.profile.bundlesprofile(package.json有序 bundle 层列表(包名)
dsh.bundle.patchbundle(package.jsonbundle 的补丁文件,相对于其包根

一个清单可以同时声明两种角色。例如 packages/bundle/base/package.json

json
{ "dsh": { "bundle": { "patch": "./cordis.patch.yml" } } }

而 profile 目录的 package.json(由 initProfile 写入):

json
{ "name": "dsh-profile-web", "private": true,
  "dependencies": {},
  "dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"] } } }

profile 目录位于 $DSH_HOME/profiles/<name>,其中包含它的 package.json、一个 cordis.patch.yml(用户自己的补丁层)以及一个 pnpm-workspace.yaml(让树外插件用 hoisted linker 安装)。

Bundle 是 Cordis 配置行的一种分发格式

Bundle其补丁文件的内容
@deepseek-ai/dsh-base每个 profile 的第一层timer 与 hmr、llm、session、typert、agent、agent-loop、jobs、settings、credentials、持久化、沙箱、审批、subprocess、工具、prompt 段、shell 栈
@deepseek-ai/dsh-web-appbase 之上web host 行(webserver、api gateway、workspace、投影缓存、存储)、浏览器插件名册、web-runtime 胶水、web-startup 命令行
@deepseek-ai/dsh-headlessbase 之上无 Host、HTTP 或浏览器层的直接核心 Agent/Session 运行器

Bundle 之所以是"Cordis 配置行及其所挂载代码的分发格式",正因为补丁按 id 瞄准一行并替换其整个 config(或插入新行),所以 bundle 插入的任何内容都仍可被其上方的层继续打补丁。

dsh web 到运行中的 server

沿着 apps/cli/src/bin.tsapps/cli/src/profile-boot.ts 走:

text
dsh web
  ├─ loadLayeredEnv('dsh')        解析 .env(调用目录 + Harness home),
  │                               拒绝 bootstrap-only 名称,冻结快照
  ├─ runProfile({ environment, profile:'web', patchFiles, args })
  │   ├─ composeProfile('web', …)
  │   │    ├─ prepareProfile -> healProfilesModuleFallback(INSTALL_ANCHOR)
  │   │    │                    (盒内插件的扁平 node_modules 回退)
  │   │    ├─ loadProfile -> 解析 bundle 层(base、web-app)+ 加载
  │   │    │                 用户 cordis.patch.yml
  │   │    ├─ homePatches   ($DSH_HOME/cordis.patch.yml)
  │   │    ├─ overlays      (--patch 文件、随附 preset 根、遥测开关)
  │   │    └─ rows map      (在空根上 composeEntries)
  │   ├─ createProcessShutdown  (有界的退出控制器)
  │   ├─ process.on SIGTERM/SIGINT + installFailLoud
  │   └─ boot(NAME, rootConfig, structuredClone(allPatches), prepare)
  │        ├─ new Context()                // Cordis 骨干
  │        ├─ ctx.baseUrl / provide dshHomePath
  │        ├─ ctx.plugin(Loader)           // cordis-plugin-loader
  │        ├─ prepare(hostCtx)             // 在任何配置条目挂载之前:
  │        │    提供 env 快照 + provideCmdline(args, exit)
  │        ├─ mountRootInclude             // 'cordis:include' + 'cordis:group'
  │        └─ await loader; assertEntriesActivated -> 已settled 的 Context
  └─ watchUserPatches                      // 通过 HMR 让 cordis.patch.yml 保持热重载

它启动的根配置是 profile/cordis.yml——一个空条目列表profile-boot.ts 中的 PROFILE_ROOT_CONFIG),每次启动都重写——因为内嵌 Loader 的树写回会把组合好的行烤进它,从而在下次启动时重复插入每个 bundle。该文件存在只是为了给 Loader 一个真实的 include 根,把 baseUrl 锚定在 profile 目录上。

补丁顺序(后者胜出):

  1. dsh.profile.bundles 顺序中的每个 bundle
  2. profile 自己的 cordis.patch.yml
  3. home 级 cordis.patch.yml
  4. --patch 覆盖
  5. 遥测开关 / 随附 preset 根

Profile 类与补丁组合

packages/boot/app-boot/src/profile.ts 暴露面向启动器的 API:

函数角色
resolveProfileDir(name, home)$DSH_HOME/profiles/<name>,拒绝路径分隔符
initProfile(dir, bundles)首次使用模板:清单、补丁模板、pnpm-workspace.yaml
loadProfile(binName, name, installAnchor)解析 bundle 层 + 解析用户补丁层
resolveBundleDir(...)bundle 解析,先安装锚点,再 profile 目录
composeEntries(layers)在空根上执行一次 applyEntryPatches——即 boot 所挂载的内容
healProfilesModuleFallback(...)维护扁平的 profiles/node_modules 符号链接回退

loadOverlayPatchesloadOptionalPatches 解析顶层 YAML 数组形式的 loader 补丁条目(支持 include 的 !!js 表达式方言与宽松的 schemastery schema);缺少可选文件意味着"没有该层",而缺少必需 overlay 则抛出异常——是调用者命名了那个文件。

--dump-configapps/cli/src/dump-config.ts)复用同一套组合,因此它打印的正是该调用会挂载的内容。它从不求值 !!jsindex.ts 中的 renderConfigDump 原样打印表达式、不求值),也从不启动树,所以配置 dump 无法显示应用标志会决定什么。

树外插件:dsh plugin --profile <name>

dsh pluginapps/cli/src/plugin.ts)是一个轻量 pnpm 转发器:

text
dsh plugin --profile tui add some-cordis-plugin
  ├─ 需要时 initProfile
  ├─ 在 profile 目录中执行 pnpm <args…>
  └─ reconcilePlugins:任何声明了 dsh.bundle 的已安装依赖
     加入 dsh.profile.bundles;被移除/无 bundle 的则离开

协调基于已安装状态而非依赖 diff,因此 update 会激活某个在新版本中获得了 dsh.bundle 声明的包。树外插件通过修复后的 profiles/node_modules 回退找到 Cordis 与 Service Definition 包,因此每个插件共享安装的单一 Cordis 实例,而不是一份重复的。

延伸阅读

  • 架构一览——本启动流程填充的插件树与 host/client 拆分。
  • 扩展(Cordis)系统——挂载的插件行到底是什么。
  • 源码:packages/boot/app-boot/src/profile.tspackages/boot/app-boot/src/index.tsbootmountRootIncluderenderConfigDump)。
  • 源码:apps/cli/src/args.tsapps/cli/src/profile-boot.tsapps/cli/src/plugin.tsapps/cli/src/dump-config.ts
  • 仓库文档:docs/architecture.md(Profiles and bundles 一节)。
  • Bundle 清单:packages/bundle/base/package.jsonpackages/bundle/web-app/package.jsonpackages/bundle/headless/package.json