# Env 文件与凭据管理

> **适用**：FFAI Workspace 在 UAT (`/srv/apps/ffworkspace-test/`) 和 Production (`/srv/apps/ffworkspace/`) 服务器上的环境变量与敏感凭据管理。

---

## 1. Env 文件软链架构（Single Source of Truth）

两台服务器都用同一套设计：根目录 `.env.<env>` 是真相源，子目录下的 `.env` 是软链。

```
/srv/apps/ffworkspace-test/         /srv/apps/ffworkspace/
├── .env.uat (真相源)               ├── .env.pro (真相源)
├── backend/.env  → ../.env.uat     ├── backend/.env  → ../.env.pro
└── frontend/.env → ../.env.uat     └── frontend/.env → ../.env.pro
```

**结论**：只改根目录的 `.env.uat` / `.env.pro`，backend 和 frontend 自动同步。**不要单独编辑 `backend/.env` 或 `frontend/.env`**（会断软链并污染真相源关系）。

辅助文件：
- `.env.example`：模板，不被进程加载，仅供参考字段名
- `.env.<x>.bak.YYYYMMDD-HHMM`：部署或人工修改时留下的备份，不影响运行

---

## 2. 改 env 标准流程（heredoc 追加）

```bash
# 1. 备份（带时间戳，不要覆盖之前的）
ssh <user>@<host> 'cp /srv/apps/<dir>/.env.<x> /srv/apps/<dir>/.env.<x>.bak.$(date +%Y%m%d-%H%M)'

# 2. 追加新变量（heredoc 避免 shell 转义陷阱）
ssh <user>@<host> 'cat >> /srv/apps/<dir>/.env.<x> << "EOF"

# ============================================================
# <Feature 名称>（<分支名> 引入，YYYY-MM-DD 加）
# ============================================================
NEW_VAR_1=value1
NEW_VAR_2=value2
EOF'

# 3. diff 验证
ssh <user>@<host> 'diff /srv/apps/<dir>/.env.<x>.bak.<TS> /srv/apps/<dir>/.env.<x>'

# 4. NOT 重启 pm2 —— 让用户自己决定时间窗口
```

**关键点**：
- heredoc 用 `"EOF"` 双引号包围（防 shell 展开变量）
- 变量值含 `$` `"` `\` 等特殊字符时尤其要双引号 EOF
- 改完**不重启 pm2**：env 改动需要 backend 重启才生效，但重启时机由用户决定
- diff 必须验证：确认追加的内容、行数、位置都对

### 2.1 新增 env var 必跑 5 处 checklist

**项目实际有 5 个部署环境**（见 [`01-server-infrastructure.md`](./01-server-infrastructure.md) §1）。新增 `process.env.XXX` 时**默认 5 处都要配**，少一处就有"FFAI 跑得通 / AIxC 挂"或"Test 行 / UAT 漏"的环境漂移风险。

```bash
# 0. 仓库模板（强制，CI 检查见 §2.2 ratchet）
edit .env.example   # 加 var + 注释说明用途/默认值/缺失症状

# 1. FFAI Test (develop 自动部署)
ssh ubuntu@170.106.161.71 'cd /srv/apps/ffworkspace-test && cp .env.test .env.test.bak.$(date +%Y%m%d-%H%M) && echo "NEW_VAR=val" >> .env.test'

# 2. FFAI UAT (staging)
ssh ubuntu@43.153.69.73 'cd /srv/apps/ffworkspace-test && cp .env.uat .env.uat.bak.$(date +%Y%m%d-%H%M) && echo "NEW_VAR=val" >> .env.uat'

# 3. FFAI Production
ssh srvadmin@43.130.6.44 'cd /srv/apps/ffworkspace && cp .env.pro .env.pro.bak.$(date +%Y%m%d-%H%M) && echo "NEW_VAR=val" >> .env.pro'

# 4. AIxC UAT
ssh itadminaixc@52.234.29.56 'cd /srv/apps/aixcworkspace && cp .env.uat .env.uat.bak.$(date +%Y%m%d-%H%M) && echo "NEW_VAR=val" >> .env.uat'

# 5. AIxC Production
ssh itadminaixc@23.101.202.65 'cd /srv/apps/aixcworkspace && cp .env.pro .env.pro.bak.$(date +%Y%m%d-%H%M) && echo "NEW_VAR=val" >> .env.pro'

# 6. 各处都 diff 验证 + 不重启 pm2（用户决定时间窗）
```

**反例**：2026-05-16 PR #389 加 `NEXT_PUBLIC_FFCTK_INSTALL_URL`，最初只配了 FFAI UAT + FFAI PROD 两台，漏了 FFAI Test + AIxC UAT + AIxC PROD 三台——典型"2 环境视角"漂移。详见 [`.learnings/2026-05-16-five-envs-not-two.md`](../../.learnings/2026-05-16-five-envs-not-two.md)。

**例外**：仅特定产品 / 仅特定环境的 var（如 AIxC 专用的 Azure tenant ID）才能只配子集——但必须**显式声明**（在 PR body + `.env.example` 注释里写"AIxC only"）。

### 2.2 `.env.example` 强制覆盖（CI 检查）

新增 `process.env.XXX` 引用**必须同步加到 `.env.example`**，附注释说明用途、默认值、缺失症状。CI 强制：[`scripts/ops/check-env-coverage.sh`](../../scripts/ops/check-env-coverage.sh)。

- 启动型 env（端口、数据库名）由 `scripts/dev/setup-worktree.sh` 注入，**不进** `.env.example`
- 历史遗留漂移在 [`scripts/ops/env-coverage-baseline.txt`](../../scripts/ops/env-coverage-baseline.txt)，新 PR 不应往里加（**ratchet 模式**只能往紧的方向走，详见 [`docs/standards/05-development-workflow.md`](../standards/05-development-workflow.md) §通用工程 pattern §Ratchet）

---

## 3. 凭据与证书存放规范

### 路径约定

```
/srv/apps/ffworkspace-test/certs/<vendor>/  # UAT
/srv/apps/ffworkspace/certs/<vendor>/        # PROD
```

`<vendor>` 是凭据来源（如 `adp/`、`sap/`）。

### 权限

```bash
mkdir -p /srv/apps/<dir>/certs/<vendor>
chmod 700 /srv/apps/<dir>/certs/<vendor>      # 目录
chmod 600 /srv/apps/<dir>/certs/<vendor>/*    # 文件
```

### 不入 git

`.gitignore` 第 84 行已包含 `certs/`（develop 分支起）。**注意多分支不同步问题**：production 分支可能漏这条规则。

**临时补丁**（不污染 production 分支）：
```bash
ssh <prod> 'cd /srv/apps/<dir> && grep -q "^certs/$" .git/info/exclude || echo "certs/" >> .git/info/exclude'
```

`.git/info/exclude` 是 git 本地仓库级 ignore，不进 commit、不影响分支内容。当 production 分支后续合入 `.gitignore` 更新后，这一行可保留或删除。

---

## 4. 部署链路与 env 生效时机

```
本地 .env / 本地 backend/.env       仅本地开发
            ↓
       feature/* 分支               不入库（.gitignore 已忽略凭据）
            ↓
        develop 合并                合并后不影响线上
            ↓
        staging 合并                deploy.sh / Gitea CI 触发 UAT 部署
            ↓
        ⚠️ pm2 restart UAT          .env.uat 修改在此处生效
            ↓
       production 合并              deploy.sh / Gitea CI 触发生产部署
            ↓
        ⚠️ pm2 restart 生产         .env.pro 修改在此处生效
```

**重要**：env 改完不会立即生效，要等下一次 pm2 restart（或部署）才注入到进程。如果代码也是新的（比如新增的模块依赖新 env），需要：
1. 代码先到位（合到对应分支并部署）
2. env 后到位或同时到位
3. pm2 restart

否则会出现"env 在但代码没用 / 代码在但 env 没配"的中间态。

---

## 5. 谁可以执行什么

| 操作 | 是否需要用户确认 |
|---|---|
| 改 `.env.uat`（追加新变量） | 任务范围内允许，无需逐次确认；但要先告知 |
| 改 `.env.pro`（追加新变量） | **每次都要用户明示授权**（生产只读铁律） |
| `pm2 restart` | **每次都要用户明示授权 + 时间窗口** |
| `git pull` / `git merge` 在生产机 | **不允许**（用 deploy.sh / Gitea CI） |
| `cp .env.<x>.bak <旧时间戳> .env.<x>`（回滚） | 紧急情况下可执行，但必须告知 |
| 删除 `.env.<x>.bak.*` | 不允许（备份是回滚保险） |

---

## 6. 常见排错

### env 改完但 backend 还按旧值跑

→ pm2 没重启。`pm2 restart ffws-uat-backend` / `ffws-backend`。

### `Error: Cannot find module 'xxx'` 启动失败

→ 代码引入了新依赖但 `npm install` 没跑。先 `cd backend && npm install`（按 deploy.sh 流程通常会做）。

### dotenv 读不到值

→ 检查 cwd：pm2 ecosystem 配置里 `cwd: /srv/apps/<dir>/backend`，dotenv 默认读 `<cwd>/.env`，即软链 `backend/.env` → `../.env.<x>`。如果软链断了或没指对，会读不到。

### `git status` 显示 `certs/` 未跟踪

→ `.gitignore` 在当前分支没有 `certs/` 规则（多分支不同步）。用 `.git/info/exclude` 临时补丁，详见第 3 节。

---

## 7. 相关文档

- `docs/ops/01-server-infrastructure.md` —— 服务器架构总表（SSH、端口、PM2、目录）
- `docs/ops/03-ops-policy.md` —— 备份策略、CI 工作流
- `.agents/skills/deploy-ops/SKILL.md` —— 部署运维 skill
