# Clover 修改处理方案 · 第二批(🟡 做了一半) > 现状代码均已逐行读核。每项含现状/改后/文件/风险/验收。 --- ## 修6 · ai_call_logs 记录不全 **根因**: - `pipeline_io.py:78-91` 只在文案后写1条,且 `prompt_tokens/completion_tokens` 从不填(httpx响应没解析usage)。 - `run_image_generation`(L122-216) 整个函数**无任何 AiCallLog 写入**——生图调用零留痕。 **现状代码**(pipeline_io.py:78-91,文案日志): ```python log = AiCallLog( workspace_id=workspace_id, user_id=task.operator_id, key_id=key_id, task_id=task.id, provider="apiports", model=clients._model, call_type="text", success=llm_success, latency_ms=latency_ms, ) # 缺 prompt_tokens / completion_tokens ``` **改后**:分两处。 **① 文案日志补token**(依赖_call_llm返回usage,需text_variants.py一并改透传usage,工作量上升)。一期可降级:token填None保持现状,但**至少把生图日志补上**。 **② 生图日志新增**(run_image_generation 末尾,db.commit前): ```python # 生图调用留痕(与文案对称) try: img_log = AiCallLog( workspace_id=workspace_id, user_id=task.operator_id, key_id=None, task_id=task.id, provider=getattr(clients, "_image_provider", "apiports"), model="gpt-image-2", call_type="image", success=any(not r.get("error") for r in image_results), latency_ms=image_latency_ms, # 需在gather前后记时间 ) db.add(img_log) except Exception as e: logger.warning("生图 ai_call_logs 写入失败(非阻断): %s", e) ``` gather 前加 `t0 = time.monotonic()`,后算 `image_latency_ms`。 **改动文件**:`pipeline_io.py`(生图日志,约12行);token数透传需 text_variants.py + 调整_call_llm返回(约20行,建议二期)。 **风险**:飞轮一期是否强依赖token数?若不依赖,token降级、只补生图成败/耗时/provider即可。 **验收**:跑一次任务,ai_call_logs 表里文案+生图各一条,生图条 provider/model/success/latency 齐。 **待拍板**:token数这期补不补?建议先补生图留痕,token二期。 --- ## 修7 · 文案 Gemini 兜底层未接 **根因**:现链路 LLM(Claude)→本地模板 两层(text_variants.py:89-91)。`gemini_factory.gemini_generate` 只服务生图,文案无Gemini通道。 **现状代码**(text_variants.py:89-91): ```python copies: list[dict] = await _generate_in_batches(llm_client, product, count, extra) if not copies: copies = list(build_local_drafts(product, count)) # 直接掉本地兜底 ``` **改后**:Claude失败后、本地兜底前插Gemini文案。需 gemini_factory 加 chat 方法(现只有图)。 ```python copies = await _generate_in_batches(llm_client, product, count, extra) if not copies: logger.warning("Claude文案失败,尝试Gemini兜底") try: copies = await _generate_via_gemini(product, count, extra) # 新函数 except Exception as e: logger.error("Gemini文案兜底也失败: %s", e) if not copies: copies = list(build_local_drafts(product, count)) # 最终本地兜底 ``` 新增 `_generate_via_gemini`:调 gemini chat → 复用 parse_json_array。 **改动文件**:`text_variants.py`(约15行)、`gemini_factory.py`(加chat_complete方法,约15行)。 **风险**:①Gemini文案质量/JSON格式遵守度要验,可能不如Claude ②Gemini key 是否各operator都配了?没配则这层形同虚设。 **验收**:停Claude(模拟503),确认走Gemini出文案而非直接本地模板。 **待拍板**:一期是否需要三层?我建议——若operator普遍没配Gemini key,先不接,留两层(Claude+本地)即可。 --- ## 修8 · 上传只查 content-type 无 magic number **根因**:`products.py:174` 只查 `file.content_type`(HTTP header,可伪造)。无字节头校验。 **现状代码**(api/v1/products.py:174-179): ```python if file.content_type not in _ALLOWED_CONTENT_TYPES: raise_business(f"不支持的文件类型 {file.content_type}...") data = await file.read() if len(data) > _MAX_SIZE_BYTES: raise_business("文件超过 10 MB 限制") ``` **改后**:读完data后加真实字节头校验。 ```python data = await file.read() if len(data) > _MAX_SIZE_BYTES: raise_business("文件超过 10 MB 限制") # magic number 校验真实文件类型(防伪造content-type) if not (data[:3] == b"\xff\xd8\xff" # JPEG or data[:8] == b"\x89PNG\r\n\x1a\n" # PNG or (data[:4] == b"RIFF" and data[8:12] == b"WEBP")): # WebP raise_business("文件内容不是有效的 JPEG/PNG/WebP 图片") ``` **改动文件**:`backend/app/api/v1/products.py`(约6行)。 **风险**:无。 **验收**:把 .exe 改名 .jpg + 伪造 content-type 上传 → 被拒。 --- ## 修9 · 文案优化轮默认关(确认题,非bug) **现状**(text_variants.py:107-110):`TEXT_OPTIMIZE_ENABLED=false` 默认关。 注释写明:apiports 60s网关下,优化轮的失败要白等60s才503,拖慢出文案+100s+。 **这是有意为之的工程权衡,不是bug。** 两个选择: - **A(建议)保持关**:等北哥prompt方案到位再开,架构已留位(改环境变量即开)。 - **B 改造绕60s**:优化轮也走分批小请求(≤4条/批),降低单请求触发60s概率。工作量M。 **改动文件**:A=0改动;B=text_variants.py优化轮分批(约20行)。 **待拍板**:A还是B?我建议A。 --- ## 修10 · 无 key 不置灰引导 **根因**:配置页有"暂无Key请录入"提示(ApiKeysTab.tsx:92),但 `/tasks/new` 开任务页不检测key、不置灰入口。用户没录key也能点开始→任务必然失败。 **现状**:`NewTaskForm.tsx:38,112-115` 只处理"产品入镜但无参考图"的置灰(blockedByNoImage),无key检测。 **改后**:开任务页加载时查当前用户是否有key,无则置灰"开始"+引导。 ```tsx // NewTaskForm 加载时 const { data: keys } = useApiKeys(); // 复用配置页的key查询 const noKey = !keys || keys.length === 0; const blocked = blockedByNoImage || noKey; // 开始按钮 disabled={blocked} {noKey && 请先到「配置中心」录入 API Key 才能开任务 去录入} ``` **改动文件**:`frontend/src/components/tasks/NewTaskForm.tsx`(约10行)、可能复用key查询hook。 **风险**:无。 **验收**:清空key→开任务页"开始"置灰+引导文案+跳配置链接。 --- > 第二批小结:修8(magic number)无歧义可直接做;修6(生图日志)建议做、token降级;修7/修9需你拍板(多半可降级/保持现状);修10纯前端体验补强。