# ERR-20260516-001 — test 服务器缺 internal-app-platform 必需 env，backend 进入 pm2 重启循环

## 现象

- test 服务器（170.106.161.71）pm2 进程 `ffws-test-backend` 反复重启（84 次），`status=waiting restart`，pid=0、uptime=0
- 前端 `ffws-test-frontend` 和 `ffws-test-backend-temporal-worker` 正常 online
- backend 日志反复抛：
  ```
  [ExceptionHandler] Error: INTERNAL_APP_ENV_MASTER_KEY 未设置或仍是占位 — env 加密能力关闭
      at new InternalAppEnvCryptoService (backend/src/modules/internal-app-platform/services/env-crypto.service.ts:37:15)
  ```
- 另有 warn（不致死、但功能不可用）：`INTERNAL_APP_GITEA_API_TOKEN 未配置，所有 Gitea 操作将立即返回 gitea_token_missing 错误`

## 根因

internal-app-platform 模块（PR #368 merge 到 develop）新增了启动期强制依赖的 env：
- `INTERNAL_APP_ENV_MASTER_KEY` — env-crypto.service 构造函数里检查；源码看是 `NODE_ENV=production` 才 fatal、其余 warn，**但 test 服务器实际 fatal**（部署版本或 pm2 ecosystem 强制 NODE_ENV=production，待求证）
- `INTERNAL_APP_GITEA_API_TOKEN` — gitea-client.service 启动 warn，调用时才报 `gitea_token_missing`

PR #368 merge 时**没有在 test 服务器同步补 env**，导致 develop 一推就把 backend 打挂。

## 解决

1. 生成 32 字节随机 key（hex）作为 `INTERNAL_APP_ENV_MASTER_KEY`，**这是 KDF 输入**（`scryptSync(masterKey, salt, KEY_LENGTH)`），任意字符串都行，但一旦写入并加密了数据就不能换（换 key 等于丢数据）。
2. 写入 test 服务器的 env 文件——**注意 `backend/.env` 是软链接到 `../.env.test`**（即 `/srv/apps/ffworkspace-test/.env.test`），不是 backend 目录里的真文件。直接改 `backend/.env` 会跟着改源文件，但更稳妥是直接编辑 `/srv/apps/ffworkspace-test/.env.test`。
3. `pm2 restart ffws-test-backend --update-env` —— **必须带 `--update-env`**，否则 pm2 沿用启动时缓存的环境变量，看不到新加的 env。
4. `INTERNAL_APP_GITEA_API_TOKEN` 是 Gitea 个人 token，需要用户决定用哪个账号的 token，不能凭空生成。

## 复用建议

- **配置类强依赖 env 必须在 PR merge 前先在 test/UAT 服务器补好**——CI 应该 gate 这类引入，否则就是给团队埋雷。
- **`.env.test` 在仓库根，不在 backend 子目录**——找 env 配置先 `ls -la backend/.env*` 看软链接指向。
- **pm2 重启读新 env 必须加 `--update-env`**，否则 silent 失败（进程起来但用的还是老变量）。
- **ssh 远程 pm2 命令需先 source nvm**：`ssh ... 'source ~/.nvm/nvm.sh && pm2 ...'`，否则 `pm2: command not found`（非 login shell 不自动加载 nvm）。

## 关联

- 触碰契约面：新增启动期 env 算运维契约变更，应在 `docs/ops/` 或 `.env.example` 同步
- 项目规则 CLAUDE.md「环境变量」段：新增 `process.env.XXX` 必须同步 `.env.example` + CI gate（`scripts/ops/check-env-coverage.sh`）——**PR #368 看起来没过这道 gate，或 gate 没覆盖 test 服务器 env 同步**
