import type { EnabledImageCapability, GenerationStatus } from "@/lib/types"; import type { PromptMaterial } from "@/lib/prompt/assembler"; import { toAbsoluteUrl } from "@/lib/server/runtime"; export const BAILIAN_DEFAULT_BASE_URL = "https://llm-126wneubbdo6dbr5.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"; export const BAILIAN_IMAGE_MODEL = "wan2.7-image-pro"; export const BAILIAN_VIDEO_MODEL = "wan2.7-i2v-2026-04-25"; export type BailianTaskResponse = Record & { output?: Record; request_id?: string; code?: string; message?: string; }; export function getBailianConfig() { const compatibleBaseUrl = (process.env.BAILIAN_BASE_URL || BAILIAN_DEFAULT_BASE_URL).replace(/\/+$/, ""); return { apiKey: process.env.BAILIAN_API_KEY?.trim() || process.env.DASHSCOPE_API_KEY?.trim(), compatibleBaseUrl, nativeBaseUrl: deriveBailianNativeBaseUrl(compatibleBaseUrl), imageModel: process.env.BAILIAN_IMAGE_MODEL || BAILIAN_IMAGE_MODEL, videoModel: process.env.BAILIAN_VIDEO_MODEL || BAILIAN_VIDEO_MODEL }; } export function deriveBailianNativeBaseUrl(baseUrl: string) { return baseUrl.replace(/\/+$/, "").replace(/\/compatible-mode\/v1$/i, ""); } export function shouldMockBailian() { const flag = (process.env.BAILIAN_MOCK || "auto").trim().toLowerCase(); if (flag === "1" || flag === "true") return true; return false; } export function buildBailianImagePayload(capability: EnabledImageCapability, input: Record) { if (capability !== "image.generate") throw new Error("百炼渠道当前仅支持图片生成与参考图生图。"); const prompt = String(input.prompt || "").trim(); if (!prompt) throw new Error("图片生成提示词不能为空。"); const imageUrls = stringArray(input.imageUrls); if (imageUrls.length > 9) throw new Error("百炼参考图生图最多支持 9 张图片。"); const width = numberValue(input.width); const height = numberValue(input.height); validateBailianImageSize(width, height, imageUrls.length > 0); return { model: getBailianConfig().imageModel, input: { messages: [{ role: "user", content: [...imageUrls.map((image) => ({ image })), { text: prompt }] }] }, parameters: { size: width && height ? `${width}*${height}` : "2K", n: 1, watermark: false, ...(imageUrls.length ? {} : { thinking_mode: true }) } }; } export function validateBailianImageSize(width?: number, height?: number, editing = false) { if (!width && !height) return; if (!width || !height || !Number.isInteger(width) || !Number.isInteger(height)) throw new Error("百炼图片宽高必须为整数。"); const pixels = width * height; const minPixels = 768 * 768; const maxPixels = (editing ? 2048 : 4096) ** 2; if (pixels < minPixels || pixels > maxPixels) throw new Error(`百炼${editing ? "参考图生图" : "文生图"}总像素须在 768×768 至 ${editing ? "2048×2048" : "4096×4096"} 之间。`); const ratio = width / height; if (ratio < 1 / 8 || ratio > 8) throw new Error("百炼图片宽高比须在 1:8 至 8:1 之间。"); } export function buildBailianVideoPayload(input: { prompt: string; materials: PromptMaterial[]; origin: string; settings: Record }) { const images = input.materials.filter((item) => item.type === "image"); if (images.length < 1 || images.length > 2 || input.materials.some((item) => item.type !== "image")) { throw new Error("百炼图生视频仅支持 1 张首帧图,或 2 张首尾帧图。"); } const duration = numberValue(input.settings.duration) ?? 10; if (!Number.isInteger(duration) || duration < 2 || duration > 15) throw new Error("百炼视频时长须为 2–15 秒。"); const resolution = String(input.settings.resolution || "720P").toUpperCase(); if (resolution !== "720P" && resolution !== "1080P") throw new Error("百炼视频分辨率仅支持 720P 或 1080P。"); return { model: getBailianConfig().videoModel, input: { prompt: input.prompt, media: images.map((item, index) => ({ type: index === 0 ? "first_frame" : "last_frame", url: toAbsoluteUrl(item.url, input.origin) })) }, parameters: { resolution, duration, prompt_extend: true, watermark: false } }; } export async function submitBailianTask(kind: "image" | "video", payload: Record) { const config = requiredConfig(); const path = kind === "image" ? "/api/v1/services/aigc/image-generation/generation" : "/api/v1/services/aigc/video-generation/video-synthesis"; return bailianFetch(`${config.nativeBaseUrl}${path}`, config.apiKey, { method: "POST", headers: { "X-DashScope-Async": "enable" }, body: JSON.stringify(payload) }); } export async function queryBailianTask(taskId: string) { const config = requiredConfig(); return bailianFetch(`${config.nativeBaseUrl}/api/v1/tasks/${encodeURIComponent(taskId)}`, config.apiKey); } export function bailianTaskId(response: BailianTaskResponse) { return stringValue(response.output?.task_id) || stringValue(response.task_id); } export function bailianStatus(response: BailianTaskResponse): GenerationStatus { const value = String(response.output?.task_status || response.status || "").toUpperCase(); if (["SUCCEEDED", "SUCCESS", "COMPLETED"].includes(value)) return "succeeded"; if (["FAILED", "UNKNOWN"].includes(value)) return "failed"; if (["CANCELED", "CANCELLED"].includes(value)) return "cancelled"; if (["RUNNING", "PROCESSING"].includes(value)) return "running"; return "queued"; } export function bailianResultUrls(response: BailianTaskResponse, kind: "image" | "video") { const output = response.output || {}; if (kind === "video") return [stringValue(output.video_url)].filter(Boolean) as string[]; const results = Array.isArray(output.results) ? output.results : []; const resultUrls = results.map((item) => stringValue((item as Record)?.url)).filter(Boolean) as string[]; const choices = Array.isArray(output.choices) ? output.choices : []; const choiceUrls = choices.flatMap((choice) => { const message = (choice as Record)?.message as Record | undefined; const content = Array.isArray(message?.content) ? message.content : []; return content.map((item) => stringValue((item as Record)?.image)).filter(Boolean) as string[]; }); return [...resultUrls, ...choiceUrls]; } async function bailianFetch(url: string, apiKey: string, init: RequestInit = {}) { const response = await fetch(url, { ...init, headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", ...(init.headers || {}) } }); const json = await response.json().catch(() => ({})) as BailianTaskResponse; if (!response.ok) throw new Error(`百炼 API 请求失败:${response.status} ${json.message || JSON.stringify(json)}`); return json; } function requiredConfig() { const config = getBailianConfig(); if (!config.apiKey) throw new Error("缺少 BAILIAN_API_KEY。"); return { ...config, apiKey: config.apiKey }; } function stringArray(value: unknown) { return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && Boolean(item)) : []; } function stringValue(value: unknown) { return typeof value === "string" && value ? value : undefined; } function numberValue(value: unknown) { const number = Number(value); return Number.isFinite(number) ? number : undefined; }