说明 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 永远最后。
- profile 列出的 bundles,按列出顺序逐一应用
- profile 的
cordis.patch.yml - 主目录级
cordis.patch.yml --patch覆盖层
后一层只按行 id 操作:整行替换,或插入新行。看到一行来自某个 bundle,就可以在任意更后的层里用同一个 id 覆盖它。更细的配置字段,见生成的配置目录(config catalog)。
API 凭据保存在 Harness 主目录(~/.dsh)的凭据存储里,与项目目录分离。即使配置层能表达凭据相关行,也不要为了图方便把真实凭据写进会被提交、分享的 cordis.patch.yml。补丁负责行的覆盖,不负责秘密的分发。
写一个最小 patch 并说明覆盖目标行
- 1
选一个目标行。
dsh-base是每套 profile 的第一层,最适合当例子:记下你要覆盖的行 id,并确认它由哪个 bundle 引入。行 id 是 patch 的唯一寻址方式。 - 2
打开或新建主目录级补丁文件
~/.dsh/cordis.patch.yml。下面是最小示例;示例只表达资料确认的语义——按行 id 定位、整行替换、可插入新行——具体 YAML 键名以所用版本的文档为准。yaml# ~/.dsh/cordis.patch.yml —— 主目录级补丁 # 一条 patch = 目标行 id + 该行整行配置 <目标行 id>: # 该行整行配置;下层同名 id 的行被整体替换 - 3
写清这条 patch 的层位:主目录级补丁在 profile bundles 与 profile 内
cordis.patch.yml之后应用,在--patch覆盖层之前。所以它压得住 bundle 的行,压不住--patch。 - 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 是整行替换;只改一个字段也必须重写整行。
排查步骤
- 对照启动插件树输出或配置目录,核对行 id 的准确写法。
- 把 patch 移到更靠后的层:主目录级
cordis.patch.yml晚于 profile 内补丁;--patch覆盖层最晚、最优先。 - 确认目标 bundle 出现在该 profile 的 bundle 列表里,否则该行根本不会被引入。
- 重写整行配置,而不是只写想改的字段。
自检:配置在启动时按什么顺序叠放?要让主目录级 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 会再压住它。