baseline: Clover 独立仓库首次基线提交
将 Clover 从上层产品包旧仓库中独立出来,建立专属版本控制。 当前状态=纵切片端到端已打通(登录→选品→出文出图→审核→下载包), M1文案质量去套路化已验收。此提交作为后续按核销清单逐条修复的基线。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
190
plans/修改处理方案-第一批.md
Normal file
190
plans/修改处理方案-第一批.md
Normal file
@@ -0,0 +1,190 @@
|
||||
# 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混在一起。
|
||||
Reference in New Issue
Block a user