DeepSeek Harness:DSH plugin 安装报 ETARGET 或装完起不来

故障排查发布于 2026-10-03作者: DeepSeek Plugin 插件市场
DeepSeek HarnessDSHnpmETARGETprereleasedist-tag安装失败启动失败
同一个 npm dist-tag 漂移会造成两种症状:npm i -g @deepseek-ai/dsh 报 ETARGET,或装完 dsh web 起不来并报依赖无法解析。前者是 rc.3 缺成员包,后者是 latest 指向 rc.2、兄弟依赖浮到 rc.3。

在 0.1.5 预发布线的那次发布事故里,npm install -g @deepseek-ai/dsh 会同时呈现两种截然不同的症状:一种是装不上**——报 ETARGET: No matching version found for @deepseek-ai/dsh-client-ui-sidebar-documentpreview@^0.1.5-rc.3;另一种是装上了却起不来——dsh web 退出 1 并报 plugin(s) failed to load: @deepseek-ai/dsh-sandbox-local,服务器根本起不来。** 两者其实是同一枚 npm dist-tag 漂移的两个切面:latest 仍指向 0.1.5-rc.2,而它的 63 个兄弟依赖全是 ^0.1.5-rc.2,会被 npm 解析到更新的 rc.3,形成「根 rc.2 + 兄弟 rc.3」的混合代际树(#7542);同时 rc.3 这个版本集漏发了一个成员包,而 prerelease 范围又只被同 major.minor.patch 的版本满足,于是 ^0.1.5-rc.3 在注册表里无解(#7436)。本文按「先分诊 → 两条机制 → 三个修法 → 结局与排查注意事项」展开,每步都给可粘贴的 registry 命令与 A/B 证据。

先分诊:装不上,还是装上了起不来

这两条症状的判据完全不同——一个停在 npm install,一个发生在 dsh web 启动时。先看错误出现在哪一步,再去对号入座。

判据装不上(ETARGET)装上了起不来(解析失败)
失败命令npm install -g @deepseek-ai/dshdsh web(安装已成功)
报错关键词npm error code ETARGET / No matching version foundplugin(s) failed to load / could not be resolved
注册表层面某个成员包的某个版本不存在版本都存在,但依赖树是混合代际
受影响的 tagnext 直接失败、latest 经 caret 传递失败latest(= rc.2)失败、next(= rc.3)正常
根因侧发布集不完整 + prerelease 匹配规则caret 浮版本 + hoist 布局变化
当时代价全新用户装不上全新用户装上了、起不来

两条命令,先把注册表读清楚

排查任何「装不上 / 装了不对」的问题,第一步都是把注册表的真实状态读出来,而不是猜:

bash
# 1. 三个通道现在各指哪个版本
npm view @deepseek-ai/dsh dist-tags --json

# 2. 一个 caret 范围到底能匹配到哪些版本(能否向前/向后兼容)
npm view "@deepseek-ai/dsh-client-ui-sidebar-documentpreview@^0.1.5-rc.3" version

# 3. 根包声明的兄弟依赖范围是什么
npm view @deepseek-ai/dsh@0.1.5-rc.2 dependencies --json

事故当天的输出(摘自两条报告的实测):

text
# 出错时
$ npm view "@deepseek-ai/dsh-client-ui-sidebar-documentpreview@^0.1.5-rc.3" version
404 No match found for version ^0.1.5-rc.3

# 修复后
$ npm view "@deepseek-ai/dsh-client-ui-sidebar-documentpreview@^0.1.5-rc.3" version
0.1.5-rc.3                     <- 现在能解析了

@deepseek-ai/dsh dist-tags:
  latest : 0.1.5-rc.3          <- 原为 0.1.5-rc.2
  next   : 0.1.5-rc.3
  alpha  : 0.1.7-alpha.2

机制一:prerelease 范围无法「向前兼容」,缺失的成员包必然硬失败

一句话:^0.1.5-rc.3 这个范围只被 0.1.5-rc.3 / 0.1.5-rc.4 这类「同一个 major.minor.patch」的 prerelease 满足;rc.3 一旦没发布,0.1.6-alpha.2 和 0.1.7-alpha.1 都救不了它,所以注册表里无解,ETARGET 是正确结果。

缺失的成员包

0.1.5-rc.3 发布集在 2026-09-22T05:55Z 发布,但 @deepseek-ai/dsh-client-ui-sidebar-documentpreview 根本不在这个集合里——它自己的 next 标签当时仍指向 0.1.5-rc.2,而 dsh 和 dsh-web-app 都把 next 移到了 rc.3。可用的 documentpreview 版本是:

text
0.1.5-alpha.2, 0.1.5-rc.1, 0.1.5-rc.2, 0.1.6-alpha.1, 0.1.6-alpha.2, 0.1.7-alpha.1

没有 0.1.5-rc.3。 而 @deepseek-ai/dsh-web-app@0.1.5-rc.3 声明了 @deepseek-ai/dsh-client-ui-sidebar-documentpreview@^0.1.5-rc.3,于是一装就断。

为什么「退不回、进不去」

这是 semver 的明确规则,用同一包做个对照实验最清楚:

text
$ npm view "@deepseek-ai/dsh-web-app@^0.1.6-alpha.1" version
@deepseek-ai/dsh-web-app@0.1.6-alpha.1 '0.1.6-alpha.1'
@deepseek-ai/dsh-web-app@0.1.6-alpha.2 '0.1.6-alpha.2'

注意 0.1.7-alpha.1 存在却不在列表里。同理,^0.1.5-rc.3 不会被 0.1.6-alpha.2 或 0.1.7-alpha.1 满足,也不会回退到 0.1.5-rc.2。「prerelease 范围只匹配同 major.minor.patch 的 prerelease」这条规则,把这类发布事故从「部分降级」直接变成「硬失败」——没有可回退的版本,npm 只能报 ETARGET。

为什么 latest 也一起挂

dsh@0.1.5-rc.2(当时的 latest)声明 @deepseek-ai/dsh-web-app: ^0.1.5-rc.2,而这个范围同时匹配 rc.2 与 rc.3:

text
$ npm view "@deepseek-ai/dsh-web-app@^0.1.5-rc.2" version
@deepseek-ai/dsh-web-app@0.1.5-rc.2 '0.1.5-rc.2'
@deepseek-ai/dsh-web-app@0.1.5-rc.3 '0.1.5-rc.3'

npm 取最高的 rc.3,于是 latest 被拖进同一条坏链。一个成员包漏发,next 直接失败、latest 经 caret 传递失败——这就是当时「连默认安装命令都用不了」的原因。

CI 为什么没拦住

发布脚本 scripts/release/publish.ts 按记录顺序发布一个打包目录,它自述的完整性契约是「顺序里的每一项最终都以已发布或已存在收尾」(:143-145),对「已存在」那一支还做了真正的完整性校验(:155-166)。但整个流程里没有任何一步去问:刚刚发布的这些条目,它们声明的范围在注册表上能不能解析。 一个后置检查(把每个集合内范围按上面那两条 npm view 的方式解析一遍)就能在 latest 还没被拖下水之前拦下它——而那正是让用户可见的那一步。

机制二:latest 指向 rc.2 时,63 个兄弟依赖浮到 rc.3,形成混合代际树

一句话:dsh@0.1.5-rc.2 的 63 个兄弟依赖全是 ^0.1.5-rc.2,会被解析到版本更高的 rc.3;于是「根是 rc.2、兄弟是 rc.3」的树里,@deepseek-ai/dsh-sandbox-local 被嵌套到两层子目录,而启动时有一次解析锚定在应用安装目录上,直接 MODULE_NOT_FOUND。

两个根版本,两种布局

dsh@0.1.5-rc.2(当时的 latest)dsh@0.1.5-rc.3(当时的 next)
启动(干净 DSH_HOME)退出 1:plugin tree failed to load: @deepseek-ai/dsh-sandbox-local ... could not be resolved,19 行 stderr,无 URL35 秒后存活,stderr 为空,打印 dsh web: http://127.0.0.1:<port>/?token=…
dsh-sandbox-local 落在哪.../dsh/node_modules/@deepseek-ai/**dsh-base**/node_modules/@deepseek-ai/dsh-sandbox-local(另外在 dsh-sdk-minimal 下还有一份).../dsh/node_modules/@deepseek-ai/dsh-sandbox-local(就在应用旁边)
安装包数563518

同一台机器、同一个 npm、同一个 Node、相隔几分钟,唯一变量是根版本——这与另一个社区的 runner A/B 完全一致:

root 包ubuntu-latestwindows-latest
0.1.5-rc.3启动成功启动成功
0.1.5-rc.2(= 当时 latest)plugin(s) failed to load: dsh-sandbox-local同样失败
0.1.2-rc.1起不来(另一种失败)起不来

三条探针:失败的是「应用安装锚点」,不是 profile 目录

这是整个线程里最关键的一步——它解开了两种看似矛盾的说法:

探针rc.2rc.3
从应用安装目录解析 @deepseek-ai/dsh-sandbox-local(<prefix>/@deepseek-ai/dsh/lib)MODULE_NOT_FOUNDOK
profile 目录里的投影链接存在,但指向两层深处存在,指向应用旁那份
从 profile 目录解析OKOK

也就是说:profile 目录的解析是通的(所以「从 profile 目录探一下」永远看不到这个故障),而启动真正失败的那次解析锚定在应用安装目录上,在 rc.2 的混合树里那里够不到 dsh-sandbox-local。rc.3 里这份包就在应用旁边,同一个锚点就成功了。「混合树让 hoist 布局变化」与「解析能成功」因此都对,只是各自测的锚点不同。

被否掉的假设:pnpm 不是启动前置

线程里一度流行「干净机器没装 pnpm,所以 profile 没被准备好」。受控实验把它排除了:缺 pnpm 的干净 runner 上 rc.3 照样启动;启动路径上也没有 pnpm spawn——apps/cli/src/plugin.ts 只在 dsh plugin …(唯一把参数转发给 pnpm 的 CLI 消费者)时提示 pnpm was not found,initProfile 只是写出 profile 的 pnpm-workspace.yaml。

但请注意区分:pnpm 不是「启动」前置,却是「插件管理」前置(dsh plugin … 会转发给 pnpm)。所以全新机器上装一份 pnpm 仍然是好习惯,只是别把它当成启动失败的根因。

另一种解释的排除:安装脚本不是变量

同一台 Windows、同一天、五次安装同一 dsh 版本的对照:

#@deepseek-ai/dsh@0.1.5-rc.2 的装法兄弟包解析到dsh-win32-process 副本数dsh --profile headless
1npx 缓存建于 rc.3 发布之前231 × rc.2,0 重复1启动并响应
2npm install -g(默认,跳过安装脚本)1 × rc.2 根 + 270 × rc.3,28 包重复4失败
3npm install -g --allow-scripts=…同 #24失败
4装进一个空本地项目1 × rc.2 根 + 230 × rc.3,但扁平 hoist,0 重复1启动并响应
5npx -y @deepseek-ai/dsh@alpha267 × alpha.2,0 重复1启动并响应

读法很干净:#2 vs #3 说明安装脚本不是变量;#2 vs #4 说明同一份混合 rc.2/rc.3 图,扁平 hoist 时能启动、dsh 自己是树根时失败;#1 vs #2 是时间线——rc.3 出现之前装的 rc.2 是自洽的、能用。「树根是谁」和「布局是否扁平」才是决定性差异。

三个修法

修法一(用户当下可用):按代际固定,不要按标签装

思路:让整棵树同代,而不是让一个包去迁就。 混合代际树的根源是「根 rc.2 + 兄弟 rc.3」,所以把入口钉到与兄弟相同的代际即可:

bash
# 方案 A:直接装 next(当时 = 0.1.5-rc.3),这是最简单有效的一条
npm uninstall -g @deepseek-ai/dsh
npm install -g @deepseek-ai/dsh@next

# 方案 B:显式钉版本
npm i -g @deepseek-ai/dsh@0.1.5-rc.3

# 顺带装上插件管理需要的 pnpm(不是启动前置,但 dsh plugin 需要)
npm i -g pnpm

两个注意点:

  1. 「钉,而不是「强塞一个包」」:单独把 @deepseek-ai/dsh-sandbox-local 加进依赖只会移动一个包,而失败来自整套不一致;把入口钉到同代才是让布局稳定的做法。若你非要留在 rc.2,就得把 63 个兄弟依赖全部钉住——那是同一件事,只是打字更多。
  2. pnpm 是独立的一半:它修不了分辨率,但插件管理要用;全新机器上先装好能少一类「Cannot find package」的噪音。

修法二(ETARGET 场景):换通道或本地 override

如果撞到的是「装不上」,当时可用的两条旁路:

bash
# 1. 换到 alpha 通道(当时 = 0.1.7-alpha.1,其 dsh-web-app / documentpreview 范围自洽)
npm install -g @deepseek-ai/dsh@alpha
jsonc
// 2. 本地项目里用 overrides 把坏掉的那一环钉回旧版
{
  "overrides": {
    "@deepseek-ai/dsh-web-app": "0.1.5-rc.2"
  }
}

alpha 之所以有效,是因为 0.1.7-alpha.1 的 dsh-web-app 是 0.1.7-alpha.1、它的 documentpreview 范围 ^0.1.7-alpha.1 能解析——整条链是自洽的,这正是它可靠的原因。

修法三(上游):移动 tag、精确锁定范围、补齐直接依赖、补诊断

针对机制给的上游修法,按重要性排序:

  1. 把 latest 移到自洽的一代(0.1.5-rc.3 能启动),或把 0.1.5-rc.2 的依赖改成精确版本——当前的 ^0.1.5-rc.2 允许更新的 prerelease,正是它造出了混合树。「锁范围」是结构上更重要的一半:只要 @deepseek-ai/dsh@X 对全家族都声明 ^X,一次安装就可能从两个发布代里各取一些包,发布时打包与探测过的布局,和用户实际拿到的布局就不是同一个。
  2. 把 @deepseek-ai/dsh-sandbox-local 声明为 dsh 的直接依赖,让 npm 无论其它包怎么排布都把它 hoist 到顶层。这项无害但不是根治:在混合树里这个包嵌套着也能被 profile 目录解析到,而且同样的静默跳过适用于每一个缺失的包——它让树更统一,但不让解析更正确。
  3. 补上发布后校验:在 scripts/release/publish.ts 里,对刚发布的集合逐条解析其声明的范围(就用本文那两条 npm view 的方式),在 latest 被拖下水之前拦下。
  4. 把「被静默跳过的依赖」说出来:闭包遍历对「声明了但在任何锚点都找不到」的依赖是静默 continue 的——
ts
const dir = packageDirFromAnchor(next.anchor, dep)
// A declared-but-uninstalled dependency cannot be a loader-visible
// plugin; skip it rather than fail the whole boot.
if (dir === undefined) continue

(packages/boot/app-boot/src/profile.ts:494-496)于是 Loader 只报那条 entry 无法解析,却不说是哪个包、被哪个 manifest 声明。解析过程本来就记录了每个包的声明者(master 的 createRuntimeResolution 保留 declarers 与 versions 映射),打一行「跳过了谁、由谁声明」,就能把「看不懂」变成五分钟诊断。报告者特别指出:当时的摘要说「see the error(s) logged above」,可它上面根本没有那些行,stderr 只有摘要和堆栈——这个诊断缺口是真实的。

结局:两处修复都已上线

先说结论:这次的坑已经填平,latest 也从 rc.2 移到了 rc.3,正好同时治好「装不上」与「起不来」两条症状。

text
$ npm view "@deepseek-ai/dsh-client-ui-sidebar-documentpreview@^0.1.5-rc.3" version
0.1.5-rc.3                     <- 此前是 "404 No match found"

@deepseek-ai/dsh dist-tags:
  latest : 0.1.5-rc.3          <- 原为 0.1.5-rc.2
  next   : 0.1.5-rc.3
  alpha  : 0.1.7-alpha.2

也就是说:缺的成员包被发布了,latest 也被移到了它上面(正是原始报告列出的两个选项中的第一个)。普通 npm install -g @deepseek-ai/dsh 应该重新能解析;还钉在 rc.2 的老机器,重装一次即可。

但机制层面的两条教训仍然成立,因为它们描述的是「类」而不是「这一次」:

  1. 失败是硬的而非优雅的,因为 prerelease 匹配规则:^0.1.5-rc.3 不能被 0.1.6-alpha.2 或 0.1.7-alpha.1 满足——prerelease 范围只匹配同 major.minor.patch 的 prerelease。发布那个成员包只治了这一次,没有消除「下一次同样静默」的理由。
  2. 建议的护栏仍然不在:发布工具自述的完整性契约还是那句「Every entry in the order settles as either published or already present」(scripts/release/publish.ts:143-145),而没有一步去解析刚发布条目声明的范围。在 tag 移动让它对用户可见之前抓住它,这仍是最便宜的办法。

排查注意事项

第一原则:先分清「装不上」与「装上了起不来」,再去读注册表——不要重装、不要删 ~/.dsh、不要先怀疑第三方插件。这两条根因都在 DSH 自己的依赖里。

  1. 先读 dist-tags:npm view @deepseek-ai/dsh dist-tags --json。latest 与 next 指哪,直接决定你会撞上哪条症状(#7542)。
  2. 别用「有没有 pnpm」解释启动失败:启动路径没有 pnpm spawn;缺 pnpm 的干净 runner 上 rc.3 照样启动。pnpm 只影响 dsh plugin …(#7542)。
  3. 别用「安装脚本没跑」解释:--allow-scripts 与默认安装得到同一份混合图、同样失败;npx @alpha 跳过脚本也能启动(#7542)。
  4. 先跑三条探针再下结论:① 从应用安装目录解析那个包;② 看 profile 里的投影链接在不在;③ 从 profile 目录解析。只测 ③ 会漏掉本故障,因为失败的那次解析锚定在应用安装目录(#7542)。
  5. 分辨「同一份图、不同布局」:同一份混合 rc.2/rc.3 图,扁平 hoist 时能启动、dsh 自己是树根时失败。布局是决定性的,不是包的完整性(#7542)。
  6. 读 summary 上面的行,而不是 summary 本身:plugin tree failed to load 只是结论,真正的原因行在它上面;当时那段 stderr 里其实没有——这也是报告建议补「跳过诊断」的原因(#7542)。
  7. 不要把「rc.2 起不来」外推成「所有旧版本都起不来」:0.1.2-rc.1 的失败方式不同(打印 URL 后不应答),环境变动与包变动要分开归因(#7542)。
  8. 顺手看一眼 tag 卫生:同一时期 @deepseek-ai/dsh-web-app 自己的 latest 还停在 0.0.1-rc.1。这类「tag 不前进」是同一类问题(scripts/release/families.ts 把含 - 的版本都映射到 next,只把稳定版交给 npm 的 latest 默认值;而从未发布过稳定版),值得一起关注(#7436、#7542)。

排查这类问题时,用 DSH Plugin Hub 的自定义安装面板确认你装的是什么通道(npm 包 / GitHub 源码 / DSH 命令),并在已安装列表里核对版本号与来源——把「装的是哪一代、从哪来」先固定下来,再去分辨是安装期缺失还是启动期解析,能少走很多弯路。

DSH Plugin Hub · 确认安装

来源:Discussion #7436、Discussion #7542。

常见问题

`npm install -g @deepseek-ai/dsh` 报 ETARGET,说找不到某个 @deepseek-ai 包,是怎么回事?

这是某个 rc 版本集**缺了一个成员包**。0.1.5-rc.3 发布时,@deepseek-ai/dsh-web-app@0.1.5-rc.3 声明了 @deepseek-ai/dsh-client-ui-sidebar-documentpreview@^0.1.5-rc.3,而该包的 rc.3 从未发布(它的 next 当时还指向 rc.2)。更关键的是 **prerelease 范围只能被同一个 major.minor.patch 的 prerelease 满足**,所以 ^0.1.5-rc.3 既不是「回退到 rc.2」,也不是「前进到 0.1.6-alpha.x」——注册表里没有任何版本能满足它,ETARGET 是正确的,不是解析器抽风(Discussion #7436)。

为什么连 `latest` 也装不上,不是只有 `@next` 坏了?

因为 dsh@0.1.5-rc.2(当时的 latest)声明的是 @deepseek-ai/dsh-web-app: ^0.1.5-rc.2,而这个 caret 范围同时匹配 rc.2 与 rc.3,npm 取最高的 **rc.3**,于是被拖进同一个坏链。一个链接断掉,latest 与 next 两条通道一起下架(Discussion #7436)。

装是装上了,但 `dsh web` 起不来并报 dsh-sandbox-local 无法解析,怎么办?

当时的 latest 是 0.1.5-rc.2,它的 63 个兄弟依赖全是 ^0.1.5-rc.2,会被 npm 解析到全新的 **rc.3**——得到一个「根是 rc.2、兄弟是 rc.3」的混合代际依赖树。在这种树里 @deepseek-ai/dsh-sandbox-local 被嵌套进两层子目录,而启动时某次解析锚定在**应用安装目录**上,于是 MODULE_NOT_FOUND。最省事的办法是**按代际固定**而不是按标签装:npm i -g @deepseek-ai/dsh@0.1.5-rc.3(或 @next),让整棵树同代(Discussion #7542)。

有人说要装 pnpm 才能启动,是真的吗?

pnpm 不是**启动**前置。启动路径上没有 pnpm spawn:只有 dsh plugin …(它把参数转发给 pnpm)会提示 pnpm was not found;initProfile 只是**写出** profile 的 pnpm-workspace.yaml。受控实验里,缺 pnpm 的干净 runner 上 rc.3 照样启动。不过**插件管理**确实要 pnpm,所以全局装一份 pnpm 仍然值得(Discussion #7542)。

现在修好了吗?

两处都已上线。注册表现在能解析 @deepseek-ai/dsh-client-ui-sidebar-documentpreview@^0.1.5-rc.3(此前是 404 No match found),并且 latest 已从 0.1.5-rc.2 移到 0.1.5-rc.3——正好把「装不上」和「装上了起不来」一起修掉。若你的机器还钉在 rc.2,重新装一次即可(Discussion #7436)。

相关术语

prerelease 范围匹配(same major.minor.patch)
semver 的一条规则:带 prerelease 的比较符(如 `^0.1.5-rc.3`)只被**同一个 `major.minor.patch`** 的 prerelease 满足。所以 `^0.1.5-rc.3` 接受 `0.1.5-rc.3`、`0.1.5-rc.4`,但**不接受** `0.1.6-alpha.2` 或 `0.1.7-alpha.1`。这正是「缺了 rc.3 就无路可退、无路可进」的原因,也让这类发布事故从「部分降级」变成「硬失败」。— https://github.com/deepseek-ai/deepseek-harness/discussions/7436
混合代际依赖树(mixed-generation tree)
根包是某一代(如 `dsh@0.1.5-rc.2`),而它的兄弟依赖因为 caret 范围浮到了下一代(rc.3)时形成的依赖树。因为 `^0.1.5-rc.2` 的 prerelease 元组与 rc.2 相同,版本更高的 rc.3 会被选中。这种树里 npm 的 hoist 布局与「整棵同代」时不同,某个包可能被嵌套到 loader 看不到的位置。— https://github.com/deepseek-ai/deepseek-harness/discussions/7542
dist-tag 漂移(latest 不自动前进)
`scripts/release/families.ts` 把任何含 `-` 的版本映射到 `next`(`alpha`/`canary` 各归自己的通道),只把稳定版交给 npm 的 `latest` 默认值。因为从未发布过稳定的 `@deepseek-ai/dsh`,`latest` 会一直停在当初指向的那个 prerelease 上——**没有任何一次发布会自动把它前移**,必须由操作者手动改注册表状态。— https://github.com/deepseek-ai/deepseek-harness/discussions/7542
静默跳过(closure walk 的 deleted 依赖)
启动器遍历安装的依赖闭包时,对「被声明但在任何锚点都找不到」的依赖采取 `continue`(`packages/boot/app-boot/src/profile.ts:494-496`),既不报错也不指名。于是安装里少了一个包时,Loader 只会把**那条 entry** 报成无法解析,却不说是哪个包、被哪个 manifest 声明。把「跳过了谁」打出来,就能把这类报错从「看不懂」变成「可定位」。— https://github.com/deepseek-ai/deepseek-harness/discussions/7542

来源