深色模式
08 · 组合机制:profile / bundle / patch
前几章讲了"运行时"的机制,这一章讲"组合时"的机制:一棵插件树如何从配置层叠出来、如何被覆盖、如何热更新。这是把 dsh 变成"自己的产品"的那一步。
8.1 三层配置的完整形态
回顾第 3 章的三层结构,这次看细节。一个 profile 目录($DSH_HOME/profiles/<name>/)长这样:
text
profiles/
└── headless/
├── package.json # ① dsh.profile 清单:有序的 bundle 列表 + 树外插件依赖
└── cordis.patch.yml # ② 用户的覆盖层package.json 里的 profile 清单:
json
{
"name": "headless",
"dependencies": {
"@deepseek-ai/dsh-base": "0.1.0-rc.6",
"@deepseek-ai/dsh-headless": "0.1.0-rc.6"
},
"dsh": {
"profile": {
"bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-headless"]
}
}
}bundle 是一个普通 npm 包,多一个声明:
json
{
"name": "@deepseek-ai/dsh-headless",
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
}
}
}bundle 的实质就是它的 patch 清单(外加它可能自带的 glue 插件代码)。dsh-base 的 patch 就是一个大 insert,把共享核心的几十行插件插进空树;dsh-headless 的 patch 再改几行、加几行。
8.2 patch 语法:替换与插入
patch 文件是一个条目数组,两种基本操作:
yaml
# ① id 定向替换:整行 config 被替换(注意:没有深合并!)
- id: agent-default-model
config:
provider: mock
model: mock-1
# ② insert:追加新行
- insert:
- id: mock-adapter
name: ../../../plugins/mock-adapter.ts条目字段速查:
| 字段 | 作用 |
|---|---|
id | 稳定标识,替换/更新/删除的锚点 |
name | 模块说明符:npm 包名,或相对 profile 目录的本地路径 |
config | 传给插件的配置(可含 !!js 表达式) |
inject | 额外的服务依赖声明 |
disabled | 停用该行(可含 !!js 条件,如按平台启用) |
group | 该行是一个组,其 config 是子条目列表 |
!!js:配置里的活代码
config 值可以用 !!js 标记为运行时表达式,在挂载时求值:
yaml
- id: headless-runner
name: '@deepseek-ai/dsh-headless'
config:
task: !!js ctx.headlessStartup.task # 引用另一个服务提供的值
- id: tools
config:
mode: !!js process.env.DSH_TOOLS_MODE # 引用环境变量
- id: bash-sandbox
disabled: !!js process.platform === 'win32' # 平台条件启停这解决了"配置文件的表达力天花板":静态写不出来的值(依赖运行时环境的),留下一小扇通向代码的门。但门很小——!!js 只能出现在 config/disabled 里,条目结构本身保持静态可分析。
8.3 分层覆盖的完整顺序
组合发生在空 entry list 上,各层按序应用,后写覆盖先写:
替换是整行,不是深合并
这是官方文档反复强调的已知限制:id 定向 patch 会替换整行的 config。如果你覆盖 tools 行的 config 只写了 mode: code,那么该行原本的其他字段全部丢失。覆盖某行时,必须复述你想保留的所有字段。(Demo 8 会演示这个坑。)
8.4 热更新:patch 文件即配置中心
dsh 启动后持续监听用户 patch 层(profile 的 cordis.patch.yml 和 home 级的 $DSH_HOME/cordis.patch.yml):
改配置 → 保存 → 生效,无需重启。这是"一切皆插件 + 注册皆 effect"的直接红利:因为任何插件留下的东西都能干净撤销,重载才敢自动化。
8.5 亲手组装:从 demo 到自定义 profile
把本章串起来,一个"自定义产品"的完整路径是:
- 写插件:一个
.ts文件,导出name/inject/apply(第 2 章); - 用 overlay 试跑:
dsh --profile headless --patch ./my.patch.yml "任务"(临时覆盖,不动 home); - 沉淀成 profile:在
$DSH_HOME/profiles/<name>/写cordis.patch.yml,以后dsh --profile <name>直接可用; - (进阶)做成 bundle:把插件 + patch 打成 npm 包,
dsh plugin --profile <name> add <pkg>安装,任何机器都能复现。
Demo 8 走完前 3 步。第 4 步的完整做法见扩展方向。
8.6 一个值得知道的事实:官方就是这么吃狗粮的
官方仓库里的 web-schedule 示例 overlay 只有 9 行:
yaml
# 给 Web 产品叠加一个"定时提醒"能力
- insert:
- id: time-context
name: '@deepseek-ai/dsh-time-context'
- id: schedule
name: '@deepseek-ai/dsh-schedule'启动:dsh web --patch examples/web-schedule/cordis.yml。一个独立的产品功能,交付物就是一个 patch 文件 + 两个插件包。 这就是这套组合机制的意义:功能开发的终点不是"合并进主分支",而是"发布一个可叠加的层"。
8.7 本章小结与练习
组合机制的心智模型:产品 = 有序的 patch 层叠;配置树 = 层的叠加结果;运行时 = 树的激活。 覆盖按 id、整行替换;!!js 给配置开小门;patch 文件热更新。到这里,"一切皆插件"从运行态贯穿到了组合态。
小练习:设计你自己的 profile 层
你要做一个"给 headless 换模型路由 + 关掉 web 搜索工具 + 加一个日志插件"的定制。写出你的 cordis.patch.yml。提示:替换 agent-default-model 行(复述 provider 和 model 两个字段)、替换或 disabled tool-web 相关行、insert 你的插件行。参考答案见 Demo 8。