# 📅 2026-05-13 日报 · lijian

## 概览

internal-app-platform 模块从零到端到端骨架就绪：契约设计 6 份文档 + Phase 0 NestJS 骨架 + MCP 5 工具实现 + Gitea 集成 + 64/64 单元测试。10 个 commit / +6 947 / -127；产出 9 篇 learnings；唯一阻塞剩"运维提供 `INTERNAL_APP_GITEA_API_TOKEN` (scope: write:organization)"。

---

## 一、产出（按价值排序）

### 🟡 P1 · feat(internal-app-platform) Phase 0 端到端骨架 — schema → MCP → Gitea

**背景**：非技术员工（HR / 运营 / 财务）通过 Claude Code 对话部署内部小工具。issue #332 推动到 Phase 0 PoC 启动条件就绪。

**今日跑通的链路**：

1. **数据层**：新 Prisma schema `platform_internal_apps`（5 张表 + 6 个 enum），含 SHA256 token 哈希存储、AES-GCM env 加密、TTL Sweeper、partial unique index（"同员工同时只能一个 ACTIVE token"硬约束）。Migration 234 行 SQL，含 CHECK 约束 + partial index，dev DB 应用成功
   - [platform_internal_apps.prisma](backend/prisma/schema/platform_internal_apps.prisma)
   - [migration.sql](backend/prisma/migrations/20260513181716_init_internal_app_platform/migration.sql)
2. **后端**：NestJS 模块挂到 `app.module.ts`，含：
   - HTTP API：`POST /tokens` / `POST /tokens/revoke` / `GET /tokens/me` / `GET /me/apps`
   - MCP 端点：`POST /api/v1/internal-apps/mcp`（Streamable HTTP transport）
   - 5 个 MCP 工具：`list_apps` / `deploy_prepare` / `logs` / `env` / `destroy`
3. **服务层**：
   - [TokenService](backend/src/modules/internal-app-platform/services/token.service.ts)：opaque `ffoa_<32 base32>` token 颁发 / 旧 token 原子撤销 / 7d 续期 warning
   - [SlugService](backend/src/modules/internal-app-platform/services/slug.service.ts)：Entra mailNickname 规范化 + 保留字白名单 + 20 字符截断 + SHA1 后缀
   - [EnvCryptoService](backend/src/modules/internal-app-platform/services/env-crypto.service.ts)：AES-256-GCM + scrypt 派生 + 篡改检测
   - [GiteaClientService](backend/src/modules/internal-app-platform/services/gitea-client.service.ts)：createRepo / getRepo / 错误结构化（scope 不足 / org 不存在 / 不可达）
4. **配套基建**：[scripts/internal-app-platform/docker-compose.minio.yml](scripts/internal-app-platform/docker-compose.minio.yml) + setup-minio.sh

**影响范围**：17 + 8 + 7 = 32 个核心文件改动；后端 build ✅；64 单元测试全过。

commits（按时间序）：
- [`0569e1d7`](backend) 新增 MVP PRD / 架构 / UI 规格（1 379 行）
- [`3caf67b2`](backend) 契约设计完成 06/07/09（+2 018 / -72）
- [`0e98e290`](backend) Phase 0 可达性实测通过
- [`d4bccfdf`](backend) Phase 0 骨架 schema/module/migration/MinIO（+1 133 / -1）
- [`dae97a1b`](backend) 修 3 个 pre-commit warn
- [`07040cde`](backend) MCP tools 路由 + list_apps + deploy_prepare + 30 单元测试（+788 / -29）
- [`aaa6505c`](backend) token revoke/me + destroy/env + AES-GCM（+652 / -10）
- [`bff36cc0`](backend) 真 Gitea API 集成（+753 / -7）

### 🟡 P1 · 模块契约设计完工（6 份文档）

`Status/PRD 中` → `Status/契约设计中` → 全部 Draft 完成：

| 文档 | 内容亮点 |
|------|---------|
| [01-prd](docs/modules/internal-app-platform/01-prd.md) | 业务边界、非技术用户 UX 5 原则、双轨网络模型（公网+SSO / git push 走 VPN）、Always/Never Do 红线 |
| [03-architecture](docs/modules/internal-app-platform/03-architecture.md) | 11 张关键流程图（token / 部署 / token 状态机 / 离职联动 / IT 强制操作） |
| [05-ui-interaction-spec](docs/modules/internal-app-platform/05-ui-interaction-spec.md) | "我的 Apps"接入页 + token 管理 + 双语 i18n |
| [06-data-model](docs/modules/internal-app-platform/06-data-model.md) | 5 张表 schema + partial unique index 强不变量 |
| [07-api](docs/modules/internal-app-platform/07-api.md) | MCP 5 工具 + HTTP API + 9 个决策（MCP=Streamable HTTP / token=opaque / quota=20 / KMS 降级方案 等） |
| [09-test-scenarios](docs/modules/internal-app-platform/09-test-scenarios.md) | L1 22 集成场景 + L2 MCP E2E 4 流程 + PoC 验收剧本 |

### 🟡 P1 · MCP 工具实现 + 单元测试覆盖

**MCP tools/list + tools/call 路由层** + 4 个工具实现：

- `list_apps`：真 DB 查询 + Date → ISO 8601 序列化
- `deploy_prepare`：runtime 自动判别（node / static / 5 种 unsupported_runtime 友好 hint），含 Dockerfile / requirements.txt / go.mod / pom.xml / package.json 缺 start 的逐一友好提示；真 Gitea 建仓 + push 凭据颁发；错误透传（gitea_token_missing / scope 不足 / org 不存在 / 不可达）
- `destroy`：DB 状态机推进 HEALTHY/FAILED/PENDING → DESTROYED，写 destroyedAt + retentionUntil=now+30d
- `env`（list/get/set/unset 4 子动作）：AES-GCM 加密 + key 校验（^[A-Z_]…$）+ 保留前缀 FFOA_/PLATFORM_ + value ≤ 4KB

**测试 64/64 全过**（[testing/backend/unit/internal-app-platform/](testing/backend/unit/internal-app-platform/)）：
- `slug.service.spec.ts` 15 用例
- `mcp-tools.service.spec.ts` 24 用例
- `env-crypto.service.spec.ts` 9 用例（round-trip / 篡改检测 / mask）
- `gitea-client.service.spec.ts` 16 用例（createRepo 6 分支 / getRepo 3 / 错误解析 6 / Phase 0 push credential 1）

### 🟢 P2 · 网络模型反转决策（PRD 第一版被推翻一次）

第一版 PRD 默认"仅内网 + VPN 必装"，审 PRD 时直属领导改为**双轨模型**：
- 同事/员工**访问 app URL**：公网 + SSO，**无 VPN**（高频，居家友好）
- 员工**部署代码（git push）**：需要 VPN（低频，仅部署瞬间）

理由：FFOA 主站已是公网+SSO 模式，员工不学两套；非技术员工"为访问连 VPN"摩擦不可接受。沉淀两条 learnings：
- [public-sso-vs-internal-vpn-tradeoff](.learnings/2026-05-13-public-sso-vs-internal-vpn-tradeoff.md)
- [dual-track-network-model](.learnings/2026-05-13-dual-track-network-model.md) — "按动作而非按用户分轨"的设计原则

### 🛠 基础设施 · Phase 0 可达性实测过

之前把"两条可达性"当做需要专项实测的高难度任务，实际一条 `curl -v` + `openssl x509` 5 分钟过：
- MCP 公网链路：HTTP/2 + TLS 1.3 + Let's Encrypt 证书未被中间代理 MITM
- Gitea 路径：HTTP API 37ms + 日常 SSH 通道长期稳定

PRD §风险段两条头号风险消除。 详见 [reachability-tests-passed](.learnings/2026-05-13-reachability-tests-passed.md)。

---

## 二、今日合入的 PR

PR #362 **open + mergeable**，等另一团队成员 approve（self-merge 被仓库策略禁）。

| PR | 标题 | 状态 | head | 备注 |
|----|------|------|------|------|
| #362 | docs+feat(internal-app-platform): 完成 MVP 契约设计 + Phase 0 骨架 | 🟡 open | `bff36cc0` | 9 commits / 30 files / mergeable=True；ai-review CI 红是 infra 问题（Anthropic org disabled），不阻塞 |

---

## 三、Git 活动

```
bff36cc0 | 23:08 | feat: 接入真 Gitea API — GiteaClientService + deploy_prepare 真实建仓        +753 -7
4de2e7e9 | 21:26 | docs(learnings): Round 5-7 实现期沉淀两条工具链陷阱                          +98
aaa6505c | 21:23 | feat: token revoke/me + destroy/env MCP 工具 + AES-GCM 加密                  +652 -10
07040cde | 20:51 | feat: MCP tools 路由 + list_apps + deploy_prepare + 30 单元测试              +788 -29
6a17c047 | 19:29 | (merge meta for #362)                                                        
dae97a1b | 18:53 | chore: 修 Phase 0 骨架 3 个 pre-commit warn                                   +16 -3
d4bccfdf | 18:35 | feat: Phase 0 骨架 — schema / module / migration / MinIO                     +1133 -1
0e98e290 | 17:51 | docs: Phase 0 两条可达性实测通过 + CI 红灯辨析                                +109 -4
3caf67b2 | 17:38 | docs: 完成契约设计阶段 — 06/07/09 + 关键决策落定                              +2018 -72
add716f8 | 15:53 | docs: 同步 03-architecture last_verified                                     +1 -1
0569e1d7 | 15:52 | docs: 新增 MVP PRD / 架构 / UI 规格 + 关联 learnings                          +1379
```

热点：[mcp-tools.service.ts](backend/src/modules/internal-app-platform/services/mcp-tools.service.ts) 改 3 次、PRD/架构改各 3 次、migration.sql 改 3 次（每次新增 column / 索引一起 squash）。

---

## 五、事故 & 教训

### ⚠️ ERR-20260513-001: `.claude/settings.json` 累积 dirty 阻塞 rebase

- **现象**：rebase 报 `cannot rebase: You have unstaged changes`，唯一 dirty 文件是 `.claude/settings.json`（session 中自动追加的 allowlist）
- **缓解**：`git stash push -m "settings noise" .claude/settings.json` 再 rebase 再 pop
- **教训**：Claude Code session 攒的 allowlist 跟功能 PR 无关，rebase / checkout 前必须先 stash 单文件
- **复发可能**：极高 —— 每次 session 都可能再撞
- 详见 [.learnings/ERRORS/ERR-20260513-001-claude-settings-rebase-block.md](.learnings/ERRORS/ERR-20260513-001-claude-settings-rebase-block.md)

---

## 六、新增 learnings（9 篇）

| 文件 | 类型 | 适用场景 |
|------|------|---------|
| [public-sso-vs-internal-vpn-tradeoff](.learnings/2026-05-13-public-sso-vs-internal-vpn-tradeoff.md) | project | "公网 + SSO" vs "内网 + VPN" 的取舍逻辑；适用任何内部工具平台 |
| [dual-track-network-model](.learnings/2026-05-13-dual-track-network-model.md) | feedback | "按动作而非按用户分轨"的设计原则；摩擦加在低频+高敏感处 |
| [reachability-tests-passed](.learnings/2026-05-13-reachability-tests-passed.md) | reference | 风险评估别被"听起来高难度"措辞误导，5 分钟 curl + openssl 就能验绝大多数 |
| [ci-red-but-not-blocking](.learnings/2026-05-13-ci-red-but-not-blocking.md) | feedback | CI 红 ≠ 阻塞合并，先看 required_status_checks + 业务/基础设施错误分类 |
| [prisma-format-touches-all-schemas](.learnings/2026-05-13-prisma-format-touches-all-schemas.md) | reference | `npx prisma format` 重格所有 schema 文件，提交前必须 `git checkout` 撤无关文件 |
| [prisma-bytes-vs-node-buffer](.learnings/2026-05-13-prisma-bytes-vs-node-buffer.md) | reference | Prisma 6 Bytes (Uint8Array<ArrayBuffer>) 与 Node Buffer 不直接兼容；写入用 `new Uint8Array(buf)`，读出用 `Buffer.from(uint8)` |
| [skip-assert-access-must-be-single-line](.learnings/2026-05-13-skip-assert-access-must-be-single-line.md) | reference | `@SkipAssertAccess(...)` 装饰器必须单行写，多行会被 testing/scripts/assert-access-check.ts 截断 |
| [gitea-api-token-scope-blocker](.learnings/2026-05-13-gitea-api-token-scope-blocker.md) | project | Gitea service token 需独立 env (`INTERNAL_APP_GITEA_API_TOKEN`)，scope: write:organization + write:repository |
| [remote-mcp-cant-read-local-files](.learnings/2026-05-13-remote-mcp-cant-read-local-files.md) | reference | 远程 MCP 物理上读不到员工本地文件，git push 是唯一传输路径 |

---

## 七、未提交改动

```
M .claude/settings.json
```

session 累积的权限 allowlist，不入 PR。

---

## 八、待决策 / 明日计划

按依赖顺序排列：

1. **运维提供 `INTERNAL_APP_GITEA_API_TOKEN`**（scope: write:organization + write:repository）—— **唯一外部阻塞**，卡 Phase 0 端到端 deploy 链路；细节见 [.learnings/2026-05-13-gitea-api-token-scope-blocker.md](.learnings/2026-05-13-gitea-api-token-scope-blocker.md)
2. **Entra ID session middleware 集成** —— 替换 controller 占位 `req.user`，token endpoints 才能真用
3. **Gitea webhook handler** —— 接收 push 事件触发部署链路
4. **容器部署 bash 脚本** —— Docker run + 健康检查 + Caddy 路由（部署链路最后一段）
5. **MCP `logs` 工具** —— 当前占位返回 `not_implemented`，依赖容器实际跑起来才有日志可读
6. **Litestream 备份** + **前端"我的 Apps"页** —— 配套基建 + 员工入口
7. **L1 集成测试**（真 DB + Gitea 测试实例）—— 待容器路径走通后补
8. **运维**：申请公网域名 `apps.faradayfuture.com` DNS + 公网 wildcard TLS 证书（Let's Encrypt DNS-01）

PR #362 等另一团队成员 approve；不能 self-merge。
