# Clover 修改处理方案 · 第一批（⚠️ 做了不合格）

> 倩倩姐审方案专用。**未拍板不动代码**。每项含：现状代码 / 改后代码 / 改动文件清单 / 风险 / 验收。
> 现状代码均已逐行读核，行号真实。

---

## 修1 · 90分阈值过滤形同虚设

**根因**：`pipeline_io.py:96-106` 循环把 `candidates_raw` 全部入库，没看 `passed`。
text_variants.py 已给每条算了 `passed`（`text_variants.py:101`），但 pipeline 没用它过滤。

**现状代码**（pipeline_io.py:95-108）：
```python
seq = seq_start
for i, c in enumerate(candidates_raw):
    tc = TextCandidate(
        workspace_id=workspace_id, task_id=task.id,
        source=CandidateSource.AI,
        angle_label=c.get("angle_label") or c.get("angle", ""),
        content=json.dumps(c, ensure_ascii=False),
        score_json=json.dumps(c.get("score_detail", []), ensure_ascii=False),
        banned_word_status=BannedWordStatus(c.get("banned_word_status", "pass")),
    )
    db.add(tc)
    db.flush()
```

**改后代码**：
```python
# 90分过滤：只入库 passed 的；硬拦截(hard_block)无论分数都剔除
qualified = [c for c in candidates_raw
             if c.get("passed") and c.get("banned_word_status") != "hard_block"]
# 回退兜底：全员未达标时不空屏，取分最高的 N 条入库并标记 below_threshold
if not qualified:
    fallback = sorted(candidates_raw, key=lambda x: x.get("score", 0), reverse=True)
    qualified = [c for c in fallback if c.get("banned_word_status") != "hard_block"][:task.text_count]
    for c in qualified:
        c["below_threshold"] = True   # 前端可标"未达90分,仅供参考"

seq = seq_start
for i, c in enumerate(qualified):
    tc = TextCandidate(...)   # 同现状，circ 改成遍历 qualified
    db.add(tc)
    db.flush()
    ...
    push_fn(..., "text_candidate", {
        ..., "below_threshold": c.get("below_threshold", False),  # 新增透传
    }, seq)
```

**改动文件**：
- `backend/app/workers/pipeline_io.py`（run_text_generation，约改15行）
- `frontend/src/components/tasks/TextCandidateCard.tsx`（below_threshold时显黄标，约5行，可选）

**风险**：阈值90偏严，正常出文案可能常触发回退。回退逻辑必须有，否则用户看到空列表（比现在更糟）。
**验收**：①跑任务，列表只剩≥90分 ②人为造全低分，确认回退展示TopN且带黄标 ③hard_block的永不出现。
**待拍板**：回退要不要？我建议要（空屏体验更差）。below_threshold黄标前端做不做？

---

## 修2 · 品牌词字段缺失（文案+生图同根因，一次解两处）

**根因**：`products` 表无 `brand_keyword` 列（model:product.py:28-40 无此字段）。
但三处代码都在读它：`_text_prompt.py:137`、`storyboard.py:133`、`image_gen.py:140`。
→ `product.get("brand_keyword")` 永远 None → 文案不植入品牌词、生图特写图不带品牌词，静默失效。

**改动分5步：**

**① 新建迁移** `backend/alembic/versions/008_product_brand_keyword.py`：
```python
"""add brand_keyword to products
Revision ID: 008
Revises: 007
"""
from alembic import op
import sqlalchemy as sa
revision = "008"
down_revision = "007"
branch_labels = None
depends_on = None

def upgrade():
    op.add_column("products", sa.Column(
        "brand_keyword", sa.String(64), nullable=True,
        comment="品牌词,植入文案每条+生图特写图(第2/6张)"))

def downgrade():
    op.drop_column("products", "brand_keyword")
```

**② 模型加字段** `models/product.py`（image_path 后插一行）：
```python
brand_keyword: Mapped[str | None] = mapped_column(String(64))  # 品牌词,植入文案+生图
```

**③ DTO加字段** `api/v1/products.py:24 ProductCreate`：
```python
brand_keyword: str | None = None   # 品牌词
```
`create_product`(L85) 构造 Product 时加 `brand_keyword=body.brand_keyword`；
`_fmt_product`(L49) 返回加 `"brand_keyword": p.brand_keyword`。

**④ 前端录入框**：产品表单加"品牌词"输入（`ProductSubComponents.tsx` 或产品创建表单）。

**⑤ 确认读取生效**：三处读取代码已存在，加字段后自动拿到值，无需改。

**改动文件**：008迁移(新)、model/product.py、api/v1/products.py、前端产品表单。
**风险**：①北哥环境要 `alembic upgrade head` 跑迁移 ②老产品该列NULL→读到空串，三处已用 `or ""` 兜底，不报错。
**验收**：建产品填"龙石"→生成文案每条含"龙石"、生图第2/6张画面带"龙石"。
**待拍板**：品牌词录在**产品级**(随产品固定，方案默认)还是**任务级**(每次任务可改)？

---

## 修4 · 侧边栏导航404（先放修4，修3叠字单列文末）

**根因**：`Sidebar.tsx:19-28` 的 NAV_ITEMS 8条里，4条指向不存在的页：
- `/tasks`（无 page）、`/images`（无 page）、`/tasks/build`（无 page）、`/tasks/list`（无 page）
实际存在的页：/dashboard /tasks/new /tasks/[id]/text /tasks/[id]/images /tasks/[id]/confirm /review /history /config

**现状代码**（Sidebar.tsx:19-28）：
```tsx
const NAV_ITEMS: NavItem[] = [
  { label: '工作台', href: '/dashboard', icon: '🏠' },
  { label: '开新任务', href: '/tasks/new', icon: '✏️' },
  { label: '选择文案', href: '/tasks', icon: '📝' },      // 404
  { label: '选择图片', href: '/images', icon: '🖼️' },     // 404
  { label: '建立任务', href: '/tasks/build', icon: '⚙️' }, // 404
  { label: '任务列表', href: '/tasks/list', icon: '📋' },  // 404
  { label: '历史归档', href: '/history', icon: '🗂️' },
  { label: '配置中心', href: '/config', roles: ['admin'], icon: '⚙️' },
];
```

**改后代码**：删掉4条死链。选文案/选图片是任务流程内的步骤页（/tasks/[id]/text 等），不该做全局导航，进了具体任务才有。审核页`/review`原本漏在导航里，补上。
```tsx
const NAV_ITEMS: NavItem[] = [
  { label: '工作台', href: '/dashboard', icon: '🏠' },
  { label: '开新任务', href: '/tasks/new', icon: '✏️' },
  { label: '审核', href: '/review', roles: ['admin', 'leader'], icon: '✅' },
  { label: '历史归档', href: '/history', icon: '🗂️' },
  { label: '配置中心', href: '/config', roles: ['admin'], icon: '⚙️' },
];
```

**改动文件**：`frontend/src/components/layout/Sidebar.tsx`（改NAV_ITEMS数组）。
**风险**：无，纯导航项删改。
**验收**：点遍每个导航项都进到真实页，无404。
**待拍板**：审核页要不要进侧边栏（现状代码注释提到/review角色守卫但导航里没有）？建议补上。

---

## 修5 · 文案调用无网络退避重试

**根因**：`text_variants.py:49 _generate_one_batch` 只在JSON解析失败时重试2次，网络层503/超时不退避。
生图侧已有成熟退避 `_retry`（image_gen.py:51-63），文案没复用。

**现状代码**（text_variants.py:47-56）：
```python
async def _generate_one_batch(llm_client, product, batch_n, extra) -> list[dict]:
    for attempt in range(2):
        raw = await _call_llm(llm_client, build_prompt(product, batch_n, extra_rules=extra))
        parsed = parse_json_array(raw)
        if parsed:
            return parsed
        logger.warning(...)
    return []
```

**改后代码**：_call_llm 返回空（含503/超时）时退避重试。注意：退避会拉长耗时，须控制总时长不撞60s网关——故退避只1次、间隔短(2s)。
```python
async def _generate_one_batch(llm_client, product, batch_n, extra) -> list[dict]:
    for attempt in range(3):   # 解析失败/网络空 共最多3次
        raw = await _call_llm(llm_client, build_prompt(product, batch_n, extra_rules=extra))
        parsed = parse_json_array(raw)
        if parsed:
            return parsed
        if not raw and attempt < 2:        # 网络层空响应→短退避再试
            await asyncio.sleep(2 * (attempt + 1))   # 2s,4s
        logger.warning(...)
    return []
```

**改动文件**：`backend/app/services/ai_engine/text_variants.py`（_generate_one_batch，约改5行）。
**风险**：退避叠加可能逼近60s网关上限；故只退避2次、间隔短。若仍超时，本地兜底(build_local_drafts)已兜底。
**验收**：模拟apiports 503，确认自动退避重试而非直接掉本地兜底。

---

> 修3（瓶身中文叠字）技术最不确定，与建1（3套图）合并放"第三批·需新建/重构"文件细化，避免和无歧义的纯bug混在一起。
