import { NextResponse } from "next/server"; import { z } from "zod"; import { getAiModelById } from "@/lib/db/queries"; import { getLanguageModelById } from "@/lib/ai/dynamic-provider"; import { ChatSDKError } from "@/lib/errors"; import { requireAdmin } from "@/lib/middleware/admin"; import { getVideoStoryboardConfig } from "@/lib/video-agent/config"; import { finalizeVideoStoryboardTask, getVideoStoryboardTaskDetail, markVideoStoryboardShotCompleted, markVideoStoryboardShotFailed, markVideoStoryboardShotRetry, markVideoStoryboardShotRunning, syncVideoStoryboardTaskStatus, } from "@/lib/video-agent/task-store"; const paramsSchema = z.object({ taskId: z.string().uuid(), }); const bodySchema = z.object({ shotIndex: z.number().int().min(1).max(99).optional(), retryFailedOnly: z.boolean().optional().default(true), }); const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); function extractVideoUrlFromText(text: string): string | null { const source = String(text || ""); if (!source) return null; const htmlMatch = source.match(/]+src="([^"]+)"/i); if (htmlMatch?.[1]) return htmlMatch[1]; const markdownMatch = source.match( /\[[^\]]*]\((https?:\/\/[^)\s]+\.mp4(?:\?[^)\s]*)?)\)/i ); if (markdownMatch?.[1]) return markdownMatch[1]; const plainMatch = source.match(/(https?:\/\/[^\s"'<>]+\.mp4(?:\?[^\s"'<>]+)?)/i); return plainMatch?.[1] || null; } function normalizeFallbackChain(value: unknown): string[] { if (!Array.isArray(value)) return []; const chain = value .map((item) => String(item || "").trim()) .filter(Boolean); return Array.from(new Set(chain)); } export async function POST( request: Request, { params }: { params: Promise<{ taskId: string }> } ) { try { await requireAdmin(); const parsedParams = paramsSchema.parse(await params); const parsedBody = bodySchema.parse(await request.json().catch(() => ({}))); const detail = await getVideoStoryboardTaskDetail(parsedParams.taskId); if (!detail) { return NextResponse.json( { success: false, error: "任务不存在" }, { status: 404 } ); } if (detail.task.providerType !== "ruxa-video") { return NextResponse.json( { success: false, error: "当前仅支持重跑 ruxa-video 任务" }, { status: 400 } ); } const modelRef = String(detail.task.modelRef || "").trim(); if (!modelRef) { return NextResponse.json( { success: false, error: "任务缺少 modelRef,无法重跑" }, { status: 400 } ); } const selectedModel = await getAiModelById({ id: modelRef }).catch(() => undefined); const selectedModelId = String(selectedModel?.modelId || "").trim(); const languageModel = await getLanguageModelById(modelRef); if (!languageModel || typeof (languageModel as any).doGenerate !== "function") { return NextResponse.json( { success: false, error: "模型不可用或不支持重跑 doGenerate" }, { status: 400 } ); } const config = await getVideoStoryboardConfig().catch(() => null); const dispatchIntervalSec = Math.max(1, Number(config?.dispatchIntervalSec || 10)); const maxRetry = Math.max(0, Number(config?.maxRetry || 3)); const enableFallback = config?.enableFallback !== false; const targetShots = detail.shots.filter((shot: any) => { if (parsedBody.shotIndex) { return Number(shot.shotIndex) === parsedBody.shotIndex; } if (parsedBody.retryFailedOnly) { return String(shot.status || "") === "failed"; } return true; }); if (targetShots.length === 0) { return NextResponse.json( { success: false, error: "没有可重跑的分镜" }, { status: 400 } ); } await finalizeVideoStoryboardTask({ taskId: detail.task.id, status: "running", }); for (let index = 0; index < targetShots.length; index += 1) { const shot = targetShots[index]!; const shotIndex = Number(shot.shotIndex || 0); const prompt = String(shot.prompt || "").trim() || "让画面动起来"; const tileImageUrl = String(shot.upscaledImageUrl || shot.tileImageUrl || "").trim(); if (!shotIndex || !tileImageUrl) { await markVideoStoryboardShotFailed({ taskId: detail.task.id, shotIndex, errorMessage: "缺少分镜素材,无法重跑", }); continue; } const fallbackChain = normalizeFallbackChain(shot.fallbackChain); const candidateModelIds = Array.from( new Set( [ String(shot.modelId || "").trim(), ...fallbackChain, selectedModelId, ].filter(Boolean) ) ); const modelPool = enableFallback && candidateModelIds.length > 0 ? candidateModelIds : candidateModelIds.slice(0, 1); let succeeded = false; let lastError = ""; for (const candidateModelId of modelPool) { for (let attempt = 1; attempt <= maxRetry + 1; attempt += 1) { try { await markVideoStoryboardShotRunning({ taskId: detail.task.id, shotIndex, modelId: candidateModelId, }); const result = await (languageModel as any).doGenerate({ messages: [ { role: "user", content: [ { type: "text", text: prompt }, { type: "image_url", image_url: { url: tileImageUrl } }, ], }, ], videoParams: { mode: "first-frame" }, overrideModelId: candidateModelId, }); const text = String(result?.text || "").trim(); const videoUrl = extractVideoUrlFromText(text); if (!videoUrl) { throw new Error(text || "未返回视频地址"); } await markVideoStoryboardShotCompleted({ taskId: detail.task.id, shotIndex, modelId: candidateModelId, videoUrl, }); succeeded = true; break; } catch (error) { lastError = error instanceof Error ? error.message : String(error); await markVideoStoryboardShotRetry({ taskId: detail.task.id, shotIndex, errorMessage: lastError, }); if (attempt <= maxRetry) { const backoffSec = Math.min(40, Math.pow(2, attempt - 1) * 10); await sleep(backoffSec * 1000); } } } if (succeeded) break; } if (!succeeded) { await markVideoStoryboardShotFailed({ taskId: detail.task.id, shotIndex, errorMessage: lastError || "重跑失败", }); } if (index < targetShots.length - 1 && dispatchIntervalSec > 0) { await sleep(dispatchIntervalSec * 1000); } } const summary = await syncVideoStoryboardTaskStatus(detail.task.id); const nextDetail = await getVideoStoryboardTaskDetail(detail.task.id); return NextResponse.json({ success: true, summary, task: nextDetail?.task || null, shots: nextDetail?.shots || [], }); } catch (error) { if (error instanceof ChatSDKError) { return error.toResponse(); } if (error instanceof z.ZodError) { return NextResponse.json( { success: false, error: error.message }, { status: 400 } ); } return NextResponse.json( { success: false, error: error instanceof Error ? error.message : "重跑失败", }, { status: 500 } ); } }