107 lines
12 KiB
Markdown
107 lines
12 KiB
Markdown
# 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 有登记,不遗失。
|