diff --git a/data/vendor/volcengine.ts b/data/vendor/volcengine.ts index 3a696c1..7409f11 100644 --- a/data/vendor/volcengine.ts +++ b/data/vendor/volcengine.ts @@ -133,11 +133,10 @@ declare const exports: { const vendor: VendorConfig = { id: "volcengine", - version: "2.2", + version: "2.3", author: "leeqi", name: "火山引擎(豆包)", - description: - "火山引擎豆包大模型,支持文本、图片生成、视频生成等能力。\n\n需要在[火山引擎控制台](https://console.volcengine.com/ark)获取API密钥。", + description: "火山引擎豆包大模型,支持文本、图片生成、视频生成等能力。\n\n需要在[火山引擎控制台](https://console.volcengine.com/ark)获取API密钥。", icon: "", inputs: [ { key: "apiKey", label: "API密钥", type: "password", required: true, placeholder: "火山引擎API Key" }, @@ -455,10 +454,8 @@ const videoRequest = async (config: VideoConfig, model: VideoModel): Promise 0 ? config.mode[0] : "text"; - - if (typeof activeMode === "string") { - switch (activeMode) { + if (typeof config.mode === "string") { + switch (config.mode) { case "singleImage": { const firstImage = config.referenceList?.find((r) => r.type === "image"); if (firstImage) { @@ -526,13 +523,13 @@ const videoRequest = async (config: VideoConfig, model: VideoModel): Promise r.type === "image") ?? []; const videoRefs = config.referenceList?.filter((r) => r.type === "video") ?? []; const audioRefs = config.referenceList?.filter((r) => r.type === "audio") ?? []; - for (const refDef of activeMode) { + for (const refDef of config.mode) { if (typeof refDef === "string") { if (refDef.startsWith("imageReference:")) { const maxCount = parseInt(refDef.split(":")[1], 10); diff --git a/src/lib/fixDB.ts b/src/lib/fixDB.ts index fd468ca..b50d9db 100644 --- a/src/lib/fixDB.ts +++ b/src/lib/fixDB.ts @@ -136,7 +136,7 @@ export default async (knex: Knex): Promise => { await dropColumn("o_vendorConfig", "createTime"); const volcengineVer = await u.vendor.getVendor("volcengine").version; - if (Number(volcengineVer) < 2.2) { + if (Number(volcengineVer) < 2.3) { u.vendor.writeCode("volcengine", vendorData["volcengine.ts"]); } const minimaxVer = await u.vendor.getVendor("minimax").version; diff --git a/src/lib/vendor.json b/src/lib/vendor.json index 1a0fb9c..1795cd6 100644 --- a/src/lib/vendor.json +++ b/src/lib/vendor.json @@ -6,5 +6,5 @@ "openai.ts": "/**\r\n * Toonflow AI供应商模板\r\n * @version 2.0\r\n */\r\n// ============================================================\r\n// 类型定义\r\n// ============================================================\r\ntype VideoMode =\r\n | \"singleImage\"\r\n | \"startEndRequired\"\r\n | \"endFrameOptional\"\r\n | \"startFrameOptional\"\r\n | \"text\"\r\n | (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[];\r\ninterface TextModel {\r\n name: string;\r\n modelName: string;\r\n type: \"text\";\r\n think: boolean;\r\n}\r\ninterface ImageModel {\r\n name: string;\r\n modelName: string;\r\n type: \"image\";\r\n mode: (\"text\" | \"singleImage\" | \"multiReference\")[];\r\n associationSkills?: string;\r\n}\r\ninterface VideoModel {\r\n name: string;\r\n modelName: string;\r\n type: \"video\";\r\n mode: VideoMode[];\r\n associationSkills?: string;\r\n audio: \"optional\" | false | true;\r\n durationResolutionMap: { duration: number[]; resolution: string[] }[];\r\n}\r\ninterface TTSModel {\r\n name: string;\r\n modelName: string;\r\n type: \"tts\";\r\n voices: { title: string; voice: string }[];\r\n}\r\ninterface VendorConfig {\r\n id: string;\r\n version: string;\r\n name: string;\r\n author: string;\r\n description?: string;\r\n icon?: string;\r\n inputs: { key: string; label: string; type: \"text\" | \"password\" | \"url\"; required: boolean; placeholder?: string }[];\r\n inputValues: Record;\r\n models: (TextModel | ImageModel | VideoModel | TTSModel)[];\r\n}\r\ninterface ImageConfig {\r\n prompt: string;\r\n imageBase64: string[];\r\n size: \"1K\" | \"2K\" | \"4K\";\r\n aspectRatio: `${number}:${number}`;\r\n}\r\ninterface VideoConfig {\r\n duration: number;\r\n resolution: string;\r\n aspectRatio: \"16:9\" | \"9:16\";\r\n prompt: string;\r\n imageBase64?: string[];\r\n audio?: boolean;\r\n mode: VideoMode[];\r\n}\r\ninterface TTSConfig {\r\n text: string;\r\n voice: string;\r\n speechRate: number;\r\n pitchRate: number;\r\n volume: number;\r\n}\r\ninterface PollResult {\r\n completed: boolean;\r\n data?: string;\r\n error?: string;\r\n}\r\n// ============================================================\r\n// 全局声明\r\n// ============================================================\r\ndeclare const axios: any;\r\ndeclare const logger: (msg: string) => void;\r\ndeclare const jsonwebtoken: any;\r\ndeclare const zipImage: (base64: string, size: number) => Promise;\r\ndeclare const zipImageResolution: (base64: string, w: number, h: number) => Promise;\r\ndeclare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise;\r\ndeclare const urlToBase64: (url: string) => Promise;\r\ndeclare const pollTask: (fn: () => Promise, interval?: number, timeout?: number) => Promise;\r\ndeclare const createOpenAI: any;\r\ndeclare const createDeepSeek: any;\r\ndeclare const createZhipu: any;\r\ndeclare const createQwen: any;\r\ndeclare const createAnthropic: any;\r\ndeclare const createOpenAICompatible: any;\r\ndeclare const createXai: any;\r\ndeclare const createMinimax: any;\r\ndeclare const createGoogleGenerativeAI: any;\r\ndeclare const exports: {\r\n vendor: VendorConfig;\r\n textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => any;\r\n imageRequest: (c: ImageConfig, m: ImageModel) => Promise;\r\n videoRequest: (c: VideoConfig, m: VideoModel) => Promise;\r\n ttsRequest: (c: TTSConfig, m: TTSModel) => Promise;\r\n checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>;\r\n updateVendor?: () => Promise;\r\n};\r\n// ============================================================\r\n// 供应商配置\r\n// ============================================================\r\nconst vendor: VendorConfig = {\r\n id: \"openai\",\r\n version: \"2.0\",\r\n author: \"Toonflow\",\r\n name: \"OpenAI标准接口\",\r\n description: \"OpenAI标准格式接口,可修改请求地址并手动添加模型。\",\r\n icon: \"\",\r\n inputs: [\r\n { key: \"apiKey\", label: \"API密钥\", type: \"password\", required: true },\r\n { key: \"baseUrl\", label: \"请求地址\", type: \"url\", required: true, placeholder: \"以v1结束,示例:https://api.openai.com/v1\" },\r\n ],\r\n inputValues: {\r\n apiKey: \"\",\r\n baseUrl: \"https://api.openai.com/v1\",\r\n },\r\n models: [\r\n { name: \"GPT-4o\", modelName: \"gpt-4o\", type: \"text\", think: false },\r\n { name: \"GPT-4.1\", modelName: \"gpt-4.1\", type: \"text\", think: false },\r\n { name: \"GPT-5.1\", modelName: \"gpt-5.1\", type: \"text\", think: false },\r\n { name: \"GPT-5.2\", modelName: \"gpt-5.2\", type: \"text\", think: false },\r\n { name: \"GPT-5.4\", modelName: \"gpt-5.4\", type: \"text\", think: false },\r\n ],\r\n};\r\n// ============================================================\r\n// 适配器函数\r\n// ============================================================\r\nconst textRequest = (model: TextModel, think: boolean, thinkLevel: 0 | 1 | 2 | 3) => {\r\n if (!vendor.inputValues.apiKey) throw new Error(\"缺少API Key\");\r\n const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\\s+/i, \"\");\r\n return createOpenAI({ baseURL: vendor.inputValues.baseUrl, apiKey }).chat(model.modelName);\r\n};\r\nconst imageRequest = async (config: ImageConfig, model: ImageModel): Promise => {\r\n return \"\";\r\n};\r\nconst videoRequest = async (config: VideoConfig, model: VideoModel): Promise => {\r\n return \"\";\r\n};\r\nconst ttsRequest = async (config: TTSConfig, model: TTSModel): Promise => {\r\n return \"\";\r\n};\r\nconst checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => {\r\n return { hasUpdate: false, latestVersion: \"2.0\", notice: \"\" };\r\n};\r\nconst updateVendor = async (): Promise => {\r\n return \"\";\r\n};\r\n// ============================================================\r\n// 导出\r\n// ============================================================\r\nexports.vendor = vendor;\r\nexports.textRequest = textRequest;\r\nexports.imageRequest = imageRequest;\r\nexports.videoRequest = videoRequest;\r\nexports.ttsRequest = ttsRequest;\r\nexports.checkForUpdates = checkForUpdates;\r\nexports.updateVendor = updateVendor;\r\nexport {};", "toonflow.ts": "/**\r\n * Toonflow官方中转平台 供应商适配\r\n * @version 2.0\r\n */\r\n\r\n// ============================================================\r\n// 类型定义\r\n// ============================================================\r\n\r\ntype VideoMode =\r\n | \"singleImage\"\r\n | \"startEndRequired\"\r\n | \"endFrameOptional\"\r\n | \"startFrameOptional\"\r\n | \"text\"\r\n | (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[];\r\n\r\ninterface TextModel {\r\n name: string;\r\n modelName: string;\r\n type: \"text\";\r\n think: boolean;\r\n}\r\n\r\ninterface ImageModel {\r\n name: string;\r\n modelName: string;\r\n type: \"image\";\r\n mode: (\"text\" | \"singleImage\" | \"multiReference\")[];\r\n associationSkills?: string;\r\n}\r\n\r\ninterface VideoModel {\r\n name: string;\r\n modelName: string;\r\n type: \"video\";\r\n mode: VideoMode[];\r\n associationSkills?: string;\r\n audio: \"optional\" | false | true;\r\n durationResolutionMap: { duration: number[]; resolution: string[] }[];\r\n}\r\n\r\ninterface TTSModel {\r\n name: string;\r\n modelName: string;\r\n type: \"tts\";\r\n voices: { title: string; voice: string }[];\r\n}\r\n\r\ninterface VendorConfig {\r\n id: string;\r\n version: string;\r\n name: string;\r\n author: string;\r\n description?: string;\r\n icon?: string;\r\n inputs: { key: string; label: string; type: \"text\" | \"password\" | \"url\"; required: boolean; placeholder?: string }[];\r\n inputValues: Record;\r\n models: (TextModel | ImageModel | VideoModel | TTSModel)[];\r\n}\r\n\r\ntype ReferenceList =\r\n | { type: \"image\"; sourceType: \"base64\"; base64: string }\r\n | { type: \"audio\"; sourceType: \"base64\"; base64: string }\r\n | { type: \"video\"; sourceType: \"base64\"; base64: string };\r\n\r\ninterface ImageConfig {\r\n prompt: string;\r\n referenceList?: Extract[];\r\n size: \"1K\" | \"2K\" | \"4K\";\r\n aspectRatio: `${number}:${number}`;\r\n}\r\n\r\ninterface VideoConfig {\r\n duration: number;\r\n resolution: string;\r\n aspectRatio: \"16:9\" | \"9:16\";\r\n prompt: string;\r\n referenceList?: ReferenceList[];\r\n audio?: boolean;\r\n mode: VideoMode[];\r\n}\r\n\r\ninterface TTSConfig {\r\n text: string;\r\n voice: string;\r\n speechRate: number;\r\n pitchRate: number;\r\n volume: number;\r\n referenceList?: Extract[];\r\n}\r\n\r\ninterface PollResult {\r\n completed: boolean;\r\n data?: string;\r\n error?: string;\r\n}\r\n\r\n// ============================================================\r\n// 全局声明\r\n// ============================================================\r\n\r\ndeclare const axios: any;\r\ndeclare const logger: (msg: string) => void;\r\ndeclare const jsonwebtoken: any;\r\ndeclare const zipImage: (base64: string, size: number) => Promise;\r\ndeclare const zipImageResolution: (base64: string, w: number, h: number) => Promise;\r\ndeclare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise;\r\ndeclare const urlToBase64: (url: string) => Promise;\r\ndeclare const pollTask: (fn: () => Promise, interval?: number, timeout?: number) => Promise;\r\ndeclare const createOpenAI: any;\r\ndeclare const createDeepSeek: any;\r\ndeclare const createZhipu: any;\r\ndeclare const createQwen: any;\r\ndeclare const createAnthropic: any;\r\ndeclare const createOpenAICompatible: any;\r\ndeclare const createXai: any;\r\ndeclare const createMinimax: any;\r\ndeclare const createGoogleGenerativeAI: any;\r\ndeclare const exports: {\r\n vendor: VendorConfig;\r\n textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => any;\r\n imageRequest: (c: ImageConfig, m: ImageModel) => Promise;\r\n videoRequest: (c: VideoConfig, m: VideoModel) => Promise;\r\n ttsRequest: (c: TTSConfig, m: TTSModel) => Promise;\r\n checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>;\r\n updateVendor?: () => Promise;\r\n};\r\n\r\n// ============================================================\r\n// 供应商配置\r\n// ============================================================\r\n\r\nconst vendor: VendorConfig = {\r\n id: \"toonflow\",\r\n version: \"2.0\",\r\n author: \"Toonflow\",\r\n name: \"Toonflow官方中转平台\",\r\n description:\r\n \"## Toonflow官方中转平台\\n\\nToonflow官方中转平台,提供**文本、图像、视频、音频**等多模态生成能力的中转服务,支持接入多个大模型供应商,方便用户统一管理和调用不同供应商的生成能力。\\n\\n🔗 [前往中转平台](https://api.toonflow.net/)\\n\\n如果这个项目对你有帮助,可以考虑支持一下我们的开发工作 ☕\",\r\n icon: \"\",\r\n inputs: [{ key: \"apiKey\", label: \"API密钥\", type: \"password\", required: true }],\r\n inputValues: {\r\n apiKey: \"\",\r\n baseUrl: \"https://api.toonflow.net/v1\",\r\n },\r\n models: [\r\n { name: \"claude-sonnet-4-6\", type: \"text\", modelName: \"claude-sonnet-4-6\", think: false },\r\n { name: \"claude-opus-4-6\", type: \"text\", modelName: \"claude-opus-4-6\", think: false },\r\n { name: \"claude-sonnet-4-5-20250929\", type: \"text\", modelName: \"claude-sonnet-4-5-20250929\", think: false },\r\n { name: \"claude-opus-4-5-20251101\", type: \"text\", modelName: \"claude-opus-4-5-20251101\", think: false },\r\n { name: \"claude-haiku-4-5-20251001\", type: \"text\", modelName: \"claude-haiku-4-5-20251001\", think: false },\r\n { name: \"gpt-5.4\", type: \"text\", modelName: \"gpt-5.4\", think: false },\r\n { name: \"gpt-5.2\", type: \"text\", modelName: \"gpt-5.2\", think: false },\r\n { name: \"MiniMax-M2.7\", type: \"text\", modelName: \"MiniMax-M2.7\", think: true },\r\n { name: \"MiniMax-M2.5\", type: \"text\", modelName: \"MiniMax-M2.5\", think: true },\r\n {\r\n name: \"Wan2.6 I2V 1080P (支持真人)\",\r\n type: \"video\",\r\n modelName: \"Wan2.6-I2V-1080P\",\r\n mode: [\"text\", \"startEndRequired\"],\r\n durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: [\"1080p\"] }],\r\n audio: true,\r\n },\r\n {\r\n name: \"Wan2.6 I2V 720P (支持真人)\",\r\n type: \"video\",\r\n modelName: \"Wan2.6-I2V-720P\",\r\n mode: [\"text\", \"startEndRequired\"],\r\n durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: [\"720p\"] }],\r\n audio: true,\r\n },\r\n {\r\n name: \"Seedance 1.5 Pro\",\r\n type: \"video\",\r\n modelName: \"doubao-seedance-1-5-pro-251215\",\r\n mode: [\"text\", \"endFrameOptional\"],\r\n durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: [\"480p\", \"720p\", \"1080p\"] }],\r\n audio: true,\r\n },\r\n {\r\n name: \"vidu2 turbo\",\r\n type: \"video\",\r\n modelName: \"ViduQ2-turbo\",\r\n mode: [\"singleImage\", \"startEndRequired\"],\r\n durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: [\"540p\", \"720p\", \"1080p\"] }],\r\n audio: false,\r\n },\r\n {\r\n name: \"ViduQ3 pro\",\r\n type: \"video\",\r\n modelName: \"ViduQ3-pro\",\r\n mode: [\"singleImage\", \"startEndRequired\"],\r\n durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], resolution: [\"540p\", \"720p\", \"1080p\"] }],\r\n audio: false,\r\n },\r\n {\r\n name: \"ViduQ2 pro\",\r\n type: \"video\",\r\n modelName: \"ViduQ2-pro\",\r\n mode: [\"singleImage\", \"startEndRequired\"],\r\n durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: [\"540p\", \"720p\", \"1080p\"] }],\r\n audio: false,\r\n },\r\n {\r\n name: \"Doubao Seedream 5.0 Lite\",\r\n type: \"image\",\r\n modelName: \"Doubao-Seedream-5.0-Lite\",\r\n mode: [\"text\", \"singleImage\", \"multiReference\"],\r\n },\r\n {\r\n name: \"Doubao Seedream 4.5\",\r\n type: \"image\",\r\n modelName: \"doubao-seedream-4-5-251128\",\r\n mode: [\"text\", \"singleImage\", \"multiReference\"],\r\n },\r\n ],\r\n};\r\n\r\n// ============================================================\r\n// 辅助工具\r\n// ============================================================\r\n\r\n// 从 markdown 内容中提取第一张图片\r\nfunction extractFirstImageFromMd(content: string) {\r\n const regex = /!\\[([^\\]]*)\\]\\((data:image\\/[^;]+;base64,[A-Za-z0-9+/=]+|https?:\\/\\/[^\\s)]+|\\/\\/[^\\s)]+|[^\\s)]+)\\)/;\r\n const match = content.match(regex);\r\n if (!match) return null;\r\n const raw = match[2].trim();\r\n const url = raw.startsWith(\"data:\") ? raw : raw.split(/\\s+/)[0];\r\n return { alt: match[1], url, type: url.startsWith(\"data:image\") ? \"base64\" : \"url\" };\r\n}\r\n\r\n// ============================================================\r\n// 适配器函数\r\n// ============================================================\r\n\r\nconst textRequest = (model: TextModel, think: boolean, thinkLevel: 0 | 1 | 2 | 3) => {\r\n if (!vendor.inputValues.apiKey) throw new Error(\"缺少API Key\");\r\n const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\\s+/i, \"\");\r\n return createOpenAI({ baseURL: vendor.inputValues.baseUrl, apiKey }).chat(model.modelName);\r\n};\r\n\r\nconst imageRequest = async (config: ImageConfig, model: ImageModel): Promise => {\r\n if (!vendor.inputValues.apiKey) throw new Error(\"缺少API Key\");\r\n const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\\s+/i, \"\");\r\n const baseUrl = vendor.inputValues.baseUrl;\r\n const lowerName = model.modelName.toLowerCase();\r\n const imageBase64List = (config.referenceList ?? []).map((r) => r.base64);\r\n\r\n // Gemini / nano 系模型:走 chat/completions 接口,从返回的 markdown 中提取图片\r\n if (lowerName.includes(\"gemini\") || lowerName.includes(\"nano\")) {\r\n const imageConfigGoogle: Record = {\r\n aspect_ratio: config.aspectRatio,\r\n image_size: config.size,\r\n };\r\n const messages: any[] = [];\r\n if (imageBase64List.length) {\r\n messages.push({\r\n role: \"user\",\r\n content: imageBase64List.map((b) => ({ type: \"image_url\", image_url: { url: b } })),\r\n });\r\n }\r\n messages.push({ role: \"user\", content: config.prompt + \"请直接输出图片\" });\r\n const body = {\r\n model: model.modelName,\r\n messages,\r\n extra_body: { google: { image_config: imageConfigGoogle } },\r\n };\r\n logger(`[imageRequest] 使用 gemini 适配器,模型: ${model.modelName}`);\r\n const response = await fetch(`${baseUrl}/chat/completions`, {\r\n method: \"POST\",\r\n headers: { Authorization: `Bearer ${apiKey}`, \"Content-Type\": \"application/json\" },\r\n body: JSON.stringify(body),\r\n });\r\n if (!response.ok) {\r\n const errorText = await response.text();\r\n throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`);\r\n }\r\n const data = await response.json();\r\n const imageResult = extractFirstImageFromMd(data.choices[0].message.content);\r\n if (!imageResult) throw new Error(\"未能从响应中提取图片\");\r\n if (imageResult.type === \"base64\") return imageResult.url;\r\n return await urlToBase64(imageResult.url);\r\n }\r\n\r\n // 豆包 / seedream 系模型:走 images/generations 接口\r\n if (lowerName.includes(\"doubao\") || lowerName.includes(\"seedream\")) {\r\n const effectiveSize = config.size === \"1K\" ? \"2K\" : config.size;\r\n const sizeMap: Record> = {\r\n \"16:9\": { \"2K\": \"2848x1600\", \"4K\": \"4096x2304\" },\r\n \"9:16\": { \"2K\": \"1600x2848\", \"4K\": \"2304x4096\" },\r\n };\r\n const resolvedSize = sizeMap[config.aspectRatio]?.[effectiveSize];\r\n const body: Record = {\r\n model: model.modelName,\r\n prompt: config.prompt,\r\n size: resolvedSize,\r\n response_format: \"url\",\r\n sequential_image_generation: \"disabled\",\r\n stream: false,\r\n watermark: false,\r\n ...(imageBase64List.length && { image: imageBase64List }),\r\n };\r\n logger(`[imageRequest] 使用 doubao 适配器,模型: ${model.modelName}`);\r\n const response = await fetch(`${baseUrl}/images/generations`, {\r\n method: \"POST\",\r\n headers: { Authorization: `Bearer ${apiKey}`, \"Content-Type\": \"application/json\" },\r\n body: JSON.stringify(body),\r\n });\r\n if (!response.ok) {\r\n const errorText = await response.text();\r\n throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`);\r\n }\r\n const data = await response.json();\r\n const resultUrl = data.data[0].url;\r\n return await urlToBase64(resultUrl);\r\n }\r\n\r\n throw new Error(`不支持的图像模型: ${model.modelName}`);\r\n};\r\n\r\nconst videoRequest = async (config: VideoConfig, model: VideoModel): Promise => {\r\n if (!vendor.inputValues.apiKey) throw new Error(\"缺少API Key\");\r\n const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\\s+/i, \"\");\r\n const baseUrl = vendor.inputValues.baseUrl;\r\n const lowerName = model.modelName.toLowerCase();\r\n\r\n // 当前激活的单一 VideoMode(取第一个非数组模式,或数组模式)\r\n const activeMode = config.mode[0];\r\n const imageRefs = (config.referenceList ?? []).filter((r) => r.type === \"image\").map((r) => r.base64);\r\n const videoRefs = (config.referenceList ?? []).filter((r) => r.type === \"video\").map((r) => r.base64);\r\n const audioRefs = (config.referenceList ?? []).filter((r) => r.type === \"audio\").map((r) => r.base64);\r\n\r\n // 构建模型专属 metadata\r\n let metadata: Record = {};\r\n\r\n if (lowerName.includes(\"wan\")) {\r\n // 万象系列\r\n if (\r\n (activeMode === \"startEndRequired\" || activeMode === \"endFrameOptional\" || activeMode === \"startFrameOptional\") &&\r\n imageRefs.length >= 2\r\n ) {\r\n if (imageRefs[0]) metadata.first_frame_url = imageRefs[0];\r\n if (imageRefs[1]) metadata.last_frame_url = imageRefs[1];\r\n } else if (imageRefs.length) {\r\n metadata.img_url = imageRefs[0];\r\n }\r\n if (typeof config.audio === \"boolean\") metadata.audio = config.audio;\r\n\r\n // 万象需要额外传 size 字段\r\n const wanSizeMap: Record> = {\r\n \"480p\": { \"16:9\": \"832*480\", \"9:16\": \"480*832\" },\r\n \"720p\": { \"16:9\": \"1280*720\", \"9:16\": \"720*1280\" },\r\n \"1080p\": { \"16:9\": \"1920*1080\", \"9:16\": \"1080*1920\" },\r\n };\r\n const wanSize = wanSizeMap[config.resolution]?.[config.aspectRatio];\r\n const body: Record = {\r\n model: model.modelName,\r\n prompt: config.prompt,\r\n duration: config.duration,\r\n size: wanSize,\r\n metadata,\r\n };\r\n logger(`[videoRequest] 提交万象视频任务,模型: ${model.modelName}`);\r\n const response = await fetch(`${baseUrl}/video/generations`, {\r\n method: \"POST\",\r\n headers: { Authorization: `Bearer ${apiKey}`, \"Content-Type\": \"application/json\" },\r\n body: JSON.stringify(body),\r\n });\r\n if (!response.ok) {\r\n const errorText = await response.text();\r\n throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`);\r\n }\r\n const data = await response.json();\r\n const taskId = data.id;\r\n logger(`[videoRequest] 万象任务ID: ${taskId}`);\r\n const res = await pollTask(async () => {\r\n const queryResponse = await fetch(`${baseUrl}/video/generations/${taskId}`, {\r\n method: \"GET\",\r\n headers: { Authorization: `Bearer ${apiKey}`, \"Content-Type\": \"application/json\" },\r\n });\r\n if (!queryResponse.ok) {\r\n const errorText = await queryResponse.text();\r\n throw new Error(`轮询失败,状态码: ${queryResponse.status}, 错误信息: ${errorText}`);\r\n }\r\n const queryData = await queryResponse.json();\r\n const status = queryData?.status ?? queryData?.data?.status;\r\n switch (status) {\r\n case \"completed\":\r\n case \"SUCCESS\":\r\n case \"success\":\r\n return { completed: true, data: queryData.data.result_url };\r\n case \"FAILURE\":\r\n case \"failed\":\r\n return { completed: true, error: queryData?.data?.fail_reason ?? \"视频生成失败\" };\r\n default:\r\n return { completed: false };\r\n }\r\n });\r\n if (res.error) throw new Error(res.error);\r\n return await urlToBase64(res.data!);\r\n }\r\n\r\n if (lowerName.includes(\"doubao\") || lowerName.includes(\"seedance\")) {\r\n // 豆包/Seedance 系列\r\n metadata = {\r\n ...(typeof config.audio === \"boolean\" && { generate_audio: config.audio }),\r\n ratio: config.aspectRatio,\r\n image_roles: [] as string[],\r\n references: [] as string[],\r\n };\r\n if (Array.isArray(activeMode)) {\r\n // 多参考模式\r\n imageRefs.forEach((b) => metadata.references.push(b));\r\n videoRefs.forEach((b) => metadata.references.push(b));\r\n audioRefs.forEach((b) => metadata.references.push(b));\r\n } else if (activeMode === \"startEndRequired\" || activeMode === \"endFrameOptional\" || activeMode === \"startFrameOptional\") {\r\n imageRefs.forEach((_, i) => (metadata.image_roles as string[]).push(i === 0 ? \"first_frame\" : \"last_frame\"));\r\n } else if (activeMode === \"singleImage\") {\r\n imageRefs.forEach(() => (metadata.image_roles as string[]).push(\"reference_image\"));\r\n }\r\n } else if (lowerName.includes(\"vidu\")) {\r\n // Vidu 系列\r\n metadata = {\r\n aspect_ratio: config.aspectRatio,\r\n audio: config.audio ?? false,\r\n off_peak: false,\r\n };\r\n } else if (lowerName.includes(\"kling\")) {\r\n // 可灵系列\r\n metadata = { aspect_ratio: config.aspectRatio };\r\n if (Array.isArray(activeMode)) {\r\n metadata.reference = [...imageRefs, ...videoRefs, ...audioRefs];\r\n } else if (activeMode === \"endFrameOptional\" && imageRefs.length) {\r\n metadata.image_tail = imageRefs[0];\r\n } else if (activeMode === \"startEndRequired\" && imageRefs.length >= 2) {\r\n metadata.image_list = [\r\n { image_url: imageRefs[0], type: \"first_frame\" },\r\n { image_url: imageRefs[1], type: \"last_frame\" },\r\n ];\r\n } else if (activeMode === \"singleImage\" && imageRefs.length) {\r\n metadata.image = imageRefs[0];\r\n }\r\n }\r\n\r\n // 公共请求体(非万象通用路径)\r\n const publicBody: Record = {\r\n model: model.modelName,\r\n ...(!Array.isArray(activeMode) && imageRefs.length ? { images: imageRefs } : {}),\r\n prompt: config.prompt,\r\n duration: config.duration,\r\n metadata,\r\n };\r\n\r\n logger(`[videoRequest] 提交视频任务,模型: ${model.modelName}`);\r\n const response = await fetch(`${baseUrl}/video/generations`, {\r\n method: \"POST\",\r\n headers: { Authorization: `Bearer ${apiKey}`, \"Content-Type\": \"application/json\" },\r\n body: JSON.stringify(publicBody),\r\n });\r\n if (!response.ok) {\r\n const errorText = await response.text();\r\n throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`);\r\n }\r\n const data = await response.json();\r\n const taskId = data.id;\r\n logger(`[videoRequest] 任务ID: ${taskId}`);\r\n\r\n const res = await pollTask(async () => {\r\n const queryResponse = await fetch(`${baseUrl}/video/generations/${taskId}`, {\r\n method: \"GET\",\r\n headers: { Authorization: `Bearer ${apiKey}`, \"Content-Type\": \"application/json\" },\r\n });\r\n if (!queryResponse.ok) {\r\n const errorText = await queryResponse.text();\r\n throw new Error(`轮询失败,状态码: ${queryResponse.status}, 错误信息: ${errorText}`);\r\n }\r\n const queryData = await queryResponse.json();\r\n const status = queryData?.status ?? queryData?.data?.status;\r\n switch (status) {\r\n case \"completed\":\r\n case \"SUCCESS\":\r\n case \"success\":\r\n return { completed: true, data: queryData.data.result_url };\r\n case \"FAILURE\":\r\n case \"failed\":\r\n return { completed: true, error: queryData?.data?.fail_reason ?? \"视频生成失败\" };\r\n default:\r\n return { completed: false };\r\n }\r\n });\r\n\r\n if (res.error) throw new Error(res.error);\r\n return await urlToBase64(res.data!);\r\n};\r\n\r\nconst ttsRequest = async (config: TTSConfig, model: TTSModel): Promise => {\r\n return \"\";\r\n};\r\n\r\nconst checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => {\r\n return { hasUpdate: false, latestVersion: \"2.0\", notice: \"\" };\r\n};\r\n\r\nconst updateVendor = async (): Promise => {\r\n return \"\";\r\n};\r\n\r\n// ============================================================\r\n// 导出\r\n// ============================================================\r\n\r\nexports.vendor = vendor;\r\nexports.textRequest = textRequest;\r\nexports.imageRequest = imageRequest;\r\nexports.videoRequest = videoRequest;\r\nexports.ttsRequest = ttsRequest;\r\nexports.checkForUpdates = checkForUpdates;\r\nexports.updateVendor = updateVendor;\r\n\r\nexport {};", "vidu.ts": "//如需遥测AI请使用在toonflow安装目录运行npx @ai-sdk/devtools (要求在其他设置中打开遥测功能,且toonflow有权限在安装目录创建.devtools文件夹)\r\n// ==================== 类型定义 ====================\r\n// 文本模型\r\ninterface TextModel {\r\n name: string; // 显示名称\r\n modelName: string;\r\n type: \"text\";\r\n think: boolean; // 前端显示用\r\n}\r\n\r\n// 图像模型\r\ninterface ImageModel {\r\n name: string; // 显示名称\r\n modelName: string;\r\n type: \"image\";\r\n mode: (\"text\" | \"singleImage\" | \"multiReference\")[];\r\n associationSkills?: string; // 关联技能,多个技能用逗号分隔\r\n}\r\n// 视频模型\r\ninterface VideoModel {\r\n name: string; // 显示名称\r\n modelName: string; //全局唯一\r\n type: \"video\";\r\n mode: (\r\n | \"singleImage\" // 单图\r\n | \"startEndRequired\" // 首尾帧(两张都得有)\r\n | \"endFrameOptional\" // 首尾帧(尾帧可选)\r\n | \"startFrameOptional\" // 首尾帧(首帧可选)\r\n | \"text\" // 文本生视频\r\n | (\"videoReference\" | \"imageReference\" | \"audioReference\" | \"textReference\")[] // 混合参考\r\n )[];\r\n associationSkills?: string; // 关联技能,多个技能用逗号分隔\r\n audio: \"optional\" | false | true; // 音频配置\r\n durationResolutionMap: { duration: number[]; resolution: string[] }[];\r\n}\r\n\r\ninterface TTSModel {\r\n name: string; // 显示名称\r\n modelName: string;\r\n type: \"tts\";\r\n voices: {\r\n title: string; //显示名称\r\n voice: string; //说话人\r\n }[];\r\n}\r\n// 供应商配置\r\ninterface VendorConfig {\r\n id: string; //供应商唯一标识,必须全局唯一\r\n author: string;\r\n description?: string; //md5格式\r\n name: string;\r\n icon?: string; //仅支持base64格式\r\n inputs: {\r\n key: string;\r\n label: string;\r\n type: \"text\" | \"password\" | \"url\";\r\n required: boolean;\r\n placeholder?: string;\r\n }[];\r\n inputValues: Record;\r\n models: (TextModel | ImageModel | VideoModel)[];\r\n}\r\n// ==================== 全局工具函数 ====================\r\n//Axios实例\r\n//压缩图片大小(1MB = 1 * 1024 * 1024)\r\ndeclare const zipImage: (completeBase64: string, size: number) => Promise;\r\n//压缩图片分辨率\r\ndeclare const zipImageResolution: (completeBase64: string, width: number, height: number) => Promise;\r\n//多图拼接乘单图 maxSize 最大输出大小,默认为 10mb\r\ndeclare const mergeImages: (completeBase64: string[], maxSize?: string) => Promise;\r\n//Url转Base64\r\ndeclare const urlToBase64: (url: string) => Promise;\r\n//轮询函数\r\ndeclare const pollTask: (\r\n fn: () => Promise<{ completed: boolean; data?: string; error?: string }>,\r\n interval?: number,\r\n timeout?: number,\r\n) => Promise<{ completed: boolean; data?: string; error?: string }>;\r\ndeclare const axios: any;\r\ndeclare const createOpenAI: any;\r\ndeclare const createDeepSeek: any;\r\ndeclare const createZhipu: any;\r\ndeclare const createQwen: any;\r\ndeclare const createAnthropic: any;\r\ndeclare const createOpenAICompatible: any;\r\ndeclare const createXai: any;\r\ndeclare const createMinimax: any;\r\ndeclare const createGoogleGenerativeAI: any;\r\ndeclare const logger: (logstring: string) => void;\r\ndeclare const jsonwebtoken: any;\r\n// ==================== 供应商数据 ====================\r\nconst vendor: VendorConfig = {\r\n id: \"vidu\",\r\n author: \"搬砖的Coder\",\r\n description:\r\n \"Vidu 官方视频生成平台。 [前往平台](https://platform.vidu.cn/login/)\",\r\n name: \"Vidu 开放平台\",\r\n inputs: [\r\n { key: \"apiKey\", label: \"API密钥\", type: \"password\", required: true, placeholder: \"请到Vidu官方申请\" },\r\n { key: \"baseUrl\", label: \"接口路径\", type: \"url\", required: true, placeholder: \"https://api.vidu.cn/ent/v2\" },\r\n ],\r\n inputValues: {\r\n apiKey: \"\",\r\n baseUrl: \"https://api.vidu.cn/ent/v2\",\r\n },\r\n models: [\r\n {\r\n name: \"ViduQ3 turbo\",\r\n type: \"video\",\r\n modelName: \"ViduQ3-turbo\",\r\n durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], resolution: [\"540p\", \"720p\", \"1080p\"] }],\r\n mode: [\"singleImage\", \"startEndRequired\", \"text\"],\r\n audio: true,\r\n },\r\n {\r\n name: \"ViduQ3 pro\",\r\n type: \"video\",\r\n modelName: \"ViduQ3-pro\",\r\n durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], resolution: [\"540p\", \"720p\", \"1080p\"] }],\r\n mode: [\"singleImage\", \"startEndRequired\", \"text\"],\r\n audio: true,\r\n },\r\n {\r\n name: \"ViduQ2 pro fast\",\r\n type: \"video\",\r\n modelName: \"ViduQ2-pro-fast\",\r\n durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: [\"720p\", \"1080p\"] }],\r\n mode: [\"singleImage\", \"startEndRequired\"],\r\n audio: true,\r\n },\r\n {\r\n name: \"viduQ2 turbo\",\r\n type: \"video\",\r\n modelName: \"ViduQ2-turbo\",\r\n durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: [\"540p\", \"720p\", \"1080p\"] }],\r\n mode: [\"singleImage\", \"startEndRequired\"],\r\n audio: true,\r\n },\r\n {\r\n name: \"ViduQ2 pro\",\r\n type: \"video\",\r\n modelName: \"ViduQ2-pro\",\r\n durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: [\"540p\", \"720p\", \"1080p\"] }],\r\n mode: [\"singleImage\", \"startEndRequired\"], //参考生视频无有效设置值\r\n audio: true,\r\n },\r\n {\r\n name: \"ViduQ2\",\r\n type: \"video\",\r\n modelName: \"ViduQ2\",\r\n durationResolutionMap: [{ duration: [5], resolution: [\"1080p\"] }],\r\n mode: [\"text\"],\r\n audio: true,\r\n },\r\n {\r\n name: \"ViduQ1\",\r\n type: \"video\",\r\n modelName: \"ViduQ1\",\r\n durationResolutionMap: [{ duration: [5], resolution: [\"1080p\"] }],\r\n mode: [\"singleImage\", \"startEndRequired\", \"text\"],\r\n audio: true,\r\n },\r\n {\r\n name: \"ViduQ1 classic\",\r\n type: \"video\",\r\n modelName: \"viduQ1-classic\",\r\n durationResolutionMap: [{ duration: [5], resolution: [\"1080p\"] }],\r\n mode: [\"singleImage\", \"startEndRequired\"],\r\n audio: true,\r\n },\r\n {\r\n name: \"Vidu2.0\",\r\n type: \"video\",\r\n modelName: \"vidu2.0\",\r\n durationResolutionMap: [{ duration: [4, 8], resolution: [\"360p\", \"720p\", \"1080p\"] }],\r\n mode: [\"singleImage\", \"startEndRequired\"],\r\n audio: true,\r\n },\r\n {\r\n name: \"viduq1 for image\",\r\n type: \"image\",\r\n modelName: \"viduq1\",\r\n mode: [\"text\"],\r\n },\r\n {\r\n name: \"viduq2 for image\",\r\n type: \"image\",\r\n modelName: \"viduq2\",\r\n mode: [\"text\", \"singleImage\", \"multiReference\"],\r\n },\r\n ],\r\n};\r\nexports.vendor = vendor;\r\n\r\n// ==================== 适配器函数 ====================\r\n\r\n// 文本请求函数\r\nconst textRequest: (textModel: TextModel) => { url: string; model: string } = (textModel) => {\r\n throw new Error(\"当前供应商仅支持视频大模型,谢谢!\");\r\n};\r\nexports.textRequest = textRequest;\r\n\r\n//图片请求函数\r\ninterface ImageConfig {\r\n prompt: string; //图片提示词\r\n imageBase64: string[]; //输入的图片提示词\r\n size: \"1K\" | \"2K\" | \"4K\"; // 图片尺寸\r\n aspectRatio: `${number}:${number}`; // 长宽比\r\n}\r\nconst imageRequest = async (imageConfig: ImageConfig, imageModel: ImageModel) => {\r\n if (!vendor.inputValues.apiKey) throw new Error(\"缺少API Key\");\r\n const apiKey = vendor.inputValues.apiKey.replace(\"Token \", \"\");\r\n\r\n const size = imageConfig.size === \"1K\" ? \"2K\" : imageConfig.size;\r\n const sizeMap: Record> = {\r\n \"16:9\": {\r\n \"1k\": \"1920x1080\",\r\n \"2K\": \"2848x1600\",\r\n \"4K\": \"4096x2304\",\r\n },\r\n \"9:16\": {\r\n \"1k\": \"1920x1080\",\r\n \"2K\": \"1600x2848\",\r\n \"4K\": \"2304x4096\",\r\n },\r\n };\r\n\r\n const body: Record = {\r\n model: imageModel.modelName,\r\n prompt: imageConfig.prompt,\r\n aspect_ratio: sizeMap[imageConfig.aspectRatio][size],\r\n seed: 0,\r\n resolution: size,\r\n ...(imageConfig.imageBase64 && { image: imageConfig.imageBase64 }),\r\n };\r\n\r\n const createImageUrl = vendor.inputValues.baseUrl + \"/reference2image\";\r\n const response = await fetch(createImageUrl, {\r\n method: \"POST\",\r\n headers: { Authorization: `Token ${apiKey}`, \"Content-Type\": \"application/json\" },\r\n body: JSON.stringify(body),\r\n });\r\n if (!response.ok) {\r\n const errorText = await response.text(); // 获取错误信息\r\n console.error(\"请求失败,状态码:\", response.status, \", 错误信息:\", errorText);\r\n throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`);\r\n }\r\n const data = await response.json();\r\n const res = await checkTaskResult(data.task_id);\r\n if (!res.data) {\r\n throw new Error(\"图片未能生成\");\r\n }\r\n const list = JSON.parse(JSON.stringify(res.data));\r\n return list[0].url;\r\n};\r\nexports.imageRequest = imageRequest;\r\n\r\ninterface VideoConfig {\r\n duration: number;\r\n resolution: string;\r\n aspectRatio: \"16:9\" | \"9:16\";\r\n prompt: string;\r\n imageBase64?: string[];\r\n audio?: boolean;\r\n mode:\r\n | \"singleImage\" // 单图\r\n | \"multiImage\" // 多图模式\r\n | \"gridImage\" // 网格单图(传入一张图片,但该图片是网格图)\r\n | \"startEndRequired\" // 首尾帧(两张都得有)\r\n | \"endFrameOptional\" // 首尾帧(尾帧可选)\r\n | \"startFrameOptional\" // 首尾帧(首帧可选)\r\n | \"text\" // 文本生视频\r\n | (\"video\" | \"image\" | \"audio\" | \"text\")[]; // 混合参考\r\n}\r\n\r\n// 构建 各个平台的metadata参数\r\n\r\nconst buildViduMetadata = (videoConfig: VideoConfig) => ({\r\n aspect_ratio: videoConfig.aspectRatio,\r\n audio: videoConfig.audio ?? false,\r\n off_peak: false,\r\n});\r\n\r\ntype MetadataBuilder = (config: VideoConfig) => Record;\r\nconst METADATA_BUILDERS: Array<[string, MetadataBuilder]> = [[\"vidu\", buildViduMetadata]];\r\nconst buildModelMetadata = (modelName: string, videoConfig: VideoConfig) => {\r\n const lowerName = modelName.toLowerCase();\r\n const match = METADATA_BUILDERS.find(([key]) => lowerName.includes(key));\r\n return match ? match[1](videoConfig) : {};\r\n};\r\n// 检查生成物结果\r\nconst checkTaskResult = async (taskId: string) => {\r\n const queryUrl = vendor.inputValues.baseUrl + \"/tasks/{id}/creations\";\r\n const apiKey = vendor.inputValues.apiKey;\r\n const res = await pollTask(async () => {\r\n const queryResponse = await fetch(queryUrl.replace(\"{id}\", taskId), {\r\n method: \"GET\",\r\n headers: { Authorization: `Token ${apiKey}`, \"Content-Type\": \"application/json\" },\r\n });\r\n if (!queryResponse.ok) {\r\n const errorText = await queryResponse.text(); // 获取错误信息\r\n console.error(\"请求失败,状态码:\", queryResponse.status, \", 错误信息:\", errorText);\r\n throw new Error(`请求失败,状态码: ${queryResponse.status}, 错误信息: ${errorText}`);\r\n }\r\n const queryData = await queryResponse.json();\r\n const status = queryData?.state ?? queryData?.data?.state;\r\n const fail_reason = queryData?.data?.err_code ?? queryData?.data;\r\n switch (status) {\r\n case \"completed\":\r\n case \"SUCCESS\":\r\n case \"success\":\r\n return { completed: true, data: queryData.creations };\r\n case \"FAILURE\":\r\n case \"failed\":\r\n return { completed: false, error: fail_reason || \"生成失败\" };\r\n default:\r\n return { completed: false };\r\n }\r\n });\r\n if (res.error) throw new Error(res.error);\r\n return res;\r\n};\r\n\r\nconst videoRequest = async (videoConfig: VideoConfig, videoModel: VideoModel) => {\r\n if (!vendor.inputValues.apiKey) throw new Error(\"缺少API Key\");\r\n const apiKey = vendor.inputValues.apiKey.replace(\"Token \", \"\");\r\n\r\n // 构建每个模型对应的附加参数\r\n const metadata = buildModelMetadata(videoModel.modelName, videoConfig);\r\n\r\n //公共请求参数\r\n const publicBody = {\r\n model: videoModel.modelName,\r\n ...(videoConfig.imageBase64 && videoConfig.imageBase64.length ? { images: videoConfig.imageBase64 } : {}),\r\n prompt: videoConfig.prompt,\r\n size: videoConfig.resolution,\r\n duration: videoConfig.duration,\r\n metadata: metadata,\r\n };\r\n\r\n const requestUrl = vendor.inputValues.baseUrl + \"/start-end2video\";\r\n const response = await fetch(requestUrl, {\r\n method: \"POST\",\r\n headers: { Authorization: `Token ${apiKey}`, \"Content-Type\": \"application/json\" },\r\n body: JSON.stringify(publicBody),\r\n });\r\n if (!response.ok) {\r\n const errorText = await response.text(); // 获取错误信息\r\n console.error(\"请求失败,状态码:\", response.status, \", 错误信息:\", errorText);\r\n throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`);\r\n }\r\n const data = await response.json();\r\n const taskId = data.id;\r\n const result = await checkTaskResult(taskId);\r\n return result.data;\r\n};\r\nexports.videoRequest = videoRequest;\r\n\r\ninterface TTSConfig {\r\n text: string;\r\n voice: string;\r\n speechRate: number;\r\n pitchRate: number;\r\n volume: number;\r\n}\r\nconst ttsRequest = async (ttsConfig: TTSConfig, ttsModel: TTSModel) => {\r\n throw new Error(\"Vidu 暂不支持语音合成(TTS)\");\r\n};\r\n", - "volcengine.ts": "/**\r\n * Toonflow AI供应商模板 - 火山引擎(豆包)\r\n * @version 2.0\r\n */\r\n\r\n// ============================================================\r\n// 类型定义\r\n// ============================================================\r\n\r\ntype VideoMode =\r\n | \"singleImage\"\r\n | \"startEndRequired\"\r\n | \"endFrameOptional\"\r\n | \"startFrameOptional\"\r\n | \"text\"\r\n | (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[];\r\n\r\ninterface TextModel {\r\n name: string;\r\n modelName: string;\r\n type: \"text\";\r\n think: boolean;\r\n}\r\n\r\ninterface ImageModel {\r\n name: string;\r\n modelName: string;\r\n type: \"image\";\r\n mode: (\"text\" | \"singleImage\" | \"multiReference\")[];\r\n associationSkills?: string;\r\n}\r\n\r\ninterface VideoModel {\r\n name: string;\r\n modelName: string;\r\n type: \"video\";\r\n mode: VideoMode[];\r\n associationSkills?: string;\r\n audio: \"optional\" | false | true;\r\n durationResolutionMap: { duration: number[]; resolution: string[] }[];\r\n}\r\n\r\ninterface TTSModel {\r\n name: string;\r\n modelName: string;\r\n type: \"tts\";\r\n voices: { title: string; voice: string }[];\r\n}\r\n\r\ninterface VendorConfig {\r\n id: string;\r\n version: string;\r\n name: string;\r\n author: string;\r\n description?: string;\r\n icon?: string;\r\n inputs: { key: string; label: string; type: \"text\" | \"password\" | \"url\"; required: boolean; placeholder?: string }[];\r\n inputValues: Record;\r\n models: (TextModel | ImageModel | VideoModel | TTSModel)[];\r\n}\r\n\r\ntype ReferenceList =\r\n | { type: \"image\"; sourceType: \"base64\"; base64: string }\r\n | { type: \"audio\"; sourceType: \"base64\"; base64: string }\r\n | { type: \"video\"; sourceType: \"base64\"; base64: string };\r\n\r\ninterface ImageConfig {\r\n prompt: string;\r\n referenceList?: Extract[];\r\n size: \"1K\" | \"2K\" | \"4K\";\r\n aspectRatio: `${number}:${number}`;\r\n}\r\n\r\ninterface VideoConfig {\r\n duration: number;\r\n resolution: string;\r\n aspectRatio: \"16:9\" | \"9:16\";\r\n prompt: string;\r\n referenceList?: ReferenceList[];\r\n audio?: boolean;\r\n mode: VideoMode[];\r\n}\r\n\r\ninterface TTSConfig {\r\n text: string;\r\n voice: string;\r\n speechRate: number;\r\n pitchRate: number;\r\n volume: number;\r\n referenceList?: Extract[];\r\n}\r\n\r\ninterface PollResult {\r\n completed: boolean;\r\n data?: string;\r\n error?: string;\r\n}\r\n\r\n// ============================================================\r\n// 全局声明\r\n// ============================================================\r\n\r\ndeclare const axios: any;\r\ndeclare const logger: (msg: string) => void;\r\ndeclare const jsonwebtoken: any;\r\ndeclare const zipImage: (base64: string, size: number) => Promise;\r\ndeclare const zipImageResolution: (base64: string, w: number, h: number) => Promise;\r\ndeclare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise;\r\ndeclare const urlToBase64: (url: string) => Promise;\r\ndeclare const pollTask: (fn: () => Promise, interval?: number, timeout?: number) => Promise;\r\ndeclare const createOpenAI: any;\r\ndeclare const createDeepSeek: any;\r\ndeclare const createZhipu: any;\r\ndeclare const createQwen: any;\r\ndeclare const createAnthropic: any;\r\ndeclare const createOpenAICompatible: any;\r\ndeclare const createXai: any;\r\ndeclare const createMinimax: any;\r\ndeclare const createGoogleGenerativeAI: any;\r\ndeclare const exports: {\r\n vendor: VendorConfig;\r\n textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => any;\r\n imageRequest: (c: ImageConfig, m: ImageModel) => Promise;\r\n videoRequest: (c: VideoConfig, m: VideoModel) => Promise;\r\n ttsRequest: (c: TTSConfig, m: TTSModel) => Promise;\r\n checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>;\r\n updateVendor?: () => Promise;\r\n};\r\n\r\n// ============================================================\r\n// 供应商配置\r\n// ============================================================\r\n\r\nconst vendor: VendorConfig = {\r\n id: \"volcengine\",\r\n version: \"2.2\",\r\n author: \"leeqi\",\r\n name: \"火山引擎(豆包)\",\r\n description:\r\n \"火山引擎豆包大模型,支持文本、图片生成、视频生成等能力。\\n\\n需要在[火山引擎控制台](https://console.volcengine.com/ark)获取API密钥。\",\r\n icon: \"\",\r\n inputs: [\r\n { key: \"apiKey\", label: \"API密钥\", type: \"password\", required: true, placeholder: \"火山引擎API Key\" },\r\n { key: \"baseUrl\", label: \"请求地址\", type: \"url\", required: true, placeholder: \"以v3结束,示例:https://ark.cn-beijing.volces.com/api/v3\" },\r\n ],\r\n inputValues: {\r\n apiKey: \"\",\r\n baseUrl: \"https://ark.cn-beijing.volces.com/api/v3\",\r\n },\r\n models: [\r\n // ===================== 文本模型 - 推荐 =====================\r\n { name: \"Doubao-Seed-2.0-Pro\", modelName: \"doubao-seed-2-0-pro-260215\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-2.0-Lite\", modelName: \"doubao-seed-2-0-lite-260215\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-2.0-Mini\", modelName: \"doubao-seed-2-0-mini-260215\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-2.0-Code-Preview\", modelName: \"doubao-seed-2-0-code-preview-260215\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-Character\", modelName: \"doubao-seed-character-251128\", type: \"text\", think: false },\r\n // ===================== 文本模型 - 往期 =====================\r\n { name: \"Doubao-Seed-1.8\", modelName: \"doubao-seed-1-8-251228\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-Code-Preview\", modelName: \"doubao-seed-code-preview-251028\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-1.6-Lite\", modelName: \"doubao-seed-1-6-lite-251015\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-1.6-Flash(0828)\", modelName: \"doubao-seed-1-6-flash-250828\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-1.6-Vision\", modelName: \"doubao-seed-1-6-vision-250815\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-1.6(1015)\", modelName: \"doubao-seed-1-6-251015\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-1.6(0615)\", modelName: \"doubao-seed-1-6-250615\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-1.6-Flash(0615)\", modelName: \"doubao-seed-1-6-flash-250615\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-Translation\", modelName: \"doubao-seed-translation-250915\", type: \"text\", think: false },\r\n { name: \"Doubao-1.5-Pro-32K\", modelName: \"doubao-1-5-pro-32k-250115\", type: \"text\", think: false },\r\n { name: \"Doubao-1.5-Pro-32K-Character(0715)\", modelName: \"doubao-1-5-pro-32k-character-250715\", type: \"text\", think: false },\r\n { name: \"Doubao-1.5-Pro-32K-Character(0228)\", modelName: \"doubao-1-5-pro-32k-character-250228\", type: \"text\", think: false },\r\n { name: \"Doubao-1.5-Lite-32K\", modelName: \"doubao-1-5-lite-32k-250115\", type: \"text\", think: false },\r\n { name: \"Doubao-1.5-Vision-Pro-32K\", modelName: \"doubao-1-5-vision-pro-32k-250115\", type: \"text\", think: false },\r\n // ===================== 文本模型 - 第三方(火山引擎托管) =====================\r\n { name: \"GLM-4-7\", modelName: \"glm-4-7-251222\", type: \"text\", think: true },\r\n { name: \"DeepSeek-V3-2\", modelName: \"deepseek-v3-2-251201\", type: \"text\", think: true },\r\n { name: \"DeepSeek-V3-1-Terminus\", modelName: \"deepseek-v3-1-terminus\", type: \"text\", think: true },\r\n { name: \"DeepSeek-V3(0324)\", modelName: \"deepseek-v3-250324\", type: \"text\", think: false },\r\n { name: \"DeepSeek-R1(0528)\", modelName: \"deepseek-r1-250528\", type: \"text\", think: true },\r\n { name: \"Qwen3-32B\", modelName: \"qwen3-32b-20250429\", type: \"text\", think: false },\r\n { name: \"Qwen3-14B\", modelName: \"qwen3-14b-20250429\", type: \"text\", think: false },\r\n { name: \"Qwen3-8B\", modelName: \"qwen3-8b-20250429\", type: \"text\", think: false },\r\n { name: \"Qwen3-0.6B\", modelName: \"qwen3-0-6b-20250429\", type: \"text\", think: false },\r\n { name: \"Qwen2.5-72B\", modelName: \"qwen2-5-72b-20240919\", type: \"text\", think: false },\r\n { name: \"GLM-4.5-Air\", modelName: \"glm-4-5-air\", type: \"text\", think: false },\r\n // ===================== 图片生成模型 =====================\r\n {\r\n name: \"Seedream-5.0\",\r\n modelName: \"doubao-seedream-5-0-260128\",\r\n type: \"image\",\r\n mode: [\"text\", \"singleImage\", \"multiReference\"],\r\n },\r\n {\r\n name: \"Seedream-5.0-Lite\",\r\n modelName: \"doubao-seedream-5-0-lite-260128\",\r\n type: \"image\",\r\n mode: [\"text\", \"singleImage\", \"multiReference\"],\r\n },\r\n {\r\n name: \"Seedream-4.5\",\r\n modelName: \"doubao-seedream-4-5-251128\",\r\n type: \"image\",\r\n mode: [\"text\", \"singleImage\", \"multiReference\"],\r\n },\r\n {\r\n name: \"Seedream-4.0\",\r\n modelName: \"doubao-seedream-4-0-250828\",\r\n type: \"image\",\r\n mode: [\"text\", \"singleImage\", \"multiReference\"],\r\n },\r\n {\r\n name: \"Seedream-3.0-T2I\",\r\n modelName: \"doubao-seedream-3-0-t2i-250415\",\r\n type: \"image\",\r\n mode: [\"text\"],\r\n },\r\n // ===================== 视频生成模型 =====================\r\n {\r\n name: \"Seedance-2.0(音画同生)\",\r\n modelName: \"doubao-seedance-2-0-260128\",\r\n type: \"video\",\r\n mode: [\"text\", \"startFrameOptional\", [\"imageReference:9\", \"videoReference:3\", \"audioReference:3\"]],\r\n audio: \"optional\",\r\n durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: [\"480p\", \"720p\"] }],\r\n },\r\n {\r\n name: \"Seedance-2.0-Fast(音画同生)\",\r\n modelName: \"doubao-seedance-2-0-fast-260128\",\r\n type: \"video\",\r\n mode: [\"text\", \"startFrameOptional\", [\"imageReference:9\", \"videoReference:3\", \"audioReference:3\"]],\r\n audio: \"optional\",\r\n durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: [\"480p\", \"720p\"] }],\r\n },\r\n {\r\n name: \"Seedance-1.5-Pro(音画同生)\",\r\n modelName: \"doubao-seedance-1-5-pro-251215\",\r\n type: \"video\",\r\n mode: [\"text\", \"startFrameOptional\"],\r\n audio: \"optional\",\r\n durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: [\"480p\", \"720p\", \"1080p\"] }],\r\n },\r\n {\r\n name: \"Seedance-1.0-Pro\",\r\n modelName: \"doubao-seedance-1-0-pro-250528\",\r\n type: \"video\",\r\n mode: [\"text\", \"startFrameOptional\"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: [\"480p\", \"720p\", \"1080p\"] }],\r\n },\r\n {\r\n name: \"Seedance-1.0-Pro-Fast\",\r\n modelName: \"doubao-seedance-1-0-pro-fast-251015\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: [\"480p\", \"720p\", \"1080p\"] }],\r\n },\r\n {\r\n name: \"Seedance-1.0-Lite-T2V\",\r\n modelName: \"doubao-seedance-1-0-lite-t2v-250428\",\r\n type: \"video\",\r\n mode: [\"text\"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: [\"480p\", \"720p\", \"1080p\"] }],\r\n },\r\n {\r\n name: \"Seedance-1.0-Lite-I2V\",\r\n modelName: \"doubao-seedance-1-0-lite-i2v-250428\",\r\n type: \"video\",\r\n mode: [\"startFrameOptional\", [\"imageReference:4\"]],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: [\"480p\", \"720p\", \"1080p\"] }],\r\n },\r\n ],\r\n};\r\n\r\n// ============================================================\r\n// 辅助工具\r\n// ============================================================\r\n\r\nconst getHeaders = () => {\r\n if (!vendor.inputValues.apiKey) throw new Error(\"缺少API Key\");\r\n return {\r\n \"Content-Type\": \"application/json\",\r\n Authorization: `Bearer ${vendor.inputValues.apiKey.replace(/^Bearer\\s+/i, \"\")}`,\r\n };\r\n};\r\n\r\nconst getBaseUrl = () => vendor.inputValues.baseUrl.replace(/\\/+$/, \"\");\r\n\r\n// ============================================================\r\n// 适配器函数\r\n// ============================================================\r\n\r\nconst textRequest = (model: TextModel, think: boolean, thinkLevel: 0 | 1 | 2 | 3) => {\r\n if (!vendor.inputValues.apiKey) throw new Error(\"缺少API Key\");\r\n const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\\s+/i, \"\");\r\n\r\n const effortMap: Record = {\r\n 0: \"minimal\",\r\n 1: \"low\",\r\n 2: \"medium\",\r\n 3: \"high\",\r\n };\r\n\r\n return createOpenAICompatible({\r\n name: \"volcengine\",\r\n baseURL: getBaseUrl(),\r\n apiKey,\r\n fetch: async (url: string, options?: RequestInit) => {\r\n const rawBody = JSON.parse((options?.body as string) ?? \"{}\");\r\n const modifiedBody = {\r\n ...rawBody,\r\n thinking: {\r\n type: \"enabled\",\r\n },\r\n reasoning_effort: effortMap[thinkLevel],\r\n };\r\n return await fetch(url, {\r\n ...options,\r\n body: JSON.stringify(modifiedBody),\r\n });\r\n },\r\n }).chatModel(model.modelName);\r\n};\r\n\r\nconst imageRequest = async (config: ImageConfig, model: ImageModel): Promise => {\r\n const baseUrl = getBaseUrl();\r\n const headers = getHeaders();\r\n\r\n const body: any = {\r\n model: model.modelName,\r\n prompt: config.prompt || \"\",\r\n response_format: \"url\",\r\n watermark: false,\r\n };\r\n\r\n const isOldModel = model.modelName.includes(\"seedream-3-0\");\r\n const is5Lite = model.modelName.includes(\"seedream-5-0-lite\");\r\n\r\n // sequential_image_generation 仅 seedream 5.0-lite/4.5/4.0 支持\r\n if (!isOldModel) {\r\n body.sequential_image_generation = \"disabled\";\r\n }\r\n\r\n // 参考图片:单图为 string,多图为 array(seedream-3.0-t2i 不支持 image 参数)\r\n if (!isOldModel && config.referenceList && config.referenceList.length > 0) {\r\n const images = config.referenceList.map((ref) => ref.base64);\r\n body.image = images.length === 1 ? images[0] : images;\r\n }\r\n\r\n // 尺寸处理:优先使用推荐像素值,未匹配则直接传分辨率字符串让模型自行决定\r\n const [w, h] = config.aspectRatio.split(\":\").map(Number);\r\n const sizeTable: Record> = {\r\n \"1K\": {\r\n \"1:1\": \"1024x1024\",\r\n \"4:3\": \"1152x864\",\r\n \"3:4\": \"864x1152\",\r\n \"16:9\": \"1280x720\",\r\n \"9:16\": \"720x1280\",\r\n \"3:2\": \"1248x832\",\r\n \"2:3\": \"832x1248\",\r\n \"21:9\": \"1512x648\",\r\n },\r\n \"2K\": {\r\n \"1:1\": \"2048x2048\",\r\n \"4:3\": \"2304x1728\",\r\n \"3:4\": \"1728x2304\",\r\n \"16:9\": \"2848x1600\",\r\n \"9:16\": \"1600x2848\",\r\n \"3:2\": \"2496x1664\",\r\n \"2:3\": \"1664x2496\",\r\n \"21:9\": \"3136x1344\",\r\n },\r\n \"4K\": {\r\n \"1:1\": \"4096x4096\",\r\n \"4:3\": \"4704x3520\",\r\n \"3:4\": \"3520x4704\",\r\n \"16:9\": \"5504x3040\",\r\n \"9:16\": \"3040x5504\",\r\n \"3:2\": \"4992x3328\",\r\n \"2:3\": \"3328x4992\",\r\n \"21:9\": \"6240x2656\",\r\n },\r\n };\r\n\r\n const sizeKey = config.size || \"2K\";\r\n const ratioKey = config.aspectRatio;\r\n const table = sizeTable[sizeKey];\r\n\r\n if (table && table[ratioKey]) {\r\n // 推荐像素值匹配到了,但需要检查是否满足模型最低像素要求\r\n const [pw, ph] = table[ratioKey].split(\"x\").map(Number);\r\n const totalPixels = pw * ph;\r\n if (isOldModel) {\r\n // seedream-3.0-t2i: 像素范围 [512x512, 2048x2048]\r\n body.size = table[ratioKey];\r\n } else if (totalPixels < 3686400) {\r\n // 1K 像素值不满足新模型最低要求,直接传 \"2K\" 让模型自行决定\r\n body.size = \"2K\";\r\n } else if (is5Lite && totalPixels > 10404496) {\r\n // seedream-5.0-lite 最高 10404496,4K 超限,回退传 \"2K\"\r\n body.size = \"2K\";\r\n } else {\r\n body.size = table[ratioKey];\r\n }\r\n } else if (isOldModel) {\r\n // seedream-3.0-t2i: 像素范围 [512x512, 2048x2048],直接按比例计算\r\n const base = sizeKey === \"1K\" ? 1024 : 2048;\r\n const calcW = Math.min(2048, Math.round(base * Math.sqrt(w / h)));\r\n const calcH = Math.min(2048, Math.round(base * Math.sqrt(h / w)));\r\n body.size = `${Math.max(512, calcW)}x${Math.max(512, calcH)}`;\r\n } else {\r\n // 新模型未匹配推荐值时,直接传分辨率字符串(方式1),由模型根据 prompt 自行决定尺寸\r\n // seedream 5.0-lite 支持 \"2K\"/\"3K\",seedream 4.5 支持 \"2K\"/\"4K\",seedream 4.0 支持 \"1K\"/\"2K\"/\"4K\"\r\n if (is5Lite) {\r\n body.size = sizeKey === \"4K\" ? \"3K\" : sizeKey === \"1K\" ? \"2K\" : sizeKey;\r\n } else {\r\n body.size = sizeKey === \"1K\" ? \"2K\" : sizeKey;\r\n }\r\n }\r\n\r\n logger(`[图片生成] 请求模型: ${model.modelName}, 尺寸: ${body.size}`);\r\n\r\n const response = await axios.post(`${baseUrl}/images/generations`, body, { headers });\r\n const data = response.data;\r\n\r\n if (data?.error) {\r\n throw new Error(`图片生成失败:${data.error.message || data.error.code}`);\r\n }\r\n\r\n // 从 data 数组中提取第一张成功的图片\r\n if (data?.data && data.data.length > 0) {\r\n for (const item of data.data) {\r\n if (item.url) {\r\n return await urlToBase64(item.url);\r\n }\r\n if (item.b64_json) {\r\n return item.b64_json;\r\n }\r\n if (item.error) {\r\n throw new Error(`图片生成失败:${item.error.message || item.error.code}`);\r\n }\r\n }\r\n }\r\n\r\n throw new Error(\"图片生成失败:未返回有效结果\");\r\n};\r\n\r\nconst videoRequest = async (config: VideoConfig, model: VideoModel): Promise => {\r\n const baseUrl = getBaseUrl();\r\n const headers = getHeaders();\r\n\r\n const content: any[] = [];\r\n\r\n if (config.prompt) {\r\n content.push({ type: \"text\", text: config.prompt });\r\n }\r\n\r\n const activeMode = config.mode && config.mode.length > 0 ? config.mode[0] : \"text\";\r\n\r\n if (typeof activeMode === \"string\") {\r\n switch (activeMode) {\r\n case \"singleImage\": {\r\n const firstImage = config.referenceList?.find((r) => r.type === \"image\");\r\n if (firstImage) {\r\n content.push({\r\n type: \"image_url\",\r\n image_url: { url: firstImage.base64 },\r\n role: \"first_frame\",\r\n });\r\n }\r\n break;\r\n }\r\n case \"startFrameOptional\": {\r\n const images = config.referenceList?.filter((r) => r.type === \"image\") ?? [];\r\n if (images.length > 0) {\r\n content.push({\r\n type: \"image_url\",\r\n image_url: { url: images[0].base64 },\r\n role: \"first_frame\",\r\n });\r\n if (images.length > 1) {\r\n content.push({\r\n type: \"image_url\",\r\n image_url: { url: images[1].base64 },\r\n role: \"last_frame\",\r\n });\r\n }\r\n }\r\n break;\r\n }\r\n case \"startEndRequired\": {\r\n const images = config.referenceList?.filter((r) => r.type === \"image\") ?? [];\r\n if (images.length >= 2) {\r\n content.push({\r\n type: \"image_url\",\r\n image_url: { url: images[0].base64 },\r\n role: \"first_frame\",\r\n });\r\n content.push({\r\n type: \"image_url\",\r\n image_url: { url: images[1].base64 },\r\n role: \"last_frame\",\r\n });\r\n }\r\n break;\r\n }\r\n case \"endFrameOptional\": {\r\n const images = config.referenceList?.filter((r) => r.type === \"image\") ?? [];\r\n if (images.length > 0) {\r\n content.push({\r\n type: \"image_url\",\r\n image_url: { url: images[0].base64 },\r\n role: \"first_frame\",\r\n });\r\n if (images.length > 1) {\r\n content.push({\r\n type: \"image_url\",\r\n image_url: { url: images[1].base64 },\r\n role: \"last_frame\",\r\n });\r\n }\r\n }\r\n break;\r\n }\r\n case \"text\":\r\n default:\r\n break;\r\n }\r\n } else if (Array.isArray(activeMode)) {\r\n // 多模态参考模式:按类型分别提取并添加\r\n const imageRefs = config.referenceList?.filter((r) => r.type === \"image\") ?? [];\r\n const videoRefs = config.referenceList?.filter((r) => r.type === \"video\") ?? [];\r\n const audioRefs = config.referenceList?.filter((r) => r.type === \"audio\") ?? [];\r\n\r\n for (const refDef of activeMode) {\r\n if (typeof refDef === \"string\") {\r\n if (refDef.startsWith(\"imageReference:\")) {\r\n const maxCount = parseInt(refDef.split(\":\")[1], 10);\r\n for (const ref of imageRefs.slice(0, maxCount)) {\r\n content.push({\r\n type: \"image_url\",\r\n image_url: { url: ref.base64 },\r\n role: \"reference_image\",\r\n });\r\n }\r\n } else if (refDef.startsWith(\"videoReference:\")) {\r\n const maxCount = parseInt(refDef.split(\":\")[1], 10);\r\n for (const ref of videoRefs.slice(0, maxCount)) {\r\n content.push({\r\n type: \"video_url\",\r\n video_url: { url: ref.base64 },\r\n role: \"reference_video\",\r\n });\r\n }\r\n } else if (refDef.startsWith(\"audioReference:\")) {\r\n const maxCount = parseInt(refDef.split(\":\")[1], 10);\r\n for (const ref of audioRefs.slice(0, maxCount)) {\r\n content.push({\r\n type: \"audio_url\",\r\n audio_url: { url: ref.base64 },\r\n role: \"reference_audio\",\r\n });\r\n }\r\n }\r\n }\r\n }\r\n }\r\n\r\n const body: any = {\r\n model: model.modelName,\r\n content,\r\n ratio: config.aspectRatio,\r\n duration: config.duration,\r\n resolution: config.resolution || \"720p\",\r\n watermark: false,\r\n };\r\n\r\n if (model.audio === \"optional\") {\r\n body.generate_audio = config.audio !== false;\r\n } else if (model.audio === true) {\r\n body.generate_audio = true;\r\n } else {\r\n body.generate_audio = false;\r\n }\r\n\r\n logger(`[视频生成] 提交任务, 模型: ${model.modelName}, 时长: ${config.duration}s, 分辨率: ${config.resolution}`);\r\n\r\n const createResponse = await axios.post(`${baseUrl}/contents/generations/tasks`, body, { headers });\r\n const taskId = createResponse.data?.id;\r\n\r\n if (!taskId) {\r\n throw new Error(\"视频生成任务创建失败:未返回任务ID\");\r\n }\r\n\r\n logger(`[视频生成] 任务已创建, ID: ${taskId}`);\r\n\r\n const result = await pollTask(\r\n async (): Promise => {\r\n const queryResponse = await axios.get(`${baseUrl}/contents/generations/tasks/${taskId}`, { headers });\r\n const task = queryResponse.data;\r\n\r\n logger(`[视频生成] 任务状态: ${task.status}`);\r\n\r\n switch (task.status) {\r\n case \"succeeded\":\r\n if (task.content?.video_url) {\r\n return { completed: true, data: task.content.video_url };\r\n }\r\n return { completed: true, error: \"任务成功但未返回视频URL\" };\r\n case \"failed\":\r\n return { completed: true, error: task.error?.message || \"视频生成失败\" };\r\n case \"expired\":\r\n return { completed: true, error: \"视频生成任务超时\" };\r\n case \"cancelled\":\r\n return { completed: true, error: \"视频生成任务已取消\" };\r\n default:\r\n return { completed: false };\r\n }\r\n },\r\n 10000,\r\n 600000,\r\n );\r\n\r\n if (result.error) {\r\n throw new Error(result.error);\r\n }\r\n\r\n return await urlToBase64(result.data!);\r\n};\r\n\r\nconst ttsRequest = async (config: TTSConfig, model: TTSModel): Promise => {\r\n return \"\";\r\n};\r\n\r\nconst checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => {\r\n return { hasUpdate: false, latestVersion: \"2.0\", notice: \"\" };\r\n};\r\n\r\nconst updateVendor = async (): Promise => {\r\n return \"\";\r\n};\r\n\r\n// ============================================================\r\n// 导出\r\n// ============================================================\r\n\r\nexports.vendor = vendor;\r\nexports.textRequest = textRequest;\r\nexports.imageRequest = imageRequest;\r\nexports.videoRequest = videoRequest;\r\nexports.ttsRequest = ttsRequest;\r\nexports.checkForUpdates = checkForUpdates;\r\nexports.updateVendor = updateVendor;\r\n\r\nexport {};\r\n" + "volcengine.ts": "/**\r\n * Toonflow AI供应商模板 - 火山引擎(豆包)\r\n * @version 2.0\r\n */\r\n\r\n// ============================================================\r\n// 类型定义\r\n// ============================================================\r\n\r\ntype VideoMode =\r\n | \"singleImage\"\r\n | \"startEndRequired\"\r\n | \"endFrameOptional\"\r\n | \"startFrameOptional\"\r\n | \"text\"\r\n | (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[];\r\n\r\ninterface TextModel {\r\n name: string;\r\n modelName: string;\r\n type: \"text\";\r\n think: boolean;\r\n}\r\n\r\ninterface ImageModel {\r\n name: string;\r\n modelName: string;\r\n type: \"image\";\r\n mode: (\"text\" | \"singleImage\" | \"multiReference\")[];\r\n associationSkills?: string;\r\n}\r\n\r\ninterface VideoModel {\r\n name: string;\r\n modelName: string;\r\n type: \"video\";\r\n mode: VideoMode[];\r\n associationSkills?: string;\r\n audio: \"optional\" | false | true;\r\n durationResolutionMap: { duration: number[]; resolution: string[] }[];\r\n}\r\n\r\ninterface TTSModel {\r\n name: string;\r\n modelName: string;\r\n type: \"tts\";\r\n voices: { title: string; voice: string }[];\r\n}\r\n\r\ninterface VendorConfig {\r\n id: string;\r\n version: string;\r\n name: string;\r\n author: string;\r\n description?: string;\r\n icon?: string;\r\n inputs: { key: string; label: string; type: \"text\" | \"password\" | \"url\"; required: boolean; placeholder?: string }[];\r\n inputValues: Record;\r\n models: (TextModel | ImageModel | VideoModel | TTSModel)[];\r\n}\r\n\r\ntype ReferenceList =\r\n | { type: \"image\"; sourceType: \"base64\"; base64: string }\r\n | { type: \"audio\"; sourceType: \"base64\"; base64: string }\r\n | { type: \"video\"; sourceType: \"base64\"; base64: string };\r\n\r\ninterface ImageConfig {\r\n prompt: string;\r\n referenceList?: Extract[];\r\n size: \"1K\" | \"2K\" | \"4K\";\r\n aspectRatio: `${number}:${number}`;\r\n}\r\n\r\ninterface VideoConfig {\r\n duration: number;\r\n resolution: string;\r\n aspectRatio: \"16:9\" | \"9:16\";\r\n prompt: string;\r\n referenceList?: ReferenceList[];\r\n audio?: boolean;\r\n mode: VideoMode[];\r\n}\r\n\r\ninterface TTSConfig {\r\n text: string;\r\n voice: string;\r\n speechRate: number;\r\n pitchRate: number;\r\n volume: number;\r\n referenceList?: Extract[];\r\n}\r\n\r\ninterface PollResult {\r\n completed: boolean;\r\n data?: string;\r\n error?: string;\r\n}\r\n\r\n// ============================================================\r\n// 全局声明\r\n// ============================================================\r\n\r\ndeclare const axios: any;\r\ndeclare const logger: (msg: string) => void;\r\ndeclare const jsonwebtoken: any;\r\ndeclare const zipImage: (base64: string, size: number) => Promise;\r\ndeclare const zipImageResolution: (base64: string, w: number, h: number) => Promise;\r\ndeclare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise;\r\ndeclare const urlToBase64: (url: string) => Promise;\r\ndeclare const pollTask: (fn: () => Promise, interval?: number, timeout?: number) => Promise;\r\ndeclare const createOpenAI: any;\r\ndeclare const createDeepSeek: any;\r\ndeclare const createZhipu: any;\r\ndeclare const createQwen: any;\r\ndeclare const createAnthropic: any;\r\ndeclare const createOpenAICompatible: any;\r\ndeclare const createXai: any;\r\ndeclare const createMinimax: any;\r\ndeclare const createGoogleGenerativeAI: any;\r\ndeclare const exports: {\r\n vendor: VendorConfig;\r\n textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => any;\r\n imageRequest: (c: ImageConfig, m: ImageModel) => Promise;\r\n videoRequest: (c: VideoConfig, m: VideoModel) => Promise;\r\n ttsRequest: (c: TTSConfig, m: TTSModel) => Promise;\r\n checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>;\r\n updateVendor?: () => Promise;\r\n};\r\n\r\n// ============================================================\r\n// 供应商配置\r\n// ============================================================\r\n\r\nconst vendor: VendorConfig = {\r\n id: \"volcengine\",\r\n version: \"2.3\",\r\n author: \"leeqi\",\r\n name: \"火山引擎(豆包)\",\r\n description: \"火山引擎豆包大模型,支持文本、图片生成、视频生成等能力。\\n\\n需要在[火山引擎控制台](https://console.volcengine.com/ark)获取API密钥。\",\r\n icon: \"\",\r\n inputs: [\r\n { key: \"apiKey\", label: \"API密钥\", type: \"password\", required: true, placeholder: \"火山引擎API Key\" },\r\n { key: \"baseUrl\", label: \"请求地址\", type: \"url\", required: true, placeholder: \"以v3结束,示例:https://ark.cn-beijing.volces.com/api/v3\" },\r\n ],\r\n inputValues: {\r\n apiKey: \"\",\r\n baseUrl: \"https://ark.cn-beijing.volces.com/api/v3\",\r\n },\r\n models: [\r\n // ===================== 文本模型 - 推荐 =====================\r\n { name: \"Doubao-Seed-2.0-Pro\", modelName: \"doubao-seed-2-0-pro-260215\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-2.0-Lite\", modelName: \"doubao-seed-2-0-lite-260215\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-2.0-Mini\", modelName: \"doubao-seed-2-0-mini-260215\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-2.0-Code-Preview\", modelName: \"doubao-seed-2-0-code-preview-260215\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-Character\", modelName: \"doubao-seed-character-251128\", type: \"text\", think: false },\r\n // ===================== 文本模型 - 往期 =====================\r\n { name: \"Doubao-Seed-1.8\", modelName: \"doubao-seed-1-8-251228\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-Code-Preview\", modelName: \"doubao-seed-code-preview-251028\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-1.6-Lite\", modelName: \"doubao-seed-1-6-lite-251015\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-1.6-Flash(0828)\", modelName: \"doubao-seed-1-6-flash-250828\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-1.6-Vision\", modelName: \"doubao-seed-1-6-vision-250815\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-1.6(1015)\", modelName: \"doubao-seed-1-6-251015\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-1.6(0615)\", modelName: \"doubao-seed-1-6-250615\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-1.6-Flash(0615)\", modelName: \"doubao-seed-1-6-flash-250615\", type: \"text\", think: true },\r\n { name: \"Doubao-Seed-Translation\", modelName: \"doubao-seed-translation-250915\", type: \"text\", think: false },\r\n { name: \"Doubao-1.5-Pro-32K\", modelName: \"doubao-1-5-pro-32k-250115\", type: \"text\", think: false },\r\n { name: \"Doubao-1.5-Pro-32K-Character(0715)\", modelName: \"doubao-1-5-pro-32k-character-250715\", type: \"text\", think: false },\r\n { name: \"Doubao-1.5-Pro-32K-Character(0228)\", modelName: \"doubao-1-5-pro-32k-character-250228\", type: \"text\", think: false },\r\n { name: \"Doubao-1.5-Lite-32K\", modelName: \"doubao-1-5-lite-32k-250115\", type: \"text\", think: false },\r\n { name: \"Doubao-1.5-Vision-Pro-32K\", modelName: \"doubao-1-5-vision-pro-32k-250115\", type: \"text\", think: false },\r\n // ===================== 文本模型 - 第三方(火山引擎托管) =====================\r\n { name: \"GLM-4-7\", modelName: \"glm-4-7-251222\", type: \"text\", think: true },\r\n { name: \"DeepSeek-V3-2\", modelName: \"deepseek-v3-2-251201\", type: \"text\", think: true },\r\n { name: \"DeepSeek-V3-1-Terminus\", modelName: \"deepseek-v3-1-terminus\", type: \"text\", think: true },\r\n { name: \"DeepSeek-V3(0324)\", modelName: \"deepseek-v3-250324\", type: \"text\", think: false },\r\n { name: \"DeepSeek-R1(0528)\", modelName: \"deepseek-r1-250528\", type: \"text\", think: true },\r\n { name: \"Qwen3-32B\", modelName: \"qwen3-32b-20250429\", type: \"text\", think: false },\r\n { name: \"Qwen3-14B\", modelName: \"qwen3-14b-20250429\", type: \"text\", think: false },\r\n { name: \"Qwen3-8B\", modelName: \"qwen3-8b-20250429\", type: \"text\", think: false },\r\n { name: \"Qwen3-0.6B\", modelName: \"qwen3-0-6b-20250429\", type: \"text\", think: false },\r\n { name: \"Qwen2.5-72B\", modelName: \"qwen2-5-72b-20240919\", type: \"text\", think: false },\r\n { name: \"GLM-4.5-Air\", modelName: \"glm-4-5-air\", type: \"text\", think: false },\r\n // ===================== 图片生成模型 =====================\r\n {\r\n name: \"Seedream-5.0\",\r\n modelName: \"doubao-seedream-5-0-260128\",\r\n type: \"image\",\r\n mode: [\"text\", \"singleImage\", \"multiReference\"],\r\n },\r\n {\r\n name: \"Seedream-5.0-Lite\",\r\n modelName: \"doubao-seedream-5-0-lite-260128\",\r\n type: \"image\",\r\n mode: [\"text\", \"singleImage\", \"multiReference\"],\r\n },\r\n {\r\n name: \"Seedream-4.5\",\r\n modelName: \"doubao-seedream-4-5-251128\",\r\n type: \"image\",\r\n mode: [\"text\", \"singleImage\", \"multiReference\"],\r\n },\r\n {\r\n name: \"Seedream-4.0\",\r\n modelName: \"doubao-seedream-4-0-250828\",\r\n type: \"image\",\r\n mode: [\"text\", \"singleImage\", \"multiReference\"],\r\n },\r\n {\r\n name: \"Seedream-3.0-T2I\",\r\n modelName: \"doubao-seedream-3-0-t2i-250415\",\r\n type: \"image\",\r\n mode: [\"text\"],\r\n },\r\n // ===================== 视频生成模型 =====================\r\n {\r\n name: \"Seedance-2.0(音画同生)\",\r\n modelName: \"doubao-seedance-2-0-260128\",\r\n type: \"video\",\r\n mode: [\"text\", \"startFrameOptional\", [\"imageReference:9\", \"videoReference:3\", \"audioReference:3\"]],\r\n audio: \"optional\",\r\n durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: [\"480p\", \"720p\"] }],\r\n },\r\n {\r\n name: \"Seedance-2.0-Fast(音画同生)\",\r\n modelName: \"doubao-seedance-2-0-fast-260128\",\r\n type: \"video\",\r\n mode: [\"text\", \"startFrameOptional\", [\"imageReference:9\", \"videoReference:3\", \"audioReference:3\"]],\r\n audio: \"optional\",\r\n durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: [\"480p\", \"720p\"] }],\r\n },\r\n {\r\n name: \"Seedance-1.5-Pro(音画同生)\",\r\n modelName: \"doubao-seedance-1-5-pro-251215\",\r\n type: \"video\",\r\n mode: [\"text\", \"startFrameOptional\"],\r\n audio: \"optional\",\r\n durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: [\"480p\", \"720p\", \"1080p\"] }],\r\n },\r\n {\r\n name: \"Seedance-1.0-Pro\",\r\n modelName: \"doubao-seedance-1-0-pro-250528\",\r\n type: \"video\",\r\n mode: [\"text\", \"startFrameOptional\"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: [\"480p\", \"720p\", \"1080p\"] }],\r\n },\r\n {\r\n name: \"Seedance-1.0-Pro-Fast\",\r\n modelName: \"doubao-seedance-1-0-pro-fast-251015\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: [\"480p\", \"720p\", \"1080p\"] }],\r\n },\r\n {\r\n name: \"Seedance-1.0-Lite-T2V\",\r\n modelName: \"doubao-seedance-1-0-lite-t2v-250428\",\r\n type: \"video\",\r\n mode: [\"text\"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: [\"480p\", \"720p\", \"1080p\"] }],\r\n },\r\n {\r\n name: \"Seedance-1.0-Lite-I2V\",\r\n modelName: \"doubao-seedance-1-0-lite-i2v-250428\",\r\n type: \"video\",\r\n mode: [\"startFrameOptional\", [\"imageReference:4\"]],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: [\"480p\", \"720p\", \"1080p\"] }],\r\n },\r\n ],\r\n};\r\n\r\n// ============================================================\r\n// 辅助工具\r\n// ============================================================\r\n\r\nconst getHeaders = () => {\r\n if (!vendor.inputValues.apiKey) throw new Error(\"缺少API Key\");\r\n return {\r\n \"Content-Type\": \"application/json\",\r\n Authorization: `Bearer ${vendor.inputValues.apiKey.replace(/^Bearer\\s+/i, \"\")}`,\r\n };\r\n};\r\n\r\nconst getBaseUrl = () => vendor.inputValues.baseUrl.replace(/\\/+$/, \"\");\r\n\r\n// ============================================================\r\n// 适配器函数\r\n// ============================================================\r\n\r\nconst textRequest = (model: TextModel, think: boolean, thinkLevel: 0 | 1 | 2 | 3) => {\r\n if (!vendor.inputValues.apiKey) throw new Error(\"缺少API Key\");\r\n const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\\s+/i, \"\");\r\n\r\n const effortMap: Record = {\r\n 0: \"minimal\",\r\n 1: \"low\",\r\n 2: \"medium\",\r\n 3: \"high\",\r\n };\r\n\r\n return createOpenAICompatible({\r\n name: \"volcengine\",\r\n baseURL: getBaseUrl(),\r\n apiKey,\r\n fetch: async (url: string, options?: RequestInit) => {\r\n const rawBody = JSON.parse((options?.body as string) ?? \"{}\");\r\n const modifiedBody = {\r\n ...rawBody,\r\n thinking: {\r\n type: \"enabled\",\r\n },\r\n reasoning_effort: effortMap[thinkLevel],\r\n };\r\n return await fetch(url, {\r\n ...options,\r\n body: JSON.stringify(modifiedBody),\r\n });\r\n },\r\n }).chatModel(model.modelName);\r\n};\r\n\r\nconst imageRequest = async (config: ImageConfig, model: ImageModel): Promise => {\r\n const baseUrl = getBaseUrl();\r\n const headers = getHeaders();\r\n\r\n const body: any = {\r\n model: model.modelName,\r\n prompt: config.prompt || \"\",\r\n response_format: \"url\",\r\n watermark: false,\r\n };\r\n\r\n const isOldModel = model.modelName.includes(\"seedream-3-0\");\r\n const is5Lite = model.modelName.includes(\"seedream-5-0-lite\");\r\n\r\n // sequential_image_generation 仅 seedream 5.0-lite/4.5/4.0 支持\r\n if (!isOldModel) {\r\n body.sequential_image_generation = \"disabled\";\r\n }\r\n\r\n // 参考图片:单图为 string,多图为 array(seedream-3.0-t2i 不支持 image 参数)\r\n if (!isOldModel && config.referenceList && config.referenceList.length > 0) {\r\n const images = config.referenceList.map((ref) => ref.base64);\r\n body.image = images.length === 1 ? images[0] : images;\r\n }\r\n\r\n // 尺寸处理:优先使用推荐像素值,未匹配则直接传分辨率字符串让模型自行决定\r\n const [w, h] = config.aspectRatio.split(\":\").map(Number);\r\n const sizeTable: Record> = {\r\n \"1K\": {\r\n \"1:1\": \"1024x1024\",\r\n \"4:3\": \"1152x864\",\r\n \"3:4\": \"864x1152\",\r\n \"16:9\": \"1280x720\",\r\n \"9:16\": \"720x1280\",\r\n \"3:2\": \"1248x832\",\r\n \"2:3\": \"832x1248\",\r\n \"21:9\": \"1512x648\",\r\n },\r\n \"2K\": {\r\n \"1:1\": \"2048x2048\",\r\n \"4:3\": \"2304x1728\",\r\n \"3:4\": \"1728x2304\",\r\n \"16:9\": \"2848x1600\",\r\n \"9:16\": \"1600x2848\",\r\n \"3:2\": \"2496x1664\",\r\n \"2:3\": \"1664x2496\",\r\n \"21:9\": \"3136x1344\",\r\n },\r\n \"4K\": {\r\n \"1:1\": \"4096x4096\",\r\n \"4:3\": \"4704x3520\",\r\n \"3:4\": \"3520x4704\",\r\n \"16:9\": \"5504x3040\",\r\n \"9:16\": \"3040x5504\",\r\n \"3:2\": \"4992x3328\",\r\n \"2:3\": \"3328x4992\",\r\n \"21:9\": \"6240x2656\",\r\n },\r\n };\r\n\r\n const sizeKey = config.size || \"2K\";\r\n const ratioKey = config.aspectRatio;\r\n const table = sizeTable[sizeKey];\r\n\r\n if (table && table[ratioKey]) {\r\n // 推荐像素值匹配到了,但需要检查是否满足模型最低像素要求\r\n const [pw, ph] = table[ratioKey].split(\"x\").map(Number);\r\n const totalPixels = pw * ph;\r\n if (isOldModel) {\r\n // seedream-3.0-t2i: 像素范围 [512x512, 2048x2048]\r\n body.size = table[ratioKey];\r\n } else if (totalPixels < 3686400) {\r\n // 1K 像素值不满足新模型最低要求,直接传 \"2K\" 让模型自行决定\r\n body.size = \"2K\";\r\n } else if (is5Lite && totalPixels > 10404496) {\r\n // seedream-5.0-lite 最高 10404496,4K 超限,回退传 \"2K\"\r\n body.size = \"2K\";\r\n } else {\r\n body.size = table[ratioKey];\r\n }\r\n } else if (isOldModel) {\r\n // seedream-3.0-t2i: 像素范围 [512x512, 2048x2048],直接按比例计算\r\n const base = sizeKey === \"1K\" ? 1024 : 2048;\r\n const calcW = Math.min(2048, Math.round(base * Math.sqrt(w / h)));\r\n const calcH = Math.min(2048, Math.round(base * Math.sqrt(h / w)));\r\n body.size = `${Math.max(512, calcW)}x${Math.max(512, calcH)}`;\r\n } else {\r\n // 新模型未匹配推荐值时,直接传分辨率字符串(方式1),由模型根据 prompt 自行决定尺寸\r\n // seedream 5.0-lite 支持 \"2K\"/\"3K\",seedream 4.5 支持 \"2K\"/\"4K\",seedream 4.0 支持 \"1K\"/\"2K\"/\"4K\"\r\n if (is5Lite) {\r\n body.size = sizeKey === \"4K\" ? \"3K\" : sizeKey === \"1K\" ? \"2K\" : sizeKey;\r\n } else {\r\n body.size = sizeKey === \"1K\" ? \"2K\" : sizeKey;\r\n }\r\n }\r\n\r\n logger(`[图片生成] 请求模型: ${model.modelName}, 尺寸: ${body.size}`);\r\n\r\n const response = await axios.post(`${baseUrl}/images/generations`, body, { headers });\r\n const data = response.data;\r\n\r\n if (data?.error) {\r\n throw new Error(`图片生成失败:${data.error.message || data.error.code}`);\r\n }\r\n\r\n // 从 data 数组中提取第一张成功的图片\r\n if (data?.data && data.data.length > 0) {\r\n for (const item of data.data) {\r\n if (item.url) {\r\n return await urlToBase64(item.url);\r\n }\r\n if (item.b64_json) {\r\n return item.b64_json;\r\n }\r\n if (item.error) {\r\n throw new Error(`图片生成失败:${item.error.message || item.error.code}`);\r\n }\r\n }\r\n }\r\n\r\n throw new Error(\"图片生成失败:未返回有效结果\");\r\n};\r\n\r\nconst videoRequest = async (config: VideoConfig, model: VideoModel): Promise => {\r\n const baseUrl = getBaseUrl();\r\n const headers = getHeaders();\r\n\r\n const content: any[] = [];\r\n\r\n if (config.prompt) {\r\n content.push({ type: \"text\", text: config.prompt });\r\n }\r\n\r\n if (typeof config.mode === \"string\") {\r\n switch (config.mode) {\r\n case \"singleImage\": {\r\n const firstImage = config.referenceList?.find((r) => r.type === \"image\");\r\n if (firstImage) {\r\n content.push({\r\n type: \"image_url\",\r\n image_url: { url: firstImage.base64 },\r\n role: \"first_frame\",\r\n });\r\n }\r\n break;\r\n }\r\n case \"startFrameOptional\": {\r\n const images = config.referenceList?.filter((r) => r.type === \"image\") ?? [];\r\n if (images.length > 0) {\r\n content.push({\r\n type: \"image_url\",\r\n image_url: { url: images[0].base64 },\r\n role: \"first_frame\",\r\n });\r\n if (images.length > 1) {\r\n content.push({\r\n type: \"image_url\",\r\n image_url: { url: images[1].base64 },\r\n role: \"last_frame\",\r\n });\r\n }\r\n }\r\n break;\r\n }\r\n case \"startEndRequired\": {\r\n const images = config.referenceList?.filter((r) => r.type === \"image\") ?? [];\r\n if (images.length >= 2) {\r\n content.push({\r\n type: \"image_url\",\r\n image_url: { url: images[0].base64 },\r\n role: \"first_frame\",\r\n });\r\n content.push({\r\n type: \"image_url\",\r\n image_url: { url: images[1].base64 },\r\n role: \"last_frame\",\r\n });\r\n }\r\n break;\r\n }\r\n case \"endFrameOptional\": {\r\n const images = config.referenceList?.filter((r) => r.type === \"image\") ?? [];\r\n if (images.length > 0) {\r\n content.push({\r\n type: \"image_url\",\r\n image_url: { url: images[0].base64 },\r\n role: \"first_frame\",\r\n });\r\n if (images.length > 1) {\r\n content.push({\r\n type: \"image_url\",\r\n image_url: { url: images[1].base64 },\r\n role: \"last_frame\",\r\n });\r\n }\r\n }\r\n break;\r\n }\r\n case \"text\":\r\n default:\r\n break;\r\n }\r\n } else if (Array.isArray(config.mode)) {\r\n // 多模态参考模式:按类型分别提取并添加\r\n const imageRefs = config.referenceList?.filter((r) => r.type === \"image\") ?? [];\r\n const videoRefs = config.referenceList?.filter((r) => r.type === \"video\") ?? [];\r\n const audioRefs = config.referenceList?.filter((r) => r.type === \"audio\") ?? [];\r\n\r\n for (const refDef of config.mode) {\r\n if (typeof refDef === \"string\") {\r\n if (refDef.startsWith(\"imageReference:\")) {\r\n const maxCount = parseInt(refDef.split(\":\")[1], 10);\r\n for (const ref of imageRefs.slice(0, maxCount)) {\r\n content.push({\r\n type: \"image_url\",\r\n image_url: { url: ref.base64 },\r\n role: \"reference_image\",\r\n });\r\n }\r\n } else if (refDef.startsWith(\"videoReference:\")) {\r\n const maxCount = parseInt(refDef.split(\":\")[1], 10);\r\n for (const ref of videoRefs.slice(0, maxCount)) {\r\n content.push({\r\n type: \"video_url\",\r\n video_url: { url: ref.base64 },\r\n role: \"reference_video\",\r\n });\r\n }\r\n } else if (refDef.startsWith(\"audioReference:\")) {\r\n const maxCount = parseInt(refDef.split(\":\")[1], 10);\r\n for (const ref of audioRefs.slice(0, maxCount)) {\r\n content.push({\r\n type: \"audio_url\",\r\n audio_url: { url: ref.base64 },\r\n role: \"reference_audio\",\r\n });\r\n }\r\n }\r\n }\r\n }\r\n }\r\n\r\n const body: any = {\r\n model: model.modelName,\r\n content,\r\n ratio: config.aspectRatio,\r\n duration: config.duration,\r\n resolution: config.resolution || \"720p\",\r\n watermark: false,\r\n };\r\n\r\n if (model.audio === \"optional\") {\r\n body.generate_audio = config.audio !== false;\r\n } else if (model.audio === true) {\r\n body.generate_audio = true;\r\n } else {\r\n body.generate_audio = false;\r\n }\r\n\r\n logger(`[视频生成] 提交任务, 模型: ${model.modelName}, 时长: ${config.duration}s, 分辨率: ${config.resolution}`);\r\n\r\n const createResponse = await axios.post(`${baseUrl}/contents/generations/tasks`, body, { headers });\r\n const taskId = createResponse.data?.id;\r\n\r\n if (!taskId) {\r\n throw new Error(\"视频生成任务创建失败:未返回任务ID\");\r\n }\r\n\r\n logger(`[视频生成] 任务已创建, ID: ${taskId}`);\r\n\r\n const result = await pollTask(\r\n async (): Promise => {\r\n const queryResponse = await axios.get(`${baseUrl}/contents/generations/tasks/${taskId}`, { headers });\r\n const task = queryResponse.data;\r\n\r\n logger(`[视频生成] 任务状态: ${task.status}`);\r\n\r\n switch (task.status) {\r\n case \"succeeded\":\r\n if (task.content?.video_url) {\r\n return { completed: true, data: task.content.video_url };\r\n }\r\n return { completed: true, error: \"任务成功但未返回视频URL\" };\r\n case \"failed\":\r\n return { completed: true, error: task.error?.message || \"视频生成失败\" };\r\n case \"expired\":\r\n return { completed: true, error: \"视频生成任务超时\" };\r\n case \"cancelled\":\r\n return { completed: true, error: \"视频生成任务已取消\" };\r\n default:\r\n return { completed: false };\r\n }\r\n },\r\n 10000,\r\n 600000,\r\n );\r\n\r\n if (result.error) {\r\n throw new Error(result.error);\r\n }\r\n\r\n return await urlToBase64(result.data!);\r\n};\r\n\r\nconst ttsRequest = async (config: TTSConfig, model: TTSModel): Promise => {\r\n return \"\";\r\n};\r\n\r\nconst checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => {\r\n return { hasUpdate: false, latestVersion: \"2.0\", notice: \"\" };\r\n};\r\n\r\nconst updateVendor = async (): Promise => {\r\n return \"\";\r\n};\r\n\r\n// ============================================================\r\n// 导出\r\n// ============================================================\r\n\r\nexports.vendor = vendor;\r\nexports.textRequest = textRequest;\r\nexports.imageRequest = imageRequest;\r\nexports.videoRequest = videoRequest;\r\nexports.ttsRequest = ttsRequest;\r\nexports.checkForUpdates = checkForUpdates;\r\nexports.updateVendor = updateVendor;\r\n\r\nexport {};\r\n" } \ No newline at end of file