---
date: 2026-05-08
type: error
tags: [latent-bomb, aixc, node, esm, otplib, production-safety, pre-incident]
status: resolved-2026-05-08
---

# AIxC UAT/PROD 仍是 Node 20 + 同样的 ESM 炸弹（已于 2026-05-08 当天升级到 Node 22 修复）

> 历史记录：本来是 FFAI 踩坑后顺手核对 AIxC 发现的潜伏炸弹，写下来准备防引爆。
> 实际同日（2026-05-08）用户授权直接升级处理，已消除隐患。
> 留这条文件给未来检索"AIxC 是不是也升 Node 22 了"用，并记录修复路径。

## 现场盘点（2026-05-08）

修完 FFAI Node 22 升级后，顺手核对 AIxC 两台机器，发现风险一模一样：

| 主机 | Node | otplib | @scure/base | @noble/hashes | PM2 uptime |
|---|---|---|---|---|---|
| **AIxC UAT** (52.234.29.56) | **20.20.2** (apt system-level) | ^13.4.0 | 2.2.0 ESM | 1.8.0 (CJS, 有 hmac.js) | 46h |
| **AIxC PROD** (23.101.202.65) | **20.20.2** (apt system-level) | ^13.4.0 | 2.2.0 ESM | 1.8.0 (CJS, 有 hmac.js) | 45h |

跟 FFAI UAT 升级前完全一致的"长 uptime + Node 20 + transitive ESM dep"组合。

## 引爆条件

任何让 PM2 fork 新进程 / 重新加载 node_modules 的操作都会触发：

- `pm2 reload` / `pm2 restart` 任意 backend 进程
- `pm2 kill` 后重启
- 服务器重启（systemd 又没注册，进程也起不来）
- deploy 脚本里的 `npm ci` + 重启
- 即使只是 `pm2 reload --update-env` 改个环境变量

错误链路：`mfa.service.ts:15 import 'otplib'` → otplib `dist/index.cjs` 顶层 `require("@otplib/plugin-base32-scure")` → plugin 顶层 `require("@scure/base")` → `@scure/base@2.2.0` 是 `type: module` 纯 ESM → Node 20 默认禁止 require ESM → `ERR_REQUIRE_ESM` → backend crash loop。

详见 `.learnings/ERRORS/ERR-20260507-002.md`（FFAI 已发生过事故）。

## AIxC 跟 FFAI 的运维栈差异（要注意的修复路径不同）

| 维度 | FFAI（已修） | AIxC（待修） |
|---|---|---|
| Node 安装方式 | nvm 用户级 | **apt 系统级** (`/usr/bin/node`) |
| PM2 安装方式 | nvm 用户级 | **apt 系统级** (`/usr/bin/pm2`) |
| 升级路径 | `nvm install 22 && nvm alias default 22` | 选项 A：`apt install nodejs=22.*`（影响所有 system 用户）；选项 B：装 nvm 后切到用户级（标准化运维但工作量大）|
| systemd 服务 | 已注册（FFAI UAT 今天首次注册，FFAI PROD 修正路径） | **未注册**（重启服务器后进程不会自动起，已经是隐患）|

## 实际修复（2026-05-08 当天，用户授权直接做）

走的是 nvm 路线（跟 FFAI 对齐成统一栈），不是 apt 升级。两台同样流程：

```bash
# 1. 装 nvm
curl -sSfL ... /v0.39.7/install.sh | bash

# 2. 装 Node 22.21.1 + 切 default
nvm install 22.21.1 && nvm alias default 22.21.1

# 3. 修 npm prefix（重要：system 残留污染）
$NVM_BIN/npm config set prefix $HOME/.nvm/versions/node/<v>
# 详见 ERR-20260508-003

# 4. 装 PM2 到 nvm v22 global
$NVM_BIN/npm install -g pm2 --prefix=$HOME/.nvm/versions/node/<v>

# 5. 旧 apt PM2 save + kill
pm2 save && pm2 kill

# 6. 重装 backend + frontend node_modules（用 nvm v22）
cd backend  && rm -rf node_modules && npm ci && npm run prisma:generate
cd frontend && rm -rf node_modules && npm ci

# 7. 用新 PM2 启动 ecosystem
pm2 start ecosystem.<env>.config.js

# 8. systemd 注册 + save
sudo env PATH=$PATH:$NVM_BIN $NVM_BIN/pm2 startup systemd -u <user> --hp <home>
pm2 save
```

UAT 3 进程、PROD 5 进程（cluster x2 backend + cluster x2 frontend + worker x1），全部 ↺=0 启动稳定。

## 治理层面（剩余 TODO）

- [ ] 写一个 `scripts/ops/check-node-version-drift.sh`，定期 SSH 4 台机器对比 `node -v` 跟 `backend/package.json` 的 `engines.node`
- [ ] CI 里加一条对 `engines.node` 的强校验（`npm ci --engine-strict` 或自定义脚本）
- [ ] 部署 `deploy-uat.yml` / `deploy-production.yml` 第一步加 Node 版本检查，版本不对直接 fail-fast
- [ ] `docs/ops/01-server-infrastructure.md` 写明 4 台 Node 版本现状（已升）+ nvm 用户级路径约定

## 适用范围 / 教训

1. **同一仓库部署到多套环境，运维栈不一定统一**：FFAI 用 nvm，AIxC 用 apt，是两条独立"事实链"。下次跨环境定位问题，先 SSH 各机器实测，不要假设一致
2. **"看似没出事"不等于"安全"**：AIxC 当前业务正常因为还没人 reload，但下一次 deploy 必爆。这种潜伏问题在每天 deploy 高频项目里特别危险
3. **核对范围要全**：本次因为 FFAI UAT 出事才顺手查 AIxC，幸运地在 AIxC 引爆前发现。下次任何重大依赖升级（特别是 transitive ESM 升级）后，应主动核对所有部署点

## 关联

- 工单 #242（FFAI UAT @otplib ESM 事故）
- `.learnings/ERRORS/ERR-20260507-002.md`（FFAI 完整事故复盘）
- `.learnings/ERRORS/ERR-20260508-001.md`（nvm Node 升级 4 连环坑）
- `.learnings/2026-05-08-node-version-drift-across-envs.md`（环境治理）
- `docs/ops/01-server-infrastructure.md`（4 台主机清单）
