# 使用说明 · internal-app-platform

> **本文档定位**：Phase 1 共机模式上线后，**员工**和 **IT-Ops** 的日常操作手册。
>
> - 想了解**为什么这么设计** → [01-prd](./01-prd.md) / [03-architecture](./03-architecture.md)
> - 想了解**首次 IT 装机** → [12-nginx-caddy-coexist](./12-nginx-caddy-coexist.md) + [11-dns-tls-rollout](./11-dns-tls-rollout.md)
> - 想了解**API/工具签名** → [07-api](./07-api.md)
>
> 本文档只覆盖"装好之后怎么用 / 怎么维护"。

---

## Part A · 员工使用说明

### A.0 你只需要做什么

```
FFOA 拿 token  →  shell 执行 mcp add  →  Claude Code 里说部署  →  5 分钟后拿到 URL
   (1 次, 2 分钟)    (1 次, 30 秒)         (1 句话)              (1 行)
```

中间所有事——建 Gitea 仓、git push、容器化、反代、域名、TLS、备份——都**不需要你操心**。
**你也不需要在你的项目里写 Dockerfile / nginx 配置 / systemd unit**，平台会自动按 runtime（Node / 静态站）兜底处理。

> 截图说明：本节截图为 v1.0 共机模式 UI（2026-05 后稳定版本）。英文界面，可在右上角语言切换器切到中文，文案对齐。

---

### A.1 第一次接入（约 2 分钟）

#### 步骤 1：进入「My Apps」页面

登录 FFOA → 左侧菜单找 **My Apps**（中文界面叫「我的 Apps」）→ 点进去。

![My Apps 页面初始状态](./assets/usage/01-page-overview.png)

页面分两部分：

1. **上方：Onboard to Claude Code** —— token 状态、`claude mcp add` 命令、部署话术模板
2. **下方：My deployed apps** —— 你已部署的 app 列表

#### 步骤 2：生成 token 并复制 mcp add 命令

- **第一次进入** → 直接点 **Generate token** 按钮（截图里你已经看到 token 状态条 = 已有 token 的样子）
- **已有 token，想换** → 点 **Regenerate**。会弹确认窗：

  ![Regenerate token 确认弹窗](./assets/usage/02-regenerate-confirm.png)

  > **重要**：Regenerate 会**立即作废旧 token**，所有现有 Claude Code 会话需要重跑 `claude mcp add`。**如果只是想在新设备上加一份，不要 regenerate** —— 同一个 token 可以多设备同用。

- 生成完点 **Copy full command**（按钮只在"刚生成"那次出现，token 明文会一起进剪贴板）

> **token 明文页面上不显示**（只显示 `••••••••`），只能通过 "Copy full command" 一次性带走。搞丢了只能 Regenerate，没有"找回"。

#### 步骤 3：在 shell 里粘贴执行

**不是在 Claude Code 里，而是在普通终端**（bash / zsh / PowerShell 都行）：

```bash
claude mcp add --transport http ffoa-apps \
  https://ffworkspace.faradayfuture.com/api/v1/internal-apps/mcp \
  --header "Authorization: Bearer <真实 token 在这里>"
```

成功后输出大致这样（截图为员工本机实拍）：

![terminal: claude mcp add 成功](./assets/usage/terminal-01-mcp-add-success.png)

#### 步骤 4：重启 Claude Code，验证接通

```bash
claude mcp list
# 期望输出包含一行：ffoa-apps   http   https://ffworkspace.faradayfuture.com/...   ✓ Connected
```

![terminal: claude mcp list 看到 ffoa-apps ✓ Connected](./assets/usage/terminal-02-mcp-list-connected.png)

#### 步骤 4：在 Claude Code 里说一句话验证

```
你：列一下我已经部署的 app
Claude: [调用 list_apps] 你目前没有部署任何 app。
```

返 `没有部署任何 app` = 接通成功。

---

### A.2 部署你的第一个 app（端到端示例）

假设你写了一个 Next.js 小工具叫 `birthday-reminder`，已经在本地 `cd` 进了项目目录。

#### 关键：用页面提供的"部署话术模板"，不要凭感觉说

页面上有这一块（截图里黄色 callout）：

> **💡 Deployment prompt (recommended)**
> Use the `deploy_prepare` tool from the ffoa-apps MCP to deploy this project to the internal app platform with app name `<change-this-to-your-app-name>`. Do NOT generate Dockerfile, systemd, nginx, or any deployment config files — the platform handles them automatically.

点 **Copy deployment prompt** → 粘到 Claude Code → 把 `<change-this-to-your-app-name>` 改成 `birthday-reminder` → 发送。

**为什么不能只说"部署一下"**：Claude 默认行为是走通用部署路径——生成 Dockerfile、写 nginx 配置、起 systemd unit、配置 GitHub Actions……一堆你**完全不需要**的文件。说这套模板等于告诉 Claude "走 MCP，别自己造轮子"。

#### Claude 会做什么（你只需要看着）

贴话术后，Claude 会先调 `deploy_prepare` 工具：

![terminal: Claude 收到部署话术，开始调 deploy_prepare](./assets/usage/terminal-03-claude-receives-deploy-prompt.png)

工具返回后，Claude 自动跑完 `git init / add / commit / remote / push`，最后用 `deploy_prepare` 返回的 **postDeployHint** 提示你 5 分钟后再验证：

![terminal: deploy_prepare 完成 + push 完成 + postDeployHint 提示](./assets/usage/terminal-04-deploy-prepare-result-with-hint.png)

> **注意**："push 完成 ≠ 上线完成"。Claude 会主动提醒等 5 分钟（来自 `deploy_prepare` 工具返的 `postDeployHint` 字段，2026-05-19 后所有 session 强制带，详见 [.learnings/2026-05-19-tool-return-value-beats-description-for-ai-followup-script.md](../../../.learnings/2026-05-19-tool-return-value-beats-description-for-ai-followup-script.md)）。如果你的 Claude 版本不带这条提示，请等 5 分钟再判断访问是否成功，不要立即认定失败。

#### 部署后查看日志

5 分钟后还打不开？跟 Claude 说"看下日志"，它会调 `logs` 工具：

![terminal: Claude 调 logs 工具返回容器日志](./assets/usage/terminal-05-logs-tool-output.png)

常见日志关键字：

- `Listening on port 3000` —— 应用正常起来了，问题在网关层 / DNS
- `Error: connect ECONNREFUSED` —— 缺数据库 / Redis 配置，让 Claude 用 `env` 工具加
- `Killed` —— 内存超 256MB，需要瘦身代码 / 拆服务
- `missing env var XXX` —— 让 Claude "把 XXX 设成 yyy"

#### 后续日常对话

| 你想做的 | 直接说 |
|---|---|
| 部署新版本（同一个 app） | "再部署一下" / "上传最新版本" |
| 查日志 | "看下 birthday-reminder 的日志" / "为什么 500 了" |
| 改环境变量 | "把 LARK_BOT_TOKEN 设成 xxx" |
| 列我所有 app | "我现在有哪些 app" |
| 销毁 | "把 birthday-reminder 删了"（30 天内可恢复） |

Claude 会自动选合适的 MCP 工具（`deploy_prepare` / `logs` / `env` / `destroy` / `list_apps`），你不用关心工具名。

---

### A.3 部署成功之后

部署成功后页面下方 "My deployed apps" 区会出现一行：

```
birthday-reminder    running    https://lijian-birthday-reminder.apps.ffworkspace.faradayfuture.com    [打开] [日志] [删除]
```

**这个 URL 公司内网随时可访问**（含 VPN）。直接复制给同事。

URL 命名规则：

```
https://<你的工号 slug>-<app slug>.apps.ffworkspace.faradayfuture.com
        \________ 自动 ________/  \___ 你起的 ___/
```

工号 slug 由系统从你的邮箱自动派生，你不用起。

---

### A.4 常见问题

| 现象 | 原因 | 解决 |
|---|---|---|
| Claude Code 里调 MCP 报 `unauthorized` | token 过期 / 已被 regenerate / 已撤销 | 回 FFOA Regenerate + 重跑 `claude mcp add` |
| `claude mcp list` 不显示 `ffoa-apps` | mcp add 命令复制不全 / 在 Claude Code 内执行（错位置）| 在普通 shell 里重执行，注意整段 `\` 续行不要断 |
| 部署报 `unsupported_runtime` | 你的项目不是 Node 也不是纯 static HTML | 跟 AI 说："给项目加个 package.json + start script"；当前 MVP 不支持 Python/Go/Java/Docker |
| Claude 生成了 Dockerfile / nginx.conf 等部署文件 | 你没用上面的话术模板，Claude 走通用路径了 | 让 Claude 删掉那些文件，然后用页面上的"Deployment prompt"模板重发一遍 |
| Push 报 `credential_expired` | 5 分钟 push 凭据过期（拖太久没 push）| 跟 Claude 说"重新部署"，会自动 `deploy_prepare` 拿新凭据 |
| URL 打不开 / 502 / 自签证书警告 | (a) 还没构建完（< 5 分钟正常） (b) 容器挂了 (c) DNS / 证书新域名兜底中 | (a) 等 5 分钟再试 (b) 说"看日志"，常见是 env 缺值 (c) IT 已知问题 |
| 我访问 URL 浏览器报"连接不安全 / 自签证书" | 平台暂时用自签证书兜底（ACME 还没签发完）| 浏览器点"高级→继续访问"，IT 已经在跟踪正式证书 |
| FFOA 页看不到「My Apps」菜单 | 还没开通 internal-app-platform 权限 | 找 IT 加 `internal-app:owner` 权限 |
| 部署报 `app_in_terminal_state` | 这个 slug 之前被销毁了（30 天保留期内不能复用） | 换一个 slug 重新部署 |
| **URL 返 200 但 body 空、HTTP 头里没 `Via: 1.1 Caddy`** | 该 app 的 Gitea 仓库**没装 push webhook**（历史 bug，2026-05-19 前建的仓库可能中招）| 让 Claude **再跑一次 `deploy_prepare`**（说"重新部署一下"），系统会幂等补齐 webhook，下次 push 就能正常构建。详见 [.learnings/...gitea-webhook...](../../../.learnings/2026-05-19-gitea-webhook-must-be-per-repo-or-org-drift-kills-deploys.md) |
| 部署报 `gitea_webhook_install_failed` | 平台装 webhook 时 Gitea 拒了（token scope 不足 / Gitea 不可达） | 找 IT 看 backend 日志 + Gitea token scope |

---

### A.5 你**不能**做什么（边界）

- 不能改别人的 app（按 employeeSlug 物理隔离）
- 不能用保留 slug（`admin` / `api` / `www` / `mail` / `login` 等系统域名前缀）
- 销毁的 app 30 天内可让 IT 恢复，30 天后物理 purge 不可恢复
- 单个 app 容器固定 **256 MB 内存 / 0.5 CPU**；超出会被 OOM kill（看 logs 会看到 `Killed`）
- 平台不提供持久化磁盘（无状态容器）——要存数据请用公司提供的 DB / 对象存储
- 不能自己改容器镜像 / 端口暴露 / 网络策略（这些是 IT 的事）

---

---

## Part B · IT-Ops 运维说明

### B.0 三环境拓扑速查

| 环境 | 服务器 IP | 域名前缀 | 备注 |
|---|---|---|---|
| test | `170.106.161.71` | `*.apps.ffworkspace.test.faradayfuturecn.com` | 共机（与 FFOA 主站 nginx 共存） |
| UAT  | `43.153.69.73`   | `*.apps.ffworkspace.test.faradayfuture.com` | 共机 |
| 生产 | `43.130.6.44`    | `*.apps.ffworkspace.faradayfuture.com`（员工唯一感知） | 共机 |

共机模式技术细节：[12-nginx-caddy-coexist](./12-nginx-caddy-coexist.md)。

### B.1 日常巡检（建议每周一过一遍）

```bash
# 1. ffoa-caddy 容器活着 + 内网 :8080 可达
ssh <env> 'docker ps --filter name=ffoa-caddy --format "{{.Status}}"'
ssh <env> 'curl -s http://127.0.0.1:8080/healthz'      # 期望 "ok"

# 2. nginx 反代健康
ssh <env> 'sudo nginx -t && sudo systemctl status nginx | head'

# 3. 通配证书剩余天数（人工续期，提前 30 天告警）
ssh <env> 'sudo openssl x509 -in /etc/nginx/ssl/wildcard.apps.crt -enddate -noout'

# 4. 已部署 app 列表 + DB 行匹配
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
  https://<ffoa-host>/api/v1/internal-apps/admin/apps | jq '.items | length'
ssh <env> 'docker ps --filter name=ffoa-app- --format "{{.Names}}" | wc -l'
# 两者应当相等（差异 → 走"DB-容器对齐排查"）

# 5. 磁盘
ssh <env> 'df -h /srv/internal-apps /srv/caddy'        # < 80% 才安全
```

### B.2 Admin 操作（需 `internal-app:admin` 权限）

| 场景 | API | 说明 |
|---|---|---|
| 列**当前 org** 全部 app | `GET /internal-apps/admin/apps?employeeSlug=&status=&page=&pageSize=` | 跨员工但**单 org**；分页默认 20，上限 100 |
| 看全 org 事件流 | `GET /internal-apps/admin/events?actorRole=ADMIN&from=&to=` | actorRole=OWNER/ADMIN/SYSTEM 过滤 |
| 强制停用（保留数据 + 反代撤） | `POST /internal-apps/admin/apps/:appId/disable` body `{reason}` | reason 必填，≤ 500 字符；记 audit |
| 强制销毁（30 天保留期） | `DELETE /internal-apps/admin/apps/:appId?reason=...` | reason ≤ 500 字符；走 owner destroy + 跳鉴权 |

完整字段定义：[07-api.md §4.3](./07-api.md)。

### B.3 部署到新员工（onboarding 一个真实用户）

1. 员工 FFOA 账号已绑定 Entra（IT 标准入职流程已做）
2. 让员工自己进「我的 Apps」生成 token → 完
3. **不需要** SQL 写 binding 或手工准备任何东西

> Phase 0 时代的"IT 手工 SQL 建 binding"已经废弃，见 [08-phase-0-poc-runbook](./08-phase-0-poc-runbook.md)（保留作历史参考）。

### B.4 常见故障定位

#### 现象 1：员工 push 后没自动部署

```bash
# 1. webhook 到了吗
ssh <env> 'pm2 logs ffoa-backend --nostream --lines 200 | grep -i webhook'

# 2. Gitea 那边 hook 配置
GITEA_TOKEN=...
curl -s -H "Authorization: token $GITEA_TOKEN" \
  http://43.130.59.228/api/v1/orgs/FFAIApps/hooks | jq '.[] | {url: .config.url, active}'
# 期望: url 指向当前环境（test/uat/prod 各自）、active=true

# 3. 是不是首次推 — internal_apps 表无行 → 当前 deploy_prepare 不写 DB（已知 follow-up），首推无效
# 解决: 让员工说"重新部署一下"再走一遍 deploy_prepare（PR #396 follow-up: deploy_prepare upsert internal_apps）
```

#### 现象 2：URL 502 / 503

```bash
# 1. 容器活着吗
ssh <env> "docker ps --filter name=ffoa-app-<slug>-<appslug>"
# 死了: docker logs <name> --tail 200

# 2. Caddy 反代了吗
ssh <env> "ls /srv/caddy/sites/ | grep <slug>"
ssh <env> "docker exec ffoa-caddy curl -s http://127.0.0.1:2019/config/ | jq '.apps.http.servers'"

# 3. nginx 前置反代了吗（共机模式）
ssh <env> "sudo nginx -T 2>&1 | grep -A3 'apps.*ffworkspace'"
```

#### 现象 3：admin API 返 `no_organization`

`req.user.organizationId` 没注入 + admin 本人没 `EmployeeSlugBinding`。

修复（任选其一）：
- **首选**：重跑 `cd backend && npm run init:itadmin` —— 检测到 NULL 会自动 UPDATE 绑首个 org（PR #396 follow-up 后默认行为）
- 让 admin 自己走一次「我的 Apps」生成 token（自动建 `EmployeeSlugBinding`）
- SQL 手工补 `platform_iam.user_role_rel.organization_id`

背景：PR #396 risk-1/risk-2 修复后 admin API 强制要求 organizationId，不再 silent fallback。

### B.5 监控位点

| 位点 | 监控目标 | 告警条件 |
|---|---|---|
| `pm2 logs ffoa-backend` | error 关键字 | 5 分钟 ≥ 5 条 |
| nginx access log `/var/log/nginx/access.log` | `apps.*` 域 5xx | 5 分钟 ≥ 10 条 |
| docker events | container die / oom-kill | 任何 |
| `df -h /srv/internal-apps` | 磁盘 | ≥ 80% |
| 通配证书 `wildcard.apps.crt` | 剩余天数 | ≤ 30 天 |

> **细节**：`pm2`、nginx 日志路径、PM2 进程名以服务器为准（[`docs/ops`](../../ops/) 有当前事实源）。

### B.6 回滚 / 应急

| 场景 | 命令 |
|---|---|
| 单 app 紧急下线 | admin API `POST /apps/:appId/disable` |
| 整个共机模式回滚（恢复独立模式） | 见 [12-nginx-caddy-coexist §8](./12-nginx-caddy-coexist.md) |
| TLS 证书坏了 | 见 [11-dns-tls-rollout §7](./11-dns-tls-rollout.md) |
| backend 挂了 → 走 `deploy-ops` skill | [`.agents/skills/deploy-ops/`](../../../.agents/skills/deploy-ops/SKILL.md) |

---

## 相关文档

- [01-prd](./01-prd.md) · [03-architecture](./03-architecture.md) · [05-ui-interaction-spec](./05-ui-interaction-spec.md)
- [06-data-model](./06-data-model.md) · [07-api](./07-api.md) · [09-test-scenarios](./09-test-scenarios.md)
- [11-dns-tls-rollout](./11-dns-tls-rollout.md) · [12-nginx-caddy-coexist](./12-nginx-caddy-coexist.md)
