将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。 当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包), M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
149 lines
6.7 KiB
Markdown
149 lines
6.7 KiB
Markdown
# 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 && <Banner>请先到「配置中心」录入 API Key 才能开任务 <Link href="/config">去录入</Link></Banner>}
|
||
```
|
||
|
||
**改动文件**:`frontend/src/components/tasks/NewTaskForm.tsx`(约10行)、可能复用key查询hook。
|
||
**风险**:无。
|
||
**验收**:清空key→开任务页"开始"置灰+引导文案+跳配置链接。
|
||
|
||
---
|
||
|
||
> 第二批小结:修8(magic number)无歧义可直接做;修6(生图日志)建议做、token降级;修7/修9需你拍板(多半可降级/保持现状);修10纯前端体验补强。
|