DSH plugin 自定义 agent preset 不生效?roots 被覆盖原因排查

故障排查发布于 2026-09-12作者: DeepSeek Plugin 插件市场
DeepSeek HarnessDSH pluginagent presetroots 覆盖composeProfile
自定义 agent preset 放进项目目录、--patch 也写了 roots,UI 预设选择器却始终看不到?真因是启动时有一层运行时覆盖把 roots 整体替换为只含内置根,而 --dump-config 显示的是覆盖前的组合结果,会给你「配置正确」的假象。

如果你把自定义 agent preset 放在项目目录里(想随仓库分发、用 git 管理),配置文件里 agent-presets.roots 也写了,Web UI 的预设选择器里却始终看不到它——那不是你的 YAML 写错了。 DeepSeek Harness 的 composeProfile() 在组合完所有 patch 层之后,会额外注入一个覆盖层,把 agent-presets 行的 roots 整体替换成只含 shipped 根;它作为最后的 overlay 传给 loader,所以你无论在哪一层配 roots 都不会被扫描到(#403)。

现象:DeepSeek Harness 的 preset 放进项目里,UI 预设选择器却看不到

这个 bug 最消耗时间的地方,是它会让你先怀疑自己写错了配置,而所有自查手段都会告诉你「配置是对的」。 具体表现:

  1. 复现路径很干净:在 D:\project\.dsh\agent-presets\my-preset\ 下放 agent.cordis.yml + preset.yml,写一个 patch 注入 roots:
yaml
# test.patch.yml
- id: agent-presets
  config:
    default: standard
    includeUserRoot: true
    roots:
      - path: D:/project/.dsh/agent-presets
        trust: user

然后 npx @deepseek-ai/dsh web --patch test.patch.yml 启动,打开 Web UI → 新建对话 → 预设选择器,my-preset 不出现#403)。 2. 双对照实验锁定问题不在 preset 文件:把同一个 preset 复制到 <dshHome>/.agent-presets/my-preset/includeUserRoot 的默认行为),立即可见。文件内容完全一样,唯一变量是根的来源,因此问题一定出在 roots 注入环节,而不是 preset 本身(#403)。 3. --dump-config 会给你「配置正确」的假象--dump-config --patch test.patch.yml 的输出里,你的 roots 清清楚楚地显示着。这不是 dump 过期或缓存,而是它打印的数组在覆盖层 push 之前就已经构建完成——你传任何参数都看不到 loader 实际收到的值#403)。 4. 失败看起来是「间歇的」而不是「全量的」:覆盖层只是展开你的配置、然后替换掉一个键,所以同一行里 defaultincludeUserRoot 等其他字段都会正常生效,只有 roots 被丢弃。用「roots + 其他字段」一起测试的人,会看到其他字段生效,从而误判成偶发问题(#403)。 5. 跨平台、跨版本一致:报错者在 Windows 11 / 0.1.0-rc.6 上定位到该行为;社区在 macOS / 0.1.1-rc.2 上拿到逐字节一致的源码位置;另一位在 0.1.0-rc.7 上确认,且 profile-boot-DG5t9aNs.js 文件名哈希在 rc.7 与 rc.2 之间完全相同,说明这个模块期间没被改动。所以这不是平台相关、也不是某一版的偶发问题(#403)。 6. 影响面:所有「把 preset 放在项目里、随仓库分发、用 git 管理」的部署场景不可用;与 dsh-agent-presets README 的 roots 配置表直接矛盾;团队共享 preset 根、多项目多根场景完全无法实现(#403)。

机制:DeepSeek Harness 的 composeProfile 最后压入覆盖层把 roots 整体替换

问题的本质不是「配置没读到」,而是「配置读到了,然后在装载前被一个你看不见的步骤改掉了」。 逐层拆解:

  1. 覆盖层代码就在 CLI 里apps/cli/src/profile-boot.tscomposeProfile()(编译产物 lib/profile-boot-DG5t9aNs.js 第 179-187 行)里有这么一段:
ts
if (rows.has("agent-presets")) composedOverlays.push({
    id: "agent-presets",
    config: {
        ...rows.get("agent-presets")?.config ?? {},   // 保留 default/includeUserRoot
        roots: [{                                      // ← roots 被整体替换
            path: SHIPPED_PRESET_ROOT,
            trust: "system"
        }]
    }
});

注意这里的顺序:先展开用户配置(所以 defaultincludeUserRoot 都保留),然后写死 roots(所以只有 roots 被覆盖)。这就是「配置看起来被尊重了一半」的原因(#403)。 2. 它为什么必然赢:这个覆盖层被 pushcomposedOverlays 数组的末尾,作为最后一个 overlay 传给 loader。后写的层覆盖先写的层,所以无论用户在 bundle、profile、home 还是 --patch 哪一层配 roots,最终值都是「只有 shipped root」(#403)。 3. --dump-config 为什么不同步:dump 输出的是 patch 层组合结果,而这次注入发生在 dump 之后、loader 加载插件之前。它不是陈旧数据,而是一个真实不同的值——这也是为什么文档化的自查手段在这个字段上全部失效(#403)。 4. 为什么用户根是唯一幸存者dsh-agent-presets 自身源码里 includeUserRoot 默认为 true,它把 <dshHome>/.agent-presets 作为用户根,在被这个 bug 破坏的那次 roots 计算之后才追加。所以它是结构上免疫,而不是碰巧能用——这一点很重要,因为它意味着这个规避不会随版本变化而失效(#403)。 5. 一个反证:社区里有工具(dsh-blueprint)之所以写入 preset 一直正常,正是因为它只写默认用户根、从不碰 roots。另一位维护者原本给自家包文档写的是「复制到 CLI 自带的 shipped 根」,读到这条后改成了用户根方案——因为复制进 CLI 安装树每次升级都要重做,而用户根在用户空间、升级后依然有效(#403)。

规避与修复:DeepSeek Harness 用户根、overlay-check 与追加式补丁

立刻能用的是用户根;想彻底修则要把「替换」改成「追加」;还想要告警可以在自己的工具里加检测。 具体做法:

  1. 当前最稳的规避——放用户根:目录形状如下,agent.cordis.yml 与 shipped 的 standard preset 同构(一个用 @deepseek-ai/dsh-personapersona 行,然后每个工具一个 - id: <row> / name: '<package>'):
$DSH_HOME/.agent-presets/
  my-preset/
    preset.yml          # name, description, order
    agent.cordis.yml    # persona row, then one flat row per tool

发现流程不做记忆化(unmemoized),所以文件落盘后无需重启就能在预设选择器里选中;另外,目录内容无法解析或不是一组具名行时,会被列为 broken 并给出原因而不是被静默跳过——手写配置时这个设计很友好(#403)。 2. 官方修复方向——把替换改成追加,让用户配置优先

ts
if (rows.has("agent-presets")) composedOverlays.push({
    id: "agent-presets",
    config: {
        ...rows.get("agent-presets")?.config ?? {},
        roots: [
            ...(rows.get("agent-presets")?.config?.roots ?? []),  // 用户配置优先
            { path: SHIPPED_PRESET_ROOT, trust: "system" }        // shipped 兜底
        ]
    }
});

关键是让 shipped 根退化成兜底项而不是唯一项,这样既修好自定义根,又保证内置 preset 仍然可见(#403)。 3. 项目根需要随仓库分发时的过渡做法:在修复合入前,把项目内目录复制或同步到用户根(写进启动脚本同步亦可)。这样既保住了 git 管理,又能被扫描到;等上游改成追加式之后再切回项目根(#403)。 4. 把「静默覆盖」变成告警:社区已经把这条检查做成了工具规则——dsh-overlay-check 0.4.0(MIT、无依赖)会对声明该配置的 overlay 明确拒绝沉默:

warning  config-silently-overwritten  "agent-presets.roots" is replaced at boot
with the shipped root, so setting it here does nothing — and --dump-config will
still show your value, because the override is applied after the composition it
prints. Put presets in $DSH_HOME/.agent-presets/<id>/ instead.

它是一个作用在 patch 行上的纯函数,因此任何会写配置的工具都能复用,不限于原作者自己的工具链;规则刻意只锁定这一行、这一个键,方便上游修好后干净删除(#403)。 5. 排查时的心态建议:遇到「配置 dump 正确但运行时无效」的组合时,优先怀疑配置在装载前被改写,而不是继续检查 YAML 语法。用运行时探针(读 agentPresets.config)直接看生效值,比任何静态检查都可靠。这也适用于你在做 DSH插件DeepSeek插件、并在 DSH Plugin Hub 上分发带 preset 的插件时——如果插件的使用说明让用户去配 roots,那今天就等于给了他们一条走不通的路(#403)。

DSH plugin 排查注意事项

先记住这里不是 YAML 写错了——配置确实被读到了,只是在装载前被一个看不见的步骤改掉,所以任何静态自查都会告诉你「配置是对的」。 八条要点:

  1. 别只信 --dump-config:它不包含 composeProfile 的运行时注入,显示的是意图而非生效值。
  2. 用户根是结构上免疫includeUserRoot 在损坏的计算之后追加,所以不受这个覆盖影响。
  3. 失败看起来是间歇的:同一行里其他字段会生效,只有 roots 被丢弃。
  4. 跨平台跨版本一致:Windows/Linux/macOS、rc.6/rc.7/rc.2 表现相同,不是偶发。
  5. 用户根的优势:在用户空间,dsh 升级后无需重新复制,优于复制进 CLI 安装树。
  6. 发现流程不记忆化:文件落盘即可选中,通常不需要重启。
  7. 修复方向是追加而非替换:shipped 根应退化为兜底项。
  8. 给插件作者的提醒:别在自己的使用说明里推荐用户去配 roots,除非已确认上游修复。
DSH Plugin Hub 插件市场:分发与安装带 preset 的插件、核对版本

来源:Discussion #403dsh-overlay-check

常见问题

DeepSeek Harness 的 preset 文件放在项目里,为什么 UI 预设选择器看不到?

DeepSeek Harness 的 composeProfile() 会把你配置的 roots 在启动时**整体替换**,所以项目里的 preset 不会被扫描到。它在组合完所有 patch 层之后额外 push 一个覆盖层,把 agent-presets 行的 roots 换成只有 shipped 根 [{ path: SHIPPED_PRESET_ROOT, trust: "system" }];它作为**最后的 overlay** 传给 loader,所以你无论在哪一层配 roots 都不生效。

DeepSeek Harness 的 --dump-config 显示 roots 正确,为什么还是不生效?

DeepSeek Harness 的 --dump-config 打印的是 **patch 层组合结果**(bundle + profile + home + --patch),而覆盖层是在这个数组构建完成**之后**才 push 进去的。所以 dump 不是过期也不是缓存,它只是**展示了一个和 loader 实际收到的不一样的值**——你传什么参数都看不到生效值,这也是这个坑最耗时的地方。

DeepSeek Harness 修复合入之前,preset 放到哪里才能生效?

DeepSeek Harness 修复合入之前,把 preset 放到用户根 <dshHome>/.agent-presets/<preset-name>/ 一定生效(Windows 默认 %USERPROFILE%\.dsh\.agent-presets\,可用 $DSH_HOME 改位置)。includeUserRoot 默认为 true,而且它是在**被这个 bug 破坏的那次 roots 计算之后**才追加的,所以结构上不受这个覆盖影响;同时它在用户空间,dsh 升级后依然存在,不需要重新复制。

怎么快速确认自己命中的就是 DeepSeek Harness 这个 roots 覆盖 bug?

最快的确认方法是做一组对照:DeepSeek Harness 的 --dump-config --patch test.patch.yml 里能看到你的 roots,但 Web UI 预设选择器里就是不出现——这就是同一问题。更直接的证据是用运行时探针(动态插件读 agentPresets.config)看运行时 roots 是不是只剩 shipped,或者直接把同一个 preset 复制到用户根,如果立刻可见,就说明 preset 文件本身没问题、问题在 roots 注入。

相关术语

composeProfile
composeProfile 是 CLI 启动时把 bundle、profile、home、--patch 等 patch 层组合成最终配置的函数。它除了做组合,还会额外注入一个覆盖层专门处理 agent-presets 的 roots,而这个注入步骤不出现在 --dump-config 输出里。https://github.com/deepseek-ai/deepseek-harness/discussions/403
shipped root
shipped root 是随 CLI 一起分发的内置 preset 根(SHIPPED_PRESET_ROOT),trust 标记为 system。它是覆盖层里唯一保留下来的 roots 项,也是为什么内置预设始终可见、而自定义根始终不可见。https://github.com/deepseek-ai/deepseek-harness/discussions/403
includeUserRoot
includeUserRoot 是 agent-presets 的配置项,默认 true,效果是把 <dshHome>/.agent-presets 作为用户根追加进去。关键点是它在被覆盖破坏的那次 roots 计算之后才追加,因此是唯一不受 composeProfile 运行时覆盖影响的根。https://github.com/deepseek-ai/deepseek-harness/discussions/403

来源