入口点
dsh 命令是 apps/cli/package.json 中声明的单一 bin:
{ "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-host | web 应用 | 由 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.bundles | profile(package.json) | 有序 bundle 层列表(包名) |
dsh.bundle.patch | bundle(package.json) | bundle 的补丁文件,相对于其包根 |
一个清单可以同时声明两种角色。例如 packages/bundle/base/package.json:
{ "dsh": { "bundle": { "patch": "./cordis.patch.yml" } } }而 profile 目录的 package.json(由 initProfile 写入):
{ "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-app | base 之上 | web host 行(webserver、api gateway、workspace、投影缓存、存储)、浏览器插件名册、web-runtime 胶水、web-startup 命令行 |
@deepseek-ai/dsh-headless | base 之上 | 无 Host、HTTP 或浏览器层的直接核心 Agent/Session 运行器 |
Bundle 之所以是"Cordis 配置行及其所挂载代码的分发格式",正因为补丁按 id 瞄准一行并替换其整个 config(或插入新行),所以 bundle 插入的任何内容都仍可被其上方的层继续打补丁。
从 dsh web 到运行中的 server
沿着 apps/cli/src/bin.ts → apps/cli/src/profile-boot.ts 走:
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 目录上。
补丁顺序(后者胜出):
dsh.profile.bundles顺序中的每个 bundle- profile 自己的
cordis.patch.yml - home 级
cordis.patch.yml --patch覆盖- 遥测开关 / 随附 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 符号链接回退 |
loadOverlayPatches 与 loadOptionalPatches 解析顶层 YAML 数组形式的 loader 补丁条目(支持 include 的 !!js 表达式方言与宽松的 schemastery schema);缺少可选文件意味着"没有该层",而缺少必需 overlay 则抛出异常——是调用者命名了那个文件。
--dump-config(apps/cli/src/dump-config.ts)复用同一套组合,因此它打印的正是该调用会挂载的内容。它从不求值 !!js(index.ts 中的 renderConfigDump 原样打印表达式、不求值),也从不启动树,所以配置 dump 无法显示应用标志会决定什么。
树外插件:dsh plugin --profile <name>
dsh plugin(apps/cli/src/plugin.ts)是一个轻量 pnpm 转发器:
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.ts、packages/boot/app-boot/src/index.ts(boot、mountRootInclude、renderConfigDump)。 - 源码:
apps/cli/src/args.ts、apps/cli/src/profile-boot.ts、apps/cli/src/plugin.ts、apps/cli/src/dump-config.ts。 - 仓库文档:
docs/architecture.md(Profiles and bundles 一节)。 - Bundle 清单:
packages/bundle/base/package.json、packages/bundle/web-app/package.json、packages/bundle/headless/package.json。