/** * 视频生成工具 * 支持火山引擎视频生成的多种模式 */ import { tool } from "ai"; import { z } from "zod"; import { doGenerateStream } from "@/lib/ai/providers/volcano-video"; import { executeAtlasCloudVideoGeneration } from "@/lib/ai/providers/atlascloud"; import { getLanguageModelById } from "@/lib/ai/dynamic-provider"; import { isVideoModelDescriptor } from "@/lib/ai/model-classification"; import { getEnabledAiModelsWithProvider, getSubModelsByParentModelId } from "@/lib/db/queries"; import { getConfiguredVideoDurationOptions } from "@/lib/ai/video-model-config"; import type { VideoGenerationMode, VideoModelCapabilities, VideoGenerationOptions, } from "@/types/ai"; export interface ExecuteDirectVideoGenerationParams { prompt: string; mode: VideoGenerationMode; images?: string[]; resolution?: "480p" | "720p" | "1080p"; ratio?: "16:9" | "4:3" | "1:1" | "3:4" | "9:16" | "21:9" | "adaptive"; duration?: number; watermark?: boolean; cameraFixed?: boolean; seed?: number; returnLastFrame?: boolean; preferredModelRef?: string; onProgress?: (chunk: string) => void | Promise; } export interface ExecuteDirectVideoGenerationResult { success: boolean; videoUrl?: string; metadata?: Record | null; modelDisplayName: string; modelId: string; modelRef: string; } function inferSupportedModesFromModelId(modelId: string): VideoGenerationMode[] { const normalized = modelId.toLowerCase(); if (normalized.includes("lite-t2v")) return ["text-to-video"]; if (normalized.includes("lite-i2v")) return ["first-frame", "first-last-frame", "reference-images"]; if (normalized.includes("pro-fast")) return ["text-to-video", "first-frame"]; if (normalized.includes("-pro-") || normalized.endsWith("-pro")) { return ["text-to-video", "first-frame", "first-last-frame"]; } return ["text-to-video", "first-frame", "first-last-frame", "reference-images"]; } function extractVideoUrlFromGeneratedText(value: unknown) { const text = String(value || ""); const srcMatch = text.match(/]+src=["'](https?:\/\/[^"']+)["']/i); if (srcMatch?.[1]) return srcMatch[1]; const markdownMatch = text.match(/\[[^\]]*\]\((https?:\/\/[^)\s]+)\)/i); if (markdownMatch?.[1]) return markdownMatch[1]; const directMatch = text.match(/https?:\/\/[^\s<>"']+/i); return directMatch?.[0]?.replace(/[),.;]+$/, "") || ""; } function getSupportedModes(caps: Partial | undefined, modelId: string): VideoGenerationMode[] { const modes = (caps?.supportedModes || []) as VideoGenerationMode[]; if (Array.isArray(modes) && modes.length > 0) { return modes; } return inferSupportedModesFromModelId(modelId); } function withConfiguredVideoCapabilities( capabilities: Partial | undefined, imageGenConfig?: { videoDurationOptions?: number[] | null } | null ): VideoModelCapabilities { const baseCapabilities = { supportedModes: ["text-to-video", "first-frame", "first-last-frame", "reference-images"], supportedResolutions: ["480p", "720p", "1080p"], supportedRatios: ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"], durationRange: { min: 2, max: 12 }, maxReferenceImages: 4, ...(capabilities || {}), } as VideoModelCapabilities; const durationOptions = getConfiguredVideoDurationOptions(imageGenConfig); if (durationOptions.length === 0) { return baseCapabilities; } return { ...baseCapabilities, durationOptions, durationRange: { min: durationOptions[0], max: durationOptions[durationOptions.length - 1], }, }; } function isAgnesVideoModel(item: any) { const capabilities = item?.model?.capabilities; const provider = capabilities && typeof capabilities === "object" ? String((capabilities as Record).provider || "") : ""; const kind = capabilities && typeof capabilities === "object" ? String((capabilities as Record).kind || "") : ""; const modelId = String(item?.model?.modelId || "").toLowerCase(); return ( item?.provider?.type === "custom" && (provider === "agnes-ai" || (kind === "video" && modelId.startsWith("agnes-video"))) ); } function isVolcanoVideoModel(item: any) { return item?.provider?.type === "volcano-video" || item?.provider?.type === "doubao-video"; } function isAtlasCloudVideoModel(item: any) { const capabilities = item?.model?.capabilities; return ( item?.provider?.type === "atlascloud" && capabilities && typeof capabilities === "object" && String((capabilities as Record).mediaType || "") === "video" ); } function isVip98kVideoModel(item: any) { return ( item?.provider?.type === "vip98k" && isVideoModelDescriptor({ providerType: item.provider.type, modelId: item.model?.modelId, capabilities: item.model?.capabilities, supportsImageGen: item.model?.supportsImageGen, }) ); } function isDirectExecutableVideoModel(item: any) { return ( isVolcanoVideoModel(item) || isAgnesVideoModel(item) || isAtlasCloudVideoModel(item) || isVip98kVideoModel(item) ); } /** * 🆕 智能选择最佳子模型 * 根据用户选择的模式和参数,从子模型列表中选择最合适的模型 */ async function selectBestSubModel( parentModelId: string, mode: VideoGenerationMode, params: Partial ): Promise<{ model: any; capabilities: VideoModelCapabilities }> { // 获取所有子模型 const subModels = await getSubModelsByParentModelId(parentModelId); if (subModels.length === 0) { const allModels = await getEnabledAiModelsWithProvider(); const directModel = allModels.find( (item) => item.model.id === parentModelId && isDirectExecutableVideoModel(item), ); if (!directModel) { throw new Error("没有可用的视频生成子模型"); } const directCapabilities = withConfiguredVideoCapabilities( directModel.model.capabilities as VideoModelCapabilities | undefined, (directModel.model as any).imageGenConfig ); const supportedModes = getSupportedModes(directCapabilities, directModel.model.modelId); if (!supportedModes.includes(mode)) { throw new Error(`模型 ${directModel.model.displayName} 不支持 ${mode} 模式`); } return { model: directModel, capabilities: { ...directCapabilities, supportedModes, } as VideoModelCapabilities, }; } console.log(`[Smart Model Selection] 找到 ${subModels.length} 个子模型`); // 筛选支持指定模式的子模型 const compatibleModels = subModels.filter((m) => { const caps = withConfiguredVideoCapabilities( m.model.capabilities as VideoModelCapabilities, (m.model as any).imageGenConfig ); const supportsMode = getSupportedModes(caps, m.model.modelId).includes(mode); // 检查分辨率支持(如果用户指定了分辨率) const supportedResolutions = caps.supportedResolutions || ["480p", "720p", "1080p"]; const supportsResolution = !params.resolution || supportedResolutions.includes(params.resolution); return supportsMode && supportsResolution; }); if (compatibleModels.length === 0) { throw new Error(`没有模型支持 ${mode} 模式和指定的参数`); } console.log(`[Smart Model Selection] ${compatibleModels.length} 个模型支持 ${mode} 模式`); // 🎯 智能优先级排序 const priority: Record = { "doubao-seedance-1-0-pro-250528": 10, // 最高优先级(全功能) "doubao-seedance-1-0-pro-fast-251015": 8, // 快速模式 "doubao-seedance-1-0-lite-i2v-250428": 6, // 图生视频轻量版 "doubao-seedance-1-0-lite-t2v-250428": 5, // 文生视频轻量版 }; // 选择最佳模型 const bestModel = compatibleModels.sort((a, b) => { const priorityA = priority[a.model.modelId] || 0; const priorityB = priority[b.model.modelId] || 0; return priorityB - priorityA; })[0]; console.log(`[Smart Model Selection] ✅ 选择模型: ${bestModel.model.displayName} (${bestModel.model.modelId})`); return { model: bestModel, capabilities: withConfiguredVideoCapabilities( bestModel.model.capabilities as VideoModelCapabilities, (bestModel.model as any).imageGenConfig ), }; } /** * 获取支持指定模式的视频模型(兼容旧逻辑) */ async function getCompatibleModel( mode: VideoGenerationMode, parentModelId?: string, params?: Partial ): Promise<{ model: any; capabilities: VideoModelCapabilities }> { // 🆕 如果提供了parentModelId,使用智能子模型选择 if (parentModelId) { return selectBestSubModel(parentModelId, mode, params || {}); } // 兼容旧逻辑:直接从所有模型中选择(用于向后兼容) const models = await getEnabledAiModelsWithProvider(); const volcanoVideoModels = models.filter( (m) => isDirectExecutableVideoModel(m) && m.model.isEnabled && m.model.capabilities ); if (volcanoVideoModels.length === 0) { throw new Error("没有启用的可执行视频模型"); } const compatibleModels = volcanoVideoModels.filter((m) => { const caps = withConfiguredVideoCapabilities( m.model.capabilities as VideoModelCapabilities, (m.model as any).imageGenConfig ); return getSupportedModes(caps, m.model.modelId).includes(mode); }); if (compatibleModels.length === 0) { throw new Error(`没有模型支持 ${mode} 模式`); } const priority: Record = { "doubao-seedance-1-0-pro-250528": 10, "doubao-seedance-1-0-pro-fast-251015": 8, "doubao-seedance-1-0-lite-i2v-250428": 6, "doubao-seedance-1-0-lite-t2v-250428": 5, }; const bestModel = compatibleModels.sort((a, b) => { const priorityA = priority[a.model.modelId] || 0; const priorityB = priority[b.model.modelId] || 0; return priorityB - priorityA; })[0]; return { model: bestModel, capabilities: withConfiguredVideoCapabilities( bestModel.model.capabilities as VideoModelCapabilities, (bestModel.model as any).imageGenConfig ), }; } /** * 验证参数是否符合模型能力 */ function validateParamsAgainstCapabilities( params: Partial, capabilities: VideoModelCapabilities ): void { // 检查分辨率 if ( params.resolution && capabilities.supportedResolutions.length > 0 && !capabilities.supportedResolutions.includes(params.resolution) ) { throw new Error( `模型不支持 ${params.resolution} 分辨率,支持的分辨率:${capabilities.supportedResolutions.join(", ")}` ); } // 检查宽高比 if ( params.ratio && capabilities.supportedRatios.length > 0 && !capabilities.supportedRatios.includes(params.ratio) ) { throw new Error( `模型不支持 ${params.ratio} 宽高比,支持的宽高比:${capabilities.supportedRatios.join(", ")}` ); } // 检查时长范围 if (params.duration) { const allowedDurations = Array.isArray(capabilities.durationOptions) ? capabilities.durationOptions : []; if (allowedDurations.length > 0) { if (!allowedDurations.includes(params.duration)) { throw new Error(`视频时长仅支持 ${allowedDurations.join(" / ")} 秒`); } } else { const { min, max } = capabilities.durationRange; if (params.duration < min || params.duration > max) { throw new Error(`视频时长必须在 ${min}-${max} 秒之间`); } } } // 检查参考图数量 if ( params.mode === "reference-images" && params.referenceImages && capabilities.maxReferenceImages ) { if (params.referenceImages.length > capabilities.maxReferenceImages) { throw new Error( `参考图数量不能超过 ${capabilities.maxReferenceImages} 张` ); } } } /** * 构建视频生成选项 */ function buildVideoOptions( mode: VideoGenerationMode, images: string[] | undefined, prompt: string, params: any, modelId: string ): VideoGenerationOptions { const options: VideoGenerationOptions = { prompt, model: modelId, mode, resolution: params.resolution || "720p", ratio: params.ratio || "16:9", duration: params.duration || 5, watermark: params.watermark ?? true, cameraFixed: params.cameraFixed ?? false, fps: 24, }; // 根据模式处理图片输入 if (mode === "first-frame" && images && images.length > 0) { options.firstFrameImage = images[0]; } else if (mode === "first-last-frame" && images && images.length >= 2) { options.firstFrameImage = images[0]; options.lastFrameImage = images[1]; } else if (mode === "reference-images" && images && images.length > 0) { options.referenceImages = images.slice(0, 4); } return options; } async function resolvePreferredVideoParentModelId(preferredModelRef?: string) { const normalizedRef = String(preferredModelRef || "").trim(); if (!normalizedRef) return undefined; const models = await getEnabledAiModelsWithProvider(); const matched = models.find( (item) => isDirectExecutableVideoModel(item) && (item.model.id === normalizedRef || item.model.modelId === normalizedRef), ); if (!matched) return undefined; if (matched.model.isSubModel && matched.model.parentModelId) { return matched.model.parentModelId; } return matched.model.id; } function resolveAgnesVideoSize( resolution: VideoGenerationOptions["resolution"], ratio: VideoGenerationOptions["ratio"], ) { const longSideByResolution: Record = { "480p": 854, "720p": 1280, "1080p": 1920, }; const longSide = longSideByResolution[resolution || "720p"] || 1280; const ratios: Record = { "16:9": [16, 9], "4:3": [4, 3], "1:1": [1, 1], "3:4": [3, 4], "9:16": [9, 16], "21:9": [21, 9], adaptive: [16, 9], }; const [rw, rh] = ratios[ratio || "16:9"] || ratios["16:9"]; const landscape = rw >= rh; const width = landscape ? longSide : Math.round((longSide * rw) / rh); const height = landscape ? Math.round((longSide * rh) / rw) : longSide; return { width: Math.max(256, Math.round(width / 8) * 8), height: Math.max(256, Math.round(height / 8) * 8), }; } function resolveAgnesFrameCount(duration?: number) { const seconds = Math.max(1, Math.min(Number(duration) || 5, 18)); const rawFrames = Math.min(441, Math.max(9, Math.round(seconds * 24))); return Math.min(441, Math.max(9, Math.floor((rawFrames - 1) / 8) * 8 + 1)); } function buildAgnesResultUrl(baseUrl: string, videoId: string, modelId: string) { const normalizedBase = String(baseUrl || "https://apihub.agnes-ai.com/v1").replace(/\/+$/, ""); const root = normalizedBase.replace(/\/v1$/i, ""); const url = new URL(`${root}/agnesapi`); url.searchParams.set("video_id", videoId); url.searchParams.set("model_name", modelId); return url.toString(); } async function executeAgnesVideoGeneration(params: { apiKey: string; baseUrl: string; options: VideoGenerationOptions; }) { const baseUrl = String(params.baseUrl || "https://apihub.agnes-ai.com/v1").replace(/\/+$/, ""); const createUrl = `${baseUrl}/videos`; const { width, height } = resolveAgnesVideoSize(params.options.resolution, params.options.ratio); const body: Record = { model: params.options.model, prompt: params.options.prompt, width, height, num_frames: resolveAgnesFrameCount(params.options.duration), frame_rate: 24, }; if (params.options.seed !== undefined) body.seed = params.options.seed; if (params.options.firstFrameImage && params.options.lastFrameImage) { body.extra_body = { image: [params.options.firstFrameImage, params.options.lastFrameImage], mode: "keyframes", }; } else if (params.options.referenceImages?.length) { body.extra_body = { image: params.options.referenceImages.slice(0, 4), mode: "keyframes", }; } else if (params.options.firstFrameImage) { body.image = params.options.firstFrameImage; body.mode = "ti2vid"; } const createResponse = await fetch(createUrl, { method: "POST", headers: { Authorization: `Bearer ${params.apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); const createText = await createResponse.text(); let createData: Record = {}; try { createData = createText ? JSON.parse(createText) : {}; } catch { createData = { raw: createText }; } if (!createResponse.ok) { throw new Error( `Agnes 视频任务创建失败: ${createData?.error?.message || createData?.message || createResponse.statusText}`, ); } const videoId = String(createData.video_id || createData.videoId || createData.id || createData.task_id || "").trim(); const taskId = String(createData.task_id || createData.id || "").trim(); if (!videoId && !taskId) { throw new Error("Agnes 视频任务创建成功但没有返回 video_id/task_id"); } const startedAt = Date.now(); const timeoutMs = 285_000; let lastData = createData; while (Date.now() - startedAt < timeoutMs) { await new Promise((resolve) => setTimeout(resolve, 5000)); const resultUrl = videoId ? buildAgnesResultUrl(baseUrl, videoId, params.options.model) : `${baseUrl}/videos/${encodeURIComponent(taskId)}`; const resultResponse = await fetch(resultUrl, { headers: { Authorization: `Bearer ${params.apiKey}` }, }); const resultText = await resultResponse.text(); let resultData: Record = {}; try { resultData = resultText ? JSON.parse(resultText) : {}; } catch { resultData = { raw: resultText }; } if (!resultResponse.ok) { throw new Error( `Agnes 视频结果查询失败: ${resultData?.error?.message || resultData?.message || resultResponse.statusText}`, ); } lastData = resultData; const status = String(resultData.status || "").toLowerCase(); if (status === "completed" && resultData.url) { return { url: String(resultData.url), metadata: { ...resultData, createTask: createData, }, }; } if (status === "failed") { throw new Error(`Agnes 视频生成失败: ${resultData?.error?.message || resultData?.error || "任务失败"}`); } } throw new Error( `Agnes 视频生成超时: ${String(lastData.status || "unknown")}`, ); } async function resolvePreferredVip98kVideoModel( preferredModelRef: string | undefined, mode: VideoGenerationMode, params: Partial, ) { const normalizedRef = String(preferredModelRef || "").trim(); if (!normalizedRef) return null; const models = await getEnabledAiModelsWithProvider(); const matched = models.find( (item) => isVip98kVideoModel(item) && item.model.isEnabled && !item.model.isSubModel && (item.model.id === normalizedRef || item.model.modelId === normalizedRef || item.model.displayName === normalizedRef), ); if (!matched) return null; const capabilities = withConfiguredVideoCapabilities( matched.model.capabilities as VideoModelCapabilities | undefined, (matched.model as any).imageGenConfig, ); const supportedModes = getSupportedModes(capabilities, matched.model.modelId); if (!supportedModes.includes(mode)) { throw new Error(`模型 ${matched.model.displayName} 不支持 ${mode} 模式`); } const resolvedCapabilities = { ...capabilities, supportedModes, } as VideoModelCapabilities; validateParamsAgainstCapabilities(params, resolvedCapabilities); return { model: matched, capabilities: resolvedCapabilities }; } async function executeVip98kVideoGeneration(params: { request: ExecuteDirectVideoGenerationParams; prompt: string; selected: { model: any; capabilities: VideoModelCapabilities }; generationParams: Partial; }): Promise { const selected = params.selected.model; const languageModel = await getLanguageModelById(selected.model.id); if (!languageModel || typeof (languageModel as any).doGenerate !== "function") { throw new Error("所选 VIP98K 视频模型初始化失败"); } await params.request.onProgress?.( `正在调用 ${selected.model.displayName || selected.model.modelId} 生成视频...`, ); const imageParts = (params.request.images || []).map((url) => ({ type: "image_url" as const, image_url: { url }, })); const result = await (languageModel as any).doGenerate({ messages: [ { role: "user", content: [{ type: "text", text: params.prompt }, ...imageParts], }, ], overrideModelId: selected.model.modelId, videoParams: { mode: params.request.mode, resolution: params.generationParams.resolution || "720p", ratio: params.generationParams.ratio || "16:9", duration: params.generationParams.duration || 8, watermark: params.generationParams.watermark, cameraFixed: params.generationParams.cameraFixed, seed: params.generationParams.seed, returnLastFrame: params.generationParams.returnLastFrame, }, }); if (result?.finishReason === "error") { throw new Error(String(result?.text || "VIP98K 视频生成失败")); } const videoUrl = extractVideoUrlFromGeneratedText(result?.text); if (!videoUrl) throw new Error("VIP98K 视频模型未返回可用视频地址"); return { success: true, videoUrl, metadata: { provider: "vip98k", temporaryVideoUrl: videoUrl, mode: params.request.mode, resolution: params.generationParams.resolution, ratio: params.generationParams.ratio, duration: params.generationParams.duration, }, modelDisplayName: String(selected.model.displayName || selected.model.modelId), modelId: String(selected.model.modelId || selected.model.id), modelRef: String(selected.model.id || selected.model.modelId), }; } export async function executeDirectVideoGeneration( params: ExecuteDirectVideoGenerationParams, ): Promise { const prompt = String(params.prompt || "").trim(); if (!prompt) { throw new Error("缺少视频生成提示词"); } const preferredParentModelId = await resolvePreferredVideoParentModelId( params.preferredModelRef, ); const allModels = await getEnabledAiModelsWithProvider(); const fallbackParentModel = allModels.find( (item) => isVolcanoVideoModel(item) && !item.model.isSubModel && item.model.modelId === "volcano-video-master", ) || allModels.find( (item) => isDirectExecutableVideoModel(item) && !item.model.isSubModel, ); const parentModelId = preferredParentModelId || fallbackParentModel?.model.id || allModels.find( (item) => isDirectExecutableVideoModel(item) && item.model.isEnabled && item.model.isSubModel && item.model.parentModelId, )?.model.parentModelId || undefined; const generationParams = { mode: params.mode, resolution: params.resolution || "720p", ratio: params.ratio || "16:9", duration: params.duration || 5, watermark: params.watermark ?? true, cameraFixed: params.cameraFixed ?? false, seed: params.seed, returnLastFrame: params.returnLastFrame ?? false, referenceImages: params.images, } satisfies Partial; const preferredVip98kModel = await resolvePreferredVip98kVideoModel( params.preferredModelRef, params.mode, generationParams, ); if (preferredVip98kModel) { return executeVip98kVideoGeneration({ request: params, prompt, selected: preferredVip98kModel, generationParams, }); } let selectedModelConfig: { model: any; capabilities: VideoModelCapabilities }; try { selectedModelConfig = await getCompatibleModel( params.mode, parentModelId, generationParams, ); } catch (preferredModelError) { if (parentModelId) { if ( preferredModelError instanceof Error && /不支持|没有模型支持/.test(preferredModelError.message) ) { throw preferredModelError; } console.warn( "[Generate Video Tool] Preferred parent model selection failed, fallback to global compatible model:", preferredModelError, ); selectedModelConfig = await getCompatibleModel( params.mode, undefined, generationParams, ); } else { throw preferredModelError; } } const { model, capabilities } = selectedModelConfig; validateParamsAgainstCapabilities(generationParams, capabilities); const options = buildVideoOptions( params.mode, params.images, prompt, generationParams, model.model.modelId, ); let videoMetadata: Record | null = null; if (isAgnesVideoModel(model)) { const agnesResult = await executeAgnesVideoGeneration({ apiKey: model.provider.apiKey, baseUrl: model.provider.baseUrl, options, }); videoMetadata = { ...(agnesResult.metadata || {}), temporaryVideoUrl: agnesResult.url, }; } else if (isAtlasCloudVideoModel(model)) { const atlasResult = await executeAtlasCloudVideoGeneration({ apiKey: model.provider.apiKey, baseURL: model.provider.baseUrl, modelId: model.model.modelId, modelConfig: model.model.imageGenConfig || {}, options, }); videoMetadata = { ...(atlasResult.metadata || {}), temporaryVideoUrl: atlasResult.url, }; } else { for await (const chunk of doGenerateStream(model.provider.apiKey, model.provider.baseUrl, options)) { await params.onProgress?.(chunk); const metadataMatch = chunk.match(//); if (metadataMatch) { try { videoMetadata = JSON.parse(metadataMatch[1]); } catch (error) { console.error("[Generate Video Tool] 解析元数据失败:", error); } } } } return { success: true, videoUrl: videoMetadata?.temporaryVideoUrl || undefined, metadata: videoMetadata, modelDisplayName: String(model.model.displayName || model.model.modelId || "视频模型"), modelId: String(model.model.modelId || model.model.id), modelRef: String(model.model.id || model.model.modelId), }; } /** * 视频生成工具 */ export function generateVideo({ dataStream }: { dataStream: any }) { return tool({ description: "生成视频,支持文生视频、图生视频(首帧/首尾帧/参考图)等多种模式", inputSchema: z.object({ prompt: z.string().describe("视频生成提示词,描述视频内容、镜头运动等"), mode: z .enum(["text-to-video", "first-frame", "first-last-frame", "reference-images"]) .describe("生成模式:文生视频/图生视频-首帧/首尾帧/参考图"), images: z .array(z.string()) .optional() .describe("图片 URL 数组(根据模式不同,需要 0-4 张)"), resolution: z .enum(["480p", "720p", "1080p"]) .default("720p") .describe("视频分辨率"), ratio: z .enum(["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"]) .default("16:9") .describe("视频宽高比"), duration: z .number() .min(1) .max(600) .default(5) .describe("视频时长(秒)。优先使用当前视频模型配置的秒数档位,服务端会按模型能力动态校验。"), watermark: z.boolean().default(true).describe("是否添加水印"), cameraFixed: z.boolean().default(false).describe("是否固定摄像头"), seed: z.number().optional().describe("随机种子(可选)"), returnLastFrame: z .boolean() .default(false) .describe("是否返回尾帧(用于连续生成)"), }), execute: async ({ prompt, mode, images, resolution, ratio, duration, watermark, cameraFixed, seed, returnLastFrame }) => { try { console.log("[Generate Video Tool] 🎬 开始生成视频"); console.log("[Generate Video Tool] 模式:", mode); console.log("[Generate Video Tool] 提示词:", prompt); dataStream.writeData({ type: "video-generation-start", data: { mode, model: "智能选择中", }, }); const result = await executeDirectVideoGeneration({ prompt, mode, images, resolution, ratio, duration, watermark, cameraFixed, seed, returnLastFrame, onProgress: async (chunk) => { dataStream.writeMessageAnnotation({ type: "video-progress", content: chunk, }); }, }); console.log("[Generate Video Tool] ✅ 视频生成完成"); return { success: true, videoUrl: result.videoUrl, metadata: result.metadata, modelDisplayName: result.modelDisplayName, modelId: result.modelId, modelRef: result.modelRef, }; } catch (error: any) { console.error("[Generate Video Tool] ❌ 生成失败:", error); dataStream.writeMessageAnnotation({ type: "video-error", content: `视频生成失败:${error.message}`, }); throw error; } }, }); }