核心主张
根 README.md 把 dsh 的架构称为「一切皆插件」。具体来说:
[Cordis] 是 dsh 下的框架:插件向共享上下文贡献服务、类型化事件与可逆效果。产品的每一部分 都是插件,包括模型适配器、工具注册表、会话日志乃至 agent 主循环本身,因此每一部分都可从配置中被替换。 ——
docs/architecture.md
你要抓住的词是每一处写的「一个插件」:模型适配器(ctx.llm)、工具注册表(ctx.tools)、会话日志 (ctx.sessions)和 agent 主循环(ctx.agentLoop)都只是 Cordis 树中的条目。没有需要修补的特权核心—— 你只需在旁边挂载一个插件来扩展 dsh。
插件向上下文贡献什么
一个 Cordis 插件是一个对象:要么是一个 Service 子类,要么是一个带可选 inject 属性、 包含 apply(ctx) 主体的普通函数。一旦 Cordis 挂载它,它就向共享 ctx 贡献三类东西:
| 贡献 | 机制 | 示例 |
|---|---|---|
| 服务 | 声称一个稳定的 ctx.<key> | ctx.llm、ctx.tools、ctx.sessions、ctx.agents |
| 类型化事件 | 通过 TypeScript 声明合并声明,用 emit / waterfall / parallel / serial 分发 | agent/pre-step、tools/execute、turn/start |
| 可逆效果 | 通过 ctx.effect() 或 ctx.on() 安装,卸载时回卷 | 一个提示词段、一个工具 schema、一个监听器 |
依赖通过 inject 声明:命名所需服务的插件会等到该服务存在后再启动,因此加载顺序被表达为服务需求,而非 手动排序。注册是效果,所以当插件卸载(切换 profile、编辑 cordis.patch.yml、重载)时,它安装的一切都会 可预测地回卷——不会留下半注册状态。
底层框架是内嵌的 Cordis,其设计在论文 A Programming Paradigm for Spatiotemporal Composability 中 阐述,仓库内解释见 docs/cordis-primer.md。提供「插件」抽象本身的包是 vendor/cordis 下的 @deepseek-ai/cordis。
没有特权核心
由于每个能力都是一种服务,一次提供商替换就足以改变整个产品。docs/architecture.md 用能力接缝 (capability seam)模型把这一点讲得很清楚:一个可替换的能力有三个角色——
- 服务定义(Service Definition)——声明接口(例如
ctx.shell)。 - 服务提供商(Service Provider)——实现它(例如
dsh-bash-local、dsh-bash-sandbox)。 - 消费者(Consumer)——使用它,通常是面向模型的工具(例如
dsh-tool-bash)。
单个角色并不构成接缝;新增一种能力意味着设计全部三者。packages/shell/shell/README.md 以微缩形式展示 了这一拆分:
| 包 | 角色 |
|---|---|
@deepseek-ai/dsh-shell | 服务定义:ShellExecutor(ctx.shell)+ 词汇类型 |
@deepseek-ai/dsh-bash-local | 服务提供商:本地子进程 |
@deepseek-ai/dsh-bash-sandbox | 服务提供商:同样的机制,但每个 spawn 都经由 ctx.sandbox 限制 |
@deepseek-ai/dsh-tool-bash | 消费者:面向模型的 bash 工具 |
由于消费者只与服务定义对话,把 dsh-bash-local 换成某个容器化或远程执行器无需创建任何 provider fork。 文件系统、子进程与子代理提供商也以同样方式工作(参见 docs/architecture.md 中的 capability-seams 一节)。
Profile 与 Bundle:命名组合
一个运行中的 dsh 并不是固定的程序;而是一棵启动时由有序层组合而成的插件树。
- Profile 是存储在 Harness home 中的命名组合。它列出所叠加的 bundle、持有其安装的树外插件,并保留 用户自己的
cordis.patch.yml。web与headless作为模板随附(app-boot 中的PROFILE_TEMPLATES)。 - Bundle 是一种 Cordis 配置行及这些行所挂载代码的分发格式。
dsh-base、dsh-web-app与dsh-headless是随附绑定的 bundle。
每个都在自己的 package.json 的 dsh 字段下声明自身:
// 一个 profile 的 package.json
{ "dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"] } } }
// 一个 bundle 的 package.json
{ "dsh": { "bundle": { "patch": "./cordis.patch.yml" } } }dsh.profile.bundles 按顺序列出 profile 的 bundle;dsh.bundle.patch 指向 bundle 的补丁文件——即 插入代码行的 YAML。组合机制位于 packages/boot/app-boot/(导出 resolveProfileDir、initProfile、 loadProfile、composeEntries、PROFILE_TEMPLATES、DEFAULT_PROFILE_BUNDLES、PROFILES_DIR、 PROFILE_PATCH_FILENAME)。
层序
层以严格的顺序应用于空条目列表,源自 CLI 与 profile 机制:
空根
→ 每个 bundle 的补丁 (按 dsh.profile.bundles 顺序)
→ 该 profile 的 cordis.patch.yml
→ 家目录层 $DSH_HOME/cordis.patch.yml
→ 任意 --patch 覆盖启动器标志本身见 apps/cli/src/args.ts;层序开示于 docs/architecture.md(Layers apply to an empty entry list in this order: each bundle in the profile's listed order, then the profile's cordis.patch.yml, then the home-level one, then any --patch overlay)。
检查组合后的树
因为运行中的树只是一叠补丁,它完全可检查。这条命令打印 web profile 的组合树,并在不启动树的情况下退出:
dsh --profile web --dump-config--dump-default-config 只打印 bundle 层(不含用户层、不含 --patch)。该 dump 由 app-boot 中的 renderConfigDump 使用 include 自身的 applyEntryPatches 渲染,因此打印出来的内容正是将要挂载的内容—— 工具不可能与启动过程漂移。它打印的任何一行都可以用你自己的补丁替换。补丁按 id 定位一行,并替换该行 的整个 config(不存在深合并层——profile 覆盖必须重述它保留的字段),或者 insert 一个新行。
为何这让每一部分都可被配置替换
回报是把「没有特权核心」变成现实:
- bundle 的行集由补丁文件而非代码决定——按 id 覆盖一行即可改变行为。
- 用户偏好存在于层级(先是每 profile,再是家目录层)中,每一层都压过其下的 bundle。
- 由 app 插件(经由
@deepseek-ai/dsh-cmdline的parseCmdline)解析的标志从惰性!!js配置中读取, 例如port: !!js ctx.webStartup.port ?? 3080,因此即便一个启动标志也只是某个配置表达式的值——在表达式 上写一个字面量,该标志就会静默失效。 - 由于注册是可逆效果,实时编辑
cordis.patch.yml(经由 HMR 插件)会就地重组这棵树,而无需完全重启。
交互与生命周期的细节——webserver 行示例、web-startup 提供商与 profile 机制——在 启动流程与 CLI 与 扩展系统(Cordis) 中有深入讲解。
延伸阅读
- 什么是 DeepSeek Harness? —— 这一哲学所驱动的产品。
- 启动流程与 CLI —— 实践中 profile、bundle 与
--dump-config。 - 扩展系统(Cordis) ——
inject、事件与ctx.effect()。 docs/cordis-primer.md—— Cordis 五大思想小结。docs/architecture.md—— 层序与能力接缝。packages/boot/app-boot/README.md—— profile 与composeEntries细节。