[Bug] dsh-app-boot 的 CJS 解析钩子被 require('process/') 打崩:resolve.paths() 返回 null → 插件 failed to import #8674
Replies: 4 comments 1 reply
|
补充:可复跑的复现件全文(维护者不用装 whale_craft,2 分钟即可复现) 1. 零依赖最小复现(纯 Node,不启动 DSH、不改任何文件)#!/usr/bin/env node
/**
* 最小复现 —— @deepseek-ai/dsh-app-boot 的 CJS 解析钩子在 `require('process/')` 上崩溃
*
* 症状(DSH 启动日志):
* dsh: warning: 1 entry did not activate
* <plugin> (<plugin>): failed to import
* 真实错误(需在 loader 上下文里 try/catch 才能看到):
* TypeError: createRequire.resolve.paths is not a function or its return value is not iterable
* at ResolutionRouter.routeScoped (@deepseek-ai/dsh-app-boot/lib/index.js:1422:58)
*
* 根因:Node 的 `require.resolve.paths(name)` 对**内置模块名**返回 `null`。
* DSH 用 `barePackageName(request)` 把请求剥成裸包名(`'process/'` → `'process'`)后再去问 paths,
* 于是拿到 null,而调用点直接 `for (const p of ...)` → TypeError。
* V8 把「不是函数」和「返回值不可迭代」合并成一句报错,掩盖了真因。
*
* 触发者:`readable-stream@4.7.0` 的 `lib/internal/streams/end-of-stream.js:8`
* const process = require('process/') // 注意尾斜杠;该包 dependencies 里确实有 process
* 原生 Node 下这句解析完全正常 —— 所以这不是依赖缺失,是宿主解析钩子的问题。
*
* 本脚本:纯 Node、零依赖、不启动 DSH、不修改任何文件。
* 用法:
* node repro-resolve-paths.mjs # 以本文件所在目录为基准
* node repro-resolve-paths.mjs --parent <路径> # 指定父文件(模拟"从某插件里 require")
* 期望输出(Node v26.7.0 / win32 实测):
* resolve.paths('process') → null ← 崩因
* resolve.paths('process/') → 18 条搜索链 ← 加斜杠即可绕开内置判定
* resolve('process/') → …/node_modules/process/index.js (若该包存在)
*/
import { createRequire, _nodeModulePaths } from 'node:module'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const argv = process.argv.slice(2)
const parentArg = argv.indexOf('--parent')
const parent = parentArg >= 0 && argv[parentArg + 1] ? argv[parentArg + 1] : fileURLToPath(import.meta.url)
const req = createRequire(parent)
const line = (label, value) => console.log(` ${label.padEnd(34)} → ${value}`)
console.log('最小复现:require.resolve.paths() 对内置模块名返回 null')
console.log(`父文件(parent):${parent}\n`)
console.log('① 关键数据(这就是根因)')
for (const name of ['process', 'process/', 'fs', 'fs/']) {
const value = req.resolve.paths(name)
line(`resolve.paths(${JSON.stringify(name)})`, Array.isArray(value) ? `${value.length} 条搜索链` : String(value))
}
console.log('\n② 原生解析本身是好的(说明不是依赖缺失)')
try {
line("resolve('process/')", req.resolve('process/'))
} catch (error) {
line("resolve('process/')", `${error.code ?? error.name}: 本机没装 process 包(不影响结论)`)
}
console.log('\n③ 复刻 DSH 的写法:把请求剥成裸包名后直接 for...of')
const bare = 'process' // barePackageName('process/')
try {
for (const searchPath of req.resolve.paths(bare)) void searchPath
console.log(' …没崩(本机环境与报告环境不同)')
} catch (error) {
line('for (… of resolve.paths(bare))', `${error.constructor.name}: ${error.message}`)
}
console.log('\n④ 两种建议修法(都能拿到可迭代的搜索链)')
const fixedA = req.resolve.paths(bare) ?? req.resolve.paths(`${bare}/`)
line('A. paths ?? resolve.paths(name + "/")', Array.isArray(fixedA) ? `${fixedA.length} 条搜索链 ✅` : String(fixedA))
const fixedB = req.resolve.paths(bare) ?? _nodeModulePaths(dirname(parent))
line('B. paths ?? _nodeModulePaths(dirname)', Array.isArray(fixedB) ? `${fixedB.length} 条搜索链 ✅` : String(fixedB))
console.log('\n⑤ 反例:只加 `?? []` 是不够的')
const empty = req.resolve.paths(bare) ?? []
line('paths ?? [] 得到的搜索链长度', String(empty.length) + ' ← 搜索链为空时,同一条 require 会变成 Cannot find module')
console.log('\n结论:`resolve.paths()` 返回 null 时必须回退到真实搜索链,不能只兜空数组。')跑法( node repro-resolve-paths.mjs --parent ".../node_modules/readable-stream/lib/internal/streams/end-of-stream.js"本机输出(Node v26.7.0 / win32): 2. 最小 profile 复现(走真实 DSH 加载器,不依赖任何第三方插件)只需要一个 home,profile 里装 // <home>/profiles/web/package.json
{
"name": "dsh-profile-web",
"private": true,
"dependencies": {
"readable-stream": "4.7.0",
"cjs-probe": "file:plugins/cjs-probe"
},
"dsh": {
"profile": {
"bundles": [
"@deepseek-ai/dsh-base",
"@deepseek-ai/dsh-web-app",
"cjs-probe"
]
}
}
}// <home>/profiles/web/plugins/cjs-probe/package.json
{
"name": "cjs-probe",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "index.js",
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
}
}
}# <home>/profiles/web/plugins/cjs-probe/cordis.patch.yml
# 探针插件的 bundle patch
- insert:
- id: cjs-probe
name: cjs-probe// <home>/profiles/web/plugins/cjs-probe/index.js
// 探针:在 DSH 的加载器上下文里 require 一个 CJS 包(readable-stream),
// 它内部有 require('process/') —— 就是触发宿主 CJS 解析钩子崩溃的那句。
import { appendFileSync } from 'node:fs'
import { createRequire } from 'node:module'
const OUT = 'C:\\Users\\Administrator\\Documents\\deepseek-harness\\default-workspace\\.probe-tmp\\minimal-020-result.txt'
const log = (s) => { try { appendFileSync(OUT, s + '\n') } catch {} }
log(`===== cjs-probe @ ${new Date().toISOString()} =====`)
const req = createRequire(import.meta.url)
try {
const rs = req('readable-stream')
log('OK require("readable-stream") 成功 version=' + (rs && rs.Writable ? 'has Writable' : '?'))
} catch (e) {
log('FAIL require("readable-stream")')
log(String((e && (e.stack || e.message)) || e))
}
export const name = 'cjs-probe'
export function apply() {}pnpm install --dir <home>/profiles/web --node-linker=hoisted # store 指向你自己的
DSH_HOME=<home> node <dsh>/lib/bin.js --profile web --port 33993. 实测输出0.1.7-rc.2(本机原状):whale_craft 启动即 0.2.0-rc.2(npm 最新发布,未打任何补丁):同一句错误、同一调用栈,只是行号 +1: |
|
补充:本机规避补丁(仅供遇到同样问题的用户参考;正式修法仍建议按正文的 A/B 两种写法改上游) 守卫函数/**
* 本地补丁 2026-10-02(守卫 require.resolve.paths 的两种失败)。
*
* 现象:profile 树里的插件 require() 一个 CommonJS 包(如 mineflayer)时,
* DSH 的 CJS 解析钩子抛
* TypeError: createRequire.resolve.paths is not a function
* or its return value is not iterable
* 于是整个插件 "failed to import"。
*
* 真因(实测三轮定位):代码把「裸包名」喂给 require.resolve.paths()。
* 该 API 对**内置模块名**返回 null(Node 文档:or null if request is a core
* module),而 `readable-stream` 会写 `require('process/')`(带斜杠,依赖表里
* 有 process 包;原生 Node 解析完全正常)。于是:
* 1) for...of 迭代 null → TypeError(原始崩溃);
* 2) 简单 `?? []` 兜底 → 搜索链变空 → 变成 "Cannot find module 'process/'"。
* 复现数据(本机实测):resolve.paths('process') === null,
* resolve.paths('process/') === 18 条搜索链。
*
* 修法:函数不存在或返回值不是数组时,**给名字加一个斜杠重试**(绕开内置判定
* 拿到 Node 原生搜索链);仍拿不到才给空数组。参照 jestjs/jest#16052 的守卫写法。
* Local guard patch.
*/
function dshResolvePaths(req, name) {
try {
const resolvePaths = req && req.resolve && req.resolve.paths;
if (typeof resolvePaths === "function") {
const value = resolvePaths.call(req.resolve, name);
if (Array.isArray(value)) return value;
const retry = resolvePaths.call(req.resolve, name + "/");
if (Array.isArray(retry)) return retry;
}
} catch {
/* 拿不到就让调用方回退到原生解析 */
}
return [];
}接入方式把
效果(本机实测)
注意
|
|
更正:本文初版声称「此前无人报告」是错的。 我最初用 GitHub 的 REST 搜索(
也就是说:这是从 0.1.6-alpha 起就被反复报告、至今未修的老问题,本文不是首次发现。我已在正文补充「同源历史报告」一节,把这 5 条汇总到一处,方便一次性处理。 本文相对这些历史报告新增的部分是:① 零依赖跨机器复现脚本 + 最小 profile 复现配方(不必装 whale_craft / jsdom / msedge-tts 即可复现);② 把根因钉到 API 语义( 对造成的误导致歉 —— 这个教训本身也值得记:在 GitHub 上判断"是否已有人报过",必须包含 Discussions(REST 搜索覆盖不到)。 |
|
Đây là một phân tích root cause rất sắc bén và bài repro được đóng gói cực kỳ chuyên nghiệp. Việc Quá trình debug loại này thường mất rất nhiều thời gian để cô lập giữa lỗi của host environment và lỗi của plugin. Gần đây tôi cũng gặp tình huống tương tự khi viết một module resolution hook cho project riêng, và tôi đã dùng AI Pro (labagent.tech) để giúp mô phỏng lại chuỗi gọi của CJS loader trong môi trường sandbox. Nó rất hữu ích trong việc tự động hóa việc tạo ra các test case boundary conditions (như việc thêm bớt Mong maintainer của |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
环境
0.1.7-rc.2(launcher 运行时…\dsh-launcher\dsh-runtime\versions\0.1.7-rc.2),profileweb@deepseek-ai/dsh-app-boot0.1.7-rc.2@deepseek-ai/dsh-app-boot的latest仍停在0.1.0-rc.6,next=0.2.0-rc.2;本机跑的是0.1.7-rc.2(launcher 显式钉版本),即该问题出现在已发布的 rc 版本上whale_craft@0.1.7(npm 包名whale_craft)mineflayer@4.39.0→minecraft-protocol→readable-stream@4.7.00.2.0-rc.2里同一处写法一模一样(lib/index.js:1423)→ 升级不解决现象
启动日志只有两行,没有任何原因:
inactiveEntries()里fiber === undefined时只记error: "failed to import",真正的 import 异常被丢掉了。用一个探针插件(profile/node_modules/<probe>/index.js里try { await import(spec) } catch (e) { 写文件 },并在自己的dsh.bundle.patch里 insert 自己)在 loader 上下文里才拿到真实错误:最小复现
在任一 profile 里启用一个带 CJS 依赖的插件(本例
whale_craft)。它的依赖链里出现对内置模块名的
require,典型是readable-stream@4.7.0的lib/internal/streams/end-of-stream.js:8:原生 Node 下这句解析完全正常(见下方实测),所以这不是依赖缺失。
启动 DSH → 插件
failed to import。不需要插件的更小复现(纯 Node,一条命令就能看出两个 API 行为差异):
根因
@deepseek-ai/dsh-app-boot/lib/index.js的ResolutionRouter.routeScoped(0.1.7-rc.2 第 1422 行;同类写法还在 1247 / 1475 / 2019 / 3263,
worker 的
lib/worker/profile-resolution-bootstrap.js第 310 / 363 行):name来自barePackageName(request),即去掉斜杠后的裸包名:'process/'→'process'。require.resolve.paths(request)在 request 是核心模块时返回null。'process/'带斜杠所以没被isBuiltin()拦住进了路由,剥成'process'后查 paths 就得到null。for…of null→ TypeError。V8 对「for-of 里的调用表达式」会把两种情况合并成一句"… is not a function or its return value is not iterable",所以报错文本指向的 API 其实是存在的,
真因是返回值为 null —— 这一点极具误导性,我们也是分三轮才定位到。
实测数据(Node v26.7.0 / win32)
resolve.paths('process')nullresolve.paths('process/')resolve.paths('fs')nullresolve.paths('fs/')resolve.paths('mineflayer')resolve('process/')<profile>\node_modules\process\index.js✅另一个坑:单纯补
?? []不够同文件第 883 行的同款调用带了
?? [],其余调用点漏了。但我们实测:只加
?? []会把错误从 TypeError 变成因为
localSearchPaths变成空数组 →cjs.resolveNative([])失败 → 该 require 仍然挂。必须给出正确的搜索链,不能只兜空。
如何定位到根因(排查过程,供其他遇到同类问题的人参考)
起点很普通:装一个带 CJS 依赖的插件 → 启动只得到一行
failed to import,没有任何原因。整个过程分四步:dsh.bundle.patch被当作 bundle 加载,然后在 loader 上下文里try { await import(spec) } catch (e) { 写文件 },逐个试
@deepseek-ai/schemastery、dsh-tools、mineflayer、目标插件本身。结果:宿主包都 OK,
mineflayer与其依赖链失败,真实错误是TypeError: createRequire.resolve.paths is not a function or its return value is not iterable(指向lib/index.js:1422)。?? []后,错误从 TypeError 变成Cannot find module 'process/'—— 说明此时搜索链被清空,只兜空数组是错的方向。resolve.paths('process')→ null;resolve.paths('process/')→ 18 条搜索链。根因就此明确:该 API 对内置模块名返回
null('process/'带斜杠所以没被isBuiltin()拦下,剥成裸名后才踩中),而 V8 对
for…of 调用表达式的报错把"不是函数"和"返回值不可迭代"合并成一句,掩盖了真因。dshResolvePaths()(拿不到就加斜杠重试)替换 9 处调用 → 插件正常加载;随后搭最小 home(只装
readable-stream+ 探针)对照0.2.0-rc.2,确认最新发布版同样中招。两条经验值得记:
fiber === undefined → "failed to import"),排障成本高一个数量级;哪怕只在 debug 开关下带上底层异常,也能省掉上面第 1 步。
resolve.paths(name) as string[],as是编译期谎言,运行期null照样穿过类型检查。已核实的版本范围(不是"升级就好")
0.1.7-rc.2failed to import;探针拿到 TypeError(lib/index.js:1422)0.2.0-rc.2(npm 最新发布)0.1.7-rc.2逐行 diff:只有 1 个 hunk,且是无关的 bundle 列表新增(@deepseek-ai/dsh-experimental-schedule-bundle);routeScoped函数体 63 行逐字节相同;无守卫的resolve.paths(name)仍在 1248 / 1423 / 1476 / 2020 / 3264 行。② 最小 home 实机验证(只装readable-stream@4.7.0+ 一个探针插件,用 0.2.0-rc.2 原样启动):仍抛同一句 TypeError、同一调用栈,只是行号变 1423(…/versions/0.2.0-rc.2/…/@deepseek-ai/dsh-app-boot/lib/index.js:1423:58)master(源码)packages/boot/app-boot/src/profile-resolution/resolver.ts第 244 / 471 / 524 行为同写法,仅多一个as string[]断言;全文件Array.isArray0 次,??均不在这些调用点。且该文件最近一次改动是 2026-09-23(fix(app-boot): preserve routed resolution errors under async module hooks),此后未再变动"resolve.paths"得 19 条,其中至少 5 条是同源报告(见下节)。本报告的价值因此调整为汇总 + 可移植复现 + 实测版本矩阵 + 可用修法,而不是"首次发现"dsh-v0.2.0-rc.2(2026-09-29);release notes 未提及resolve.paths/ CJS 解析同源历史报告(此前已有人报告,均为同一根因;截至目前都未修)
jsdom → whatwg-url → tr46 → require("punycode/")?? []后错误前进到Cannot find module 'punycode/'"(与我们第 2 轮完全一致);0.1.6-alpha.1 vs .2 对照;含 keep-alive 无限重启的次生灾害process/(另有string_decoder/)isBuiltin('process/')===false/resolve.paths('process')===null/for…of抛错);09-30 由 Sabraker 补充:0.2.0-rc.2(桌面版)仍受影响,且"切子路径后重判内建名"与?? []两处改动仍必要 —— 即"最新版仍未修"这一点此前已有人报告jsdom → … → require("punycode/")ResolutionRouter不再守卫resolve.paths()";0.1.5-rc.2 正常 vs 0.1.7-rc.2 失败对照createRequire(...).resolve.paths()require.resolve未携带原生resolve.paths";点名 whale_craft、行号 1422、同一句 TypeErrormsedge-tts本报告相对它们新增/不同的部分(⚠️ 以下三项经我们逐条核对,在 5 条历史报告里没有出现;不声称"唯一"):
repro-resolve-paths.mjs)+ 最小 profile 复现配方(只装readable-stream+ 一个探针插件)——历史报告都是"装某个具体插件"(jsdom / office-toolkit / msedge-tts)或几行片段,维护者要复现得先凑环境;
resolve.paths(<内置名>)→null,而加斜杠('process/')→ 18 条搜索链,据此给出
?? resolve.paths(name + "/")这一可跑通的写法,并实测说明为什么?? []不够(会前进成Cannot find module);(注:「切子路径后重判内建名」这个思路 DSH 0.1.6-alpha profile 模块解析缺陷:require('包名/') 触发 resolve.paths 崩溃 #7377 主贴已提出;本报告给的是另一种、可直接套用的最小改法)
packages/boot/app-boot/src/profile-resolution/resolver.ts第 244 / 471 / 524 行同写法,且该文件自 2026-09-23 起未再改动 —— 这一条我们没在历史报告里见到
(「
0.2.0-rc.2仍受影响」DSH 0.1.6-alpha profile 模块解析缺陷:require('包名/') 触发 resolve.paths 崩溃 #7377 的 09-30 评论已报告、插件依赖 `msedge-tts` 时导入失败(`createRequire.resolve.paths is not a function`) #8250 亦核对了 release notes;本报告只是把它补成最小 home 的实机复现);本报告把它们收敛成一个入口,并同时附上可跑复现与可用修法 —— 便于维护者一次性处理,也便于后来者先搜到这里。
建议修复(二选一,或都做)
另外两个建议:
fiber === undefined时若能带上底层异常(哪怕只在DSH_DEBUG=1下),排障成本会低一个量级。本次光是拿到这个 TypeError 就花了三轮(探针插件 + 分步守卫)。Module._resolveFilename的 monkey-patch 有已知的生态风险,官方替代是module.registerHooks()(Node ≥22.15/24 稳定)。相关盘点见参考链接。本地规避(我们先用着,供其他用户参考)
在
dsh-app-boot里插入守卫函数,并把上述 9 处调用替换为它(关键是加斜杠重试):替换后实测(同一探针):
OK mineflayer/OK whale_craft,启动日志不再有failed to import,whale_craft 自己的日志出现
插件已加载|工具注册中…,29 个mc_*工具全部可用。附件(可直接跑)
repro-resolve-paths.mjsbarePackageName()剥名 +for…of resolve.paths()得到同一句 TypeError;并验证两种建议修法patch-appboot.pydshResolvePaths()最小 profile 复现(不依赖任何插件,已在 0.2.0-rc.2 上实测通过)
比装 whale_craft 更干净,维护者 2 分钟就能跑:
预期:探针写出的结果文件里出现
参考
require.resolve.pathsmodule.registerHooks():nub — Monkey-patching of Node's CJS resolver: blast-radius estimatecreateRequire找不到宿主包"的同类讨论:discussion #380附:顺手提交给
dsh-why(它的规则库还不认识这个模式)dsh-why v0.3.3对这段报错的回答是「暂不认识这个报错模式」,它支持的模式只有客户端模块表那一类(
failed to import loader entry <hash>/require("…") missed the module table/bundle script … failed to load/cannot resolve "…"/Failed to load plugins)。可提交给 https://github.com/ice5kysl/dsh-why/issues 扩充:All reactions