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

12 KiB
Raw Blame History

Phase 1 骨架审核报告(T-101 ~ T-104)

审核人:Claude Code(全栈视角)|日期:2026-07-02|结论:验收通过,但最高风险未退。 Phase 1 是全项目最高风险段(AI 上游同步调用 + 可插拔供应商 + 密钥)。代码质量高、三模型机制忠实落地;但 T-104 只验证了管路,没退掉「图片同步耗时」这个核心风险,且 parameters 透传存在资金安全隐患。 本文面向 codex 执行:每条修补项给出「症状 / 位置 / 怎么改 / 怎么验证」。修补任务见 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 有登记,不遗失。