Skip to content

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

把本章串起来,一个"自定义产品"的完整路径是:

  1. 写插件:一个 .ts 文件,导出 name / inject / apply(第 2 章);
  2. 用 overlay 试跑dsh --profile headless --patch ./my.patch.yml "任务"(临时覆盖,不动 home);
  3. 沉淀成 profile:在 $DSH_HOME/profiles/<name>/cordis.patch.yml,以后 dsh --profile <name> 直接可用;
  4. (进阶)做成 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

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