Files
beige/backend/app/api/v1/fission.py
yangqianqian d85dcd401b 第11环裂变重写:对齐上线版 split.js 一次LLM出N套完整笔记包
架构从"扇出N个GenerationTask各跑完整管道"改为"一次LLM调用直接出N套
完整笔记包(N=1~5)",落 FissionNote 表 + 独立展示页。

后端:
- 018迁移:fission_notes 表(文案JSON+score+passed+imagePlan+images+status)
- fission_prompt:FISSION_SYSTEM+三档参考度(low/mid/high)+normalize_tags+品类兜底
- fission_pipeline:一次LLM出N套→各评分(@80合格线)→排序→落库,不达标标
  needs_optimization 非丢弃;apiports 503 回落 codeproxy gpt-5.5 强档兜底
- fission_images:每套串行调现有生图接口(零改动image_gen/storyboard)
- tasks.py:run_fission_pipeline Celery task,删旧扇出注入
- api/v1/fission:进度聚合FissionNote + GET /fission/{id}/notes(剥内部字段)

前端:FissionProgress对齐状态机 + N套独立展示页 + FissionNoteCard

测试:test_fission_engine(19)+test_fission_pipeline(5) 全过;104 全量回归绿

实测task5(fanout=2,mid)端到端跑通:一次出2套→seq0=85过/seq1=79标优化→
生图codeproxy/edits→1024×1536去AI化→task completed→notes端点返完整数据

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 11:17:37 +08:00

118 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
app/api/v1/fission.py — 第11环裂变路由对齐上线版 split.js一次出N套
POST /fission 触发裂变(1源任务→一次LLM出N套完整笔记包)
GET /fission/{id} 查裂变进度(聚合 FissionNote 落库/生图状态)
GET /fission/{id}/notes 取 N 套完整笔记包(独立展示页用)
工作台内呈现N 套笔记落 FissionNote 表,非扇出 GenerationTask。
"""
import json
import logging
from typing import Annotated
from fastapi import APIRouter, Depends
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.response import ok, raise_not_found
from app.middleware.workspace_guard import CurrentUser, require_write_permission
from app.models.fission import FissionTask, FissionNote
logger = logging.getLogger(__name__)
router = APIRouter(tags=["fission"])
class FissionCreate(BaseModel):
source_task_id: int
reference_level: str = Field(default="mid") # low/mid/high
fanout_count: int = Field(default=3, ge=1, le=5)
@router.post("/fission")
def create_fission_endpoint(
body: FissionCreate,
current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None,
db: Session = Depends(get_db),
):
"""触发裂变:从源任务一次出 N 套完整笔记包。返回 fission_id。"""
from app.services.fission_service import create_fission
fission_id, _ = create_fission(
db, current_user, body.source_task_id, body.reference_level, body.fanout_count,
)
return ok({"fission_id": fission_id,
"reference_level": body.reference_level, "fanout_count": body.fanout_count})
@router.get("/fission/{fission_id}")
def get_fission_progress(
fission_id: int,
current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None,
db: Session = Depends(get_db),
):
"""查裂变进度:聚合 FissionNote 落库/生图状态x/N 完成)。"""
ft = db.query(FissionTask).filter(
FissionTask.id == fission_id,
FissionTask.workspace_id == current_user.workspace_id,
).first()
if not ft:
raise_not_found("裂变任务不存在")
notes = db.query(FissionNote).filter(
FissionNote.fission_id == fission_id,
FissionNote.workspace_id == current_user.workspace_id,
).all()
done = sum(1 for n in notes if n.status == "image_done")
failed = sum(1 for n in notes if n.status == "failed")
total = len(notes)
# FissionTask.status 由 pipeline 维护(generating→completed/failed);此处只读不改,
# 避免 API 轮询与 worker 写状态打架。进度数 = 已落库套数的生图完成度。
return ok({
"fission_id": ft.id, "reference_level": ft.reference_level,
"fanout_count": ft.fanout_count, "status": ft.status,
"progress": {"done": done, "failed": failed, "total": total},
})
@router.get("/fission/{fission_id}/notes")
def get_fission_notes(
fission_id: int,
current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None,
db: Session = Depends(get_db),
):
"""取 N 套完整笔记包(独立展示页用):每套含文案+标签+imagePlan+图+分数。"""
ft = db.query(FissionTask).filter(
FissionTask.id == fission_id,
FissionTask.workspace_id == current_user.workspace_id,
).first()
if not ft:
raise_not_found("裂变任务不存在")
rows = db.query(FissionNote).filter(
FissionNote.fission_id == fission_id,
FissionNote.workspace_id == current_user.workspace_id,
).order_by(FissionNote.seq.asc()).all()
notes = []
for r in rows:
try:
note = json.loads(r.note_json) if r.note_json else {}
except (ValueError, TypeError):
note = {}
try:
images = json.loads(r.images_json) if r.images_json else []
except (ValueError, TypeError):
images = []
# 内部评分中间字段(_score/_passed/_block)不外泄,前端用平铺的 score/passed
for k in ("_score", "_passed", "_block"):
note.pop(k, None)
notes.append({
"seq": r.seq, "note": note, "images": images,
"score": r.score, "passed": bool(r.passed),
"needs_optimization": bool(r.needs_optimization),
"dimension": r.dimension, "status": r.status,
})
return ok({"fission_id": ft.id, "status": ft.status,
"fanout_count": ft.fanout_count, "notes": notes})