From f64a4b42135efe4718bb46e091ec456abb952acd Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 12 Aug 2026 22:07:19 +0800 Subject: [PATCH 1/5] =?UTF-8?q?fix(#1341):=20=E5=A2=9E=E5=BC=BA=E5=B0=81?= =?UTF-8?q?=E9=9D=A2=E7=94=9F=E6=88=90=E9=94=99=E8=AF=AF=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E5=92=8C=E5=89=8D=E7=AB=AF=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端 cover.py: 在 3 步查找预览视频的每个步骤增加详细日志 - 后端 cover.py: 在调用 MediaKit 前后增加日志 - 前端 useStep6Cover.ts: catch 块中解析后端返回的详细错误信息 - 前端: 区分后端错误、网络错误、超时错误,显示具体原因 目的:让封面生成失败时能看到具体卡在哪一步,便于定位问题 --- .../app/api/routes/templates_editor/cover.py | 12 +- .../api/routes/templates_editor/cover.py.bak | 168 ++++++++++++++++++ .../src/pages/generate/hooks/useStep6Cover.ts | 20 ++- 3 files changed, 197 insertions(+), 3 deletions(-) create mode 100755 apps/api/app/api/routes/templates_editor/cover.py.bak diff --git a/apps/api/app/api/routes/templates_editor/cover.py b/apps/api/app/api/routes/templates_editor/cover.py index c05467224..a112942d7 100755 --- a/apps/api/app/api/routes/templates_editor/cover.py +++ b/apps/api/app/api/routes/templates_editor/cover.py @@ -52,11 +52,13 @@ def editor_generate_cover( # ── 3 步查找预览视频 URL ────────────────────────────────────────── # 第一步:从 plan.config 读取 + logger.info("[封面生成] 步骤1: 从 plan.config 查找 rendered_storage_key: plan_id=%s", plan_id) rendered_storage_key = (plan.config or {}).get("rendered_storage_key", "") # 第二步:如果还没有,通过 generation_task_id 查找预览任务的产物 if not rendered_storage_key: generation_task_id = (plan.config or {}).get("generation_task_id", "") + logger.info("[封面生成] 步骤2: 通过 generation_task_id 查找: plan_id=%s task_id=%s", plan_id, generation_task_id) if generation_task_id: try: gen_task_repo = SQLAlchemyGenerationTaskRepository(db) @@ -68,7 +70,12 @@ def editor_generate_cover( if videos: rendered_storage_key = getattr(videos[0], "file_url", "") or "" logger.info( - "封面生成: 通过 generation_task_id 找到视频: plan_id=%s task_id=%s", + "[封面生成] ✅ 步骤2找到视频: plan_id=%s task_id=%s url=%s", + plan_id, + generation_task_id, + rendered_storage_key[:80], + ) + logger.info( plan_id, generation_task_id, ) @@ -83,6 +90,7 @@ def editor_generate_cover( if not rendered_storage_key: try: gen_task_repo = SQLAlchemyGenerationTaskRepository(db) + logger.info("[封面生成] 步骤3: 通过 user+template 查找: plan_id=%s template_id=%s", plan_id, template_id) preview_tasks = gen_task_repo.list_latest_completed_preview( user_id=str(current_user.user.id), template_id=template_id, @@ -110,6 +118,7 @@ def editor_generate_cover( # 仍然找不到才报 400 if not rendered_storage_key: + logger.error("[封面生成] ❌ 找不到预览视频: plan_id=%s", plan_id) raise HTTPException( status_code=400, detail="请先生成预览视频,再生成封面", @@ -142,6 +151,7 @@ def editor_generate_cover( from packages.shared.ai_service import run_generate_cover try: + logger.info("[封面生成] 开始调用 AI 封面生成服务: plan_id=%s", plan_id) cover_data = run_generate_cover( plan_id=plan_id, asset_ids=body.asset_ids, diff --git a/apps/api/app/api/routes/templates_editor/cover.py.bak b/apps/api/app/api/routes/templates_editor/cover.py.bak new file mode 100755 index 000000000..c05467224 --- /dev/null +++ b/apps/api/app/api/routes/templates_editor/cover.py.bak @@ -0,0 +1,168 @@ +"""封面管理路由. + +端点: +- POST /generate-cover AI 生成封面(从预览视频中抽帧) +""" + +from __future__ import annotations + +import logging + +from app.auth import AuthenticatedUser, get_current_user +from app.dependencies import get_db_session, get_generated_video_repository +from app.services.edit_plan_service import EditPlanService +from app.services.edit_template_service import EditTemplateService +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session + +from packages.adapters.sqlalchemy_impl.generation_task_repository import ( + SQLAlchemyGenerationTaskRepository, +) +from packages.application import ListGeneratedVideosByTaskUseCase +from packages.domain.config_schemas import normalize_plan_config + +from .dependencies import get_draft_plan_id, get_editor_services +from .schemas import ( + GenerateCoverRequest, + GenerateCoverResponse, +) + +logger = logging.getLogger(__name__) +router = APIRouter(tags=["Template Editor"]) + + +@router.post("/generate-cover", response_model=GenerateCoverResponse) +def editor_generate_cover( + template_id: str, + body: GenerateCoverRequest, + plan_id: str = Depends(get_draft_plan_id), + services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services), + db: Session = Depends(get_db_session), + current_user: AuthenticatedUser = Depends(get_current_user), +) -> GenerateCoverResponse: + """AI 生成封面 — 从预览视频中抽帧. + + 流程(串行): + 1. 预览视频已渲染完成(通过 3 步查找获取 URL) + 2. 用裸 URL 让 MediaKit 下载视频并抽帧 + 3. 帧图下载后上传到 OSS covers/ 路径 + """ + _, plan_svc = services + plan = plan_svc.get_plan_or_raise(plan_id) + + # ── 3 步查找预览视频 URL ────────────────────────────────────────── + # 第一步:从 plan.config 读取 + rendered_storage_key = (plan.config or {}).get("rendered_storage_key", "") + + # 第二步:如果还没有,通过 generation_task_id 查找预览任务的产物 + if not rendered_storage_key: + generation_task_id = (plan.config or {}).get("generation_task_id", "") + if generation_task_id: + try: + gen_task_repo = SQLAlchemyGenerationTaskRepository(db) + task = gen_task_repo.get(generation_task_id) + if task: + video_repo = get_generated_video_repository(db) + use_case = ListGeneratedVideosByTaskUseCase(video_repo) + videos = use_case.execute(task.id) + if videos: + rendered_storage_key = getattr(videos[0], "file_url", "") or "" + logger.info( + "封面生成: 通过 generation_task_id 找到视频: plan_id=%s task_id=%s", + plan_id, + generation_task_id, + ) + except Exception: + logger.warning( + "封面生成: 通过 generation_task_id 查找视频失败: plan_id=%s", + plan_id, + exc_info=True, + ) + + # 第三步:按 user + template 查找最近的已完成预览任务(兜底) + if not rendered_storage_key: + try: + gen_task_repo = SQLAlchemyGenerationTaskRepository(db) + preview_tasks = gen_task_repo.list_latest_completed_preview( + user_id=str(current_user.user.id), + template_id=template_id, + ) + if preview_tasks: + completed_preview = preview_tasks[0] + video_repo = get_generated_video_repository(db) + use_case = ListGeneratedVideosByTaskUseCase(video_repo) + videos = use_case.execute(completed_preview.id) + if videos: + rendered_storage_key = getattr(videos[0], "file_url", "") or "" + logger.info( + "封面视频: 通过 user+template 找到预览任务: plan_id=%s template_id=%s task_id=%s", + plan_id, + template_id, + completed_preview.id, + ) + except Exception: + logger.warning( + "封面警告: user+template 查找预览任务失败: plan_id=%s template_id=%s", + plan_id, + template_id, + exc_info=True, + ) + + # 仍然找不到才报 400 + if not rendered_storage_key: + raise HTTPException( + status_code=400, + detail="请先生成预览视频,再生成封面", + ) + + # 回写到 plan.config + plan_svc.update_plan_config(plan_id, {"rendered_storage_key": rendered_storage_key}) + + # 使用裸 URL(rendered/* 已配置公开读) + primary_video_url = None + try: + if rendered_storage_key.startswith("http"): + primary_video_url = rendered_storage_key + else: + from packages.shared.storage import get_shared_storage_service + + storage_svc = get_shared_storage_service() + primary_video_url = storage_svc.get_url(rendered_storage_key) + logger.info( + "获取预览视频URL用于封面生成: plan_id=%s url=%s", + plan_id, + primary_video_url[:80] if primary_video_url else "", + ) + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"获取预览视频URL失败: {e}", + ) from e + + from packages.shared.ai_service import run_generate_cover + + try: + cover_data = run_generate_cover( + plan_id=plan_id, + asset_ids=body.asset_ids, + cover_type=body.cover_type, + frame_time=body.frame_time, + primary_video_url=primary_video_url, + ) + except RuntimeError as e: + raise HTTPException(status_code=500, detail=str(e)) from e + + current_config = dict(plan.config) if plan.config else {} + current_config["cover"] = cover_data + normalized = normalize_plan_config(current_config) + plan_svc.update_plan_config(plan_id, {"cover": normalized["cover"]}) + + logger.info( + "封面生成完成: template_id=%s plan_id=%s type=%s by user=%s", + template_id, + plan_id, + body.cover_type, + current_user.user.id, + ) + + return GenerateCoverResponse(plan_id=plan_id, cover=cover_data) diff --git a/apps/web/src/pages/generate/hooks/useStep6Cover.ts b/apps/web/src/pages/generate/hooks/useStep6Cover.ts index 8e1d1a262..d530f8409 100644 --- a/apps/web/src/pages/generate/hooks/useStep6Cover.ts +++ b/apps/web/src/pages/generate/hooks/useStep6Cover.ts @@ -106,8 +106,24 @@ export function useStep6Cover({ } } catch (err) { clearTimeout(timeoutId) - console.warn("[Step6] 智能封面生成失败:", err) - message.error("封面生成失败,请稍后重试") + console.error("[Step6] 智能封面生成失败:", err) + + // 提取详细错误信息 + let errorMsg = "封面生成失败" + if (err.response) { + // 后端返回错误 + const detail = err.response.data?.detail || err.response.data?.message || "" + errorMsg = detail || `后端错误 (${err.response.status})` + console.error("[Step6] 后端返回:", err.response.data) + } else if (err.request) { + // 请求已发送但无响应 + errorMsg = "服务器无响应,请检查网络连接" + console.error("[Step6] 请求无响应:", err.request) + } else if (err.message) { + errorMsg = err.message + } + + message.error(errorMsg) } finally { clearTimeout(timeoutId) setGenerating(false) -- 2.54.0 From b81d61b614177af908d3db7356ede84d1dfa3be5 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Wed, 12 Aug 2026 22:12:32 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix(#1341):=20=E4=BF=AE=E5=A4=8D=20ESLint?= =?UTF-8?q?=20TS=20=E7=B1=BB=E5=9E=8B=E9=94=99=E8=AF=AF=EF=BC=8C=E5=88=A0?= =?UTF-8?q?=E9=99=A4=20cover.py.bak?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useStep6Cover.ts: catch(err) 用 as 做类型断言,避免 strict 模式下 unknown 属性访问 - 删除误提交的 cover.py.bak 文件 --- .../api/routes/templates_editor/cover.py.bak | 168 ------------------ .../src/pages/generate/hooks/useStep6Cover.ts | 21 +-- 2 files changed, 11 insertions(+), 178 deletions(-) delete mode 100755 apps/api/app/api/routes/templates_editor/cover.py.bak diff --git a/apps/api/app/api/routes/templates_editor/cover.py.bak b/apps/api/app/api/routes/templates_editor/cover.py.bak deleted file mode 100755 index c05467224..000000000 --- a/apps/api/app/api/routes/templates_editor/cover.py.bak +++ /dev/null @@ -1,168 +0,0 @@ -"""封面管理路由. - -端点: -- POST /generate-cover AI 生成封面(从预览视频中抽帧) -""" - -from __future__ import annotations - -import logging - -from app.auth import AuthenticatedUser, get_current_user -from app.dependencies import get_db_session, get_generated_video_repository -from app.services.edit_plan_service import EditPlanService -from app.services.edit_template_service import EditTemplateService -from fastapi import APIRouter, Depends, HTTPException -from sqlalchemy.orm import Session - -from packages.adapters.sqlalchemy_impl.generation_task_repository import ( - SQLAlchemyGenerationTaskRepository, -) -from packages.application import ListGeneratedVideosByTaskUseCase -from packages.domain.config_schemas import normalize_plan_config - -from .dependencies import get_draft_plan_id, get_editor_services -from .schemas import ( - GenerateCoverRequest, - GenerateCoverResponse, -) - -logger = logging.getLogger(__name__) -router = APIRouter(tags=["Template Editor"]) - - -@router.post("/generate-cover", response_model=GenerateCoverResponse) -def editor_generate_cover( - template_id: str, - body: GenerateCoverRequest, - plan_id: str = Depends(get_draft_plan_id), - services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services), - db: Session = Depends(get_db_session), - current_user: AuthenticatedUser = Depends(get_current_user), -) -> GenerateCoverResponse: - """AI 生成封面 — 从预览视频中抽帧. - - 流程(串行): - 1. 预览视频已渲染完成(通过 3 步查找获取 URL) - 2. 用裸 URL 让 MediaKit 下载视频并抽帧 - 3. 帧图下载后上传到 OSS covers/ 路径 - """ - _, plan_svc = services - plan = plan_svc.get_plan_or_raise(plan_id) - - # ── 3 步查找预览视频 URL ────────────────────────────────────────── - # 第一步:从 plan.config 读取 - rendered_storage_key = (plan.config or {}).get("rendered_storage_key", "") - - # 第二步:如果还没有,通过 generation_task_id 查找预览任务的产物 - if not rendered_storage_key: - generation_task_id = (plan.config or {}).get("generation_task_id", "") - if generation_task_id: - try: - gen_task_repo = SQLAlchemyGenerationTaskRepository(db) - task = gen_task_repo.get(generation_task_id) - if task: - video_repo = get_generated_video_repository(db) - use_case = ListGeneratedVideosByTaskUseCase(video_repo) - videos = use_case.execute(task.id) - if videos: - rendered_storage_key = getattr(videos[0], "file_url", "") or "" - logger.info( - "封面生成: 通过 generation_task_id 找到视频: plan_id=%s task_id=%s", - plan_id, - generation_task_id, - ) - except Exception: - logger.warning( - "封面生成: 通过 generation_task_id 查找视频失败: plan_id=%s", - plan_id, - exc_info=True, - ) - - # 第三步:按 user + template 查找最近的已完成预览任务(兜底) - if not rendered_storage_key: - try: - gen_task_repo = SQLAlchemyGenerationTaskRepository(db) - preview_tasks = gen_task_repo.list_latest_completed_preview( - user_id=str(current_user.user.id), - template_id=template_id, - ) - if preview_tasks: - completed_preview = preview_tasks[0] - video_repo = get_generated_video_repository(db) - use_case = ListGeneratedVideosByTaskUseCase(video_repo) - videos = use_case.execute(completed_preview.id) - if videos: - rendered_storage_key = getattr(videos[0], "file_url", "") or "" - logger.info( - "封面视频: 通过 user+template 找到预览任务: plan_id=%s template_id=%s task_id=%s", - plan_id, - template_id, - completed_preview.id, - ) - except Exception: - logger.warning( - "封面警告: user+template 查找预览任务失败: plan_id=%s template_id=%s", - plan_id, - template_id, - exc_info=True, - ) - - # 仍然找不到才报 400 - if not rendered_storage_key: - raise HTTPException( - status_code=400, - detail="请先生成预览视频,再生成封面", - ) - - # 回写到 plan.config - plan_svc.update_plan_config(plan_id, {"rendered_storage_key": rendered_storage_key}) - - # 使用裸 URL(rendered/* 已配置公开读) - primary_video_url = None - try: - if rendered_storage_key.startswith("http"): - primary_video_url = rendered_storage_key - else: - from packages.shared.storage import get_shared_storage_service - - storage_svc = get_shared_storage_service() - primary_video_url = storage_svc.get_url(rendered_storage_key) - logger.info( - "获取预览视频URL用于封面生成: plan_id=%s url=%s", - plan_id, - primary_video_url[:80] if primary_video_url else "", - ) - except Exception as e: - raise HTTPException( - status_code=500, - detail=f"获取预览视频URL失败: {e}", - ) from e - - from packages.shared.ai_service import run_generate_cover - - try: - cover_data = run_generate_cover( - plan_id=plan_id, - asset_ids=body.asset_ids, - cover_type=body.cover_type, - frame_time=body.frame_time, - primary_video_url=primary_video_url, - ) - except RuntimeError as e: - raise HTTPException(status_code=500, detail=str(e)) from e - - current_config = dict(plan.config) if plan.config else {} - current_config["cover"] = cover_data - normalized = normalize_plan_config(current_config) - plan_svc.update_plan_config(plan_id, {"cover": normalized["cover"]}) - - logger.info( - "封面生成完成: template_id=%s plan_id=%s type=%s by user=%s", - template_id, - plan_id, - body.cover_type, - current_user.user.id, - ) - - return GenerateCoverResponse(plan_id=plan_id, cover=cover_data) diff --git a/apps/web/src/pages/generate/hooks/useStep6Cover.ts b/apps/web/src/pages/generate/hooks/useStep6Cover.ts index d530f8409..879103213 100644 --- a/apps/web/src/pages/generate/hooks/useStep6Cover.ts +++ b/apps/web/src/pages/generate/hooks/useStep6Cover.ts @@ -107,22 +107,23 @@ export function useStep6Cover({ } catch (err) { clearTimeout(timeoutId) console.error("[Step6] 智能封面生成失败:", err) - + // 提取详细错误信息 let errorMsg = "封面生成失败" - if (err.response) { + const e = err as { response?: { data?: { detail?: string; message?: string }; status?: number }; request?: unknown; message?: string } + if (e.response) { // 后端返回错误 - const detail = err.response.data?.detail || err.response.data?.message || "" - errorMsg = detail || `后端错误 (${err.response.status})` - console.error("[Step6] 后端返回:", err.response.data) - } else if (err.request) { + const detail = e.response.data?.detail || e.response.data?.message || "" + errorMsg = detail || `后端错误 (${e.response.status})` + console.error("[Step6] 后端返回:", e.response.data) + } else if (e.request) { // 请求已发送但无响应 errorMsg = "服务器无响应,请检查网络连接" - console.error("[Step6] 请求无响应:", err.request) - } else if (err.message) { - errorMsg = err.message + console.error("[Step6] 请求无响应:", e.request) + } else if (e.message) { + errorMsg = e.message } - + message.error(errorMsg) } finally { clearTimeout(timeoutId) -- 2.54.0 From 7c1235c0181f514047929396aecef94977319107 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 12 Aug 2026 22:13:52 +0800 Subject: [PATCH 3/5] =?UTF-8?q?perf(#1341):=20=E4=BC=98=E5=8C=96=E5=B0=81?= =?UTF-8?q?=E9=9D=A2=E7=94=9F=E6=88=90=E6=80=A7=E8=83=BD=EF=BC=8C=E6=94=B9?= =?UTF-8?q?=E7=94=A8=20SceneChange=20=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 抽帧策略: TimeInterval → SceneChange(自动识别画面变化,选最佳帧) - 抽帧数量: 5 → 3(平衡速度和质量) - 轮询间隔: 3.0s → 2.0s(加快轮询) 预期效果: - 封面质量更高(SceneChange 自动选最佳画面) - 生成速度更快(3帧 vs 5帧) - 减少超时风险 配合 PR #1347 的错误日志增强,便于定位问题 --- packages/shared/ai_service.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/shared/ai_service.py b/packages/shared/ai_service.py index eb55d92fa..637053eb7 100755 --- a/packages/shared/ai_service.py +++ b/packages/shared/ai_service.py @@ -402,10 +402,10 @@ def _call_ai_cover_service( logger.info("调用 MediaKit 抽帧: plan_id=%s video=%s", plan_id, primary_video_url[:80]) frames = client.extract_frames( video_url=primary_video_url, - strategy="TimeInterval", - max_frames=5, - poll_interval=3.0, - max_poll_attempts=60, # 180秒超时 + strategy="SceneChange", # 自动识别画面变化,选最佳帧 + max_frames=3, # 减少到 3 帧,平衡速度和质量 + poll_interval=2.0, # 缩短轮询间隔 + max_poll_attempts=60, # 120秒超时 ) if frames and len(frames) > 0: -- 2.54.0 From cada58e9ea6f783297a27deb561e1052e0882233 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Wed, 12 Aug 2026 22:21:47 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(#1341):=20=E4=BF=AE=E5=A4=8D3=E4=B8=AAE?= =?UTF-8?q?SLint=20warning=EF=BC=8C=E7=A1=AE=E4=BF=9DCI=20Frontend=20Lint?= =?UTF-8?q?=E9=80=9A=E8=BF=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useGenerateVideo.ts: 移除useCallback多余的selectedTemplate依赖 - useStep4Preview.ts: 删除不必要的as any类型断言(EditPlanConfig已有rendered_storage_key字段) - useStep4Preview.ts: useCallback deps补充selectedTemplate --- apps/web/src/pages/generate/hooks/useGenerateVideo.ts | 2 +- apps/web/src/pages/generate/hooks/useStep4Preview.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/useGenerateVideo.ts b/apps/web/src/pages/generate/hooks/useGenerateVideo.ts index 42b3f06ee..b3a15fd26 100755 --- a/apps/web/src/pages/generate/hooks/useGenerateVideo.ts +++ b/apps/web/src/pages/generate/hooks/useGenerateVideo.ts @@ -78,7 +78,7 @@ export function useGenerateVideo(props: UseGenerateVideoProps) { setGenerateError(finalMsg) message.error(finalMsg) } - }, [props, selectedTemplate, clearTimer, startPolling]) + }, [props, clearTimer, startPolling]) /* 重新生成(失败后重试) */ const retry = useCallback(() => { diff --git a/apps/web/src/pages/generate/hooks/useStep4Preview.ts b/apps/web/src/pages/generate/hooks/useStep4Preview.ts index 59cbfd180..fea5ac525 100644 --- a/apps/web/src/pages/generate/hooks/useStep4Preview.ts +++ b/apps/web/src/pages/generate/hooks/useStep4Preview.ts @@ -255,7 +255,7 @@ export function useStep4Preview({ // 保存预览视频 URL 到 plan config,供封面生成使用 if (result.videoUrl && selectedTemplate) { updateEditPlan(selectedTemplate, { - config: { rendered_storage_key: result.videoUrl } as any, + config: { rendered_storage_key: result.videoUrl }, }).catch((err) => { console.warn("[Step4] 保存预览视频URL到plan config失败:", err) }) @@ -304,7 +304,7 @@ export function useStep4Preview({ } pollTimersRef.current.set(index, setTimeout(poll, 1000)) - }, []) + }, [selectedTemplate]) /** 生成所有预览 */ const generatePreview = useCallback(async () => { -- 2.54.0 From efab5b45cd38c6b4cad9fb34c6e542445febd796 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 12 Aug 2026 22:26:19 +0800 Subject: [PATCH 5/5] =?UTF-8?q?style:=20prettier=20=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=8C=96=20useStep4Preview=20+=20useStep6Cover?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/generate/hooks/useStep4Preview.ts | 161 +++++++++--------- .../src/pages/generate/hooks/useStep6Cover.ts | 6 +- 2 files changed, 87 insertions(+), 80 deletions(-) diff --git a/apps/web/src/pages/generate/hooks/useStep4Preview.ts b/apps/web/src/pages/generate/hooks/useStep4Preview.ts index fea5ac525..cbbe8342a 100644 --- a/apps/web/src/pages/generate/hooks/useStep4Preview.ts +++ b/apps/web/src/pages/generate/hooks/useStep4Preview.ts @@ -212,99 +212,102 @@ export function useStep4Preview({ }, [clearPollTimer]) /** 轮询单个预览任务状态 */ - const pollPreviewStatus = useCallback((index: number, taskId: string) => { - const poll = async () => { - // 竞态检查 - if (taskIdsRef.current.get(index) !== taskId) return - - // 超时检查 - if (Date.now() - startTimeRef.current > POLL_TIMEOUT_MS) { - setItems((prev) => - prev.map((it) => - it.index === index ? { ...it, status: "error", error: "预览生成超时,请重试" } : it, - ), - ) - return - } - - try { - const data: PreviewTaskResponse = await getPreviewStatus(taskId) - + const pollPreviewStatus = useCallback( + (index: number, taskId: string) => { + const poll = async () => { + // 竞态检查 if (taskIdsRef.current.get(index) !== taskId) return - const status = data.status as ApiPreviewStatus + // 超时检查 + if (Date.now() - startTimeRef.current > POLL_TIMEOUT_MS) { + setItems((prev) => + prev.map((it) => + it.index === index ? { ...it, status: "error", error: "预览生成超时,请重试" } : it, + ), + ) + return + } - if (status === "completed") { - const result: PreviewResult = { - taskId: safeString(data.task_id, ""), - videoUrl: safeString(data.video_url, ""), - clipCount: safeNumber(data.clip_count), - transitionCount: safeNumber(data.transition_count), - materialUsage: safeNumber(data.material_usage), - duration: safeNumber(data.duration), - fileSize: safeNumber(data.file_size), - generateDuration: safeNumber(data.generate_duration), - progress: 100, + try { + const data: PreviewTaskResponse = await getPreviewStatus(taskId) + + if (taskIdsRef.current.get(index) !== taskId) return + + const status = data.status as ApiPreviewStatus + + if (status === "completed") { + const result: PreviewResult = { + taskId: safeString(data.task_id, ""), + videoUrl: safeString(data.video_url, ""), + clipCount: safeNumber(data.clip_count), + transitionCount: safeNumber(data.transition_count), + materialUsage: safeNumber(data.material_usage), + duration: safeNumber(data.duration), + fileSize: safeNumber(data.file_size), + generateDuration: safeNumber(data.generate_duration), + progress: 100, + } + setItems((prev) => + prev.map((it) => + it.index === index ? { ...it, status: "ready", result, progress: 100 } : it, + ), + ) + + // 保存预览视频 URL 到 plan config,供封面生成使用 + if (result.videoUrl && selectedTemplate) { + updateEditPlan(selectedTemplate, { + config: { rendered_storage_key: result.videoUrl }, + }).catch((err) => { + console.warn("[Step4] 保存预览视频URL到plan config失败:", err) + }) + } + return } - setItems((prev) => - prev.map((it) => - it.index === index ? { ...it, status: "ready", result, progress: 100 } : it, - ), - ) - // 保存预览视频 URL 到 plan config,供封面生成使用 - if (result.videoUrl && selectedTemplate) { - updateEditPlan(selectedTemplate, { - config: { rendered_storage_key: result.videoUrl }, - }).catch((err) => { - console.warn("[Step4] 保存预览视频URL到plan config失败:", err) - }) + if (status === "failed") { + setItems((prev) => + prev.map((it) => + it.index === index + ? { + ...it, + status: "error", + error: safeString(data.error_message, "预览生成失败,请重试"), + } + : it, + ), + ) + return } - return - } - if (status === "failed") { + if (status === "cancelled") { + setItems((prev) => + prev.map((it) => + it.index === index ? { ...it, status: "error", error: "预览任务已取消" } : it, + ), + ) + return + } + + // pending / generating 状态继续轮询 + const prog = safeNumber(data.progress) + const nextStatus: PreviewStatus = status === "pending" ? "pending" : "generating" setItems((prev) => prev.map((it) => - it.index === index - ? { - ...it, - status: "error", - error: safeString(data.error_message, "预览生成失败,请重试"), - } - : it, + it.index === index ? { ...it, status: nextStatus, progress: prog } : it, ), ) - return + const delay = status === "pending" ? 5000 : 2000 + pollTimersRef.current.set(index, setTimeout(poll, delay)) + } catch { + if (taskIdsRef.current.get(index) !== taskId) return + pollTimersRef.current.set(index, setTimeout(poll, 3000)) } - - if (status === "cancelled") { - setItems((prev) => - prev.map((it) => - it.index === index ? { ...it, status: "error", error: "预览任务已取消" } : it, - ), - ) - return - } - - // pending / generating 状态继续轮询 - const prog = safeNumber(data.progress) - const nextStatus: PreviewStatus = status === "pending" ? "pending" : "generating" - setItems((prev) => - prev.map((it) => - it.index === index ? { ...it, status: nextStatus, progress: prog } : it, - ), - ) - const delay = status === "pending" ? 5000 : 2000 - pollTimersRef.current.set(index, setTimeout(poll, delay)) - } catch { - if (taskIdsRef.current.get(index) !== taskId) return - pollTimersRef.current.set(index, setTimeout(poll, 3000)) } - } - pollTimersRef.current.set(index, setTimeout(poll, 1000)) - }, [selectedTemplate]) + pollTimersRef.current.set(index, setTimeout(poll, 1000)) + }, + [selectedTemplate], + ) /** 生成所有预览 */ const generatePreview = useCallback(async () => { diff --git a/apps/web/src/pages/generate/hooks/useStep6Cover.ts b/apps/web/src/pages/generate/hooks/useStep6Cover.ts index 879103213..e76c5146a 100644 --- a/apps/web/src/pages/generate/hooks/useStep6Cover.ts +++ b/apps/web/src/pages/generate/hooks/useStep6Cover.ts @@ -110,7 +110,11 @@ export function useStep6Cover({ // 提取详细错误信息 let errorMsg = "封面生成失败" - const e = err as { response?: { data?: { detail?: string; message?: string }; status?: number }; request?: unknown; message?: string } + const e = err as { + response?: { data?: { detail?: string; message?: string }; status?: number } + request?: unknown + message?: string + } if (e.response) { // 后端返回错误 const detail = e.response.data?.detail || e.response.data?.message || "" -- 2.54.0