# 内部小工具自助部署平台 - UI 交互规格

> **module**: internal-app-platform
> **doc_type**: UI Interaction Spec
> **status**: Draft
> **owner**: lijian.dai
> **upstream_docs**: 01-prd.md（§F2.5 FFOA 接入页 / §核心业务约束 / §角色权限矩阵）
> **last_verified**: 2026-05-13
>
> **事实源**: 本文档定义 MVP 阶段 FFOA Web 端唯一前端页面"我的 Apps"的页面结构、关键交互、文案与可见态边界
>
> **范围声明**: 本平台**不提供日常运维 UI**——员工日常部署 / 查日志 / 改 env / 销毁全部走 Claude Code 对话（远程 MCP）。本页仅承担 **onboarding（接入）+ 自助管理（token 与 app 列表）** 职责。
>
> **术语 placeholder**:
> - `faradayfuture.com` = 公网域名（已锁定 2026-05-13）
> - `https://ffworkspace.faradayfuture.com/api/v1/internal-apps/mcp` = 远程 MCP server 端点 URL（待 07-api 敲定；前端**不得硬编码**，由 token 颁发接口同时返回，见 §7）

---

## 1. 页面边界

| 页面 | 路径 | 角色 | 用途 |
|---|---|---|---|
| 我的 Apps | `/internal-apps` | 全员（Entra ID 已登录）| token 颁发 / 撤销 / 复制 `claude mcp add` 命令 + 查看自己已部署 app 列表 |

**侧边栏入口**：作为一级菜单项暴露给全员（无 permission 门槛——平台层 SSO 通过即可见）。i18n key = `nav.internalApps`（中：`我的 Apps` / 英：`My Apps`）。理由：员工接入漏斗的起点；URL-only 入口在内网工具里转化率极差，菜单入口直接决定 PRD 成功指标"3 个月活跃 app 数 ≥ 20"曲线斜率。

**环境可见性**：员工只感知生产环境（详见 [01-prd.md 环境拓扑表](./01-prd.md)）。测试 / UAT 环境的菜单入口虽然代码层可见，但**对员工不暴露**——测试/UAT 不开通给真员工账号，只给平台开发者 + IT 自用验证。生产环境是员工唯一感知的环境。

显式不做（MVP）：
- 部署 / 重新部署按钮（走 MCP）
- 日志查看面板（走 MCP `logs`）
- env 编辑表单（走 MCP `env`）
- 销毁 app 按钮（走 MCP `destroy`，避免 UI / 对话两套入口的状态错位）
- IT 管理员后台（MVP 阶段 IT 用 CLI / 直连 API，见 PRD §F4）

---

## 2. "我的 Apps" 页结构

```
┌───────────────────────────────────────────────────────────────┐
│  我的 Apps                                                    │
│  全程通过 Claude Code 对话部署，本页只做接入与查看           │
├───────────────────────────────────────────────────────────────┤
│                                                               │
│  ▌1. 接入 Claude Code                                         │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │ 当前 token 状态：✓ 有效（剩 87 天 / 2026-08-08 过期）   │ │
│  │                                                         │ │
│  │ ┌───────────────────────────────────────────────────┐   │ │
│  │ │ claude mcp add --transport http ffoa-apps https://ffworkspace.faradayfuture.com/api/v1/internal-apps/mcp \ │   │ │
│  │ │   --header "Authorization: Bearer ••••••••"       │   │ │
│  │ └───────────────────────────────────────────────────┘   │ │
│  │ token 明文不在页面显示，点 [复制命令] 直接到剪贴板。     │ │
│  │ [复制命令]   [重新生成]   [撤销]                         │ │
│  │                                                         │ │
│  │ 把上面这行粘到 Claude Code 命令行执行即可。            │ │
│  └─────────────────────────────────────────────────────────┘ │
│                                                               │
│  ▌2. 我已部署的 apps（3）                                     │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │ birthday-reminder                       [Tag: 运行中]   │ │
│  │ https://zhang-san-birthday-reminder.faradayfuture.com       │ │
│  │ 最后部署 2026-05-10 14:23      [复制 URL] [打开 ↗]      │ │
│  ├─────────────────────────────────────────────────────────┤ │
│  │ team-survey                             [Tag: 运行中]   │ │
│  │ https://zhang-san-team-survey.faradayfuture.com             │ │
│  │ 最后部署 2026-05-08 09:11      [复制 URL] [打开 ↗]      │ │
│  ├─────────────────────────────────────────────────────────┤ │
│  │ old-tool                  [Tag: 已销毁 · 可恢复至 06-05]│ │
│  │ — (URL 已下线)                                          │ │
│  │ 销毁于 2026-05-06 16:40                                 │ │
│  └─────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
```

### 2.1 区块 A：接入 Claude Code（token 卡片）

**目标**：让员工在 ≤ 30 秒内完成 Claude Code 接入。

**安全模型（前置约定）**：
- **token 明文永不渲染到可见 DOM**——后端只在"刚刚生成"那一次的响应里返回明文，前端拿到后直接写入剪贴板缓冲（或一次性弹窗 + 确认后销毁），**不写到 React state / 不渲染到页面**。
- 后端**不存明文**（仅存哈希），员工"忘了复制 / 关掉了弹窗"的恢复路径**只能是重新生成**——这是有意的安全取舍，不提供"再看一次"。

**展示态**（按 token 当前状态分支）：

| 状态 | 顶部状态行 | 主体显示 | 按钮 |
|---|---|---|---|
| 从未生成 | "尚未生成 token" | 占位说明 + 引导 | [生成新 token] |
| 有效 · **本次生成** | "有效（剩 N 天 / YYYY-MM-DD 过期）" | 命令模板（token 段显示 `••••••••`）+ 一次性"复制完整命令"按钮（含明文，复制即从内存清除）| [复制完整命令]（一次性）/ [重新生成] / [撤销] |
| 有效 · **非本次生成**（刷新页 / 跨设备打开）| "有效（剩 N 天 / YYYY-MM-DD 过期）" | 命令模板（token 段显示 `••••••••`）+ 提示"明文已不可恢复，如需在新设备接入请重新生成" | [重新生成] / [撤销] |
| 即将过期（`0 < N ≤ 7`）| "还有 N 天过期" （warning 色）| 同"非本次生成" + warning 提示去续期 | [重新生成] / [撤销] |
| 已过期（`N ≤ 0`）| "已过期（YYYY-MM-DD）" （danger 色）| 仅文字提示"token 已失效，需重新生成" | [生成新 token] |
| 已撤销 | "已撤销于 YYYY-MM-DD HH:mm" | 仅文字提示 | [生成新 token] |

> **N 边界**：`N > 7` = 有效；`0 < N ≤ 7` = 即将过期；`N ≤ 0` = 已过期。N 由后端计算返回（避免前端时区漂移，见 §7）。

**交互细节**：
- **[生成新 token] / [重新生成]**：调用 OA API 颁发新 bearer → 后端返回明文（仅本次响应）+ 哈希入库。前端拿到明文 → 立即调 `navigator.clipboard.writeText(完整命令)` + 弹一次性 toast "已复制完整命令到剪贴板"，**不渲染明文到任何 DOM 节点**。卡片切换到"有效 · 本次生成"态。
- **[重新生成]** 二次确认文案：强调"旧 token 立即失效，已接入的 Claude Code 会话需重新执行 `claude mcp add`；如果只是想加新设备，无需重新生成（同一 token 可多设备共用）"。
- **[复制完整命令]**（仅"本次生成"态短暂可见，建议 5 分钟有效期或刷新页失效）：复制成功后按钮短暂变"已复制"（2 秒后恢复），同时把内存中持有的明文置空。
- **[撤销]**（danger 按钮）：弹**强确认**模态，要求员工在输入框输入 `REVOKE`（或 zh-CN locale 下输入 `撤销`）才能放行——避免误点。撤销成功后写 audit-system，卡片切到"已撤销"态。
- 所有按钮 + 状态文本走 i18n（见 §4）；emoji 不嵌 i18n 字符串，状态徽标走 Tag 组件 props。

**审计**：[生成新 token] / [重新生成] / [撤销] 三个动作均触发 audit-system 写入（PRD §核心约束已要求，前端无需额外处理，后端落库即可）。

**异常态**：
- API 5xx → inline error "服务暂时不可用，请稍后重试" + [重试] 按钮，不阻塞 app 列表加载
- 网络错误 → inline error "网络异常，请检查网络后重试"
- API 4xx（含 token 已被另一 tab 撤销）→ 静默重新拉 token 状态 + inline info "token 状态已变化，已为你刷新"
- 用户 Entra session 过期 → 走 FFOA 全局 401 跳登录流程

### 2.2 区块 B：我已部署的 apps（列表）

**数据来源**：OA API `GET /apps?owner=me`（同 MCP `list` 工具的数据）。

**列项**：
- app 名（slug）
- 状态徽标：用 Lark Tag 组件，色彩 token = `success`（运行中）/ `default`（已停）/ `danger`（构建失败）/ `default + 副标 "可恢复至 MM-DD"`（已销毁）
- 状态徽标配色细则（区分语义近似但不可逆程度不同的状态）：
  - `DISABLED`（IT-Admin 强停 · 可恢复）→ 暖色（orange / 警示），暗示"暂时"
  - `DISABLED_ARCHIVED`（30 天到期归档 · 终态）→ 冷色（slate / 归档），暗示"终结"
  - `DESTROYED`（已销毁 · 30 天恢复期）→ 浅灰
  - `PURGED`（已彻底清除）→ 深灰
  - 理由：避免 `DISABLED` 与 `DISABLED_ARCHIVED` 用同色徽标导致"两种语义视觉坍缩"
- URL（运行中态可点击；已销毁态显示 "—"）
- "最后部署 YYYY-MM-DD HH:mm"（已销毁态改为 "销毁于 ..."）
- **行级操作按钮**（仅运行中态可见）：
  - **[复制 URL]**：一键复制 URL 到剪贴板（高频：员工部署完最常做的事是把 URL 发同事），按钮短暂变"已复制"（2 秒后恢复）
  - **[打开 ↗]**：新窗口打开 URL（公网可达 + SSO 闸门，员工已登录 FFOA 时 session 自动延伸，无 VPN 需求）

**交互**：
- 行级**只读**（除上述 2 个按钮）——不提供"重启 / 销毁 / 改 env"，引导员工去 Claude Code 操作（在空状态 / 列表底部 placeholder 文案中明示）
- 行本身**不可交互**，仅 hover 视觉反馈（MVP 不做 detail 弹窗）
- 列表为空 → 显示空态："还没部署过 app，去 Claude Code 说'帮我部署一下'即可"（此时区块标题中的 count 计数隐藏，避免显示 "(0)"）

**排序**：按"最后部署时间"倒序；已销毁 app 排在末尾（恢复期内仍展示，过期后从列表消失）。

**分页**：MVP 不分页（员工 app 上限按 PRD 假设 ≤ 几十个）；超过 50 个时再加滚动加载，记入 V2 待办。

---

## 3. 关键交互流程

### 3.1 首次接入（员工从未生成过 token）

1. 员工打开 `https://ffworkspace.faradayfuture.com/internal-apps` → 区块 A 显示"尚未生成 token" + [生成新 token] 按钮
2. 点 [生成新 token] → 后端返回明文（仅本次响应）→ 前端立即写入剪贴板 → 弹一次性 toast "已复制完整命令到剪贴板，去 Claude Code 粘贴执行即可"
3. 卡片切到"有效 · 本次生成"态（命令模板里 token 段始终显示 `••••••••`，明文不渲染到 DOM）
4. 员工到 Claude Code 粘贴执行 → 接入完成

**SLA 目标**：员工从打开页面到完成接入 ≤ 30 秒。

### 3.2 token 过期续期

1. token 进入"≤ 7 天过期"窗口 → 区块 A 顶部 warning 色状态行（页面侧硬提醒）
2. 员工通过 Claude Code 调用 MCP 工具时，结构化返回里也含 `warning` 字段，Claude Code 会主动提示"token 还有 N 天过期，去 `https://ffworkspace.faradayfuture.com/internal-apps` 续期"（PRD §核心业务约束）
3. 员工打开本页 → 点 [重新生成] → 弹二次确认（含"加新设备无需重新生成"提示）→ 颁发新 token + 旧 token 立即失效 → 新命令自动复制到剪贴板
4. 员工在 shell 重新执行 `claude mcp add`

### 3.3 撤销（设备丢失 / 怀疑泄漏）

1. 员工点 [撤销]（danger 按钮）→ 强确认模态，要求输入 `REVOKE` / `撤销` 关键字
2. 确认 → token 失效 + 写 audit-system → 区块 A 切"已撤销"态
3. 若需重新接入 → 点 [生成新 token]

---

## 4. 文案与 i18n

所有可见文案必须双语化（zh-CN / en-US），key 命名约定 `internal_apps.*`。

**约束**：
- i18n 字符串**纯文字、不嵌 emoji / unicode 图标**——状态色与图标走 Lark Tag/Icon 组件 props（`status="success"` / `icon="warning"` 等），跨 OS 渲染一致 + 可主题化。
- 占位符用 ICU MessageFormat（`{days}` / `{date}`）。

| key | zh-CN | en-US |
|---|---|---|
| `internal_apps.page.title` | 我的 Apps | My Apps |
| `internal_apps.page.subtitle` | 全程通过 Claude Code 对话部署，本页只做接入与查看 | Deploy via Claude Code chat — this page is for onboarding and overview only |
| `internal_apps.token.section_title` | 接入 Claude Code | Connect Claude Code |
| `internal_apps.token.status.never` | 尚未生成 token | No token generated yet |
| `internal_apps.token.status.active` | 有效（剩 {days} 天 / {date} 过期） | Active ({days} days left, expires {date}) |
| `internal_apps.token.status.active_just_created` | 有效（刚刚生成 / {date} 过期） | Active (just created, expires {date}) |
| `internal_apps.token.status.expiring` | 还有 {days} 天过期 | Expires in {days} days |
| `internal_apps.token.status.expired` | 已过期（{date}） | Expired on {date} |
| `internal_apps.token.status.revoked` | 已撤销于 {datetime} | Revoked at {datetime} |
| `internal_apps.token.hint.opaque` | token 明文已不可恢复，如需在新设备接入请重新生成 | Token plaintext is no longer retrievable — regenerate to onboard a new device |
| `internal_apps.token.hint.same_token_multi_device` | 同一 token 可在多个设备共用，加新设备无需重新生成 | The same token works across multiple devices — no need to regenerate just to add one |
| `internal_apps.token.btn.generate` | 生成新 token | Generate new token |
| `internal_apps.token.btn.regenerate` | 重新生成 | Regenerate |
| `internal_apps.token.btn.revoke` | 撤销 | Revoke |
| `internal_apps.token.btn.copy_full` | 复制完整命令 | Copy full command |
| `internal_apps.token.btn.copied` | 已复制 | Copied |
| `internal_apps.token.toast.copied` | 已复制完整命令到剪贴板，去 Claude Code 粘贴执行即可 | Full command copied — paste it into Claude Code |
| `internal_apps.token.confirm.regenerate.title` | 确认重新生成 token？ | Regenerate token? |
| `internal_apps.token.confirm.regenerate.body` | 旧 token 立即失效，已接入的 Claude Code 会话需在 shell 重新执行 claude mcp add。如果你只是想加新设备，无需重新生成（同一 token 可多设备共用）。 | The current token will be revoked immediately and active Claude Code sessions must re-run `claude mcp add` in shell. If you just want to add another device, you don't need to regenerate (the same token works across devices). |
| `internal_apps.token.confirm.revoke.title` | 确认撤销 token？ | Revoke token? |
| `internal_apps.token.confirm.revoke.body` | 撤销后所有 Claude Code 会话立即失效。请输入 {keyword} 确认。 | All Claude Code sessions will stop working immediately. Type {keyword} to confirm. |
| `internal_apps.token.confirm.revoke.keyword` | 撤销 | REVOKE |
| `internal_apps.list.section_title` | 我已部署的 apps（{count}） | My deployed apps ({count}) |
| `internal_apps.list.status.running` | 运行中 | Running |
| `internal_apps.list.status.stopped` | 已停 | Stopped |
| `internal_apps.list.status.failed` | 构建失败 | Build failed |
| `internal_apps.list.status.destroyed` | 已销毁（可恢复至 {date}）| Destroyed (recoverable until {date}) |
| `internal_apps.list.last_deployed` | 最后部署 {datetime} | Last deployed {datetime} |
| `internal_apps.list.destroyed_at` | 销毁于 {datetime} | Destroyed at {datetime} |
| `internal_apps.list.btn.copy_url` | 复制 URL | Copy URL |
| `internal_apps.list.btn.open` | 打开 | Open |
| `internal_apps.list.btn.open.tooltip` | 公网可达，已登录 FFOA 即可直接打开 | Public URL — works wherever you're logged into FFOA |
| `internal_apps.list.empty` | 还没部署过 app，去 Claude Code 说"帮我部署一下"即可 | No apps yet — say "deploy this" in Claude Code to get started |
| `internal_apps.error.5xx` | 服务暂时不可用，请稍后重试 | Service temporarily unavailable — please retry |
| `internal_apps.error.network` | 网络异常，请检查网络后重试 | Network error — please check your connection and retry |
| `internal_apps.error.token_state_changed` | token 状态已变化，已为你刷新 | Token state changed — refreshed for you |

---

## 5. 设计系统对齐

- 沿用 FFOA 现有 Lark 设计系统组件（Button / Card / Tag / EmptyState / ConfirmModal），不引入自定义组件
- 状态徽标颜色用 `tag.success` / `tag.default` / `tag.danger` / `tag.warning`，与全局 token 一致
- 复制命令的代码块用 `<Code>` 组件 + 等宽字体；超出 1 行允许横向滚动，不换行（避免 token 中间被换行符截断后粘贴失败）

---

## 6. 权限与可见态

- **本页对所有 Entra ID 已登录员工可见**——是 onboarding 入口，不需要额外权限码
- **每个员工只能看到自己的 token 与 app 列表**——区块 B 后端按 `employeeSlug` 过滤，无"看他人 app"能力（IT 管理员看全量走 PRD §F4 CLI / API，不做 Web 视图）
- 未登录用户访问 `/internal-apps` → 走 FFOA 全局 SSO 跳转

---

## 7. 与 MCP 工具的状态一致性

由于员工同一个 app 可能从两端被触达（本页只读 + Claude Code 写），需要保证：

- **本页所有数据均由 OA API 实时拉取**，不缓存到前端 store；切页 / 刷新都重拉
- 区块 B 列表与 MCP `list` 工具调用**同一接口**（`GET /apps?owner=me`），**核心字段**（name / url / status / lastDeployedAt）含义完全一致
- **销毁态差异**：MCP `list` 默认只返回活跃 app（PRD §F2.3 描述），本页传 `?include=destroyed` 多拉一段 30 天恢复期内的销毁记录用于展示。该 query 参数由 07-api 敲定，不影响 MCP 默认行为
- token 颁发接口（`POST /tokens`）的响应里**同时返回 `https://ffworkspace.faradayfuture.com/api/v1/internal-apps/mcp` URL**，前端拼装 `claude mcp add --transport http` 命令时使用，**不在前端硬编码端点**——便于后端后续切换 SSE / Streamable HTTP transport
- token 状态的"剩余天数"由后端计算返回，前端不本地推算（避免时区漂移）

---

## 8. 待定项（实施前敲定）

- [ ] FFOA 页面挂载位置（侧边导航哪个分组下？路径 `/internal-apps` 是否撞现有 namespace？需与 FFOA 信息架构 owner 对齐）
- [ ] "本次生成 token" 临时态有效期（5 分钟还是刷新页即失效）
- [ ] 是否支持"多设备共用同一 token"——影响重新生成 confirm 文案与策略；当前文档按"是"撰写（与 PRD §核心约束未冲突），需 07-api 最终确认
- [ ] 列表"已销毁"行点击后是否给"提工单恢复"的快捷链接（依赖 §F5.3 工单流程是否就绪）
- [ ] i18n key 是否进 i18n 平台预审（避免与现有 namespace 冲突）
- [ ] 加载态 skeleton 规格 + 后端响应 SLA（影响 loading 切换阈值）
- [ ] 响应式 / 移动端：MVP 是"桌面优先，移动可读不可操作" 还是"双端等价"
- [ ] 员工电脑未装 git 的引导（首次接入时自检 `git --version`，缺失则在区块 A 顶部加 banner：「检测到你的电脑没有 git，部署需要先安装：[下载 Git for Windows ↗] / [下载 Git for macOS ↗]」；员工装完刷新页即可继续接入）
