上线版: 产品表单统一+form嵌套修复+用户管理+部署+三套叙事

- 产品编辑入口统一走 ProductFormFull(卖点/风格/人群/品牌词全字段);
  修复开任务页 <form> 套 <form> 致"编辑产品"报错、改不了、跳回首个产品
- dashboard 入口卡片对齐实际路由: 系统管理(/config) 与 工作配置(/settings) 分开;
  settings ?tab=products 直达改用挂载后读 URL, 消除 hydration mismatch
- 新增用户管理(users API/admin service/改密页) + alembic 022/023/024
- 上线部署: Dockerfile / docker-compose.prod+https / nginx https / .env.example
- A8 三套正交叙事(痛点/场景/成分背书) + beige 调色去AI化 + 飞轮 text_import 高权重信号

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yangqianqian
2026-06-30 18:08:13 +08:00
parent a77212781c
commit df1856d793
150 changed files with 8616 additions and 1765 deletions

View File

@@ -33,6 +33,13 @@ def _parse_fission_response(
build_fallback_image_plan, build_fallback_notes,
)
def _fallback(reason: str) -> list[dict]:
notes = build_fallback_notes(source_note, product, note_count, image_count)
for note in notes:
note["is_fallback"] = True
note["fallback_reason"] = reason
return notes
notes = notes_array_from_parsed(parse_json_array(raw))
if not notes:
# parse_json_array 只认数组;再试整段当对象解析(容错单对象返回)
@@ -42,7 +49,7 @@ def _parse_fission_response(
notes = []
if not notes:
logger.warning("裂变 LLM 返回不可解析启用品类兜底。raw[:120]=%s", str(raw)[:120])
return build_fallback_notes(source_note, product, note_count, image_count)
return _fallback("llm_parse_failed")
out: list[dict] = []
for n in notes:
@@ -53,17 +60,21 @@ def _parse_fission_response(
if not isinstance(plan, list) or len(plan) != image_count:
n["imagePlan"] = build_fallback_image_plan(n, image_count)
out.append(n)
return out or build_fallback_notes(source_note, product, note_count, image_count)
return out or _fallback("llm_empty_notes")
def _score_notes(clients, notes: list[dict], source_note: dict, banned: list[str]) -> None:
def _score_notes(
clients, notes: list[dict], source_note: dict, banned: list[str],
product: dict | None = None,
) -> None:
"""对每套笔记 LLM 评分限2并发结果就地写回 note['_score']/_passed/_block。
Celery 同步环境:用 asyncio.run 跑一个内部 gather信号量限2并发
对齐生图限流,避免中转站 429
D1修复product 传给 llm_score_copy → build_score_prompt 组装【产品语境】
评委不再盲评,买点转化/痛点人群精准两维能基于产品信息给分
Celery 同步环境:用 asyncio.run 跑一个内部 gather信号量限2并发
"""
import asyncio
from app.services.ai_engine.llm_scorer import llm_score_copy
from app.services.ai_engine.llm_scorer import llm_score_copy, ScoringUnavailableError
from app.services.ai_engine.constants import QUALITY_PASS_SCORE
async def _run() -> None:
@@ -78,8 +89,17 @@ def _score_notes(clients, notes: list[dict], source_note: dict, banned: list[str
async with sem:
try:
r = await llm_score_copy(
clients, copy, source_note, banned, pass_score=QUALITY_PASS_SCORE,
clients, copy, product or source_note, banned,
pass_score=QUALITY_PASS_SCORE,
)
except ScoringUnavailableError as exc:
# B1红线对齐评分两通道都挂≠质量差明确标记不可用绝不静默记0分糊弄
logger.error("裂变评分通道不可用(非质量问题): %s", exc)
note["_scoring_unavailable"] = True
note["_score"] = 0
note["_block"] = False
note["_passed"] = False
return
except Exception as exc: # noqa: BLE001
logger.warning("裂变评分异常记0分不达标: %s", exc)
r = {"score": 0, "passed": False, "banned_words_found": []}
@@ -91,6 +111,11 @@ def _score_notes(clients, notes: list[dict], source_note: dict, banned: list[str
await asyncio.gather(*(_one(n) for n in notes))
asyncio.run(_run())
# 整批评分全因通道不可用而失败 → 抛错让上层(execute_fission_pipeline)感知,
# 标记任务"评分服务不可用"而非误判成完成/质量差
if notes and all(n.get("_scoring_unavailable") for n in notes):
raise ScoringUnavailableError(
f"裂变 {len(notes)} 套笔记评分全部失败:评分通道(apiports+codeproxy)均不可用")
def execute_fission_pipeline(db: Session, clients, fission_id: int, source_task_id: int) -> dict:
@@ -147,7 +172,8 @@ def execute_fission_pipeline(db: Session, clients, fission_id: int, source_task_
logger.warning("裂变 LLM 调用失败,启用品类兜底: %s", exc)
notes = _parse_fission_response(raw, source_note, product, n, image_count)
_score_notes(clients, notes, source_note, banned)
# D1修复传 product 给评委,不再盲评(买点转化/痛点人群精准需要产品语境)
_score_notes(clients, notes, source_note, banned, product=product)
# 排序:过线优先,再按分降序;取前 N 套(不足用兜底草稿补齐)
notes.sort(key=lambda x: (x.get("_passed", False), x.get("_score", 0)), reverse=True)
@@ -175,5 +201,9 @@ def execute_fission_pipeline(db: Session, clients, fission_id: int, source_task_
from app.services.fission_images import generate_fission_images
generate_fission_images(db, clients, ft, product, image_count, saved_ids)
ft.status = "completed"; db.commit()
return {"fission_id": fission_id, "status": "completed", "note_ids": saved_ids}
has_fallback = any(bool(note.get("is_fallback")) for note in chosen)
# status 字段是 varchar(20)"completed_with_fallback"(23字符)会撑爆触发 DataError 1406
# 故用短码 "completed_fb"(倩倩姐2026-06-30修);语义=完成但含降级草稿。
ft.status = "completed_fb" if has_fallback else "completed"
db.commit()
return {"fission_id": fission_id, "status": ft.status, "note_ids": saved_ids}