Files
beige/backend/app/api/v1/task_actions.py
yangqianqian 7f419f4c8b 存量积累:生图素人感约束+评图分+幂等防重跑+审核回路
- 015-017迁移:image_candidate 文案复审/AI视觉分/重生标记
- constants C7素人感约束(反电商摆拍对齐真实笔记)+C3叠字口子
- celery visibility_timeout=2h 防长任务被误判重投重复烧钱(task75教训)
- image_scorer 评图分(只筛选+展示,真实信号才进飞轮权重)
- storyboard/image_gen/pipeline_io 生图存量
- task_actions/tasks/task_service/flywheel 审核回路+飞轮存量

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

198 lines
7.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/task_actions.py — 任务操作端点
选文案 / 选图 / 导入文案 / 重新生成 / 提交审核 / 偏好上下文
DTOs 和辅助函数从 tasks.py 导入。
"""
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, raise_state_invalid
from app.middleware.workspace_guard import CurrentUser, require_write_permission
from app.models.task import GenerationTask, ImageCandidate, TextCandidate
from app.api.v1.tasks import (
SelectCandidateRequest,
ImportTextRequest,
_fmt_text,
_fmt_image,
_check_task_ownership,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/tasks", tags=["tasks"])
@router.post("/{task_id}/text-candidates/select")
def select_text(
task_id: int, body: SelectCandidateRequest,
current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None,
db: Session = Depends(get_db),
):
"""选文案(飞轮信号 text_select +3"""
from app.services.flywheel_service import record_signal
task = _check_task_ownership(
db.query(GenerationTask).filter(GenerationTask.id == task_id).first(),
current_user.workspace_id,
)
tc = db.query(TextCandidate).filter(
TextCandidate.id == body.candidate_id, TextCandidate.task_id == task_id
).first()
if not tc:
raise_not_found("文案候选不存在")
tc.is_selected = True
db.commit()
record_signal(db, current_user, task, "text_select", candidate_id=tc.id, angle_label=tc.angle_label)
return ok({"selected": body.candidate_id})
@router.post("/{task_id}/image-candidates/select")
def select_image(
task_id: int, body: SelectCandidateRequest,
current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None,
db: Session = Depends(get_db),
):
"""选图(飞轮信号 image_select +3"""
from app.services.flywheel_service import record_signal
task = _check_task_ownership(
db.query(GenerationTask).filter(GenerationTask.id == task_id).first(),
current_user.workspace_id,
)
ic = db.query(ImageCandidate).filter(
ImageCandidate.id == body.candidate_id, ImageCandidate.task_id == task_id
).first()
if not ic:
raise_not_found("图片候选不存在")
ic.is_selected = True
db.commit()
record_signal(db, current_user, task, "image_select", candidate_id=ic.id)
return ok({"selected": body.candidate_id})
@router.post("/{task_id}/import-text")
def import_text(
task_id: int, body: ImportTextRequest,
current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None,
db: Session = Depends(get_db),
):
"""轨B导入外部文案直接进候选池跳过 AI 生成洞2"""
_check_task_ownership(
db.query(GenerationTask).filter(GenerationTask.id == task_id).first(),
current_user.workspace_id,
)
tc = TextCandidate(
workspace_id=current_user.workspace_id, task_id=task_id,
source="import", content=body.content, angle_label=body.angle_label,
)
db.add(tc)
db.commit()
db.refresh(tc)
return ok(_fmt_text(tc))
class RegenerateRequest(BaseModel):
"""R2 重生参数(全 None=整体重生,兼容旧调用)。
strategy='A'/'B'/'C' 指定单套role 指定单张(需配 strategy)custom_prompt 人工追加提示词。"""
strategy: str | None = Field(default=None, pattern="^[ABC]$")
role: str | None = None
custom_prompt: str | None = Field(default=None, max_length=500)
@router.post("/{task_id}/regenerate")
def regenerate(
task_id: int,
body: RegenerateRequest | None = None,
current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None,
db: Session = Depends(get_db),
):
"""重新生成(飞轮信号 regenerate -1
body 全空=整体重生;带 strategy=单套重生;带 strategy+role=单张重生custom_prompt=人工提示词。"""
from app.services.flywheel_service import record_signal
from app.services.task_service import enqueue_generation
task = _check_task_ownership(
db.query(GenerationTask).filter(GenerationTask.id == task_id).first(),
current_user.workspace_id,
)
body = body or RegenerateRequest()
# role 必须配 strategy单张重生需知道是哪套的哪张
if body.role and not body.strategy:
raise_state_invalid("指定单张重生(role)时必须同时指定 strategy(套别)")
record_signal(db, current_user, task, "regenerate")
enqueue_generation(task.id, regen_strategy=body.strategy,
regen_role=body.role, custom_prompt=body.custom_prompt)
scope = "单张" if body.role else ("单套" if body.strategy else "整体")
return ok({"task_id": task_id, "status": "regenerating", "scope": scope})
@router.post("/{task_id}/submit-review")
def submit_review(
task_id: int,
current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None,
db: Session = Depends(get_db),
):
"""提交审核(状态 pending_selection → pending_review"""
task = _check_task_ownership(
db.query(GenerationTask).filter(GenerationTask.id == task_id).first(),
current_user.workspace_id,
)
if task.status != "pending_selection":
raise_state_invalid("只有 pending_selection 状态可提审")
task.status = "pending_review"
db.commit()
return ok({"task_id": task_id, "status": "pending_review"})
@router.get("/{task_id}/preference/context")
def get_preference_context(
task_id: int,
current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None,
db: Session = Depends(get_db),
):
"""取本任务飞轮偏好上下文(最近选中样本+打回原因)。"""
task = _check_task_ownership(
db.query(GenerationTask).filter(GenerationTask.id == task_id).first(),
current_user.workspace_id,
)
from app.services.flywheel_service import get_preference_context
ctx = get_preference_context(db, current_user.workspace_id, task.product_id)
return ok(ctx)
class EditTextRequest(BaseModel):
"""改稿请求candidate_id + 用户改后的新文案。"""
candidate_id: int
content: str = Field(min_length=1, max_length=5000)
@router.post("/{task_id}/text-candidates/{candidate_id}/edit")
def edit_text(
task_id: int, candidate_id: int, body: EditTextRequest,
current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None,
db: Session = Depends(get_db),
):
"""改稿(飞轮信号 text_edit +5最强真实信号
用户真改了文案的字=最在乎这条方向。回写 content + edited=True
并写 text_edit 飞轮偏好事件。倩倩姐2026-06-16拍板先激进入飞轮跑一周再校准。
"""
from app.services.flywheel_service import record_signal
task = _check_task_ownership(
db.query(GenerationTask).filter(GenerationTask.id == task_id).first(),
current_user.workspace_id,
)
tc = db.query(TextCandidate).filter(
TextCandidate.id == candidate_id, TextCandidate.task_id == task_id
).first()
if not tc:
raise_not_found("文案候选不存在")
tc.content = body.content
tc.edited = True
db.commit()
record_signal(db, current_user, task, "text_edit", candidate_id=tc.id, angle_label=tc.angle_label)
return ok({"candidate_id": candidate_id, "edited": True})