ClawHub 作用域 npm 包安装失败并抛出 ENOENT
使用 npm 样式作用域名称(@scope/package)发布的插件和技能在安装时会抛出 ENOENT 错误,因为临时归档路径包含未解析的目录段。
🔍 症状
主要错误表现
尝试从 ClawHub 安装任何带作用域的 npm 包时,在归档写入阶段会出现 ENOENT 错误:
$ openclaw plugins install @axonflow/[email protected]
Resolving clawhub:@axonflow/[email protected]…
ClawHub code-plugin @axonflow/[email protected] channel=community verification=source-linked
Compatibility: pluginApi=>=2026.3.22 minGateway=2026.3.22
ClawHub package "@axonflow/openclaw" is community; review source and verification before enabling.
ENOENT: no such file or directory, open '/var/folders/ld/8b9xk7n52sg7q5vz7q1l8r840000gn/T/openclaw-clawhub-package-XXXXXX/@axonflow/openclaw.zip'
诊断观察
- 非作用域包安装成功:不带作用域段的包可以正常安装
$ openclaw plugins install mywallet # Output: Successfully installed plugin "mywallet"- 错误在各个版本中持续出现:所有 `@scope/name` 模式都会触发此错误,与具体的作用域或包名称无关
- 退出代码:进程以退出代码
ENOENT终止(数值等效:34)
错误对象详情
JavaScript 错误对象提供以下属性:
{
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/var/folders/.../openclaw-clawhub-package-XXXXXX/@axonflow/openclaw.zip'
}关键观察是路径在文件名段包含正斜杠,POSIX 路径解析会将其解释为目录分隔符。
🧠 根因分析
架构故障序列
dist/clawhub-CFvPS51z.js 中的安装管道按以下顺序执行:
- 使用
fs.mkdtemp()创建唯一临时目录 - 将临时目录与包名称拼接构成归档路径
- 将下载的 zip 字节直接写入构造的路径
代码路径分析
有问题的代码(插件安装,约第 89 行):
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-clawhub-package-"));
const archivePath = path.join(tmpDir, `${params.name}.zip`);
await fs.writeFile(archivePath, bytes);技能安装路径中存在相同的模式(downloadClawHubSkillArchive,约第 232 行):
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-clawhub-package-"));
const archivePath = path.join(tmpDir, `${params.name}.zip`);
await fs.writeFile(archivePath, bytes);路径解析故障
当 params.name 等于 @axonflow/openclaw 时:
| 步骤 | 操作 | 结果 |
|---|---|---|
| 1 | path.join(tmpDir, "@axonflow/openclaw.zip") | <tmpDir>/@axonflow/openclaw.zip |
| 2 | path.dirname(archivePath) | <tmpDir>/@axonflow |
| 3 | fs.mkdtemp() 创建 | 仅 <tmpDir> |
| 4 | fs.writeFile() 尝试写入 | <tmpDir>/@axonflow/openclaw.zip |
| 5 | 父目录 @axonflow 不存在 | 抛出 ENOENT |
为什么 fs.mkdtemp 不会创建中间目录
fs.mkdtemp(prefix) 仅创建一个目录并返回其绝对路径。它不会:
- 解析前缀中的目录段
- 创建前缀字符串隐含的任何子目录
- 验证返回的路径是否与前缀结构匹配
因此,调用生成 <tmpDir>(例如 /tmp/openclaw-clawhub-package-abc123),但代码随后尝试写入 <tmpDir>/@axonflow/openclaw.zip,这需要一个不存在的 @axonflow/ 子目录。
npm 作用域约定
@scope/name 语法是带作用域包的 npm 标准约定。ClawHub 支持此命名方案用于插件和技能标识符,使此缺陷成为使用此约定的所有社区包的系统性障碍。
🛠️ 逐步修复
选项 A:清理文件名(推荐)
在构造归档路径之前替换包名称中的正斜杠。这是最简单的修复方法,行为变化最小。
修复前:
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-clawhub-package-"));
const archivePath = path.join(tmpDir, `${params.name}.zip`);
await fs.writeFile(archivePath, bytes);修复后:
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-clawhub-package-"));
const safeName = params.name.replace(/\//g, '_');
const archivePath = path.join(tmpDir, `${safeName}.zip`);
await fs.writeFile(archivePath, bytes);这将 @axonflow/openclaw 转换为 @axonflow_openclaw,生成有效路径 <tmpDir>/@axonflow_openclaw.zip。
选项 B:确保父目录存在
在写入前使用 fs.mkdir 的 recursive 标志创建必要的父目录。如果其他代码依赖完整目录结构,这会保留该结构。
修复前:
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-clawhub-package-"));
const archivePath = path.join(tmpDir, `${params.name}.zip`);
await fs.writeFile(archivePath, bytes);修复后:
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-clawhub-package-"));
const archivePath = path.join(tmpDir, `${params.name}.zip`);
await fs.mkdir(path.dirname(archivePath), { recursive: true });
await fs.writeFile(archivePath, bytes);这将创建 <tmpDir>/@axonflow/ 作为目录,然后写入 <tmpDir>/@axonflow/openclaw.zip。
应用目标
将修复应用于两个位置:
- 插件安装:处理
openclaw plugins install的函数(约第 89 行) - 技能安装:
downloadClawHubSkillArchive函数(约第 232 行)
构建和部署
修改源代码 TypeScript/JavaScript 文件后:
# Rebuild the affected module
npm run build -- --filter=clawhub
# Or rebuild all packages if build system requires it
npm run build
# Run tests to verify no regressions
npm test -- --grep "clawhub"🧪 验证
功能验证步骤
- 测试带作用域的插件安装:
$ openclaw plugins install @axonflow/[email protected]Resolving clawhub:@axonflow/[email protected]… ClawHub code-plugin @axonflow/[email protected] channel=community verification=source-linked Compatibility: pluginApi=>=2026.3.22 minGateway=>=2026.3.22 Downloading package archive… Extracting to plugins directory… Successfully installed plugin “@axonflow/openclaw” (version 1.2.1)
- 验证插件已被识别:
$ openclaw plugins list | grep axonflow @axonflow/openclaw 1.2.1 enabled - 测试带作用域的技能安装(如果适用):
$ openclaw skills install @company/[email protected] Successfully installed skill "@company/enterprise-skill" (version 2.0.0)
回归测试
确认非作用域包继续正常工作:
$ openclaw plugins install mywallet
$ openclaw plugins list | grep mywallet
mywallet 1.5.0 enabled
$ openclaw plugins install unpkg-test
$ openclaw plugins list | grep unpkg-test
unpkg-test 0.1.0 enabled退出代码验证
$ openclaw plugins install @axonflow/[email protected]
$ echo $?
0成功安装返回退出代码 0。之前失败的 ENOENT 会返回非零退出代码。
单元测试验证
如果代码库包含 ClawHub 集成的单元测试:
$ npm test -- --grep "downloadClawHubPlugin"
downloadClawHubPlugin
✓ should handle scoped package names (@scope/name)
✓ should handle unscoped package names
✓ should handle version specifiers
$ npm test -- --grep "downloadClawHubSkillArchive"
downloadClawHubSkillArchive
✓ should handle scoped skill names
✓ should handle nested scope names (@org/team/skill)⚠️ 常见陷阱
环境特定陷阱
- 使用 tmpfs 的 Docker 容器:某些 Docker 配置将
/tmp挂载为 tmpfs,空间有限。带作用域的大 zip 工件包可能会因ENOSPC而失败,而非ENOENT。验证 tmpfs 大小分配:df -h /tmp # Ensure adequate space for plugin archives (default tmpfs often 64MB) - Windows 路径分隔符:虽然缺陷在技术上是平台无关的,但 Windows 路径可能对路径中的
@字符表现出不同行为。在安全敏感上下文中避免包含@的路径。 - tmp 中的符号链接:如果
os.tmpdir()解析到符号链接路径(开发环境中常见),确保目标支持目录创建。
用户配置错误
- 包名称格式错误:用户有时会省略存在中的作用域:
# Wrong openclaw plugins install axonflow/openclawCorrect
openclaw plugins install @axonflow/openclaw
- 大小写敏感性:npm 作用域区分大小写。
@Axonflow和@axonflow是不同的作用域。 - 指定版本但缺少作用域:指定版本时,确保整个包标识符被引号括起,以防止 shell 解释:
# Quotes prevent shell expansion issues openclaw plugins install "@axonflow/[email protected]"Without quotes on some shells, @ may trigger variable expansion
openclaw plugins install @axonflow/[email protected] # May fail depending on shell
边界情况
- 双斜杠注入:如果
params.name包含前导或尾随斜杠,清理正则表达式应处理它们。扩展选项 A:const safeName = params.name.replace(/\//g, '_').replace(/^_+|_+$/g, ''); - 包名称中的 Unicode 和表情符号:虽然不是标准 npm 实践,但某些注册表允许非 ASCII 字符。如果支持,请使用国际化包名称进行测试。
- 极长的包名称:npm 将包名称限制为 214 个字符。非常长的带作用域名称可能接近某些平台上文件系统的路径长度限制(通常每个路径组件 255 字节)。
临时目录清理
fs.mkdtemp 创建的临时目录在进程崩溃或 SIGKILL 时不会自动清理。考虑添加清理处理程序:
process.on('SIGINT', () => {
fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
process.exit(1);
});🔗 相关错误
直接相关
ENOENT: no such file or directory— 尝试写入不存在路径时的规范错误。当路径包含意外目录分隔符时,这是一个特定实例。ENOTDIR: not a directory— 当应该是目录的路径组件被当作文件处理,或反之时,可能会发生。相关于文件名清理可能会意外产生冲突。EISDIR: is a directory— 如果包名称清理产生空字符串,尝试写入临时目录本身时可能发生。
类似的历史问题
- 在自定义输出目录中使用带作用域包的 npm install — 当指定包含带作用域包的自定义
--prefix目录时,类似的路径构造问题影响了早期 npm CLI 版本。 - 使用 @ 前缀名称的 GitHub Actions 工件上传 — 当工件名称包含
@字符时,工件上传步骤会失败,因为后端将@解释为对另一个工件的引用。 - Webpack 块文件名
[name]包含斜杠 — 当解析的[name]包含路径分隔符时,构建输出将无法写入块,需要相同的清理修复。
受影响的 ClawHub 包模式
此缺陷影响整个 ClawHub 生态系统中特定的命名约定模式:
# Scoped packages (BROKEN)
@axonflow/openclaw
@company/workflow-engine
@org/team/shared-utilities
# Unscoped packages (WORKING)
mywallet
workflow-engine
shared-utilities任何使用 npm 标准 @scope/name 约定的包在修复部署之前都会受到影响。
监控和检测
要在生产日志中检测此问题:
# Filter logs for the specific ENOENT pattern
grep "ENOENT.*openclaw-clawhub-package.*@.*\.zip" /var/log/openclaw/*.log
# Count affected installations per time window
grep "clawhub.*ENOENT" /var/log/openclaw/*.log | \
awk '{print $4}' | sort | uniq -c | sort -rn | head -20