""" 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)) @router.post("/{task_id}/regenerate") def regenerate( task_id: int, current_user: Annotated[CurrentUser, Depends(require_write_permission)] = None, db: Session = Depends(get_db), ): """重新生成(飞轮信号 regenerate -1)。""" 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, ) record_signal(db, current_user, task, "regenerate") enqueue_generation(task.id) return ok({"task_id": task_id, "status": "regenerating"}) @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})