DeepSeek Harness rc.2:DSH plugin pnpm run clean 报 outDir 错

故障排查发布于 2026-10-03作者: DeepSeek Plugin 插件市场
DeepSeek HarnessDSHpnpm run cleanTypeScriptoutDir项目引用构建脚本dsh-v0.1.7-rc.2
在 dsh-v0.1.7-rc.2 全新检出上跑 pnpm run clean 会退出 1:expected TypeScript outDir to end in /types。原因是新增的键盘测试 tsconfig 把非 /types 的 outDir 拉进了项目引用图。本文给三种修法与一个残留。

在 dsh-v0.1.7-rc.2(477b4f4205)的全新检出上跑 pnpm run clean,会立刻以退出码 1 失败——错误是 clean: expected TypeScript outDir to end in /types: lib/desktop-keyboard-test-types。 原因是 rc.2 新增的 tsconfig.desktop-keyboard-tests.json(提交 6a82709a2b)声明了一个不以 /types 结尾的 outDir,并且能经由 tsconfig.client.json 进入根项目的引用图;而 scripts/clean.ts 的 buildOutputDirectories() 只接受两种形态的 outDir,其余一律抛错(#8127)。它最讽刺的地方在于:clean 脚本存在的意义正是「清掉上一版本留下的陈旧 lib/」,而它在最需要这个能力的场景里恰恰坏了。 该问题已在 master 上由 a08472e 修掉并随 dsh-v0.2.0-rc.1 发布;本文按「先分诊 → 机制 → 三种修法 → 修好后的一个残留」展开,并给出一份可以照抄的手工清理清单。

先分诊:这是「计划期」的失败,不是「执行期」的失败

一句话:这条错误是在读 tsconfig、计算「该删什么」的阶段抛出来的,跟磁盘上有没有那些目录、有没有权限都无关——所以它不会因为你先手工删掉某个子目录而消失。

先把两种失败模式分开,能省掉一轮无效尝试:

判据计划期失败(本例)执行期失败
报错时机立刻,还没开始删任何东西删到一半才报
错误文本关于 tsconfig / outDir 的语义校验关于文件占用、权限、路径
先手工删目录能否绕过不能常常能
修法改配置或改脚本关进程 / 提权 / 清占用

本例的证据很直接:错误文本是 expected TypeScript outDir to end in /types: lib/desktop-keyboard-test-types,这是对配置值的断言,不是对文件系统的断言。所以一切「先删那个目录再跑」的尝试都是方向错误的。

机制:clean 的计划器只认两种 outDir 形态

一句话:buildOutputDirectories() 的合法输入只有「basename 为 types」和「native 入口的输出目录」,其它名字直接抛错;而 rc.2 新增的键盘测试 tsconfig 正好是第三种名字,还挂在引用图上。

三个条件同时成立,故障才会发生:

  1. 有一个非 /types 结尾的 outDir。 tsconfig.desktop-keyboard-tests.json 声明:

    jsonc
    {
      // emitDeclarationOnly 的键盘测试 fixture
      "outDir": "lib/desktop-keyboard-test-types"
    }
    
  2. 它能被根项目的引用图看到。 该 tsconfig 经由 tsconfig.client.json 被根项目引用——只要在图上,就会被 clean 检查,无论它服务的是产品代码还是测试夹具。

  3. 脚本对未知形态零容忍。 scripts/clean.ts 的 buildOutputDirectories() 接受且仅接受两种形态:

    text
    ① basename 为 types          (常规类型产物目录)
    ② native 入口的输出目录        (已被特判)
    
    其它任何名字 → 抛错,而不是跳过或警告
    

于是「新增一个测试用 tsconfig」这个看似无害的构建改动,直接把 clean 变成了全局失败。

为什么不是「警告后跳过」就好

因为对 clean 来说,「不认识的 outDir」是一个意义不明的状态。 跳过它可能漏清理(留下陈旧产物),照删又可能删掉不该删的东西。抛错是保守选择——问题是它把「一个测试 fixture 的命名」升级成了「整条 clean 不可用」。上游的修法因此不是放宽校验,而是把第三种形态从根上消掉(见修法一)。

为什么这个 bug 值得单独立文

它符合三个「构建脚本站得住脚、用起来要命」的特征:

  1. 只在全新检出上炸。 全新 checkout + pnpm install 之后是 clean 的正题场景——你要清掉上一版本留下的 lib/。平时本地反复构建、目录早被清过的环境反而可能没撞上。
  2. 症状与意图相反。 clean 的存在意义是「清掉陈旧产物」,而它恰恰在「有陈旧产物要清」时失败。
  3. 错误信息指向配置,不指向行为。 看到 outDir 的人第一反应是改 outDir,而正确方向是删掉那个 tsconfig(见 FAQ)。

修法与残留:升级 / cherry-pick / 手工清理,以及修复后活下来的目录

修法一(推荐):升级到 dsh-v0.2.0-rc.1

一句话:该问题已在 master 上由 a08472e(fix(build): fold desktop keyboard tests into client typecheck)修复,并随 dsh-v0.2.0-rc.1 发布。

修复做的事:

  • 删除 tsconfig.desktop-keyboard-tests.json;
  • 把这些文件并入 tsconfig.client.json 的类型检查;
  • 于是项目图里唯一不以 /types 结尾的 outDir 只剩 native 入口的 lib,而 scripts/clean.ts 本来就特判了它。

在全新克隆上对照验证:

bash
git checkout dsh-v0.1.7-rc.2 && pnpm run clean
# clean: expected TypeScript outDir to end in /types: lib/desktop-keyboard-test-types   # exit 1

git checkout dsh-v0.2.0-rc.1 && pnpm run clean
# clean: already clean                                                                   # exit 0

这是最省事的路线,也是生产团队倾向的选择:不在发布波浪中途改动构建脚本,等下一次固定的版本升级时自然带上。

修法二(留在 rc.2):cherry-pick 修复提交

一句话:如果你必须钉在 rc.2,这个修复能干净地摘过来,摘完 clean 立刻退出 0。

bash
git cherry-pick a08472e982
pnpm run clean

适用与不适用:

  • 适用:你已经钉在 rc.2、且需要一个能用的 clean;
  • 不适用:你正处在发布窗口内、不希望引入构建脚本改动——那就用修法三。

修法三(不改版本):按清单手工清理

一句话:这是上游与生产团队都文档化过的临时办法——手工删掉 gitignore 的 lib/ 目录与根级 *.tsbuildinfo,等价于把 clean 的计划手动执行一遍。

报告人验证过的顺序与结果:

  1. 手工删除各个 gitignored 的 lib/ 目录;
  2. 删除根级的 *.tsbuildinfo(这一步别漏:TS 增量构建的缓存文件没清掉,下一次 build 可能仍按旧图的判定走);
  3. 之后 pnpm run build 退出 0,产出 343 个 client artifact。

这份清单之所以靠谱,是因为它复刻的就是 clean 原本要做的事,只是把「计算」换成了「人工枚举」。它的代价是需要随版本更新人工维护——所以生产团队的态度是「先用着,等下一次版本升级换掉它」。

修好之后的一个残留:那个目录会活下来

一句话:修法是删掉 tsconfig,于是 clean 从此不再知道 lib/desktop-keyboard-test-types 的存在;如果 rc.2 已经把它生成出来,它会在 pnpm run clean 之后依然存活。

这是修法一唯一的副作用,值得单独提醒(已在讨论中被实测确认:跑出 already clean 之后,那个文件夹仍然在):

bash
# 一次性手工删除
rm -rf lib/desktop-keyboard-test-types
powershell
# PowerShell
Remove-Item -Recurse -Force lib\desktop-keyboard-test-types

删一次即可——配置没了,之后不会再生成它。但如果你在升级前就用 rc.2 构建过,务必记得删这一步,否则一个陈旧的类型目录会一直留在工作区里,而 clean 再也不会替你处理它。

排查注意事项

  1. 先看错误是「配置语义」还是「文件系统」。 前者改配置,后者清占用;本例属于前者,先删目录没用。
  2. 别去改那个 outDir 的名字。 它服务于键盘测试 fixture,改成 lib/types 会与真正的类型目录互相覆盖——上游的方向是删掉这个 tsconfig。
  3. 检查新增的 tsconfig 是否挂在项目引用图上。 只要能从根项目引用到,就会被所有基于引用图运行的脚本(clean、build、增量检查)看到。
  4. 构建脚本对未知输入零容忍时要当心。 抛错比跳过保守,但会把「一个 fixture 的命名」升级成「整条链路不可用」——新增配置时值得先跑一遍 clean。
  5. 全新检出才是这类问题的正题场景。 本地反复构建过的环境可能掩盖它;验证修复请在 fresh clone 上做。
  6. 手工清理别漏 *.tsbuildinfo。 只删 lib/ 而留下增量缓存,下一次构建可能仍按旧判定走。
  7. 升级时记得手工删一次残留目录。 lib/desktop-keyboard-test-types 不会因修复被自动清掉。
  8. 发布窗口内优先「等等再升」。 生产团队的取舍是有道理的:钉在 rc.2 + 手工清理,比波浪中途 cherry-pick 一个构建脚本改动更稳。
  9. cherry-pick 前确认干净度。 该提交能干净摘取(讨论中已验证),但摘取后仍要跑一次 pnpm run clean 确认退出 0。
  10. 把「clean 能否通过在全新检出上」纳入升级检查表。 这类问题 CI 未必覆盖,但一次 fresh clone 就能发现。

来源


这条例子的价值不在修法有多难,而在于它暴露了一条容易被忽略的耦合:项目引用图是「所有基于 TS 的构建脚本」的共同输入,往图上加一个 tsconfig 的副作用,可能落在与它毫不相干的脚本上。 如果你也在维护带项目引用的 monorepo,建议在新增任何 tsconfig 后跑一遍 clean 与 build,并把「fresh clone 上 clean 是否退出 0」写进升级检查表——这类只在新检出上复现的失败,靠日常开发是很难撞见的。把插件的安装、升级与系统日志集中到 DSH Plugin Hub 里对照,升级类问题会更容易定位。

DSH Plugin Hub 系统日志页:记录安装、卸载、设置变更与诊断等操作轨迹,可按分类与级别查看

常见问题

为什么这条错误只在全新检出上出现,我平时跑 `pnpm run clean` 好像没事?

因为触发它的是**项目引用图里那个新加入的 tsconfig**,而不是你本地有没有构建产物。tsconfig.desktop-keyboard-tests.json 在 rc.2 里通过 tsconfig.client.json 进入根项目的引用图,scripts/clean.ts 的计划器一看到它的 outDir 不以 /types 结尾就抛错。如果你本地那份 lib/ 早就被旧版本或其它方式清过,可能一时没撞上;但在**全新检出、且上一版本留下 lib/** 这个 clean 存在的正题场景里,它必然失败。

报错说 outDir 必须以 `/types` 结尾,那我把它改成 `lib/types` 不就行了?

不建议。lib/desktop-keyboard-test-types 是那个 **emitDeclarationOnly 的键盘测试 fixture** 的输出目录,改成 lib/types 会与真正的类型产物目录撞名、互相覆盖。上游选择的修法方向恰好相反:**删掉这个独立 tsconfig**,把这些文件并入 tsconfig.client.json 的类型检查,让引用图里只剩「以 /types 结尾」和「native 入口的 lib」两种形态——后者 scripts/clean.ts 本来就特判了。

我能不能只删掉 `lib/desktop-keyboard-test-types` 再跑 clean?

没用,错误跟那个目录是否存在无关——它是在**读 tsconfig 的计划阶段**抛的,不是遍历文件时抛的。要留在 rc.2,正确做法是 cherry-pick a08472e982,或者按手工清单清理。

升级到 0.2.0-rc.1 之后,为什么 `lib/desktop-keyboard-test-types` 还是删不掉?

这是修复留下的一个残留:修法是**删掉那个 tsconfig**,于是 clean 从此不再知道这个目录的存在。如果某个 rc.2 构建已经把它生成出来,它会**在 pnpm run clean 之后依然存活**(已被实测确认:「already clean」跑完目录仍在)。手工删一次即可,之后不会再生成。

如果我不能马上动版本基线,最稳的路径是什么?

按讨论里生产团队的取舍来:**保留已文档化的手工清理流程**(清 gitignore 的 lib/ 目录加根级 *.tsbuildinfo),不做波浪中途的 cherry-pick,等下一次固定的版本升级时随 a08472e 一起带上。这样避免在发布窗口里引入构建脚本改动。

相关术语

scripts/clean.ts / buildOutputDirectories()
DSH 的 clean 脚本用它计算「该删哪些构建输出目录」。它只接受两种形态的 `outDir`:①basename 为 `types`;②native 入口的输出目录。**任何其它名字都会直接抛错**,而不是跳过或警告——这就是一条新增的测试 tsconfig 能整体打断 clean 的原因。— https://github.com/deepseek-ai/deepseek-harness/discussions/8127
项目引用(project references)图
TypeScript 的 `references` 机制把多个 tsconfig 组织成有向图,根项目通过它聚合各子项目。`tsconfig.desktop-keyboard-tests.json` 正是因为能经由 `tsconfig.client.json` 被根项目引用到,才进入了 clean 的扫描范围——**只要在图上,就会被检查,无论它服务于产品代码还是测试 fixture**。— https://github.com/deepseek-ai/deepseek-harness/discussions/8127
emitDeclarationOnly fixture
`tsconfig.desktop-keyboard-tests.json` 是一个只产出 `.d.ts`(不产出 JS)的配置,用于键盘测试相关的类型夹具,其 `outDir` 被命名为 `lib/desktop-keyboard-test-types`。它是本次故障的触发物:既不在 clean 的两种合法形态里,又确实挂在项目引用图上。— https://github.com/deepseek-ai/deepseek-harness/discussions/8127
a08472e(修复提交)
`fix(build): fold desktop keyboard tests into client typecheck`。它删除 `tsconfig.desktop-keyboard-tests.json`,把这些文件并入 `tsconfig.client.json` 的类型检查,于是项目图里唯一不以 `/types` 结尾的 outDir 只剩 native 入口的 `lib`(clean 已特判)。该提交随 **dsh-v0.2.0-rc.1** 发布,也能干净地 cherry-pick 回 rc.2。— https://github.com/deepseek-ai/deepseek-harness/discussions/8127

来源