video-flow-toon/src/lib/vendor.json

10 lines
136 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"grsai.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; //唯一ID作为文件名存储用户磁盘上禁止符号\r\n version: string; //版本号格式为x.y需遵守语义化版本控制\r\n name: string; //供应商名称\r\n author: string; //作者\r\n description?: string; //描述支持Markdown格式\r\n icon?: string; //图标仅支持Base64格式建议尺寸为128x128像素\r\n inputs: { key: string; label: string; type: \"text\" | \"password\" | \"url\"; required: boolean; placeholder?: string }[];\r\n inputValues: Record<string, string>;\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<ReferenceList, { type: \"image\" }>[];\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<ReferenceList, { type: \"audio\" }>[];\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; // HTTP请求库\r\ndeclare const logger: (msg: string) => void; // 日志函数\r\ndeclare const jsonwebtoken: any; // JWT处理库\r\ndeclare const zipImage: (base64: string, size: number) => Promise<string>; // 图片压缩函数返回有头base64字符串\r\ndeclare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>; // 图片分辨率调整函数返回有头base64字符串\r\ndeclare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>; // 图片合成函数返回有头base64字符串\r\ndeclare const urlToBase64: (url: string) => Promise<string>; // URL转Base64函数返回有头base64字符串\r\ndeclare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>; // 轮询函数fn为异步函数interval为轮询间隔timeout为超时时间返回fn的结果\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<string>; //图片模型返回有头base64字符串\r\n videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>; //视频模型返回有头base64字符串\r\n ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>; //暂未开放语音模型返回有头base64字符串\r\n checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>; //检查更新函数返回是否有更新和最新版本号和更公告支持Markdown格式\r\n updateVendor?: () => Promise<string>; //更新函数,返回最新的代码文本\r\n};\r\n\r\n// ============================================================\r\n// 供应商配置\r\n// ============================================================\r\n\r\nconst vendor: VendorConfig = {\r\n id: \"grsai\",\r\n version: \"2.0\",\r\n author: \"Toonflow\",\r\n name: \"Grsai\",\r\n description: \"Grsai AI平台适配支持文生图、图生图、文生视频、Gemini兼容文本模型 \\n [前往中转平台](https://tf.grsai.ai/zh)\",\r\n inputs: [\r\n { key: \"apiKey\", label: \"API密钥\", type: \"password\", required: true },\r\n { key: \"baseUrl\", label: \"请求地址\", type: \"url\", required: true, placeholder: \"示例https://grsai.dakka.com.cn\" },\r\n ],\r\n inputValues: { apiKey: \"\", baseUrl: \"https://grsai.dakka.com.cn\" },\r\n models: [\r\n { name: \"Nano Banana Fast\", modelName: \"nano-banana-fast\", type: \"image\", mode: [\"text\", \"singleImage\", \"multiReference\"] },\r\n { name: \"Nano Banana 2\", modelName: \"nano-banana-2\", type: \"image\", mode: [\"text\", \"singleImage\", \"multiReference\"] },\r\n { name: \"Nano Banana Pro\", modelName: \"nano-banana-pro\", type: \"image\", mode: [\"text\", \"singleImage\", \"multiReference\"] },\r\n ],\r\n};\r\n\r\n// ============================================================\r\n// 辅助工具\r\n// ============================================================\r\n\r\nconst getHeaders = () => {\r\n const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\\s+/i, \"\");\r\n return {\r\n \"Content-Type\": \"application/json\",\r\n Authorization: `Bearer ${apiKey}`,\r\n };\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 createGoogleGenerativeAI({\r\n baseURL: `${vendor.inputValues.baseUrl}/v1beta`,\r\n apiKey,\r\n }).chat(model.modelName);\r\n};\r\n\r\nconst imageRequest = async (config: ImageConfig, model: ImageModel): Promise<string> => {\r\n if (!vendor.inputValues.apiKey) throw new Error(\"缺少API Key\");\r\n const baseUrl = vendor.inputValues.baseUrl;\r\n const headers = getHeaders();\r\n\r\n // 构造请求参数\r\n const requestBody: any = {\r\n model: model.modelName,\r\n prompt: config.prompt,\r\n aspectRatio: config.aspectRatio,\r\n webHook: \"-1\",\r\n shutProgress: true,\r\n };\r\n\r\n // 补充模型专属参数\r\n if (model.modelName.startsWith(\"nano-banana\")) {\r\n requestBody.imageSize = config.size;\r\n } else {\r\n requestBody.size = config.aspectRatio;\r\n requestBody.variants = 1;\r\n }\r\n\r\n // 处理参考图\r\n if (config.referenceList && config.referenceList.length > 0) {\r\n requestBody.urls = config.referenceList.map((img) => img.base64);\r\n }\r\n\r\n // 选择接口路径\r\n const apiPath = model.modelName.startsWith(\"nano-banana\") ? \"/v1/draw/nano-banana\" : \"/v1/draw/completions\";\r\n\r\n logger(`开始提交图片生成任务,模型:${model.modelName}`);\r\n const submitResp = await axios.post(`${baseUrl}${apiPath}`, requestBody, { headers });\r\n if (submitResp.data.code !== 0) throw new Error(`任务提交失败:${submitResp.data.msg}`);\r\n\r\n const taskId = submitResp.data.data.id;\r\n logger(`图片任务提交成功任务ID${taskId}`);\r\n\r\n // 轮询结果\r\n const pollResult = await pollTask(\r\n async () => {\r\n const resp = await axios.post(`${baseUrl}/v1/draw/result`, { id: taskId }, { headers });\r\n if (resp.data.code !== 0) return { completed: true, error: resp.data.msg };\r\n\r\n const taskData = resp.data.data;\r\n if (taskData.status === \"failed\") return { completed: true, error: taskData.failure_reason || taskData.error };\r\n if (taskData.status === \"succeeded\") {\r\n const imgUrl = taskData.results?.[0]?.url || taskData.url;\r\n return { completed: true, data: imgUrl };\r\n }\r\n logger(`图片任务生成中,进度:${taskData.progress}%`);\r\n return { completed: false };\r\n },\r\n 3000,\r\n 600000,\r\n );\r\n\r\n if (pollResult.error) throw new Error(pollResult.error);\r\n logger(`图片生成完成开始转换Base64`);\r\n return await urlToBase64(pollResult.data!);\r\n};\r\n\r\nconst videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {\r\n if (!vendor.inputValues.apiKey) throw new Error(\"缺少API Key\");\r\n const baseUrl = vendor.inputValues.baseUrl;\r\n const headers = getHeaders();\r\n\r\n // 构造请求参数\r\n const requestBody: any = {\r\n model: model.modelName,\r\n prompt: config.prompt,\r\n aspectRatio: config.aspectRatio,\r\n webHook: \"-1\",\r\n shutProgress: true,\r\n };\r\n\r\n // 处理参考资源\r\n if (config.referenceList && config.referenceList.length > 0) {\r\n const imageRefs = config.referenceList.filter((item) => item.type === \"image\") as Extract<ReferenceList, { type: \"image\" }>[];\r\n if (config.mode.includes(\"endFrameOptional\") && imageRefs.length >= 1) {\r\n requestBody.firstFrameUrl = imageRefs[0].base64;\r\n if (imageRefs.length >= 2) requestBody.lastFrameUrl = imageRefs[1].base64;\r\n } else if (config.mode.some((m) => Array.isArray(m) && m.includes(\"imageReference:3\"))) {\r\n requestBody.urls = imageRefs.map((img) => img.base64);\r\n }\r\n }\r\n\r\n logger(`开始提交视频生成任务,模型:${model.modelName}`);\r\n const submitResp = await axios.post(`${baseUrl}/v1/video/veo`, requestBody, { headers });\r\n if (submitResp.data.code !== 0) throw new Error(`任务提交失败:${submitResp.data.msg}`);\r\n\r\n const taskId = submitResp.data.data.id;\r\n logger(`视频任务提交成功任务ID${taskId}`);\r\n\r\n // 轮询结果\r\n const pollResult = await pollTask(\r\n async () => {\r\n const resp = await axios.post(`${baseUrl}/v1/draw/result`, { id: taskId }, { headers });\r\n if (resp.data.code !== 0) return { completed: true, error: resp.data.msg };\r\n\r\n const taskData = resp.data.data;\r\n if (taskData.status === \"failed\") return { completed: true, error: taskData.failure_reason || taskData.error };\r\n if (taskData.status === \"succeeded\") {\r\n return { completed: true, data: taskData.url };\r\n }\r\n logger(`视频任务生成中,进度:${taskData.progress}%`);\r\n return { completed: false };\r\n },\r\n 5000,\r\n 1800000,\r\n );\r\n\r\n if (pollResult.error) throw new Error(pollResult.error);\r\n logger(`视频生成完成开始转换Base64`);\r\n return await urlToBase64(pollResult.data!);\r\n};\r\n\r\nconst ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<string> => {\r\n return \"\";\r\n};\r\n\r\nconst checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => {\r\n return { hasUpdate: false, latestVersion: \"1.0\", notice: \"## 新版本更新公告\" };\r\n};\r\n\r\nconst updateVendor = async (): Promise<string> => {\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\n// 这行代码用于确保当前文件被识别为模块,避免全局变量冲突\r\nexport {};\r\n",
"klingai.ts": "/**\r\n * Toonflow AI供应商模板 - 可灵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<string, string>;\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<ReferenceList, { type: \"image\" }>[];\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<ReferenceList, { type: \"audio\" }>[];\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<string>;\r\ndeclare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>;\r\ndeclare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>;\r\ndeclare const urlToBase64: (url: string) => Promise<string>;\r\ndeclare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>;\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<string>;\r\n videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>;\r\n ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>;\r\n checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>;\r\n updateVendor?: () => Promise<string>;\r\n};\r\n\r\n// ============================================================\r\n// 供应商配置\r\n// ============================================================\r\n\r\nconst vendor: VendorConfig = {\r\n id: \"klingai\",\r\n version: \"2.0\",\r\n author: \"Toonflow\",\r\n name: \"可灵AI\",\r\n description:\r\n \"可灵AI视频生成\\n\\n支持可灵全系列视频模型包括 kling-video-o1、kling-v3-omni、kling-v3、kling-v2-6、kling-v2-5-turbo、kling-v2-1、kling-v2-master、kling-v1-6、kling-v1-5、kling-v1 等。\\n\\n需要在[可灵AI开放平台](https://klingai.com)\\n\\n获取 Access Key 和 Secret Key。\",\r\n inputs: [\r\n { key: \"accessKey\", label: \"Access Key\", type: \"password\", required: true, placeholder: \"请输入可灵AI的Access Key\" },\r\n { key: \"secretKey\", label: \"Secret Key\", type: \"password\", required: true, placeholder: \"请输入可灵AI的Secret Key\" },\r\n { key: \"baseUrl\", label: \"请求地址\", type: \"url\", required: true, placeholder: \"默认https://api-beijing.klingai.com\" },\r\n ],\r\n inputValues: { accessKey: \"\", secretKey: \"\", baseUrl: \"https://api-beijing.klingai.com\" },\r\n models: [\r\n // kling-video-o1 (Omni)\r\n {\r\n name: \"kling-video-o1 标准\",\r\n modelName: \"kling-video-o1:std\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\", \"startEndRequired\", [\"imageReference:7\", \"videoReference:1\"]],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [5, 10], resolution: [\"720p\"] }],\r\n },\r\n {\r\n name: \"kling-video-o1 专家\",\r\n modelName: \"kling-video-o1:pro\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\", \"startEndRequired\", [\"imageReference:7\", \"videoReference:1\"]],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [5, 10], resolution: [\"720p\"] }],\r\n },\r\n // kling-v3-omni (Omni)\r\n {\r\n name: \"kling-v3-omni 标准\",\r\n modelName: \"kling-v3-omni:std\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\", \"startEndRequired\", [\"imageReference:7\", \"videoReference:1\"]],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: [\"720p\"] }],\r\n },\r\n {\r\n name: \"kling-v3-omni 专家\",\r\n modelName: \"kling-v3-omni:pro\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\", \"startEndRequired\", [\"imageReference:7\", \"videoReference:1\"]],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: [\"720p\"] }],\r\n },\r\n // kling-v3\r\n {\r\n name: \"kling-v3 标准\",\r\n modelName: \"kling-v3:std\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\", \"startEndRequired\"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: [\"720p\"] }],\r\n },\r\n {\r\n name: \"kling-v3 专家\",\r\n modelName: \"kling-v3:pro\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\", \"startEndRequired\"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: [\"720p\"] }],\r\n },\r\n // kling-v2-6\r\n {\r\n name: \"kling-v2-6 标准\",\r\n modelName: \"kling-v2-6:std\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [5, 10], resolution: [\"720p\"] }],\r\n },\r\n {\r\n name: \"kling-v2-6 专家\",\r\n modelName: \"kling-v2-6:pro\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\", \"startEndRequired\"],\r\n audio: \"optional\",\r\n durationResolutionMap: [{ duration: [5, 10], resolution: [\"1080p\"] }],\r\n },\r\n // kling-v2-5-turbo\r\n {\r\n name: \"kling-v2-5-turbo 标准\",\r\n modelName: \"kling-v2-5-turbo:std\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [5, 10], resolution: [\"1080p\"] }],\r\n },\r\n {\r\n name: \"kling-v2-5-turbo 专家\",\r\n modelName: \"kling-v2-5-turbo:pro\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\", \"startEndRequired\"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [5, 10], resolution: [\"1080p\"] }],\r\n },\r\n // kling-v2-1\r\n {\r\n name: \"kling-v2-1 标准\",\r\n modelName: \"kling-v2-1:std\",\r\n type: \"video\",\r\n mode: [\"singleImage\"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [5, 10], resolution: [\"720p\"] }],\r\n },\r\n {\r\n name: \"kling-v2-1 专家\",\r\n modelName: \"kling-v2-1:pro\",\r\n type: \"video\",\r\n mode: [\"singleImage\", \"startEndRequired\"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [5, 10], resolution: [\"1080p\"] }],\r\n },\r\n // kling-v2-1-master\r\n {\r\n name: \"kling-v2-1 Master\",\r\n modelName: \"kling-v2-1-master:pro\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [5, 10], resolution: [\"1080p\"] }],\r\n },\r\n // kling-v2-master\r\n {\r\n name: \"kling-v2 Master\",\r\n modelName: \"kling-v2-master:pro\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [5, 10], resolution: [\"720p\"] }],\r\n },\r\n // kling-v1-6\r\n {\r\n name: \"kling-v1-6 标准\",\r\n modelName: \"kling-v1-6:std\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\", [\"imageReference:4\"]],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [5, 10], resolution: [\"720p\"] }],\r\n },\r\n {\r\n name: \"kling-v1-6 专家\",\r\n modelName: \"kling-v1-6:pro\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\", \"endFrameOptional\", [\"imageReference:4\"]],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [5, 10], resolution: [\"1080p\"] }],\r\n },\r\n // kling-v1-5\r\n {\r\n name: \"kling-v1-5 标准\",\r\n modelName: \"kling-v1-5:std\",\r\n type: \"video\",\r\n mode: [\"singleImage\"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [5, 10], resolution: [\"720p\"] }],\r\n },\r\n {\r\n name: \"kling-v1-5 专家\",\r\n modelName: \"kling-v1-5:pro\",\r\n type: \"video\",\r\n mode: [\"singleImage\", \"endFrameOptional\"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [5, 10], resolution: [\"1080p\"] }],\r\n },\r\n // kling-v1\r\n {\r\n name: \"kling-v1 标准\",\r\n modelName: \"kling-v1:std\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\", \"startEndRequired\"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [5, 10], resolution: [\"720p\"] }],\r\n },\r\n {\r\n name: \"kling-v1 专家\",\r\n modelName: \"kling-v1:pro\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\", \"startEndRequired\"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [5, 10], resolution: [\"720p\"] }],\r\n },\r\n ],\r\n};\r\n\r\n// ============================================================\r\n// 辅助工具\r\n// ============================================================\r\n\r\n/**\r\n * 生成可灵AI的JWT鉴权Token\r\n */\r\nconst generateAuthToken = (): string => {\r\n const now = Math.floor(Date.now() / 1000);\r\n const payload = {\r\n iss: vendor.inputValues.accessKey,\r\n exp: now + 1800,\r\n nbf: now - 5,\r\n };\r\n return jsonwebtoken.sign(payload, vendor.inputValues.secretKey, {\r\n algorithm: \"HS256\",\r\n header: { alg: \"HS256\", typ: \"JWT\" },\r\n });\r\n};\r\n\r\n/**\r\n * 获取基础请求地址\r\n */\r\nconst getBaseUrl = (): string => {\r\n return vendor.inputValues.baseUrl || \"https://api-beijing.klingai.com\";\r\n};\r\n\r\n/**\r\n * 从 ReferenceList 条目中提取可用的数据字符串\r\n * 对于 url 类型返回 url对于 base64 类型返回纯 base64去掉 data: 前缀)\r\n */\r\nconst extractRawBase64 = (ref: ReferenceList): string => {\r\n return ref.base64.replace(/^data:[^;]+;base64,/, \"\");\r\n};\r\n\r\n/**\r\n * 从 ReferenceList 条目中提取带头的 base64 或 url\r\n * 用于 omni-video 接口,该接口的 image_url 支持带前缀的 base64 和 url\r\n */\r\nconst extractImageUrl = (ref: ReferenceList): string => {\r\n return ref.base64.startsWith(\"data:\") ? ref.base64 : `data:image/jpeg;base64,${ref.base64}`;\r\n};\r\n\r\n/**\r\n * 提交任务并轮询获取结果的通用函数\r\n */\r\nconst submitAndPoll = async (submitUrl: string, queryUrlBase: string, requestBody: any): Promise<string> => {\r\n const token = generateAuthToken();\r\n\r\n logger(`开始提交可灵AI视频生成任务: ${submitUrl}`);\r\n logger(\r\n `请求参数: ${JSON.stringify({\r\n ...requestBody,\r\n image: requestBody.image ? \"[BASE64]\" : undefined,\r\n image_tail: requestBody.image_tail ? \"[BASE64]\" : undefined,\r\n image_list: requestBody.image_list ? \"[IMAGES]\" : undefined,\r\n })}`,\r\n );\r\n\r\n const submitResp = await axios.post(submitUrl, requestBody, {\r\n headers: {\r\n \"Content-Type\": \"application/json\",\r\n Authorization: `Bearer ${token}`,\r\n },\r\n });\r\n\r\n if (submitResp.data.code !== 0) {\r\n throw new Error(`提交任务失败: ${submitResp.data.message || JSON.stringify(submitResp.data)}`);\r\n }\r\n\r\n const taskId = submitResp.data.data.task_id;\r\n logger(`任务已提交任务ID: ${taskId}`);\r\n\r\n const result = await pollTask(\r\n async () => {\r\n const freshToken = generateAuthToken();\r\n const queryResp = await axios.get(`${queryUrlBase}/${taskId}`, {\r\n headers: {\r\n Authorization: `Bearer ${freshToken}`,\r\n },\r\n });\r\n\r\n if (queryResp.data.code !== 0) {\r\n return { completed: true, error: `查询任务失败: ${queryResp.data.message}` };\r\n }\r\n\r\n const taskData = queryResp.data.data;\r\n const status = taskData.task_status;\r\n logger(`轮询中... 任务状态: ${status}`);\r\n\r\n if (status === \"succeed\") {\r\n const videoUrl = taskData.task_result?.videos?.[0]?.url;\r\n if (!videoUrl) {\r\n return { completed: true, error: \"任务完成但未获取到视频URL\" };\r\n }\r\n return { completed: true, data: videoUrl };\r\n }\r\n\r\n if (status === \"failed\") {\r\n return { completed: true, error: `视频生成失败: ${taskData.task_status_msg || \"未知错误\"}` };\r\n }\r\n\r\n return { completed: false };\r\n },\r\n 5000,\r\n 600000,\r\n );\r\n\r\n if (result.error) throw new Error(result.error);\r\n logger(`视频生成完成正在转换为Base64...`);\r\n return await urlToBase64(result.data!);\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 throw new Error(\"可灵AI不支持文本模型\");\r\n};\r\n\r\nconst imageRequest = async (config: ImageConfig, model: ImageModel): Promise<string> => {\r\n throw new Error(\"可灵AI不支持图片模型\");\r\n};\r\n\r\nconst videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {\r\n if (!vendor.inputValues.accessKey) throw new Error(\"缺少Access Key\");\r\n if (!vendor.inputValues.secretKey) throw new Error(\"缺少Secret Key\");\r\n\r\n const baseUrl = getBaseUrl();\r\n\r\n // 解析 modelName格式kling-video-o1:pro => modelName=kling-video-o1, mode=pro\r\n const colonIdx = model.modelName.indexOf(\":\");\r\n const modelName = colonIdx > -1 ? model.modelName.substring(0, colonIdx) : model.modelName;\r\n const mode = colonIdx > -1 ? model.modelName.substring(colonIdx + 1) : \"pro\";\r\n\r\n // 判断是否为 Omni 模型\r\n const isOmniModel = modelName === \"kling-video-o1\" || modelName === \"kling-v3-omni\";\r\n\r\n // 判断当前选中的视频生成模式\r\n const currentMode = config.mode;\r\n const isText = currentMode.includes(\"text\");\r\n const isSingleImage = currentMode.includes(\"singleImage\");\r\n const isStartEndRequired = currentMode.includes(\"startEndRequired\");\r\n const isEndFrameOptional = currentMode.includes(\"endFrameOptional\");\r\n const isStartFrameOptional = currentMode.includes(\"startFrameOptional\");\r\n const hasMultiRef = currentMode.some((m) => Array.isArray(m));\r\n\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\r\n // =====================================================\r\n // Omni 模型 —— 使用 /v1/videos/omni-video 接口\r\n // =====================================================\r\n if (isOmniModel) {\r\n const requestBody: any = {\r\n model_name: modelName,\r\n mode: mode,\r\n duration: String(config.duration),\r\n sound: config.audio === true ? \"on\" : \"off\",\r\n };\r\n\r\n if (config.prompt) {\r\n requestBody.prompt = config.prompt;\r\n }\r\n\r\n if (isSingleImage && imageRefs.length > 0) {\r\n const imageUrl = extractImageUrl(imageRefs[0]);\r\n requestBody.image_list = [{ image_url: imageUrl, type: \"first_frame\" }];\r\n if (!requestBody.prompt) requestBody.prompt = \"根据图片生成视频\";\r\n } else if (isStartEndRequired && imageRefs.length >= 2) {\r\n const firstUrl = extractImageUrl(imageRefs[0]);\r\n const endUrl = extractImageUrl(imageRefs[1]);\r\n requestBody.image_list = [\r\n { image_url: firstUrl, type: \"first_frame\" },\r\n { image_url: endUrl, type: \"end_frame\" },\r\n ];\r\n if (!requestBody.prompt) requestBody.prompt = \"根据首尾帧图片生成过渡视频\";\r\n } else if (isEndFrameOptional && imageRefs.length >= 1) {\r\n const firstUrl = extractImageUrl(imageRefs[0]);\r\n requestBody.image_list = [{ image_url: firstUrl, type: \"first_frame\" }];\r\n if (imageRefs.length >= 2) {\r\n const endUrl = extractImageUrl(imageRefs[1]);\r\n requestBody.image_list.push({ image_url: endUrl, type: \"end_frame\" });\r\n }\r\n if (!requestBody.prompt) requestBody.prompt = \"根据图片生成视频\";\r\n } else if (isStartFrameOptional && imageRefs.length >= 1) {\r\n if (imageRefs.length >= 2) {\r\n const firstUrl = extractImageUrl(imageRefs[0]);\r\n const endUrl = extractImageUrl(imageRefs[1]);\r\n requestBody.image_list = [\r\n { image_url: firstUrl, type: \"first_frame\" },\r\n { image_url: endUrl, type: \"end_frame\" },\r\n ];\r\n } else {\r\n const endUrl = extractImageUrl(imageRefs[0]);\r\n requestBody.image_list = [{ image_url: endUrl, type: \"end_frame\" }];\r\n }\r\n if (!requestBody.prompt) requestBody.prompt = \"根据图片生成视频\";\r\n } else if (hasMultiRef && (imageRefs.length > 0 || videoRefs.length > 0)) {\r\n requestBody.image_list = [];\r\n for (let i = 0; i < imageRefs.length; i++) {\r\n const imageUrl = extractImageUrl(imageRefs[i]);\r\n requestBody.image_list.push({ image_url: imageUrl });\r\n }\r\n if (!requestBody.prompt) {\r\n const refs = imageRefs.map((_, idx) => `<<<image_${idx + 1}>>>`).join(\"、\");\r\n requestBody.prompt = `参考${refs}生成视频`;\r\n }\r\n }\r\n\r\n // 文生视频或无图片输入时需要设置宽高比\r\n const hasImageInput = requestBody.image_list && requestBody.image_list.length > 0;\r\n if (!hasImageInput) {\r\n requestBody.aspect_ratio = config.aspectRatio || \"16:9\";\r\n if (!requestBody.prompt) throw new Error(\"文生视频模式需要提供提示词\");\r\n }\r\n\r\n const apiPath = \"/v1/videos/omni-video\";\r\n return await submitAndPoll(`${baseUrl}${apiPath}`, `${baseUrl}${apiPath}`, requestBody);\r\n }\r\n\r\n // =====================================================\r\n // 非 Omni 模型 —— 根据模式选择不同接口\r\n // =====================================================\r\n\r\n // 多图参考模式 —— 使用 /v1/videos/multi-image2video 接口(仅 kling-v1-6 支持)\r\n if (hasMultiRef && imageRefs.length > 0) {\r\n const imageList = [];\r\n for (let i = 0; i < imageRefs.length; i++) {\r\n const rawBase64 = extractRawBase64(imageRefs[i]);\r\n imageList.push({ image: rawBase64 });\r\n }\r\n\r\n const requestBody: any = {\r\n model_name: modelName,\r\n image_list: imageList,\r\n prompt: config.prompt || \"根据参考图片生成视频\",\r\n mode: mode,\r\n duration: String(config.duration),\r\n aspect_ratio: config.aspectRatio || \"16:9\",\r\n };\r\n\r\n const apiPath = \"/v1/videos/multi-image2video\";\r\n return await submitAndPoll(`${baseUrl}${apiPath}`, `${baseUrl}${apiPath}`, requestBody);\r\n }\r\n\r\n // 文生视频模式 —— 使用 /v1/videos/text2video 接口\r\n if (isText) {\r\n if (!config.prompt) throw new Error(\"文生视频模式需要提供提示词\");\r\n\r\n const requestBody: any = {\r\n model_name: modelName,\r\n prompt: config.prompt,\r\n mode: mode,\r\n duration: String(config.duration),\r\n aspect_ratio: config.aspectRatio || \"16:9\",\r\n sound: config.audio === true ? \"on\" : \"off\",\r\n };\r\n\r\n const apiPath = \"/v1/videos/text2video\";\r\n return await submitAndPoll(`${baseUrl}${apiPath}`, `${baseUrl}${apiPath}`, requestBody);\r\n }\r\n\r\n // 图生视频模式(单图 / 首尾帧 / 尾帧可选等)—— 使用 /v1/videos/image2video 接口\r\n if ((isSingleImage || isStartEndRequired || isEndFrameOptional || isStartFrameOptional) && imageRefs.length > 0) {\r\n const requestBody: any = {\r\n model_name: modelName,\r\n prompt: config.prompt || \"根据图片生成视频\",\r\n mode: mode,\r\n duration: String(config.duration),\r\n sound: config.audio === true ? \"on\" : \"off\",\r\n };\r\n\r\n if (isSingleImage) {\r\n requestBody.image = extractRawBase64(imageRefs[0]);\r\n } else if (isStartEndRequired && imageRefs.length >= 2) {\r\n requestBody.image = extractRawBase64(imageRefs[0]);\r\n requestBody.image_tail = extractRawBase64(imageRefs[1]);\r\n } else if (isEndFrameOptional) {\r\n requestBody.image = extractRawBase64(imageRefs[0]);\r\n if (imageRefs.length >= 2) {\r\n requestBody.image_tail = extractRawBase64(imageRefs[1]);\r\n }\r\n } else if (isStartFrameOptional) {\r\n if (imageRefs.length >= 2) {\r\n requestBody.image = extractRawBase64(imageRefs[0]);\r\n requestBody.image_tail = extractRawBase64(imageRefs[1]);\r\n } else {\r\n requestBody.image = extractRawBase64(imageRefs[0]);\r\n }\r\n }\r\n\r\n const apiPath = \"/v1/videos/image2video\";\r\n return await submitAndPoll(`${baseUrl}${apiPath}`, `${baseUrl}${apiPath}`, requestBody);\r\n }\r\n\r\n throw new Error(\"不支持的视频生成模式或缺少必要的输入参数\");\r\n};\r\n\r\nconst ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<string> => {\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\n\r\n// 这行代码用于确保当前文件被识别为模块,避免全局变量冲突\r\nexport {};",
"minimax.ts": "/**\r\n * Toonflow AI供应商模板 - MiniMax(海螺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<string, string>;\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<ReferenceList, { type: \"image\" }>[];\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<ReferenceList, { type: \"audio\" }>[];\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<string>;\r\ndeclare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>;\r\ndeclare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>;\r\ndeclare const urlToBase64: (url: string) => Promise<string>;\r\ndeclare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>;\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 uploadReference: (base64: string, fileType: \"image\" | \"audio\" | \"video\") => Promise<ReferenceList>;\r\n imageRequest: (c: ImageConfig, m: ImageModel) => Promise<string>;\r\n videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>;\r\n ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>;\r\n checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>;\r\n updateVendor?: () => Promise<string>;\r\n};\r\n\r\n// ============================================================\r\n// 供应商配置\r\n// ============================================================\r\n\r\nconst vendor: VendorConfig = {\r\n id: \"minimax\",\r\n version: \"2.1\",\r\n author: \"Toonflow\",\r\n name: \"MiniMax(海螺AI)\",\r\n description: \"MiniMax官方接口适配支持M系列推理文本模型、文生图/图生图、视频生成(文生视频、图生视频、首尾帧生成)能力 \\n [前往平台](https://minimaxi.com/)\",\r\n inputs: [\r\n { key: \"apiKey\", label: \"API密钥\", type: \"password\", required: true },\r\n { key: \"baseUrl\", label: \"请求地址\", type: \"url\", required: true, placeholder: \"示例https://api.minimaxi.com\" },\r\n ],\r\n inputValues: { apiKey: \"\", baseUrl: \"https://api.minimaxi.com\" },\r\n models: [\r\n // 文本模型\r\n { name: \"MiniMax-M2.7 (推理版)\", modelName: \"MiniMax-M2.7\", type: \"text\", think: true },\r\n { name: \"MiniMax-M2.7 极速版 (推理版)\", modelName: \"MiniMax-M2.7-highspeed\", type: \"text\", think: true },\r\n { name: \"MiniMax-M2.5 (推理版)\", modelName: \"MiniMax-M2.5\", type: \"text\", think: true },\r\n { name: \"MiniMax-M2.5 极速版 (推理版)\", modelName: \"MiniMax-M2.5-highspeed\", type: \"text\", think: true },\r\n { name: \"MiniMax-M2.1 (编程版)\", modelName: \"MiniMax-M2.1\", type: \"text\", think: true },\r\n { name: \"MiniMax-M2.1 极速版 (编程版)\", modelName: \"MiniMax-M2.1-highspeed\", type: \"text\", think: true },\r\n { name: \"MiniMax-M2 (Agent版)\", modelName: \"MiniMax-M2\", type: \"text\", think: false },\r\n // 图片模型\r\n { name: \"海螺图像V1\", modelName: \"image-01\", type: \"image\", mode: [\"text\", \"singleImage\"] },\r\n { name: \"海螺图像V1 Live版\", modelName: \"image-01-live\", type: \"image\", mode: [\"text\", \"singleImage\"], associationSkills: \"支持自定义画风\" },\r\n // 视频模型\r\n {\r\n name: \"海螺2.3\",\r\n modelName: \"MiniMax-Hailuo-2.3\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\"],\r\n audio: false,\r\n durationResolutionMap: [\r\n { duration: [6], resolution: [\"768P\", \"1080P\"] },\r\n { duration: [10], resolution: [\"768P\"] },\r\n ],\r\n },\r\n {\r\n name: \"海螺2.3极速版\",\r\n modelName: \"MiniMax-Hailuo-2.3-Fast\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\"],\r\n audio: false,\r\n durationResolutionMap: [\r\n { duration: [6], resolution: [\"768P\", \"1080P\"] },\r\n { duration: [10], resolution: [\"768P\"] },\r\n ],\r\n },\r\n {\r\n name: \"海螺02\",\r\n modelName: \"MiniMax-Hailuo-02\",\r\n type: \"video\",\r\n mode: [\"text\", \"singleImage\", \"startEndRequired\"],\r\n audio: false,\r\n durationResolutionMap: [\r\n { duration: [6], resolution: [\"512P\", \"768P\", \"1080P\"] },\r\n { duration: [10], resolution: [\"512P\", \"768P\"] },\r\n ],\r\n },\r\n ],\r\n};\r\n\r\n// ============================================================\r\n// 辅助工具\r\n// ============================================================\r\n\r\n/**\r\n * 获取请求头\r\n */\r\nconst getHeaders = (): Record<string, string> => {\r\n const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\\s+/i, \"\");\r\n return {\r\n Authorization: `Bearer ${apiKey}`,\r\n \"Content-Type\": \"application/json\",\r\n };\r\n};\r\n\r\n/**\r\n * 获取基础请求地址\r\n */\r\nconst getBaseUrl = (): string => {\r\n return vendor.inputValues.baseUrl.replace(/\\/$/, \"\");\r\n};\r\n\r\n/**\r\n * 从 ReferenceList 条目中提取有头 base64 字符串\r\n */\r\nconst extractBase64WithHead = (ref: ReferenceList): string => {\r\n return ref.base64.startsWith(\"data:\") ? ref.base64 : `data:image/png;base64,${ref.base64}`;\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 const baseUrl = getBaseUrl();\r\n\r\n const openaiBaseUrl = `${baseUrl}/v1`;\r\n const extraBody = model.think ? { reasoning_split: true } : {};\r\n return createOpenAI({ baseURL: openaiBaseUrl, apiKey, extraBody }).chat(model.modelName);\r\n};\r\n\r\nconst uploadReference = async (base64: string, fileType: \"image\" | \"audio\" | \"video\"): Promise<ReferenceList> => {\r\n // MiniMax的图片接口直接接受 base64压缩后原样返回\r\n if (fileType === \"image\") {\r\n const compressed = await zipImage(base64, 10 * 1024);\r\n return { type: \"image\", sourceType: \"base64\", base64: compressed };\r\n }\r\n // 视频接口的图片参数也是 base64压缩到20MB\r\n return { type: fileType, sourceType: \"base64\", base64 } as ReferenceList;\r\n};\r\n\r\nconst imageRequest = async (config: ImageConfig, model: ImageModel): Promise<string> => {\r\n if (!vendor.inputValues.apiKey) throw new Error(\"缺少API Key\");\r\n const baseUrl = getBaseUrl();\r\n const headers = getHeaders();\r\n\r\n const reqBody: any = {\r\n model: model.modelName,\r\n prompt: config.prompt,\r\n aspect_ratio: config.aspectRatio,\r\n response_format: \"base64\",\r\n n: 1,\r\n prompt_optimizer: true,\r\n aigc_watermark: false,\r\n };\r\n\r\n // 处理图生图参考\r\n const imageRefs = config.referenceList || [];\r\n if (imageRefs.length > 0) {\r\n const refBase64 = extractBase64WithHead(imageRefs[0]);\r\n reqBody.subject_reference = [{ type: \"character\", image_file: refBase64 }];\r\n }\r\n\r\n logger(\"开始提交MiniMax图像生成任务\");\r\n const resp = await axios.post(`${baseUrl}/v1/image_generation`, reqBody, { headers });\r\n if (resp.data.base_resp.status_code !== 0) {\r\n throw new Error(`图像生成失败:${resp.data.base_resp.status_msg}`);\r\n }\r\n if (resp.data.metadata.success_count === 0) {\r\n throw new Error(\"图像生成被安全策略拦截请调整prompt或参考图\");\r\n }\r\n\r\n const imgBase64 = resp.data.data.image_base64[0];\r\n return imgBase64.startsWith(\"data:\") ? imgBase64 : `data:image/png;base64,${imgBase64}`;\r\n};\r\n\r\nconst videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {\r\n if (!vendor.inputValues.apiKey) throw new Error(\"缺少API Key\");\r\n const baseUrl = getBaseUrl();\r\n const headers = getHeaders();\r\n\r\n const reqBody: any = {\r\n model: model.modelName,\r\n prompt: config.prompt,\r\n duration: config.duration,\r\n resolution: config.resolution,\r\n aigc_watermark: false,\r\n prompt_optimizer: true,\r\n };\r\n\r\n // 提取图片类型的引用\r\n const imageRefs = (config.referenceList || []).filter((r) => r.type === \"image\");\r\n\r\n if (imageRefs.length > 0) {\r\n // 压缩图片到20MB以内\r\n const compressedImages: string[] = [];\r\n for (const ref of imageRefs) {\r\n const base64 = extractBase64WithHead(ref);\r\n const compressed = await zipImage(base64, 20 * 1024);\r\n compressedImages.push(compressed);\r\n }\r\n\r\n if (config.mode.includes(\"startEndRequired\")) {\r\n if (compressedImages.length < 2) throw new Error(\"首尾帧模式需要上传两张图片\");\r\n reqBody.first_frame_image = compressedImages[0];\r\n reqBody.last_frame_image = compressedImages[1];\r\n } else if (config.mode.includes(\"singleImage\")) {\r\n reqBody.first_frame_image = compressedImages[0];\r\n }\r\n }\r\n\r\n logger(\"开始提交MiniMax视频生成任务\");\r\n const submitResp = await axios.post(`${baseUrl}/v1/video_generation`, reqBody, { headers });\r\n if (submitResp.data.base_resp.status_code !== 0) {\r\n throw new Error(`任务提交失败:${submitResp.data.base_resp.status_msg}`);\r\n }\r\n const taskId = submitResp.data.task_id;\r\n logger(`视频任务提交成功任务ID: ${taskId}`);\r\n\r\n // 轮询任务状态\r\n const pollResult = await pollTask(\r\n async () => {\r\n const queryResp = await axios.get(`${baseUrl}/v1/query/video_generation`, {\r\n headers: getHeaders(),\r\n params: { task_id: taskId },\r\n });\r\n if (queryResp.data.base_resp.status_code !== 0) {\r\n return { completed: true, error: queryResp.data.base_resp.status_msg };\r\n }\r\n const status = queryResp.data.status;\r\n if (status === \"Success\") {\r\n return { completed: true, data: queryResp.data.file_id };\r\n }\r\n if (status === \"Fail\") {\r\n return { completed: true, error: \"视频生成失败\" };\r\n }\r\n logger(`视频任务生成中,当前状态:${status}`);\r\n return { completed: false };\r\n },\r\n 5000,\r\n 600000,\r\n );\r\n\r\n if (pollResult.error) throw new Error(pollResult.error);\r\n const fileId = pollResult.data!;\r\n logger(`视频任务生成成功文件ID: ${fileId}`);\r\n\r\n // 获取下载地址\r\n const fileResp = await axios.get(`${baseUrl}/v1/files/retrieve`, {\r\n headers: getHeaders(),\r\n params: { file_id: fileId },\r\n });\r\n if (fileResp.data.base_resp.status_code !== 0) {\r\n throw new Error(`获取文件地址失败:${fileResp.data.base_resp.status_msg}`);\r\n }\r\n const downloadUrl = fileResp.data.file.download_url;\r\n logger(`视频下载地址获取成功开始转Base64`);\r\n\r\n return await urlToBase64(downloadUrl);\r\n};\r\n\r\nconst ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<string> => {\r\n return \"\";\r\n};\r\n\r\nconst checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => {\r\n return {\r\n hasUpdate: false,\r\n latestVersion: \"2.0\",\r\n notice:\r\n \"## 新版本更新公告\\n1. 适配新版模板架构,支持 ReferenceList 统一引用类型\\n2. 新增 uploadReference 前置处理器\\n3. 优化图片压缩和引用提取逻辑\",\r\n };\r\n};\r\n\r\nconst updateVendor = async (): Promise<string> => {\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.uploadReference = uploadReference;\r\nexports.imageRequest = imageRequest;\r\nexports.videoRequest = videoRequest;\r\nexports.ttsRequest = ttsRequest;\r\nexports.checkForUpdates = checkForUpdates;\r\nexports.updateVendor = updateVendor;\r\n\r\n// 这行代码用于确保当前文件被识别为模块,避免全局变量冲突\r\nexport {};",
"null.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; //唯一ID作为文件名存储用户磁盘上禁止符号\r\n version: string; //版本号格式为x.y需遵守语义化版本控制\r\n name: string; //供应商名称\r\n author: string; //作者\r\n description?: string; //描述支持Markdown格式\r\n icon?: string; //图标仅支持Base64格式建议尺寸为128x128像素\r\n inputs: { key: string; label: string; type: \"text\" | \"password\" | \"url\"; required: boolean; placeholder?: string }[];\r\n inputValues: Record<string, string>;\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<ReferenceList, { type: \"image\" }>[];\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<ReferenceList, { type: \"audio\" }>[];\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; // HTTP请求库\r\ndeclare const logger: (msg: string) => void; // 日志函数\r\ndeclare const jsonwebtoken: any; // JWT处理库\r\ndeclare const zipImage: (base64: string, size: number) => Promise<string>; // 图片压缩函数返回有头base64字符串\r\ndeclare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>; // 图片分辨率调整函数返回有头base64字符串\r\ndeclare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>; // 图片合成函数返回有头base64字符串\r\ndeclare const urlToBase64: (url: string) => Promise<string>; // URL转Base64函数返回有头base64字符串\r\ndeclare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>; // 轮询函数fn为异步函数interval为轮询间隔timeout为超时时间返回fn的结果\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<string>; //图片模型返回有头base64字符串\r\n videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>; //视频模型返回有头base64字符串\r\n ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>; //暂未开放语音模型返回有头base64字符串\r\n checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>; //检查更新函数返回是否有更新和最新版本号和更公告支持Markdown格式\r\n updateVendor?: () => Promise<string>; //更新函数,返回最新的代码文本\r\n};\r\n\r\n// ============================================================\r\n// 供应商配置\r\n// ============================================================\r\n\r\nconst vendor: VendorConfig = {\r\n id: \"null\",\r\n version: \"2.0\",\r\n author: \"Toonflow\",\r\n name: \"空模板\",\r\n description: \"## 开发模板您可以使用此模板进行Vibe Coding\",\r\n inputs: [\r\n { key: \"apiKey\", label: \"API密钥\", type: \"password\", required: true },\r\n { key: \"baseUrl\", label: \"请求地址\", type: \"url\", required: true, placeholder: \"示例https://api.openai.com/v1\" },\r\n ],\r\n inputValues: { apiKey: \"\", baseUrl: \"https://api.openai.com/v1\" },\r\n models: [{ name: \"GPT-4o\", modelName: \"gpt-4o\", type: \"text\", think: false }],\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<string> => {\r\n return \"\";\r\n};\r\n\r\nconst videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {\r\n return \"\";\r\n};\r\n\r\nconst ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<string> => {\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<string> => {\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\n// 这行代码用于确保当前文件被识别为模块,避免全局变量冲突\r\nexport {};\r\n\r\n/**\r\n * ============================================================\r\n * AI 代码生成指南\r\n * ============================================================\r\n *\r\n * 【概述】\r\n * 本文件是 Toonflow AI 供应商适配模板。AI 在生成新供应商适配代码时,\r\n * 必须严格遵守以下规则,并要求用户提供目标平台的 curl 调用示例或 API 文档信息作为输入依据。\r\n *\r\n * 【前置要求】\r\n * 在生成代码前,请向用户索要以下信息(至少其一):\r\n * 1. 目标 API 的 curl 请求示例包含请求地址、Headers、Body 结构、响应结构)\r\n * 2. 目标 API 的官方文档链接或文档截图/文本内容\r\n * 3. 需要适配的模型类型text / image / video / tts及其能力说明\r\n * 没有足够信息时,应主动追问,不要凭空编造 API 结构。\r\n *\r\n * 【代码规则】\r\n *\r\n * 1. 禁止引入任何外部包\r\n * 不可使用 import / require仅能使用本文件「全局声明」区域中已声明的方法和对象\r\n * 包括axios、logger、jsonwebtoken、zipImage、zipImageResolution、mergeImages、\r\n * urlToBase64、pollTask以及 createOpenAI、createDeepSeek、createZhipu、createQwen、\r\n * createAnthropic、createOpenAICompatible、createXai、createMinimax、\r\n * createGoogleGenerativeAI 等 AI SDK 工厂函数。\r\n *\r\n * 2. 禁止在 exports.* 函数外部声明离散的全大写常量\r\n * 错误示例const API_URL = \"https://...\"; const MAX_RETRY = 3;\r\n * 如果确实需要可配置的常量值,必须将其声明在 vendor.inputValues 中,\r\n * 通过 vendor.inputValues.xxx 访问,让用户可在界面上配置。\r\n * 如果是纯逻辑内部使用的临时变量,应内联在对应的 exports.* 函数体内部,使用小驼峰命名。\r\n *\r\n * 3. 逻辑尽量聚合在 exports.* 对应的函数内部\r\n * 每个适配函数textRequest / imageRequest / videoRequest / ttsRequest\r\n * 应自包含,将请求构造、发送、轮询、结果解析等逻辑写在函数体内,避免拆分出大量外部辅助函数。\r\n * 如果多个函数确实存在公共逻辑如签名计算、Token 生成、请求头构造),\r\n * 可提取为文件内的小驼峰命名函数,放在「适配器函数」区块之前的「辅助工具」区块中,\r\n * 且不可使用全大写命名。\r\n *\r\n * 4. 命名规范\r\n * 所有变量、函数一律使用小驼峰命名camelCase禁止使用 UPPER_SNAKE_CASE。\r\n *\r\n * 5. 不需要重新声明类型\r\n * 本文件顶部已完整定义了所有接口和类型VendorConfig、ImageConfig、VideoConfig、\r\n * TTSConfig、TextModel、ImageModel、VideoModel、TTSModel、ReferenceList、PollResult 等),\r\n * AI 生成代码时直接使用即可,不要重复声明。\r\n *\r\n * 6. 返回值规范\r\n * - textRequest(model):返回 AI SDK 的 chat model 实例(通过 createOpenAI 等工厂函数创建)。\r\n * - imageRequest(config, model):返回有头 base64 字符串(如 \"data:image/png;base64,...\")。\r\n * config.referenceList 为 Extract<ReferenceList, { type: \"image\" }>[] 类型,\r\n * 每个引用条目均为 base64 形式sourceType 固定为 \"base64\")。\r\n * - videoRequest(config, model):返回有头 base64 字符串(如 \"data:video/mp4;base64,...\")。\r\n * config.referenceList 为 ReferenceList[] 类型,可包含 image / video / audio 三种引用,\r\n * 每个引用条目均为 base64 形式sourceType 固定为 \"base64\")。\r\n * config.mode 为当前激活的视频模式数组,需根据 mode 决定如何使用 referenceList。\r\n * - ttsRequest(config, model):返回有头 base64 字符串(如 \"data:audio/mp3;base64,...\")。\r\n * config.referenceList 为 Extract<ReferenceList, { type: \"audio\" }>[] 类型(音频参考)。\r\n * 当 API 返回的是 URL 而非二进制数据时,使用 urlToBase64(url) 转换。\r\n *\r\n * 7. ReferenceList 与 VideoMode 说明\r\n * ReferenceList 是统一的多媒体引用类型,每个条目包含:\r\n * - type: \"image\" | \"audio\" | \"video\"(媒体类型)\r\n * - sourceType: \"base64\"(当前模板固定为 base64\r\n * - base64对应的数据\r\n *\r\n * VideoMode 定义了视频模型支持的输入模式:\r\n * - \"text\":纯文本生成视频\r\n * - \"singleImage\":单张首帧图片\r\n * - \"startEndRequired\":首尾帧(两张都必须提供)\r\n * - \"endFrameOptional\":首尾帧(尾帧可选)\r\n * - \"startFrameOptional\":首尾帧(首帧可选)\r\n * - 数组形式如 [\"imageReference:9\", \"videoReference:3\", \"audioReference:3\"]\r\n * 多模态参考模式,数字表示该类型的最大数量限制。\r\n *\r\n * 在 videoRequest 中config.mode 表示当前选择的模式,需根据其值决定:\r\n * - 如何从 config.referenceList 中提取对应类型的引用\r\n * - 如何构造 API 请求体中的图片/视频/音频参数\r\n *\r\n * 8. 异步任务处理\r\n * 对于视频生成等需要轮询的异步任务,使用全局的 pollTask 函数:\r\n * const result = await pollTask(async () => {\r\n * const resp = await axios.get(...);\r\n * if (resp.data.status === \"SUCCESS\") return { completed: true, data: resp.data.url };\r\n * if (resp.data.status === \"FAILED\") return { completed: true, error: resp.data.message };\r\n * return { completed: false };\r\n * }, 5000, 600000); // 每5秒轮询10分钟超时\r\n * if (result.error) throw new Error(result.error);\r\n * return await urlToBase64(result.data!);\r\n *\r\n * 9. 错误处理\r\n * 在每个函数开头校验必需参数(如 API Key缺失时使用 throw new Error(\"...\") 抛出。\r\n * API 请求失败时,从响应中提取有意义的错误信息抛出,不要吞掉异常。\r\n *\r\n * 10. 日志输出\r\n * 在关键步骤使用 logger(\"...\") 输出日志(如\"开始提交任务\"、\"任务ID: xxx\"、\"轮询中...\"\r\n * 便于调试。\r\n *\r\n * 11. vendor 配置填写\r\n * - id纯英文小写作为文件名使用禁止特殊符号和空格。\r\n * - version语义化版本格式 \"x.y\"。\r\n * - inputs根据目标 API 所需的认证信息配置API Key、Secret、请求地址等。\r\n * - models根据目标平台支持的模型列表填写注意正确设置 type 和各模型特有字段。\r\n * - VideoModel 的 mode 对应 API 支持的输入模式(参见规则 7 的 VideoMode 说明)。\r\n * - VideoModel 的 audio 字段true始终生成音频、false不生成、\"optional\"(用户可选)。\r\n * - VideoModel 的 durationResolutionMap 对应各时长下可选的分辨率。\r\n * - VideoModel 的 associationSkills 可选,用于描述模型的特殊能力。\r\n * - ImageModel 的 mode 对应 API 支持的生图模式(\"text\" 纯文本、\"singleImage\" 单图参考、\"multiReference\" 多图参考)。\r\n * - TTSModel 的 voices 对应可选的音色列表。\r\n *\r\n * 12. 图片处理\r\n * - 需要压缩图片体积时使用 zipImage(base64, maxSizeKB)。\r\n * - 需要调整图片分辨率时使用 zipImageResolution(base64, width, height)。\r\n * - 需要将多张图片拼合为一张时使用 mergeImages(base64Arr, maxSize)。\r\n * - 以上函数均接收和返回有头 base64 字符串。\r\n *\r\n * 13. 文件结构\r\n * 生成的代码必须保持本模板的整体结构:\r\n * 类型定义区 → 全局声明区 → 供应商配置区 → [辅助工具区(可选)] → 适配器函数区 → 导出区\r\n * 不要打乱顺序,不要删除已有的结构注释分隔线。\r\n * 辅助工具区用于放置多个适配器函数共享的小驼峰命名辅助函数(如 getHeaders、getBaseUrl。\r\n *\r\n * 14. 导出规范\r\n * 必须导出以下字段(通过 exports.xxx = xxx 赋值):\r\n * - exports.vendor必须\r\n * - exports.textRequest必须\r\n * - exports.imageRequest必须\r\n * - exports.videoRequest必须\r\n * - exports.ttsRequest必须\r\n * - exports.checkForUpdates可选\r\n * - exports.updateVendor可选\r\n * 未实现的适配器函数保留空实现return \"\"),不可省略导出。\r\n * 文件末尾必须包含 export {}; 以确保文件被识别为模块。\r\n *\r\n * 【生成流程】\r\n * 当用户请求生成新的供应商适配时:\r\n * 1. 确认用户已提供 curl 示例或 API 文档。\r\n * 2. 分析 API 的认证方式、端点地址、请求/响应结构。\r\n * 3. 基于本模板结构,填充 vendor 配置和对应的适配器函数。\r\n * 4. 根据当前模板的 ReferenceList 定义,按 base64 形式构造和消费 referenceList。\r\n * 5. 仅实现用户需要的模型类型未用到的函数保留空实现return \"\")。\r\n * 6. 生成完整可用的代码,确保无语法错误、无遗漏导出。\r\n */\r\n",
"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<string, string>;\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<string>;\r\ndeclare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>;\r\ndeclare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>;\r\ndeclare const urlToBase64: (url: string) => Promise<string>;\r\ndeclare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>;\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<string>;\r\n videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>;\r\n ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>;\r\n checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>;\r\n updateVendor?: () => Promise<string>;\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<string> => {\r\n return \"\";\r\n};\r\nconst videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {\r\n return \"\";\r\n};\r\nconst ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<string> => {\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<string> => {\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<string, string>;\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<ReferenceList, { type: \"image\" }>[];\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<ReferenceList, { type: \"audio\" }>[];\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<string>;\r\ndeclare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>;\r\ndeclare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>;\r\ndeclare const urlToBase64: (url: string) => Promise<string>;\r\ndeclare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>;\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<string>;\r\n videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>;\r\n ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>;\r\n checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>;\r\n updateVendor?: () => Promise<string>;\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<string> => {\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<string, string> = {\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<string, Record<string, string>> = {\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<string, any> = {\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<string> => {\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<string, any> = {};\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<string, Record<string, string>> = {\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<string, any> = {\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<string, any> = {\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<string> => {\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<string> => {\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<string, string>;\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<string>;\r\n//压缩图片分辨率\r\ndeclare const zipImageResolution: (completeBase64: string, width: number, height: number) => Promise<string>;\r\n//多图拼接乘单图 maxSize 最大输出大小,默认为 10mb\r\ndeclare const mergeImages: (completeBase64: string[], maxSize?: string) => Promise<string>;\r\n//Url转Base64\r\ndeclare const urlToBase64: (url: string) => Promise<string>;\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<string, Record<string, string>> = {\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<string, any> = {\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<string, any>;\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<string, string>;\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<ReferenceList, { type: \"image\" }>[];\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<ReferenceList, { type: \"audio\" }>[];\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<string>;\r\ndeclare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>;\r\ndeclare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>;\r\ndeclare const urlToBase64: (url: string) => Promise<string>;\r\ndeclare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>;\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<string>;\r\n videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>;\r\n ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>;\r\n checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>;\r\n updateVendor?: () => Promise<string>;\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<number, string> = {\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<string> => {\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多图为 arrayseedream-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<string, Record<string, string>> = {\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 最高 104044964K 超限,回退传 \"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<string> => {\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<PollResult> => {\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<string> => {\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<string> => {\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"
}