diff --git a/apps/api/app/api/routes/generation_tasks.py b/apps/api/app/api/routes/generation_tasks.py old mode 100644 new mode 100755 index 7f7b3fa60..4f625ec53 --- a/apps/api/app/api/routes/generation_tasks.py +++ b/apps/api/app/api/routes/generation_tasks.py @@ -427,3 +427,53 @@ def retry_generation_task( detail="系统繁忙,请稍后再试", ) from None return _to_generation_task_response(retried) + + +@router.post("/tasks/{task_id}/cancel", response_model=GenerationTaskResponse) +def cancel_generation_task( + task_id: str, + authenticated_user: AuthenticatedUser = Depends(get_current_user), + generation_task_repository: Any = Depends(get_generation_task_repository), +) -> GenerationTaskResponse: + """取消生成任务。 + + 仅 pending / running 状态的任务可取消;取消后状态变为 cancelled。 + 对于已在运行的 Celery 任务,标记为 cancelled 后,worker 在下次检查点会中止执行。 + """ + task = generation_task_repository.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="Generation task not found") + + # 权限校验 + if task.created_by_user_id and task.created_by_user_id != authenticated_user.user.id: + raise HTTPException(status_code=403, detail="Access denied to this task") + + status_val = task.status.value if hasattr(task.status, "value") else str(task.status) + + # 终态不可取消 + if status_val in ("completed", "failed", "cancelled"): + raise HTTPException( + status_code=409, + detail=f"Cannot cancel task in {status_val} status", + ) + + # 执行取消 + try: + task.mark_cancelled() + task.append_log( + stage="cancelled", + message="用户主动取消任务", + level="INFO", + cancelled_by=authenticated_user.user.id, + ) + generation_task_repository.update(task) + logger.info( + "生成任务已取消: task_id=%s user_id=%s previous_status=%s", + task_id, + authenticated_user.user.id, + status_val, + ) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + + return _to_generation_task_response(task) diff --git a/apps/worker/worker_app/tasks/edit_plan_generation.py b/apps/worker/worker_app/tasks/edit_plan_generation.py index 3eb54cc93..b671df4b5 100755 --- a/apps/worker/worker_app/tasks/edit_plan_generation.py +++ b/apps/worker/worker_app/tasks/edit_plan_generation.py @@ -491,6 +491,28 @@ def render_edit_plan(self, plan_id: str) -> dict: gen_task_repo.update(gen_task) # 4. 根据引擎选择渲染方式 + # 取消检查:素材下载完后,确认任务没有被用户取消 + if generation_task_id: + current_task = gen_task_repo.get(generation_task_id) + if current_task: + task_status = ( + current_task.status.value + if hasattr(current_task.status, "value") + else str(current_task.status) + ) + if task_status == "cancelled": + logger.info("任务已被取消,中止渲染: plan_id=%s task_id=%s", plan_id, generation_task_id) + # 计划回到 editing 状态,用户可以继续编辑 + from packages.domain.edit_plan import EditPlanStatus + + if plan.status.value == "rendering": + try: + plan.resume_editing() + plan_repo.update(plan) + except ValueError: + pass + return {"status": "cancelled", "plan_id": plan_id, "message": "任务已取消"} + if engine == "unified": result = _render_with_unified( plan=plan,