Files
cmhub/docs/phase-1-review.md
T
2026-07-02 14:47:22 +08:00

107 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 1 骨架审核报告(T-101 ~ T-104)
> 审核人:Claude Code(全栈视角)|日期:2026-07-02|结论:**验收通过,但最高风险未退**。
> Phase 1 是全项目最高风险段(AI 上游同步调用 + 可插拔供应商 + 密钥)。代码质量高、三模型机制忠实落地;但 **T-104 只验证了管路,没退掉「图片同步耗时」这个核心风险**,且 `parameters` 透传存在资金安全隐患。
> 本文面向 codex 执行:每条修补项给出「症状 / 位置 / 怎么改 / 怎么验证」。修补任务见 [`06-tasks.md`](06-tasks.md) 的 **T-105**。
## 一、验收核对
| 任务 | 验收要点 | 结果 |
| --- | --- | --- |
| T-101 | Provider 接口 + 按 api_type 注册;移植 cmbot 调用去桌面依赖;3 模型机制分别解析;mock 单测 | ✅ |
| T-102 | AiModel/ModelAlias + `api_key` 加密存储(admin 脱敏不回显);`resolve_alias()` 按能力校验;导入 `ai_models.json`;后台改配置热生效 | ✅ |
| T-103 | AiModel/ModelAlias/密钥后台变更留痕(谁/何时/改了什么);admin 可查 | ✅ |
| T-104 | 用别名 + 最小输入跑通一次生成,结论写入 progress(含耗时,**验证图片同步可行性与超时配置**) | ⚠️ **部分**:管路跑通,但只跑了 recorded **标题**,图片同步耗时/超时这一核心目的未验证(见 P1-2) |
## 二、做对的(勿在修补中回退)
以下是已正确落地的关键点,T-105 修补时**不要改坏**:
1. **三模型机制忠实落地**:`chat`(GPT-5.5 文本)、`auto→chat` 多模态返图(Nano Banana 2 走 `ChatCompletionsProvider.generate_image`)、`images_edits` 改图(GPT Image 2,`ImagesEditsProvider` 强制要求原图)。注册表按 `api_type` 选适配器,`auto` 按 URL 探测(`detect_api_type`),与 `04-architecture.md` 3.1 一致。
2. **超时策略修对了桌面端的坑**:`timeout_seconds=0` 不再是「无限」,而是按分辨率映射到有限值(512→180s / 1K→240s / 2K→360s / 4K→600s,见 `providers/utils.py:RESOLUTION_TIMEOUTS`);connect/read 分离。这正是同步部署最关键的一处。
3. **密钥零泄露闭环**:Fernet 加密、`fernet:` 前缀、**拒绝明文 fallback**、缺 `AI_KEY_ENCRYPTION_KEY` 时 `_fernet()` fail-closed 报错;admin 用写入型 `api_key` 字段(`PasswordInput`,不回显)、`api_key_encrypted` 设 `editable=False`;审计只记 `api_key: empty/set` 状态,**绝不记明文或密文**(`audit._sanitize_value`)。测试正向断言了「密文不含明文、不含 `fernet:`」。
4. **审计 admin 真只读**:`AiConfigAuditLogAdmin` 的 add/change/delete 权限全 `False`,`get_readonly_fields` 覆盖全字段;`delete_queryset` 也逐条留痕,不只 `delete_model`。
5. **测试与运行卫生**:`session.trust_env=False`(防止串到系统代理);smoke 命令用 `override_settings + transaction.atomic + set_rollback(True)` 临时建配置跑完即回滚,不落业务库、不打印 `Bearer`/占位 key,并有测试断言。
6. **热生效 + 能力前置校验**:`resolve_alias()` 每次查库取 active 配置(无进程内缓存到失效);标题要求 `text` 能力、图片要求 `image` 能力,能力不匹配抛 `ModelCapabilityError`。
7. **别名唯一性 DB 级**:`ModelAlias` 有 `UniqueConstraint(operation_type, alias)`;单默认由 `clean()`/`save(full_clean)` 兜。
## 三、修补清单
### P1 · 进 T-302(对外生成接口)落地前必须处理
#### P1-1 `parameters` / `extra_body` 无差别覆盖核心字段 —— 资金安全隐患
- **症状**:`providers/openai_compatible.py:apply_extra_body()` 做的是 `payload.update(model.extra_body)` 后再 `payload.update(parameters)`。`parameters` 是**调用方透传**(见 `api.md`)。这意味着 T-302 把外部请求的 `parameters` 直接喂进来时,调用方可以覆盖服务端固定的核心/计费字段:
- 覆盖 `model` → 实际调用与「计费别名」不同的上游 SKU(更贵/未授权),**直接击穿「按别名计费、换底层模型不影响计费」的契约**(`05-coding-rules.md` §8、`04-architecture.md` 3.1)。
- 注入/覆盖 `n`(chat/images 一次返多图)、`size`/`resolution`/`aspect_ratio` → **绕过按分辨率定价**,一次请求放大上游成本。
- 覆盖 `messages`/`stream` → 破坏请求编排。
- `images_edits` 走的是 multipart `data`,`apply_extra_body(data, ...)` 同样能被覆盖 `model`/`size`/`n`。
- **位置**:`apps/ai/providers/openai_compatible.py`(`apply_extra_body` 及各 provider 组装 payload 处)。
- **怎么改**:把「服务端权威字段」与「调用方可透传字段」分离——
1. 服务端固定字段(`model`、`messages`/`contents`、`stream`、`n`、`size`、`resolution`、`aspect_ratio`、`image*`)在 payload 组装后**最后写入,不允许被 parameters 覆盖**;
2. `parameters` 只允许一个**白名单安全子集**(如 `temperature`、`top_p`、`max_tokens`、`seed` 等无计费/无路由影响的),未知键丢弃或显式拒绝;
3. `extra_body`(后台运营配置,可信)可保留覆盖能力,但仍不应覆盖 `model`。
- **归属**:这个 seam 在 T-101 建成,真正被外部输入触达是在 **T-302**。**至少要在 T-302 开工前修好**,或在本层加显式约束 + 注释,避免 T-302 顺手 `parameters=request.data` 造成越权计费。
- **验证**:新增单测——传 `parameters={"model":"gpt-image-2","n":5,"temperature":0.2}`,断言最终 payload 的 `model` 仍是 `ResolvedModel.model`、`n` 未被改,`temperature` 生效。
#### P1-2 T-104 未退掉「图片同步耗时」这一全项目最高风险
- **症状**:Phase 1 存在的**唯一理由**是验证最高风险假设——「同步请求上游图片生成,在 Gunicorn(默认 30s worker timeout)/ Nginx 下不会超时」。T-104 的验收要点原文即「验证图片同步可行性与超时配置」。但实际只跑了 `smoke_ai_generation title --recorded`:
- 是 **标题**(chat 文本),不是图片;
- 是 **recorded**(`RecordedSession` 本地返回,无网络),`elapsed_ms=149` 对「同步耗时够不够」这个问题**没有任何信息量**。
- 结论:管路(别名解析 → provider 选择 → 响应解析 → 耗时记录)已验证;**核心风险未退**。
- **位置**:`apps/ai/management/commands/smoke_ai_generation.py`(目前 `operation` 只接受 `title`);风险登记见 `06-tasks.md` / `progress.md`。
- **怎么改**:
1. 把 T-104 的结论明确改写为「管路已验、图片同步风险未退」,不要留下「T-104 = 同步可行性已验证」的错觉;
2. 在依赖同步部署前(最迟 **T-403 部署**、建议随 T-302 一并)**跑一次真实或贴近真实延迟的「图片」生成**(Nano Banana 2 或 GPT Image 2),记录**真实耗时**,据此定 Gunicorn `--timeout` 与 Nginx `proxy_read_timeout`;
3. smoke 命令补 `operation=image`(可先 recorded 验证图片解析路径,再在配置真实 key 后去掉 `--recorded` 实测)。
- **不阻塞**:Phase 2 计费编码可先行;但这条风险必须在看板**显式登记为未决**,不能被 T-104 的 DONE 状态掩盖。
- **验证**:progress 中出现一条带**真实网络耗时**的图片生成记录(毫秒级不算),且 T-403 的超时配置引用该实测值。
### P2 · 规范性 / 一致性
#### P2-1 `.env.example` 的 AI 超时默认键代码未读取
- **症状**:`.env.example` 声明了 `AI_DEFAULT_CONNECT_TIMEOUT_SECONDS=10` / `AI_DEFAULT_READ_TIMEOUT_SECONDS=300`,但**全代码库无任何地方读取**(provider 用的是 `model.connect_timeout_seconds` 默认 30 与分辨率超时表)。属于「文档承诺了、代码没兑现」的死配置,会误导运维以为改这两个值能生效。
- **位置**:`.env.example` / `docs/env.md` ↔ `apps/ai/providers/`。
- **怎么改(二选一)**:**A(推荐)** 在 provider/`ResolvedModel` 缺省值处接上这两个 env 作为**全局兜底默认**(模型未单独配置时用它),保持文档兑现;**B** 从 `.env.example` 与 `env.md` 删除这两个键,避免误导。选哪个都要让文档与代码一致。
- **验证**:`grep` 确认「声明即被读取」或「已删除」;若走 A,加一条断言全局默认生效的测试。
#### P2-2 `resolution_to_size` 未做大小写归一
- **症状**:`utils.resolution_timeout()` 有 `.upper()` 归一,但 `resolution_to_size()` 没有。传入 `"1k"`(小写)时,超时表能命中,size 表 miss → 原样把 `"1k"` 作为 `size` 发给 `images/edits`,上游大概率 400。
- **位置**:`apps/ai/providers/utils.py:resolution_to_size()`。
- **怎么改**:与 `resolution_timeout` 一致地归一化键(如统一 `.strip().upper()` 后查表,或在入口统一 resolution 取值)。
- **验证**:单测 `resolution_to_size("1k") == "1024x1024"`。
#### P2-3 导入器默认别名 `image-standard` 与文档示例 `image-hd` 不一致
- **症状**:`importers._ensure_default_aliases` 建的默认图片别名叫 `image-standard`;而 `api.md` / `02-requirements.md` / `04-architecture.md` 的示例别名是 `image-hd`。别名是运营可配的、非硬契约,功能无害,但对外文档与默认落地口径不一致,容易让接入方困惑。
- **位置**:`apps/ai/importers.py` ↔ `docs/api.md` 等。
- **怎么改**:二选一对齐——把默认别名改成 `image-hd`,或在文档注明「`image-hd` 仅为示例,导入器默认建 `image-standard`,以后台实际配置为准」。
- **验证**:文档与导入器口径一致。
### P3 · 登记 / 后续处理(不在 T-105 硬性范围)
- **`Provider.capabilities()` 半冗余**:全项目无调用点(能力校验实际走 `AiModel.capabilities_set()`)。保留无害,建议加注释说明用途,或在 T-302 用作「provider 能力 ∩ 模型声明能力」的二次校验。
- **图生图 vs 文生图都归 `operation_type=IMAGE`**:当别名指向 `images_edits` 模型而调用方没传原图,当前抛 `AiCapabilityError`。T-302 需把它翻译成干净的 **400(缺原图)** 而不是 500。
- **CallRecord 写入禁止 dump `result.raw`**:`raw` 可能含 base64 大图/上游敏感字段;T-201/T-302 落记录时只存摘要/引用,别整包入库或打日志。
- **Provider 密钥无轮换**:单 `AI_KEY_ENCRYPTION_KEY`(非 `MultiFernet`),`env.md` 已注明「生产不可更换除非完成密钥轮换」,但轮换流程未实现。登记 Backlog(现有条目是 user API Key 轮换,补「provider key 轮换」)。
- **ModelAlias 单默认/唯一仅 app 级保证**:`clean()` 有理论并发 race;后台改配置低并发,可接受。codex 已记「MySQL partial unique 约束留待评估」,保留登记即可。
## 四、说明:未本地复跑
审核机(WSL)无 `python3.12`,**未本地复跑 `check`/`test`**。本报告基于:静态审查(providers / models / security / aliases / audit / importers / admin / tests / 迁移 / settings)+ codex 的执行记录(T-101 7 tests、T-102 18 tests、T-103 23 tests、T-104 recorded smoke,含真实 MySQL 建库超时的诚实记录,可信度高)。
其中 **T-104 的 `elapsed_ms=149` 是 recorded 本地耗时,不能作为「图片同步可行性」的证据**(见 P1-2)。
T-105 修补后,请重跑 `check` / `test` / `init`,并把命令与结果记入 `progress.md`(按 `06-tasks.md` 使用规则第 5 条)。
## 五、T-105 完成定义
- **P1-1** 已处理:`parameters` 透传白名单化 / 核心字段不可被覆盖,且有单测;或在本层加显式约束 + 注释并在 T-302 验收项中明确落地要求。
- **P1-2** 已处理:T-104 结论改写为「图片同步风险未退」,`06-tasks.md` / `progress.md` **显式登记该未决风险**并挂到 T-302 / T-403;smoke 命令支持 `image`。(真实图片实测可延到有真实 key 时,但登记不能少。)
- **P2-1 / P2-2 / P2-3** 全部处理(文档与代码一致、`resolution_to_size` 归一、别名口径对齐)。
- `check` 0 issues、`test` 全绿、`init` 通过,证据入 `progress.md`。
- P3 各项已在 `06-tasks.md` 对应任务(T-302 / T-403)或 Backlog 有登记,不遗失。