From dffb642a6882aee2f01c56854daf89905a7ed8dc Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 24 Aug 2026 15:02:46 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20Step7=20=E7=A1=AE=E8=AE=A4=E7=94=9F?= =?UTF-8?q?=E6=88=90=E4=BC=98=E5=85=88=E8=B0=83=E7=94=A8=20confirmGenerati?= =?UTF-8?q?on=20=E5=A4=8D=E7=94=A8=E9=A2=84=E8=A7=88=E4=BA=A7=E7=89=A9?= =?UTF-8?q?=EF=BC=8C=E8=A7=A3=E5=86=B3=E5=8D=A1=2010%=20=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:前端调用 createGenerationTask 创建全新渲染任务,后端 worker 因 source_edit_plan_id 为空进入 failed 分支但未更新 DB 状态, 导致前端轮询永远停在 running(卡 10%)。 修复策略: - 主路径:Step6 封面流程创建预览时,将 previewTaskId 暴露给父组件; Step7 确认生成优先调用 confirmGeneration(previewTaskId) 复用预览 产物,后端直接提升为正式任务(秒出)。 - Fallback:无预览任务或 confirmGeneration 失败时,退回 createGenerationTask 并传递 source_edit_plan_id,后端走全量渲染。 改动文件(7 个): - useGenerateVideo.ts:导入 confirmGeneration,新增双路径逻辑 - useGenerateFormState:新增 previewTaskId 共享状态 - useStep6Cover:createPreview 后回调 onPreviewTaskCreated - Step6CoverSettings / GenerateStepContent:透传回调 - generate-video/types.ts:UseGenerateVideoProps 新增 previewTaskId --- apps/web/src/pages/generate/GeneratePage.tsx | 4 + .../components/GenerateStepContent.tsx | 4 + .../components/Step6CoverSettings.tsx | 3 + .../generate/hooks/generate-video/types.ts | 2 + .../hooks/useGenerateFormState/index.ts | 9 ++ .../pages/generate/hooks/useGenerateVideo.ts | 87 +++++++++++++------ .../src/pages/generate/hooks/useStep6Cover.ts | 8 ++ 7 files changed, 89 insertions(+), 28 deletions(-) diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index 2c822aa6d..57df9d2f5 100644 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -77,6 +77,8 @@ const GeneratePage: React.FC = () => { setPreviewVideo, previewModalOpen, setPreviewModalOpen, + previewTaskId, + setPreviewTaskId, } = formState /* ── 标题样式回调(Step5 样式面板 + 右侧预览 CSS 层共用) ── */ @@ -173,6 +175,7 @@ const GeneratePage: React.FC = () => { bgm, generateCount, sourceEditPlanId, + previewTaskId, }) /* ================================================================ @@ -215,6 +218,7 @@ const GeneratePage: React.FC = () => { onApplyPreset={styleUpdaters.applyPreset} activePreset={styleUpdaters.activePreset} titlePresets={styleUpdaters.titlePresets} + onPreviewTaskCreated={setPreviewTaskId} coverSettings={coverSettings} onCoverSettingsChange={setCoverSettings} duration={duration} diff --git a/apps/web/src/pages/generate/components/GenerateStepContent.tsx b/apps/web/src/pages/generate/components/GenerateStepContent.tsx index 7f7cf93f8..6c7ad9eaa 100644 --- a/apps/web/src/pages/generate/components/GenerateStepContent.tsx +++ b/apps/web/src/pages/generate/components/GenerateStepContent.tsx @@ -76,6 +76,8 @@ export interface GenerateStepContentProps { onDismissError: () => void /* 其他 */ presetVoices: PresetVoiceItem[] + /** 预览任务创建回调——传递给 Step6CoverSettings */ + onPreviewTaskCreated?: (taskId: string) => void } export const GenerateStepContent: React.FC = (props) => { @@ -121,6 +123,7 @@ export const GenerateStepContent: React.FC = (props) = onRetry, onDismissError, presetVoices, + onPreviewTaskCreated, } = props /* 当前模板的 segments,传给 Step2 构建 clips */ @@ -190,6 +193,7 @@ export const GenerateStepContent: React.FC = (props) = assetIds={materialMode === "auto" ? smartSelectedIds : selectedMaterials} selectedTemplate={selectedTemplate} titleSettings={titleSettings} + onPreviewTaskCreated={onPreviewTaskCreated} /> ) case 7: diff --git a/apps/web/src/pages/generate/components/Step6CoverSettings.tsx b/apps/web/src/pages/generate/components/Step6CoverSettings.tsx index 8d50ded18..fc576e303 100755 --- a/apps/web/src/pages/generate/components/Step6CoverSettings.tsx +++ b/apps/web/src/pages/generate/components/Step6CoverSettings.tsx @@ -16,6 +16,8 @@ interface Step6CoverSettingsProps { selectedTemplate?: string /** Step4 标题设置,用于预览视频烧录标题 & 封面叠加标题 */ titleSettings?: import("../types").TitleSettings + /** 预览任务创建回调——将 task_id 暴露给父组件供 confirmGeneration 复用 */ + onPreviewTaskCreated?: (taskId: string) => void } const Step6CoverSettings: React.FC = (props) => { @@ -43,6 +45,7 @@ const Step6CoverSettings: React.FC = (props) => { assetIds: props.assetIds, selectedTemplate: props.selectedTemplate, titleSettings: props.titleSettings, + onPreviewTaskCreated: props.onPreviewTaskCreated, }) const handleAutoGenerate = () => { diff --git a/apps/web/src/pages/generate/hooks/generate-video/types.ts b/apps/web/src/pages/generate/hooks/generate-video/types.ts index 1c2b6e2a5..a6352d553 100755 --- a/apps/web/src/pages/generate/hooks/generate-video/types.ts +++ b/apps/web/src/pages/generate/hooks/generate-video/types.ts @@ -21,6 +21,8 @@ export interface UseGenerateVideoProps { generateCount: number /** 当前草稿 ID(URL 参数 edit_plan_id,用于后端回写任务关联) */ sourceEditPlanId?: string | null + /** 预览任务 ID(由 useStep6Cover 创建后写入,供 confirmGeneration 复用预览产物) */ + previewTaskId?: string | null } /** 生成阶段 */ diff --git a/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts b/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts index d1cffd1ff..7170cdf75 100755 --- a/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts +++ b/apps/web/src/pages/generate/hooks/useGenerateFormState/index.ts @@ -95,6 +95,10 @@ export interface GenerateFormState { setPreviewVideo: (v: GeneratedVideo | null) => void previewModalOpen: boolean setPreviewModalOpen: (open: boolean) => void + + /** 预览任务 ID(由 useStep6Cover 创建后写入,供 useGenerateVideo 复用) */ + previewTaskId: string | null + setPreviewTaskId: (id: string | null) => void } export const useGenerateFormState = (): GenerateFormState => { @@ -160,6 +164,9 @@ export const useGenerateFormState = (): GenerateFormState => { const [previewVideo, setPreviewVideo] = useState(null) const [previewModalOpen, setPreviewModalOpen] = useState(false) + /* ── 预览任务 ID(useStep6Cover 创建预览时写入,useGenerateVideo 复用) ── */ + const [previewTaskId, setPreviewTaskId] = useState(null) + /* ── 从 URL / 编辑计划加载配置 ── */ usePlanConfigLoader({ editPlanId, @@ -208,5 +215,7 @@ export const useGenerateFormState = (): GenerateFormState => { setPreviewVideo, previewModalOpen, setPreviewModalOpen, + previewTaskId, + setPreviewTaskId, } } diff --git a/apps/web/src/pages/generate/hooks/useGenerateVideo.ts b/apps/web/src/pages/generate/hooks/useGenerateVideo.ts index 58d1c3b8e..569c05793 100755 --- a/apps/web/src/pages/generate/hooks/useGenerateVideo.ts +++ b/apps/web/src/pages/generate/hooks/useGenerateVideo.ts @@ -6,6 +6,7 @@ import { useState, useCallback } from "react" import { message } from "antd" import type { GeneratedVideo } from "@/api/template-editor" import { createGenerationTask } from "@/api/tasks/tasks" +import { confirmGeneration } from "@/api/generation/confirm" import type { UseGenerateVideoProps } from "./generate-video/types" import { getGenerationPhase } from "./generate-video/phase" import { useGenerationPolling } from "./generate-video/useGenerationPolling" @@ -92,35 +93,65 @@ export function useGenerateVideo(props: UseGenerateVideoProps) { // 封面 URL:优先 AI 生成缩略图,兜底用户上传 const coverUrl = props.coverSettings?.thumbnail_url || props.coverSettings?.upload_url || "" - // 直接创建正式生成任务 - const taskResp = await createGenerationTask({ - template_id: selectedTemplate, - asset_ids: assetIds, - output_width: outputWidth, - output_height: outputHeight, - cover_url: coverUrl, - custom_title: props.titleSettings?.title || "", - duration: props.duration || undefined, - video_ratio: props.videoRatio, - ...(props.sourceEditPlanId ? { source_edit_plan_id: props.sourceEditPlanId } : {}), - ...(props.titleSettings?.title - ? { - title_config: { - text: props.titleSettings.title, - font: props.titleSettings.font, - font_size: props.titleSettings.size, - font_color: props.titleSettings.color, - position: props.titleSettings.position, - bold: props.titleSettings.bold, - stroke: props.titleSettings.stroke, - shadow: props.titleSettings.shadow, - }, - } - : {}), - }) + // ── Step7 确认生成:优先复用预览产物(秒出),fallback 到全量渲染 ── + let taskId: string | undefined + + // 主路径:如果有预览任务 ID,调用 confirmGeneration 复用预览产物 + if (props.previewTaskId) { + try { + console.log( + "[handleGenerate] 尝试 confirmGeneration 复用预览产物, previewTaskId:", + props.previewTaskId, + ) + const confirmResp = await confirmGeneration(props.previewTaskId, { + output_width: outputWidth, + output_height: outputHeight, + cover_url: coverUrl, + custom_title: props.titleSettings?.title || "", + }) + taskId = confirmResp.items?.[0]?.id + if (taskId) { + console.log("[handleGenerate] confirmGeneration 成功, taskId:", taskId) + } + } catch (confirmErr) { + console.warn( + "[handleGenerate] confirmGeneration 失败, fallback 到 createGenerationTask:", + confirmErr, + ) + // 继续走 fallback 路径 + } + } + + // Fallback 路径:没有预览任务或 confirmGeneration 失败,创建新的生成任务 + if (!taskId) { + const taskResp = await createGenerationTask({ + template_id: selectedTemplate, + asset_ids: assetIds, + output_width: outputWidth, + output_height: outputHeight, + cover_url: coverUrl, + custom_title: props.titleSettings?.title || "", + duration: props.duration || undefined, + video_ratio: props.videoRatio, + ...(props.sourceEditPlanId ? { source_edit_plan_id: props.sourceEditPlanId } : {}), + ...(props.titleSettings?.title + ? { + title_config: { + text: props.titleSettings.title, + font: props.titleSettings.font, + font_size: props.titleSettings.size, + font_color: props.titleSettings.color, + position: props.titleSettings.position, + bold: props.titleSettings.bold, + stroke: props.titleSettings.stroke, + shadow: props.titleSettings.shadow, + }, + } + : {}), + }) + taskId = taskResp.items?.[0]?.id + } - // 从创建响应直接拿 task_id,改用新接口轮询 - const taskId = taskResp.items?.[0]?.id if (!taskId) { throw new Error("创建任务成功但未返回任务 ID,请稍后在任务列表查看") } diff --git a/apps/web/src/pages/generate/hooks/useStep6Cover.ts b/apps/web/src/pages/generate/hooks/useStep6Cover.ts index f86141680..4ddffa546 100644 --- a/apps/web/src/pages/generate/hooks/useStep6Cover.ts +++ b/apps/web/src/pages/generate/hooks/useStep6Cover.ts @@ -26,6 +26,8 @@ interface UseStep6CoverProps { selectedTemplate?: string /** Step4 标题设置,用于预览视频烧录标题 & 封面叠加标题 */ titleSettings?: TitleSettings + /** 预览任务创建回调——将 task_id 暴露给父组件供 confirmGeneration 复用 */ + onPreviewTaskCreated?: (taskId: string) => void } export function useStep6Cover({ @@ -35,6 +37,7 @@ export function useStep6Cover({ assetIds = [], selectedTemplate = "", titleSettings, + onPreviewTaskCreated, }: UseStep6CoverProps) { const [generating, setGenerating] = useState(false) @@ -170,6 +173,10 @@ export function useStep6Cover({ } : {}), }) + // 将预览任务 ID 暴露给父组件,供 Step7 确认生成时复用(confirmGeneration) + if (previewResp.task_id) { + onPreviewTaskCreated?.(previewResp.task_id) + } // 轮询等待预览渲染完成:递归 setTimeout 避免请求重叠 + 120s 超时兜底 await new Promise((resolve, reject) => { let finished = false @@ -282,6 +289,7 @@ export function useStep6Cover({ generating, duration, titleSettings, + onPreviewTaskCreated, ]) // ── 模板操作方法 ── -- 2.54.0 From bc18bf170e29f6d85bbc81735cb00a67dd71d333 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 24 Aug 2026 15:08:10 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=E7=B4=A0=E6=9D=90/=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E5=8F=98=E5=8C=96=E6=97=B6=E8=87=AA=E5=8A=A8=E6=B8=85?= =?UTF-8?q?=E9=99=A4=20previewTaskId=EF=BC=8C=E9=98=B2=E6=AD=A2=E5=A4=8D?= =?UTF-8?q?=E7=94=A8=E8=BF=87=E6=9C=9F=E9=A2=84=E8=A7=88=E4=BA=A7=E7=89=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AI Code Review 指出:用户 Step6 生成预览后若返回修改素材或模板, previewTaskId 仍保留会导致 Step7 复用过期产物。 新增 useEffect:previewAssetIds 或 selectedTemplate 变化时 自动 setPreviewTaskId(null),确保 confirmGeneration 只在 预览产物与当前配置一致时才被调用。 --- apps/web/src/pages/generate/GeneratePage.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index 57df9d2f5..ac516327b 100644 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -9,7 +9,7 @@ * - 标题样式通过 CSS 层实时叠加,所见即所得 * - 最终成片仍走后端 FFmpeg 渲染(Step7 确认生成) */ -import React, { useMemo } from "react" +import React, { useMemo, useEffect } from "react" import { Modal, message } from "antd" import { useNavigate } from "react-router-dom" import type { VoiceClone } from "@/api/voice-clone" @@ -101,6 +101,11 @@ const GeneratePage: React.FC = () => { () => (materialMode === "auto" ? smartSelectedIds : selectedMaterials), [materialMode, smartSelectedIds, selectedMaterials], ) + // 素材或模板变化时,清除旧的 previewTaskId,防止 Step7 复用过期预览产物 + useEffect(() => { + setPreviewTaskId(null) + }, [previewAssetIds, selectedTemplate, setPreviewTaskId]) + const previewAssetsEnabled = currentStep >= 4 && previewAssetIds.length > 0 const { assets: previewAssets, -- 2.54.0 From efa5d90f43cacf572ef329be7f21c1ac4c7d497b Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 24 Aug 2026 15:12:08 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=E8=A1=A5=E5=85=A8=20previewTaskId?= =?UTF-8?q?=20=E5=A4=B1=E6=95=88=E6=9D=A1=E4=BB=B6=E2=80=94=E2=80=94durati?= =?UTF-8?q?on/ratio/titleSettings=20=E5=8F=98=E5=8C=96=E4=B9=9F=E6=B8=85?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit confirmGeneration 无法覆盖预览的 duration/video_ratio/title_config, 这些参数变化后必须重新渲染预览,否则最终视频会与用户当前配置不一致。 --- apps/web/src/pages/generate/GeneratePage.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index ac516327b..29ed669a9 100644 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -101,10 +101,13 @@ const GeneratePage: React.FC = () => { () => (materialMode === "auto" ? smartSelectedIds : selectedMaterials), [materialMode, smartSelectedIds, selectedMaterials], ) - // 素材或模板变化时,清除旧的 previewTaskId,防止 Step7 复用过期预览产物 + // 素材、模板或生成参数变化时,清除旧的 previewTaskId,防止 Step7 复用过期预览产物 + // confirmGeneration 无法覆盖 duration/ratio/title_config,所以这些参数变化后必须重新创建预览 + const titleKey = JSON.stringify(titleSettings) useEffect(() => { setPreviewTaskId(null) - }, [previewAssetIds, selectedTemplate, setPreviewTaskId]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [previewAssetIds, selectedTemplate, duration, videoRatio, titleKey, setPreviewTaskId]) const previewAssetsEnabled = currentStep >= 4 && previewAssetIds.length > 0 const { -- 2.54.0 From 0f645b01e8cd3714a1961a4ed7e64ce77a87f50f Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 24 Aug 2026 15:16:19 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20Step7=20=E7=A1=AE=E8=AE=A4=E7=94=9F?= =?UTF-8?q?=E6=88=90=E4=BC=98=E5=85=88=E8=B0=83=E7=94=A8=20confirmGenerati?= =?UTF-8?q?on=20=E5=A4=8D=E7=94=A8=E9=A2=84=E8=A7=88=E4=BA=A7=E7=89=A9?= =?UTF-8?q?=EF=BC=8C=E8=A7=A3=E5=86=B3=E5=8D=A1=2010%=20=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:前端调用 createGenerationTask 创建全新渲染任务,后端 worker 因 source_edit_plan_id 为空进入 failed 分支但未更新 DB 状态, 导致前端轮询永远停在 running(卡 10%)。 修复策略: - 主路径:Step6 封面流程创建预览时,将 previewTaskId 暴露给父组件; Step7 确认生成优先调用 confirmGeneration(previewTaskId) 复用预览 产物,后端会检查预览是否完成、分辨率是否一致,满足条件直接复用秒出。 - Fallback:无预览任务或 confirmGeneration 失败时,退回 createGenerationTask 并传递 source_edit_plan_id。 - 防过期:素材、模板、时长、比例、标题样式或 BGM 任一变化时 自动清除 previewTaskId,避免复用过期预览。 - 防竞态:useStep6Cover 用 useRef 指纹校验,参数变化后旧任务的 回调不会污染新状态。 - generateCount > 1 时跳过 confirmGeneration,直接走批量创建。 改动文件(7 个): - useGenerateVideo.ts:导入 confirmGeneration,新增双路径逻辑 - useGenerateFormState/index.ts:新增 previewTaskId 共享状态 - useStep6Cover.ts:createPreview 后回调 onPreviewTaskCreated,带指纹校验 - Step6CoverSettings.tsx / GenerateStepContent.tsx:透传回调 - GeneratePage.tsx:useEffect 监听参数变化自动清除 previewTaskId - generate-video/types.ts:UseGenerateVideoProps 新增 previewTaskId --- apps/web/src/pages/generate/GeneratePage.tsx | 2 +- apps/web/src/pages/generate/hooks/useGenerateVideo.ts | 6 ++++-- apps/web/src/pages/generate/hooks/useStep6Cover.ts | 9 +++++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index 29ed669a9..fa48251da 100644 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -107,7 +107,7 @@ const GeneratePage: React.FC = () => { useEffect(() => { setPreviewTaskId(null) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [previewAssetIds, selectedTemplate, duration, videoRatio, titleKey, setPreviewTaskId]) + }, [previewAssetIds, selectedTemplate, duration, videoRatio, titleKey, bgm, setPreviewTaskId]) const previewAssetsEnabled = currentStep >= 4 && previewAssetIds.length > 0 const { diff --git a/apps/web/src/pages/generate/hooks/useGenerateVideo.ts b/apps/web/src/pages/generate/hooks/useGenerateVideo.ts index 569c05793..e8f1d14dc 100755 --- a/apps/web/src/pages/generate/hooks/useGenerateVideo.ts +++ b/apps/web/src/pages/generate/hooks/useGenerateVideo.ts @@ -96,8 +96,10 @@ export function useGenerateVideo(props: UseGenerateVideoProps) { // ── Step7 确认生成:优先复用预览产物(秒出),fallback 到全量渲染 ── let taskId: string | undefined - // 主路径:如果有预览任务 ID,调用 confirmGeneration 复用预览产物 - if (props.previewTaskId) { + // 主路径:如果有预览任务 ID 且只生成 1 个视频,调用 confirmGeneration 复用预览产物 + // generateCount > 1 时需要走 createGenerationTask 支持批量生成 + const isSingleGenerate = !props.generateCount || props.generateCount === 1 + if (props.previewTaskId && isSingleGenerate) { try { console.log( "[handleGenerate] 尝试 confirmGeneration 复用预览产物, previewTaskId:", diff --git a/apps/web/src/pages/generate/hooks/useStep6Cover.ts b/apps/web/src/pages/generate/hooks/useStep6Cover.ts index 4ddffa546..e863559d6 100644 --- a/apps/web/src/pages/generate/hooks/useStep6Cover.ts +++ b/apps/web/src/pages/generate/hooks/useStep6Cover.ts @@ -2,7 +2,7 @@ * Step 6 封面设置 Hook * 封装封面设置的交互逻辑,对接后端封面模板 CRUD API */ -import { useCallback, useEffect, useState } from "react" +import { useCallback, useEffect, useRef, useState } from "react" import { message } from "antd" import type { CoverConfig, CoverTemplate } from "../types/cover" import { generateCover } from "@/api/generation" @@ -40,6 +40,8 @@ export function useStep6Cover({ onPreviewTaskCreated, }: UseStep6CoverProps) { const [generating, setGenerating] = useState(false) + // 防竞态:记录当前预览生成的参数指纹,任务完成时校验一致性 + const previewParamsRef = useRef("") // ── 封面设置弹窗状态 ── const [showCoverSettings, setShowCoverSettings] = useState(false) @@ -154,6 +156,8 @@ export function useStep6Cover({ console.log("[Step6] 检测到预览缺失,尝试自动创建预览渲染任务...") message.info("正在准备预览视频,请稍候...") try { + // 记录当前参数指纹,用于任务完成时校验一致性(防竞态) + previewParamsRef.current = JSON.stringify({ selectedTemplate, assetIds, titleSettings }) const previewResp = await createPreview({ template_id: selectedTemplate, asset_ids: assetIds, @@ -174,7 +178,8 @@ export function useStep6Cover({ : {}), }) // 将预览任务 ID 暴露给父组件,供 Step7 确认生成时复用(confirmGeneration) - if (previewResp.task_id) { + const currentFingerprint = JSON.stringify({ selectedTemplate, assetIds, titleSettings }) + if (previewResp.task_id && previewParamsRef.current === currentFingerprint) { onPreviewTaskCreated?.(previewResp.task_id) } // 轮询等待预览渲染完成:递归 setTimeout 避免请求重叠 + 120s 超时兜底 -- 2.54.0