import { ProviderTaskFailedError, type ProviderTaskLifecycle, } from "@/lib/video-canvas/video-task-lifecycle"; import type { ExecuteDirectVideoGenerationParams } from "@/lib/ai/tools/generate-video"; // This adapter deliberately exposes only the official model whose contract we support. export const ALIYUN_CANVAS_VIDEO_MODEL = "wan2.6-i2v-flash"; export function buildAliyunCanvasVideoRequest(params: ExecuteDirectVideoGenerationParams, modelId: string) { if (modelId !== ALIYUN_CANVAS_VIDEO_MODEL) throw new Error("当前阿里云视频模型尚未接入画布"); if (params.mode !== "first-frame" || params.images?.length !== 1) throw new Error("万相首帧生视频需要一张参考图"); if (params.audioUrls?.length) throw new Error("此画布接入暂不支持参考音频"); const duration = params.duration ?? 5; const resolution = params.resolution ?? "720p"; if (!Number.isInteger(duration) || duration < 2 || duration > 15) throw new Error("万相视频时长需为 2–15 秒整数"); if (!["720p", "1080p"].includes(resolution)) throw new Error("万相视频仅支持 720P / 1080P"); if (params.cameraFixed || params.returnLastFrame) throw new Error("此万相接入不支持固定镜头参数或返回尾帧参数"); return { model: modelId, input: { prompt: params.prompt, img_url: params.images[0] }, parameters: { resolution: resolution.toUpperCase(), duration, // The upstream derives aspect ratio from the input image, not a ratio parameter. audio: params.generateAudio ?? false, prompt_extend: false, watermark: params.watermark ?? false, ...(params.seed != null ? { seed: params.seed } : {}), }, }; } export async function executeAliyunCanvasVideo( params: ExecuteDirectVideoGenerationParams & { modelId: string; apiKey: string; baseURL: string; lifecycle?: ProviderTaskLifecycle; }, runtime = { fetch: globalThis.fetch, sleep: (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)), }, ) { const body = buildAliyunCanvasVideoRequest(params, params.modelId); const base = new URL(params.baseURL); base.pathname = "/api/v1"; base.search = ""; base.hash = ""; const headers = { Authorization: `Bearer ${params.apiKey}`, "Content-Type": "application/json", "X-DashScope-Async": "enable" }; const deadline = Date.now() + 275_000; let taskId = params.lifecycle?.resume?.taskId || ""; const request = async (path: string, init: RequestInit = {}) => { const remaining = deadline - Date.now(); if (remaining <= 0) throw new Error(`阿里云视频查询超时,任务 ${taskId},请勿重复生成`); const response = await runtime.fetch(`${base.toString().replace(/\/$/, "")}${path}`, { ...init, headers, signal: AbortSignal.timeout(Math.min(30_000, remaining)), }); const data = await response.json(); if (!response.ok) throw new Error(`阿里云视频请求失败(${response.status}):${data.code || data.error?.code || "UnknownError"}${taskId ? `,任务 ${taskId}` : ""}`); return data; }; await params.lifecycle?.assertActive(); if (!taskId) { const submitted = await request("/services/aigc/video-generation/video-synthesis", { method: "POST", body: JSON.stringify(body), }); taskId = submitted.output?.task_id; if (!taskId) throw new Error("阿里云未返回视频任务 ID"); await params.lifecycle?.accepted({ taskId }); } await params.onProgress?.(`阿里云视频任务已提交:${taskId}`); while (Date.now() < deadline) { await runtime.sleep(Math.min(5_000, Math.max(0, deadline - Date.now()))); await params.lifecycle?.assertActive(); const result = await request(`/tasks/${encodeURIComponent(taskId)}`); const output = result.output || {}; if (output.task_status === "SUCCEEDED") { if (!/^https?:\/\//i.test(output.video_url || "")) throw new Error("阿里云任务完成但没有返回视频"); return { url: output.video_url as string, metadata: { provider: "aliyun", taskId, requestId: result.request_id, duration: body.parameters.duration, resolution: params.resolution ?? "720p", generateAudio: body.parameters.audio, } }; } if (["FAILED", "CANCELED", "UNKNOWN"].includes(output.task_status)) { throw new ProviderTaskFailedError( `阿里云视频生成失败:${output.code || result.code || output.task_status},任务 ${taskId}`, ); } } throw new Error(`阿里云视频查询超时,任务 ${taskId},请勿重复生成`); }