DeepSeek Harness 静默退出码 0:Node 24.0/24.1 缺 import.meta.main
在 Node.js v24.0.x / v24.1.0 上运行 dsh web:什么都不打印、日志文件为空、从不监听 3080 端口,进程随即以退出码 0 结束——看起来像「正常退出」,实际是 CLI 主体根本没被执行。 根因在 lib/bin.js 的入口守卫:if (import.meta.main) await runCli();,而 import.meta.main 直到 Node v24.2.0(22.x 线是 v22.18.0)才被引入;在 v24.1.0 上这个属性是 undefined,于是 runCli() 永远不会被调用,进程加载完模块就干净退出(#6584)。更糟的是没有任何护栏:根 package.json 的 engines 范围 ^22.19.0 || >=24.0.0 恰好把出问题的 24.0 / 24.1 包含在内,apps/cli/package.json 干脆没有 engines,而 npm 对 engines 本身也只警告不拦截。本文按「先分诊 → 根因与可用边界 → 三条修法 → 同族与排查注意事项」展开。
先分诊:退出码 0、无输出、无日志,先量 Node 版本
这类「什么都没发生」的故障,第一步永远是确认你实际跑的是哪个 Node(以及 nvm 是否切对了)——因为症状里没有任何其它线索。
| 判据 | 版本断点导致的静默退出 | 真的启动失败(端口占用 / 原生模块 / 代理) |
|---|---|---|
| 有输出吗 | 完全没有(stdout / stderr 都空) | 通常有报错、栈或至少一行提示 |
| 退出码 | 0 | 多为非 0 |
| 日志文件 | 空 | 可能有内容 |
| 是否监听端口 | 从不监听 | 可能已监听但被占用 / 起不来 |
| 换 Node 版本后 | 立即恢复 | 症状不变 |
| 关键判据 | import.meta.main 为 undefined | import.meta.main 正常 |
两行命令先定性
# 1. 你实际在跑哪个 Node(nvm 切换后务必重开终端或用绝对路径确认)
node -v
# 2. 直接问运行时:这个 API 存不存在
node -e "console.log(import.meta.main)"
# Node 24.1.0 → undefined ← 命中本问题
# Node 24.2.0+ → true
如果第 1 步落在 24.0.x / 24.1.0,第 2 步又是 undefined,基本就锁定了。这条判据的价值在于:它把「我是不是被 WDAC / 原生模块 / 代理坑了」这类猜测一次性排除掉——报告者正是先查了这些,花了大量时间才发现是 Node 小版本差异。另一位用户也提到,某开源镜像站上的 Node 恰好就停在 v24.1.0,导致「完美闪退」排查了三小时。
根因:import.meta.main 在 Node 24.0/24.1 上是 undefined
一句话:入口判断依赖了一个「较新才引入的 Node API」,而声明的最低 Node 版本却把不支持它的小版本包含进来了——于是守卫恒为假,CLI 从不启动。
入口代码(lib/bin.js):
if (import.meta.main) await runCli();
import.meta.main 用于判断「当前模块是不是入口模块」,它在 Node 24.2.0(22.x 线 22.18.0)才引入。在 v24.1.0 上访问它得到 undefined,if 为假 → runCli() 不执行 → 模块执行完毕 → 退出码 0。
真实二进制验证的可用边界
用真实 Node 二进制与真实已发布 CLI 复现(不是读源码推断):
| Node | import.meta.main | lib/bin.js --version |
|---|---|---|
| 24.1.0 | undefined | (无输出,exit 0) |
| 24.2.0 | true | 0.1.5-rc.1 |
| 26.7.0 | true | 0.1.5-rc.1 |
精确边界是 24.2.0 与 22.18.0,不是笼统的「Node 24」——24.0.x / 24.1.0 同样受影响。这一点很重要:把问题描述成「Node 24 不兼容」会让人误以为要降大版本,实际只需升到 ≥ 24.2。
engines 为什么没拦住
三重原因叠加:
- 范围本身写错了:根
package.json声明"engines": { "node": "^22.19.0 || >=24.0.0" },>=24.0.0把缺import.meta.main的 24.0.0 / 24.1.0 明确包含在内——这个范围声明了「支持」,而实际不支持。 - 入口包没有 engines:
apps/cli/package.json没有 engines 字段;apps/cli/src/bin.ts入口共 66 行,没有任何运行时 Node 版本检查。 - npm 不拦截:即便 engines 存在且正确,npm 对 engines 也只打警告、不阻止安装。于是用户在 24.1.0 上「安装成功」,然后静默退出,完全无法联想到是版本不匹配。
补充一点版本线判断:复核结论是 rc.2 不会修复它——rc.1 / rc.2 的入口路径是同一条 master 线。这个问题属于「Node 版本无运行时门槛」一族(#6520 清单第 7 条),链接的 #4047(Node 24.0 同样闪退)是同一族。
修法一(用户侧):升级 Node 到 ≥ 24.2 或 22.18+
思路:既然断点在小版本,最直接的修法就是把运行时抬过断点。
- 用 nvm / nvm-windows 切到 Node ≥ 24.2(或 22.x 线的 ≥ 22.18)。
- 重开终端确认切换生效:
node -v,再node -e "console.log(import.meta.main)"应为true。 - 重新运行
dsh web,应正常输出并监听端口。
如果你用的是某个开源镜像站,注意它可能只同步到 v24.1.0——升级前先确认目标版本真实存在(这正是另一位报告者踩的坑)。
另一个可尝试的旁路(据复核者所述):全局安装后直接运行 dsh,以绕开 npx 路径下的静默失败表现。不过从机制上看,import.meta.main 的缺失是运行时属性问题,升级 Node 才是正解,这条旁路只在特定入口差异下有效。
修法二(桥):dsh-node-compat——今天就能用
思路:不改上游,也能让旧小版本上的 CLI 跑起来。 lib/bin.js 本来就导出了 runCli,而 runCli() 读取 process.argv.slice(2);启动器 import 同一个模块、自己调用 runCli(),把参数原样透传即可。
npx dsh-node-compat web
# 或:npm i -g dsh-node-compat && alias dsh=dsh-compat
它在不同运行时上的行为(真实二进制验证):
| Node | import.meta.main | lib/bin.js --version | dsh-compat --version |
|---|---|---|---|
| 24.1.0 | undefined | (无输出,exit 0) | 诊断 + 0.1.5-rc.1 |
| 24.2.0 | true | 0.1.5-rc.1 | 0.1.5-rc.1 |
| 26.7.0 | true | 0.1.5-rc.1 | 0.1.5-rc.1 |
两个关键性质:
- 在守卫正常的运行时上它完全静默——不会给正常用户增加任何噪音;
- 绝不会重复执行——当启动器是主模块时,
bin.js看到的import.meta.main是false,因此它自己不会再调一次runCli()。
桥的 npm 包与仓库:dsh-node-compat(作者 Robin1987China,属社区方案,非官方组件;原始运行记录在其仓库的 docs/verification.md)。
修法三(上游):可移植守卫 + 收紧 engines + 运行时版本守卫
思路:把「入口判断」从版本依赖里解耦,再补上两道护栏,让同类问题以后以「响亮失败」而不是「静默退出」呈现。
- 把入口守卫换成可移植写法(临时修法,两行):
import { fileURLToPath } from "node:url";
if (process.argv[1] === fileURLToPath(import.meta.url)) {
await runCli();
}
-
收紧并补全
engines:改成"node": "^22.19.0 || >=24.2.0",并确保它真的被包含进发布的 npm 包(入口包apps/cli/package.json也要有)。注意这只让它「在安装时警告」,不是硬阻断。 -
在启动时加运行时版本守卫:如果 Node < 22.19.0,或 Node 24.x 且 < 24.2.0,就打印明确的版本要求并以非 0 退出。这是三项里最重要的一项——它把静默失败变成响亮失败,而报告者的核心诉求正是这一点:「一个清晰的错误提示或更严格的 engines 字段,可以为用户省去很多麻烦。」
这三条与 #6605 的提案方向一致;复核者已把 #6584 / #6605 补进 #6520 第 7 条的「相关讨论」。
排查注意事项
第一原则:遇到「退出码 0、无输出、无日志」,先用 node -e "console.log(import.meta.main)" 量运行时能力,而不是去怀疑系统策略或网络。这类静默失败会把所有常规线索都抹掉。
node -v不足以定性:24.0.x / 24.1.0 / 24.2.0 都是「Node 24」,但行为完全不同。要按小版本核对(#6584)。- nvm 切版本后一定要重开终端再确认:切了没生效会让你在错误的版本上反复验证。
- 别一上来查 WDAC / 原生模块 / 代理:报告者先查了这些,最后才发现是版本差异。先做「两行定性」能省下大量时间(#6584)。
- 注意镜像站的版本天花板:某开源镜像的 Node 恰好停在 v24.1.0,是这一类「装了就闪退」的高发来源;升级前确认目标版本真实可下载。
engines不是护栏:npm 默认只警告、不阻止安装;而且本例的 engines 范围本身就把坏版本包含进去了。真正可靠的兜底是运行时版本守卫(#6584)。- 别指望 rc.2 顺手修好:rc.1 / rc.2 入口路径同一条 master 线,
apps/cli/src/bin.ts无运行时版本检查、apps/cli/package.json无 engines(#6584)。 - 这是「族」而非孤例:#4047(Node 24.0 闪退)、#6520 第 7 条(Node 版本无运行时门槛)同族;遇到一个就先按同一模式排查(#6584)。
排查这类问题时,用 DSH Plugin Hub 的系统日志页导出诊断、确认 DSH 与插件的版本状态,能先把「插件/组件不兼容」这一层排除掉,再回到 Node 运行时这一层——本例的根因恰恰不在任何插件里,而在运行时能力的边界上。

来源:Discussion #6584。
常见问题
不是崩溃——**是 CLI 主体根本没被执行**。入口守卫写的是 if (import.meta.main) await runCli();,而 import.meta.main 到 Node **24.2.0**(22.x 线是 **22.18.0**)才引入;在 Node 24.0.x / 24.1.0 上它是 undefined,于是 runCli() 从不调用,进程加载完模块就干净退出。所以**没有错误、没有日志、退出码 0**(Discussion #6584)。
两重原因叠加。第一,根 package.json 声明的范围是 ^22.19.0 || >=24.0.0,**它本身就把缺 import.meta.main 的 24.0.0 / 24.1.0 包含在内**。第二,apps/cli/package.json **没有 engines**,且 engines 在实际发布的包里似乎并未携带;即便携带,**npm 对 engines 也只打警告、不拦截**。于是 Node 24.1 会「安装成功」,然后静默退出(Discussion #6584)。
**不是笼统的 Node 24 问题,而是小版本断点**。真实 Node 二进制验证的可用边界是 **24.2.0** 与 **22.18.0**——24.0.x / 24.1.0 受影响,24.2.0 起正常。所以升到 **Node ≥ 24.2**(或 22.18+)即可,不需要降大版本。报告链接的 #4047(Node 24.0 同样闪退)与本文同族(Discussion #6584)。
可以先用社区桥 dsh-node-compat:它 import 同一个 lib/bin.js(该模块已导出 runCli),自己调用 runCli() 并把参数原样透传——npx dsh-node-compat web,或 npm i -g dsh-node-compat && alias dsh=dsh-compat。在守卫正常的运行时上它完全静默;在守卫失效的运行时上先打印诊断。它不会重复执行,因为启动器成为主模块时 bin.js 看到的 import.meta.main 是 false(Discussion #6584)。
不会。复核结论是 rc.1 / rc.2 的入口路径是**同一条 master 线**:apps/cli/src/bin.ts 入口共 66 行、**没有任何运行时 Node 版本检查**,apps/cli/package.json 也没有 engines。这个问题属于「Node 版本无运行时门槛」一族(#6520 清单第 7 条),需要上游入口加守卫才能根治(Discussion #6584)。
相关术语
- import.meta.main
- Node.js 的 ESM 元属性,用于判断「当前模块是否是入口模块」。它在 **Node 24.2.0**(22.x 线是 **22.18.0**)才引入;在此之前访问得到 `undefined`。用它做守卫(`if (import.meta.main) await runCli()`)时,旧小版本上会把「本该执行的主体」静默跳过——这是本文静默退出的直接原因。— https://github.com/deepseek-ai/deepseek-harness/discussions/6584
- 可移植的入口判断
- 不依赖 `import.meta.main` 的等价写法:`import { fileURLToPath } from "node:url"; if (process.argv[1] === fileURLToPath(import.meta.url)) { await runCli(); }`。它在所有受支持的运行时上都成立,因此把「入口判断」从版本依赖中解耦出来,是这次事故最直接的临时修法之一。— https://github.com/deepseek-ai/deepseek-harness/discussions/6584
- engines 字段(npm)
- `package.json` 里声明运行环境要求的字段,例如 `"engines": { "node": "^22.19.0 || >=24.2.0" }`。两点常被误解:其一,**npm 对 engines 默认只打警告、不阻止安装**;其二,范围本身要写对——把不支持的小版本包含进去,比不写还容易误导。真正可靠的兜底是**运行时版本守卫**:不满足时打印明确要求并以非 0 退出。— https://github.com/deepseek-ai/deepseek-harness/discussions/6584
- 静默失败(silent failure)
- 进程既没有输出、也没有非 0 退出码,甚至表现出「一切正常」的假象。它比崩溃更难排查,因为所有常规线索(错误栈、日志、退出码)都消失了——本例里报告者为此检查了 WDAC 策略、原生模块构建、代理设置,最后才发现是 Node 小版本差异。**把静默失败换成响亮失败,本身就是一项修复。**— https://github.com/deepseek-ai/deepseek-harness/discussions/6584
来源
- #6584 — Bug Report: dsh 0.1.5-rc.1 silently exits on Node.js v24.1.0 / 缺陷报告:dsh 0.1.5-rc.1 在 Node.js v24.1.0 上静默退出· deepseek-ai(GitHub Discussions)