fix: 电商视频生成链路稳定性 — AI超时/重试/断点续传 + 404页面 + DashScope Key移除

- adVideoPlanClient: 模型级联降级(qwen-max→plus→turbo), 5xx/网络错误可重试, 超时延长至180s, 错误信息包含上游响应体
- 服务端ai/chat: 超时60s→120s, AbortError返回504(非500), PM2已热重载
- EcommerceVideoWorkspace: 策划失败后支持从断点继续(保留已完成步骤的中间产物), 分镜图/视频生成仅重做失败场景
- scriptEvalClient: 移除客户端DASHSCOPE_API_KEY引用(Nginx代理注入)
- NotFoundPage: 未知路由显示404页面(替代兜底跳首页)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 12:16:33 +08:00
parent d70f7a231f
commit 56dabf1f7d
11 changed files with 373 additions and 120 deletions
+93 -40
View File
@@ -12,6 +12,7 @@ import { aiGenerationClient } from "../../api/aiGenerationClient";
import { waitForTask } from "../../api/taskSubscription";
import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
import type {
EcommerceVideoPlanProgress,
EcommerceVideoPlanResult,
EcommerceVideoSceneTask,
PlanStep,
@@ -21,66 +22,118 @@ export interface PlanCallbacks {
onStepStart: (step: PlanStep) => void;
onStepDone: (step: PlanStep) => void;
onImagesUploaded?: (urls: string[]) => void;
onPartialProgress?: (progress: EcommerceVideoPlanProgress) => void;
signal?: AbortSignal;
/** Partial state from a previous run; steps with existing data are skipped. */
resumeFrom?: EcommerceVideoPlanProgress;
}
/**
* Run the full ad video planning pipeline.
* Supports resumption: if `resumeFrom` contains data for a step, that step is skipped.
* After each step, `onPartialProgress` fires so callers can persist intermediate state.
*/
export async function runVideoPlan(
imageDataUrls: string[],
manualText: string,
config: AdVideoUserConfig,
callbacks: PlanCallbacks,
): Promise<EcommerceVideoPlanResult> {
const { onStepStart, onStepDone, signal } = callbacks;
const { onStepStart, onStepDone, signal, resumeFrom = {} } = callbacks;
const progress: EcommerceVideoPlanProgress = { ...resumeFrom };
const emit = () => callbacks.onPartialProgress?.({ ...progress });
onStepStart("upload");
const imageUrls: string[] = [];
const SUPPORTED_IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
for (const srcUrl of imageDataUrls) {
try {
const resp = await fetch(srcUrl);
const rawBlob = await resp.blob();
const mimeType = SUPPORTED_IMAGE_TYPES.has(rawBlob.type) ? rawBlob.type : "image/png";
const blob = rawBlob.type === mimeType ? rawBlob : new Blob([rawBlob], { type: mimeType });
const result = await aiGenerationClient.uploadAssetBinary(blob, { mimeType, scope: "ecommerce-product" });
imageUrls.push(result.url);
} catch {
// skip images that fail to upload
// ── Step: upload ──────────────────────────────────────
if (!progress.imageUrls?.length) {
onStepStart("upload");
const imageUrls: string[] = [];
const SUPPORTED_IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
for (const srcUrl of imageDataUrls) {
try {
const resp = await fetch(srcUrl);
const rawBlob = await resp.blob();
const mimeType = SUPPORTED_IMAGE_TYPES.has(rawBlob.type) ? rawBlob.type : "image/png";
const blob = rawBlob.type === mimeType ? rawBlob : new Blob([rawBlob], { type: mimeType });
const result = await aiGenerationClient.uploadAssetBinary(blob, { mimeType, scope: "ecommerce-product" });
imageUrls.push(result.url);
} catch {
// skip images that fail to upload
}
}
if (!imageUrls.length) throw new Error("图片上传失败,请检查图片格式或网络后重试");
progress.imageUrls = imageUrls;
onStepDone("upload");
callbacks.onImagesUploaded?.(imageUrls);
emit();
}
if (!imageUrls.length) throw new Error("图片上传失败,请检查图片格式或网络后重试");
onStepDone("upload");
callbacks.onImagesUploaded?.(imageUrls);
onStepStart("analyze");
const imageDesc = await analyzeProductImages(imageUrls, signal);
onStepDone("analyze");
// ── Step: analyze ─────────────────────────────────────
if (progress.imageDescription === undefined) {
onStepStart("analyze");
progress.imageDescription = await analyzeProductImages(progress.imageUrls!, signal);
onStepDone("analyze");
emit();
}
onStepStart("summary");
const summary = await buildProductSummary(imageDesc, manualText, signal);
onStepDone("summary");
// ── Step: summary ─────────────────────────────────────
if (!progress.summary) {
onStepStart("summary");
progress.summary = await buildProductSummary(progress.imageDescription || "", manualText, signal);
onStepDone("summary");
emit();
}
onStepStart("selling");
const selling = await extractSellingPoints(summary, signal);
onStepDone("selling");
// ── Step: selling ─────────────────────────────────────
if (!progress.selling) {
onStepStart("selling");
progress.selling = await extractSellingPoints(progress.summary, signal);
onStepDone("selling");
emit();
}
onStepStart("creative");
const creatives = await generateCreativeOptions(selling, config, signal);
if (!creatives.length) throw new Error("未能生成有效的广告创意");
onStepDone("creative");
// ── Step: creative ────────────────────────────────────
if (!progress.creatives?.length) {
onStepStart("creative");
progress.creatives = await generateCreativeOptions(progress.selling, config, signal);
if (!progress.creatives.length) throw new Error("未能生成有效的广告创意");
onStepDone("creative");
emit();
}
onStepStart("storyboard");
const storyboard = await generateStoryboard(creatives[0], summary, config, signal);
onStepDone("storyboard");
// ── Step: storyboard ──────────────────────────────────
if (!progress.storyboard) {
onStepStart("storyboard");
progress.storyboard = await generateStoryboard(progress.creatives[0], progress.summary, config, signal);
onStepDone("storyboard");
emit();
}
onStepStart("prompts");
const videoPrompts = await generateVideoPrompts(storyboard, summary, signal);
onStepDone("prompts");
// ── Step: prompts ─────────────────────────────────────
if (!progress.videoPrompts) {
onStepStart("prompts");
progress.videoPrompts = await generateVideoPrompts(progress.storyboard, progress.summary, signal);
onStepDone("prompts");
emit();
}
onStepStart("compliance");
const compliance = await checkCompliance(summary, selling, storyboard, signal);
onStepDone("compliance");
// ── Step: compliance ──────────────────────────────────
if (!progress.compliance) {
onStepStart("compliance");
progress.compliance = await checkCompliance(progress.summary, progress.selling, progress.storyboard, signal);
onStepDone("compliance");
emit();
}
return { imageUrls, summary, selling, creatives, storyboard, videoPrompts, compliance };
return {
imageUrls: progress.imageUrls!,
imageDescription: progress.imageDescription,
summary: progress.summary!,
selling: progress.selling!,
creatives: progress.creatives!,
storyboard: progress.storyboard!,
videoPrompts: progress.videoPrompts!,
compliance: progress.compliance!,
};
}
export interface RenderSceneImageInput {