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