用 profiles 与 bundles 装配启动配置

约 20 分钟

说明 profile、bundle 与 cordis.patch.yml 的分层应用顺序,并演示如何用补丁覆盖配置行。

上一课确认了没有需要打补丁的特权核心:模型适配器、工具、会话与 Agent 循环都是插件,向共享上下文贡献服务、类型化事件与可逆效应,注册随卸载自动回滚。那么这些插件从哪来、按什么顺序装、如何叠成一层层?答案就在运行中的 dsh 里——它是一棵插件树,启动时从空配置按层装配。树不是写死在代码里的,而是由两个 manifest 决定:profile 与 bundle。

profile 是存储于 Harness 主目录的命名组合;bundle 是配置行与其挂载代码的分发格式。web 与 headless 是随附的 profile 模板。两者配合,决定你的实例装哪些层、每层是什么、谁先谁后。

下面先看两个 manifest 各自是什么,再看它们如何叠成一层一层的树,最后写一个最小 patch 覆盖一行配置。

核心概念

profile:命名组合

类比:歌单。歌单不存放音乐文件,只按顺序列出要播的专辑,再记上你自己加的单曲。profile 就是 dsh 的歌单。

定义:profile 是存储于 Harness 主目录的命名组合。它列出自己要叠加的 bundles,持有安装的 out-of-tree 插件,并保存用户自己的 cordis.patch.yml。web 与 headless 是随附的 profile 模板,不必从零造;需要时也可以自建命名组合。

Harness 主目录是 ~/.dsh。settings.yaml、profiles、会话与凭据都保存在这里。选定一个 profile,就是选定一张歌单;启动时按它的顺序叠层。

核心概念

bundle:配置行与代码的打包格式

类比:带说明书的快递盒。盒里装着配置行和它们要挂载的代码;说明书就是它自己的 package.json。盒里装什么、按什么顺序,由说明书声明。

定义:bundle 是 Cordis 配置行与其挂载代码的分发格式。它插入的每一行都保留为独立行,因此能被上层补丁继续覆盖——这是它与直接改代码的根本区别。

每个 bundle 在自己的 package.json 的 dsh 字段里声明自己:dsh.profile 列出 profile 的 bundles;dsh.bundle 指向该 bundle 的 patch 文件。官方 bundle 位于仓库 packages/bundle 下:base、web-app、headless。

dsh-base 是每套 profile 的第一层:模型适配器、工具、持久化、沙箱与审批策略、设置、凭据、遥测。dsh-web-app 在其上加浏览器应用;dsh-headless 加一个无服务器的单次运行器。

核心概念

分层顺序与 cordis.patch.yml

类比:叠画纸。后放的纸盖住先放纸上的内容,但每张纸都原样保留;你随时可以再放一张新的上去。补丁就是新放的那张纸。

定义:层从空配置列表开始按序应用:先按 profile 列出的顺序应用每个 bundle;然后应用 profile 自己的 cordis.patch.yml;接着应用主目录级的 cordis.patch.yml;最后应用任何 --patch 覆盖层。

patch 的对象是行。它按行 id 定位,整行替换该行的配置;行 id 不存在时,就插入新行。补丁不合并字段——想改一个字段,就要重写整行。

越靠后的层越优先。profile 级补丁能盖掉 bundle 的行;主目录级补丁能盖掉 profile 级补丁;--patch 覆盖层最晚、最优先。这就是“一切皆插件”在配置层的体现:不改源码,只叠层。

把顺序固定成一句口诀:bundle 在前,补丁在后;profile 内补丁在前,主目录级补丁在后;--patch 永远最后。

  1. profile 列出的 bundles,按列出顺序逐一应用
  2. profile 的 cordis.patch.yml
  3. 主目录级 cordis.patch.yml
  4. --patch 覆盖层

后一层只按行 id 操作:整行替换,或插入新行。看到一行来自某个 bundle,就可以在任意更后的层里用同一个 id 覆盖它。更细的配置字段,见生成的配置目录(config catalog)。

写一个最小 patch 并说明覆盖目标行

  1. 1

    选一个目标行。dsh-base 是每套 profile 的第一层,最适合当例子:记下你要覆盖的行 id,并确认它由哪个 bundle 引入。行 id 是 patch 的唯一寻址方式。

  2. 2

    打开或新建主目录级补丁文件 ~/.dsh/cordis.patch.yml。下面是最小示例;示例只表达资料确认的语义——按行 id 定位、整行替换、可插入新行——具体 YAML 键名以所用版本的文档为准。

    yaml
    # ~/.dsh/cordis.patch.yml —— 主目录级补丁
    # 一条 patch = 目标行 id + 该行整行配置
    <目标行 id>:
      # 该行整行配置;下层同名 id 的行被整体替换
    
  3. 3

    写清这条 patch 的层位:主目录级补丁在 profile bundles 与 profile 内 cordis.patch.yml 之后应用,在 --patch 覆盖层之前。所以它压得住 bundle 的行,压不住 --patch。

  4. 4

    用一句话说明覆盖目标:“这条 patch 替换了 <bundle名> 引入的 <行 id> 这一整行配置”。行 id 不存在时,它会是插入的新行。

你应该看到

你得到一份写在 ~/.dsh/cordis.patch.yml 的最小 patch,并能同时说出三件事:目标行 id、该行来自哪个 bundle、这条 patch 处于分层顺序中的哪一层。

遇到问题:你写了一条 patch,重启后目标行还是旧配置。

可能原因

  • 行 id 对不上。patch 按行 id 定位;写错或记错 id,它不会覆盖任何行。
  • 层位太低。patch 放在 profile 内 cordis.patch.yml,被主目录级补丁或 --patch 覆盖层压住。
  • 目标行不存在。它来自一个没被加载的 bundle;profile 的 bundle 列表由 dsh.profile 声明。
  • 误以为补丁会合并字段。patch 是整行替换;只改一个字段也必须重写整行。

排查步骤

  1. 对照启动插件树输出或配置目录,核对行 id 的准确写法。
  2. 把 patch 移到更靠后的层:主目录级 cordis.patch.yml 晚于 profile 内补丁;--patch 覆盖层最晚、最优先。
  3. 确认目标 bundle 出现在该 profile 的 bundle 列表里,否则该行根本不会被引入。
  4. 重写整行配置,而不是只写想改的字段。
自检:配置在启动时按什么顺序叠放?要让主目录级 cordis.patch.yml 里的一条 patch 盖住 dsh-base 的某一行,需要满足什么条件?

顺序:profile 列出的 bundles(按列出顺序)→ profile 的 cordis.patch.yml → 主目录级 cordis.patch.yml → --patch 覆盖层。主目录级 patch 要生效:行 id 与目标行完全一致,且该行确实由已加载的 bundle 引入;patch 按行 id 整行替换。它晚于 profile 内补丁,能压住同 id 行;若还有 --patch,则 --patch 会再压住它。