From 11c00811ebd37934d49243db9e0a1df13a1681a6 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Thu, 16 Jul 2026 09:52:58 +0800 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20=E7=94=9F=E6=88=90=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=8F=96=E6=B6=88=E6=8E=A5=E5=8F=A3=20-=20POST=20/tas?= =?UTF-8?q?ks/{task=5Fid}/cancel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增生成任务取消接口: - 支持 pending / running 状态的任务取消 - 取消后状态变为 cancelled,记录取消日志 - 终态(completed/failed/cancelled)不可取消,返回 409 - 权限校验:只能取消自己创建的任务 - 复用领域模型已有的 mark_cancelled 方法 --- apps/api/app/api/routes/generation_tasks.py | 50 +++++++++++++++++++++ 1 file changed, 50 insertions(+) mode change 100644 => 100755 apps/api/app/api/routes/generation_tasks.py 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) -- 2.54.0 From a08eea16309f705fd8be343dc307d946fd3b14fb Mon Sep 17 00:00:00 2001 From: CI Bot Date: Thu, 16 Jul 2026 10:06:02 +0800 Subject: [PATCH 02/17] =?UTF-8?q?feat:=20worker=E6=B8=B2=E6=9F=93=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=8F=96=E6=B6=88=E6=A3=80=E6=9F=A5=E7=82=B9=20-=20?= =?UTF-8?q?=E7=B4=A0=E6=9D=90=E4=B8=8B=E8=BD=BD=E5=90=8E=E6=A3=80=E6=B5=8B?= =?UTF-8?q?cancelled=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在渲染主流程中增加取消检查点: - 素材下载完成后、启动FFmpeg前,检查generation_task状态 - 如果已被取消(cancelled),立即中止,不启动渲染 - 计划状态从 rendering 切回 editing,用户可继续编辑 - 与 API 层的取消接口配套 不修改 FFmpeg 运行时取消(后续迭代),覆盖排队/下载期间取消的场景 --- .../worker_app/tasks/edit_plan_generation.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/apps/worker/worker_app/tasks/edit_plan_generation.py b/apps/worker/worker_app/tasks/edit_plan_generation.py index 3eb54cc93..19ce9d826 100755 --- a/apps/worker/worker_app/tasks/edit_plan_generation.py +++ b/apps/worker/worker_app/tasks/edit_plan_generation.py @@ -491,6 +491,23 @@ 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, -- 2.54.0 From 7060fa31248140490519f4efb0c8c15e23ff51c4 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 16 Jul 2026 10:18:22 +0800 Subject: [PATCH 03/17] =?UTF-8?q?feat:=20=E7=94=9F=E6=88=90=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E5=BC=B9=E7=AA=97=E5=A2=9E=E5=8A=A0=E5=8F=96=E6=B6=88?= =?UTF-8?q?=E6=8C=89=E9=92=AE=20-=20=E5=AF=B9=E6=8E=A5=20cancelGenerationT?= =?UTF-8?q?ask=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EditPlanStatus 新增 cancelled 状态及标签 - 新增 cancelGenerationTask API 函数 - 生成进度弹窗生成中状态增加"取消生成"按钮 - 取消前二次确认,防止误操作 - 轮询检测 cancelled 状态,取消后显示已取消提示 - 保存当前 generation_task_id 用于取消调用 --- apps/web/src/api/editPlans.ts | 15 +++- .../pages/editing-planner/EditingPlanner.tsx | 78 ++++++++++++++++++- 2 files changed, 89 insertions(+), 4 deletions(-) mode change 100644 => 100755 apps/web/src/api/editPlans.ts diff --git a/apps/web/src/api/editPlans.ts b/apps/web/src/api/editPlans.ts old mode 100644 new mode 100755 index 93bc13477..7465edba7 --- a/apps/web/src/api/editPlans.ts +++ b/apps/web/src/api/editPlans.ts @@ -20,7 +20,12 @@ import type { /** 剪辑计划状态枚举 */ export type EditPlanStatus = - "draft" | "editing" | "rendering" | "completed" | "failed"; + | "draft" + | "editing" + | "rendering" + | "completed" + | "failed" + | "cancelled"; /** 标题配置(对齐后端 title_config) */ export interface TitleConfig { @@ -453,6 +458,13 @@ export async function getGenerationTaskResults( return response.data.items || response.data || []; } +/** 取消生成任务 */ +export async function cancelGenerationTask( + taskId: string, +): Promise { + await apiClient.post(`/generation/tasks/${taskId}/cancel`); +} + /** * 获取素材库列表 — 调用 GET /api/v1/assets?library_id=xxx * 将后端 AssetResponse 映射为前端 MediaAsset 类型 @@ -553,6 +565,7 @@ export const PLAN_STATUS_LABELS: Record = { rendering: "渲染中", completed: "已完成", failed: "失败", + cancelled: "已取消", }; /** 质量分筛选选项 */ diff --git a/apps/web/src/pages/editing-planner/EditingPlanner.tsx b/apps/web/src/pages/editing-planner/EditingPlanner.tsx index 783e502a4..b4c85dc47 100755 --- a/apps/web/src/pages/editing-planner/EditingPlanner.tsx +++ b/apps/web/src/pages/editing-planner/EditingPlanner.tsx @@ -37,6 +37,7 @@ import { generateEditPlan, getGenerationStatus, getGenerationTaskResults, + cancelGenerationTask, } from "@/api/editPlans"; import { useUndoRedo } from "./hooks/useUndoRedo"; import type { @@ -270,6 +271,8 @@ const EditingPlanner: React.FC = () => { const [generated, setGenerated] = useState(false); const [generatedVideos, setGeneratedVideos] = useState([]); const [genError, setGenError] = useState(null); + const [genCancelled, setGenCancelled] = useState(false); + const [currentGenTaskId, setCurrentGenTaskId] = useState(null); const genTimerRef = useRef | null>(null); /* ── 播放 ── */ @@ -949,6 +952,8 @@ const EditingPlanner: React.FC = () => { setGeneratedVideos([]); setGenError(null); setGenProgress(0); + setGenCancelled(false); + setCurrentGenTaskId(null); try { const config = buildPlanConfig(); @@ -989,6 +994,7 @@ const EditingPlanner: React.FC = () => { // 触发生成 const genRes = await generateEditPlan(planId); setGenTotalClips(genRes.clip_count); + setCurrentGenTaskId(genRes.generation_task_id); message.info("已提交生成,等待处理..."); // 开始轮询 @@ -1041,6 +1047,13 @@ const EditingPlanner: React.FC = () => { return; // 停止轮询 } + if (status.plan_status === "cancelled") { + setGenerating(false); + setGenCancelled(true); + message.info("生成已取消"); + return; // 停止轮询 + } + // 继续轮询 genTimerRef.current = setTimeout(poll, 2000); } catch (err) { @@ -1060,6 +1073,30 @@ const EditingPlanner: React.FC = () => { }; }, []); + /* 取消生成 */ + const handleCancelGeneration = async () => { + if (!currentGenTaskId) return; + Modal.confirm({ + title: "确认取消生成?", + content: "取消后已处理的片段不会保留,需要重新生成。", + okText: "取消生成", + cancelText: "继续等待", + okButtonProps: { danger: true }, + onOk: async () => { + try { + await cancelGenerationTask(currentGenTaskId); + message.info("取消请求已提交,正在停止..."); + // 停止本地轮询,等待服务端状态通过轮询或下一次检查返回 cancelled + // 这里不立即停止,让轮询检测到 cancelled 状态后再收尾,以确保服务端确实处理了 + } catch (err: any) { + console.error("[取消生成失败]", err); + const msg = err?.response?.data?.detail || "取消失败,请稍后重试"; + message.error(msg); + } + }, + }); + }; + /* 查看生成历史 */ const handleViewGenHistory = async () => { const targetId = loadedPlanId || loadedTemplateId; @@ -1288,8 +1325,16 @@ const EditingPlanner: React.FC = () => { {/* ═══ 生成进度弹窗 ═══ */} { ), ] - : null + : genCancelled + ? [ + , + ] + : generating + ? [ + , + ] + : null } closable={!generating} maskClosable={false} @@ -1341,6 +1405,14 @@ const EditingPlanner: React.FC = () => {

)} + {genCancelled && ( +
+

生成已取消

+

+ 你可以继续编辑后重新生成 +

+
+ )} {generated && generatedVideos.length > 0 && (