5.6 KiB
Agent Note: profile 插件组合包取代固定的表层 overlay
Status: implemented
English | 中文
Problem
dsh 启动器硬编码了自己的组合:base.cordis.yml + web.cordis.yml 随 apps/cli 一起交付,三种各自定制的入口模式(--config、web、-p)各带一套层栈,外加一个全局的个人 overlay($DSH_HOME/config.yaml)。想把树外插件(一个 TUI、一个提供方扩展包)装进已交付的表层,只能修改仓库;第三方包也没有任何位置可以贡献默认组合。
Decision
一切都变成 profile:即目录 $DSH_HOME/profiles/<name>,其中包含一个 package.json(pnpm 管理的树外插件 dependencies,加上 profile manifest(元数据清单)dsh.profile 及其有序的 bundles 层列表)和一份用户 cordis.patch.yml。组合包(bundle)是声明了 "dsh": { "bundle": { "patch": "./cordis.patch.yml" } } 的 npm 包;两种 manifest 分别位于互不相同的 dsh.profile / dsh.bundle 键下,因此一份 package.json 能说明自己扮演哪种角色。配置树在空的根之上组合:按 dsh.profile.bundles 顺序应用每个组合包的 patch,然后是用户层,然后是 --patch overlay,最后是 flag patch——全部收敛为一次 applyEntryPatches 调用,启动、flag 派生与 --dump-config 使用完全相同的路径。
随附的组合包是 @deepseek-ai/dsh-base(共享核心配置行)、@deepseek-ai/dsh-web-app(浏览器 Host 配置行与 Web 运行时粘合层)和 @deepseek-ai/dsh-headless(直接叠加在 base 上且不含 web-app 的一次性 runner)。dsh web 是携带 Web flag 家族的 --profile web 别名;dsh run [--profile <name>] "task" 负责一次性执行,默认使用 headless profile;通用的 dsh --profile <name> 启动 profile 而不携带任务。patch overlay 使用 --patch。dsh plugin --profile <name> <args...> 是一层薄薄的 pnpm 转发器,负责初始化 profile,并依据已安装包的组合包声明调和 dsh.profile.bundles;没有组合包声明的包保持为普通依赖。Headless 作为直接 core 入口负责 headless 组合约定。
dsh run 命令决策负责一次性语法;本 Agent Note 负责该语法所选择的 profile 组合。
解析在构造上就是双锚点的:dsh.profile.bundles 中的名称先从 dsh 安装目录解析,再从 profile 目录解析——因此内置组合包始终来自与运行中 dsh 相同的安装,pnpm 从不管理它们——而 patch 行中的裸插件名称经 profile 目录的 Node 父目录逐级查找,落到受维护的扁平回退目录 $DSH_HOME/profiles/node_modules(安装目录的应用与各组合包所依赖的每个包各一个符号链接,每次启动时修复)。
两项配套重构:webserver 内置的静态 dist 服务改为单一所有者的回退席位(registerFallback/applyIndexTaps),SPA 服务器提取到 @deepseek-ai/dsh-frontend-static,使 web 组合包以组合的方式持有自己的 dist,而不是靠启动器代码;dsh CLI 个人配置决策的个人 overlay 机制(loadPersonalPatches、$DSH_HOME/config.yaml)改为面向逐 profile 与 home 级的 cordis.patch.yml 层(loadOptionalPatches、接受文件名的 watchUserPatches),取代该笔记的各入口模式与文件位置,同时保留其 Harness home 根目录、patch 语义与大声失败的解析。
Alternatives considered
- 依赖扫描加部分
patchOrder(最初的草案):扫描dependencies找出组合包、未列出者按字母序排列,会产生两个真源和一条隐式决胜规则;一份显式有序的dsh.profile.bundles列表更小、完全确定。在 profile 内直接pnpm add只会安装一个库,不激活任何 patch——行为显式,没有暗中扫描。 - 内置组合包使用
link:条目:pnpm 无法对指向安装目录的link:做版本管理、安装或更新,它会把机器路径嵌进用户文件,并且在安装目录移动后失效。双锚点解析加上每次启动修复的符号链接回退提供了同样的保证(「组合包来自安装目录」),且没有这些繁文缛节。 - 在组合包 manifest 中放一个启动前
context模块承载启动期取值(dist 路径、flag 事实):否决,改用纯插件——粘合逻辑就是启动器 patch 的普通配置行,因此组合始终可完整 dump,manifest 保持纯数据。启动器持有的ctx.headlessIo宿主钩子是唯一由宿主提供的 slot,且在任何配置树条目挂载之前,于boot()的prepare钩子中提供。 - 组合包的传递式自动应用:只有直接列在
dsh.profile.bundles中的条目才贡献层;想重新导出另一个组合包 patch 的元组合包,必须在自己的 patch 文件中显式完成。
Consequences
- 新的组合表层(TUI、提供方扩展包)以普通 npm 包形式交付,可按 profile 安装;仓库不再需要为每种部署形态各留一行。
apps/cli收缩为 argv 解析、profile 机制的消费方和 pnpm 转发器;AppCLIEntry与各表层专属的启动路径全部移除。- 无密钥 web e2e 脚手架以与生产相同的空根形态启动相同的组合包层,包括 profiles 模块回退,因此测试与产品之间的组合漂移会大声失败。
- 后端不拒绝磁盘上的任何旧格式(发布前姿态):
$DSH_HOME/config.yaml只是不再被读取。