//如需遥测AI请使用在toonflow安装目录运行npx @ai-sdk/devtools (要求在其他设置中打开遥测功能,且toonflow有权限在安装目录创建.devtools文件夹) // ==================== 类型定义 ==================== // 文本模型 interface TextModel { name: string; // 显示名称 modelName: string; type: "text"; think: boolean; // 前端显示用 } // 图像模型 interface ImageModel { name: string; // 显示名称 modelName: string; type: "image"; mode: ("text" | "singleImage" | "multiReference")[]; associationSkills?: string; // 关联技能,多个技能用逗号分隔 } // 视频模型 interface VideoModel { name: string; // 显示名称 modelName: string; //全局唯一 type: "video"; mode: ( | "singleImage" // 单图 | "startEndRequired" // 首尾帧(两张都得有) | "endFrameOptional" // 首尾帧(尾帧可选) | "startFrameOptional" // 首尾帧(首帧可选) | "text" // 文本生视频 | ("videoReference" | "imageReference" | "audioReference" | "textReference")[] )[]; // 混合参考 associationSkills?: string; // 关联技能,多个技能用逗号分隔 audio: "optional" | false | true; // 音频配置 durationResolutionMap: { duration: number[]; resolution: string[] }[]; } interface TTSModel { name: string; // 显示名称 modelName: string; type: "tts"; voices: { title: string; //显示名称 voice: string; //说话人 }[]; } // 供应商配置 interface VendorConfig { id: string; //供应商唯一标识,必须全局唯一 author: string; description?: string; //md5格式 name: string; icon?: string; //仅支持base64格式 inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string; }[]; inputValues: Record; models: (TextModel | ImageModel | VideoModel)[]; } // ==================== 全局工具函数 ==================== //Axios实例 //压缩图片大小(1MB = 1 * 1024 * 1024) declare const zipImage: (completeBase64: string, size: number) => Promise; //压缩图片分辨率 declare const zipImageResolution: (completeBase64: string, width: number, height: number) => Promise; //多图拼接乘单图 maxSize 最大输出大小,默认为 10mb declare const mergeImages: (completeBase64: string[], maxSize?: string) => Promise; //Url转Base64 declare const urlToBase64: (url: string) => Promise; //轮询函数 declare const pollTask: ( fn: () => Promise<{ completed: boolean; data?: string; error?: string }>, interval?: number, timeout?: number, ) => Promise<{ completed: boolean; data?: string; error?: string }>; declare const axios: any; declare const createOpenAI: any; declare const createDeepSeek: any; declare const createZhipu: any; declare const createQwen: any; declare const createAnthropic: any; declare const createOpenAICompatible: any; declare const createXai: any; declare const createMinimax: any; declare const createGoogleGenerativeAI: any; declare const logger: (logstring: string) => void; declare const jsonwebtoken: any; // ==================== 供应商数据 ==================== const vendor: VendorConfig = { id: "toonflow", author: "Toonflow", description: "## Toonflow官方中转平台\n\nToonflow官方中转平台,提供**文本、图像、视频、音频**等多模态生成能力的中转服务,支持接入多个大模型供应商,方便用户统一管理和调用不同供应商的生成能力。\n\n🔗 [前往中转平台](https://api.toonflow.net/)\n\n如果这个项目对你有帮助,可以考虑支持一下我们的开发工作 ☕", name: "Toonflow官方中转平台", icon: "", inputs: [{ key: "apiKey", label: "API密钥", type: "password", required: true }], inputValues: { apiKey: "", baseUrl: "https://api.toonflow.net/v1", }, models: [ { name: "claude-sonnet-4-6", type: "text", modelName: "claude-sonnet-4-6", think: false, }, { name: "claude-opus-4-6", type: "text", modelName: "claude-opus-4-6", think: false, }, { name: "claude-sonnet-4-5-20250929", type: "text", modelName: "claude-sonnet-4-5-20250929", think: false, }, { name: "claude-opus-4-5-20251101", type: "text", modelName: "claude-opus-4-5-20251101", think: false, }, { name: "claude-haiku-4-5-20251001", type: "text", modelName: "claude-haiku-4-5-20251001", think: false, }, { name: "gpt-5.4", type: "text", modelName: "gpt-5.4", think: false, }, { name: "gpt-5.2", type: "text", modelName: "gpt-5.2", think: false, }, { name: "MiniMax-M2.7", type: "text", modelName: "MiniMax-M2.7", think: true, }, { name: "MiniMax-M2.5", type: "text", modelName: "MiniMax-M2.5", think: true, }, { name: "Wan2.6 I2V 1080P (支持真人)", type: "video", modelName: "Wan2.6-I2V-1080P", mode: ["text", "startEndRequired"], durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["1080p"] }], audio: true, }, { name: "Wan2.6 I2V 720P (支持真人)", type: "video", modelName: "Wan2.6-I2V-720P", mode: ["text", "startEndRequired"], durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["720p"] }], audio: true, }, { name: "Seedance 1.5 Pro", type: "video", modelName: "doubao-seedance-1-5-pro-251215", durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }], mode: ["text", "endFrameOptional"], audio: true, }, { name: "vidu2 turbo", type: "video", modelName: "ViduQ2-turbo", durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["540p", "720p", "1080p"] }], mode: ["singleImage", "startEndRequired"], audio: false, }, { name: "ViduQ3 pro", type: "video", modelName: "ViduQ3-pro", durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], resolution: ["540p", "720p", "1080p"] }], mode: ["singleImage", "startEndRequired"], audio: false, }, { name: "ViduQ2 pro", type: "video", modelName: "ViduQ2-pro", durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["540p", "720p", "1080p"] }], mode: ["singleImage", "startEndRequired"], audio: false, }, { name: "Doubao Seedream 5.0 Lite", type: "image", modelName: "Doubao-Seedream-5.0-Lite", mode: ["text", "singleImage", "multiReference"], }, { name: "Doubao Seedream 4.5", type: "image", modelName: "doubao-seedream-4-5-251128", mode: ["text", "singleImage", "multiReference"], }, ], }; exports.vendor = vendor; // ==================== 适配器函数 ==================== // 文本请求函数 const textRequest: (textModel: TextModel) => { url: string; model: string } = (textModel) => { if (!vendor.inputValues.apiKey) throw new Error("缺少API Key"); const apiKey = vendor.inputValues.apiKey.replace("Bearer ", ""); return createOpenAI({ baseURL: vendor.inputValues.baseUrl, apiKey: apiKey, }).chat(textModel.modelName); }; exports.textRequest = textRequest; //图片请求函数 interface ImageConfig { prompt: string; //图片提示词 imageBase64: string[]; //输入的图片提示词 size: "1K" | "2K" | "4K"; // 图片尺寸 aspectRatio: `${number}:${number}`; // 长宽比 } //豆包格式适配 function doubaoAdaptor(imageConfig: ImageConfig, imageModel: ImageModel) { const size = imageConfig.size === "1K" ? "2K" : imageConfig.size; const sizeMap: Record> = { "16:9": { "2k": "2848x1600", "2K": "2848x1600", "4K": "4096x2304", "4k": "4096x2304", }, "9:16": { "4k": "2304x4096", "2k": "1600x2848", "2K": "1600x2848", "4K": "2304x4096", }, }; const body = { model: imageModel.modelName, prompt: imageConfig.prompt, size: sizeMap[imageConfig.aspectRatio][size], response_format: "url", sequential_image_generation: "disabled", stream: false, watermark: false, ...(imageConfig.imageBase64 && { image: imageConfig.imageBase64 }), }; return { body, processFn: (data) => { return data.data[0].url; }, }; } // 提取图片内容 function extractFirstImageFromMd(content) { const regex = /!\[([^\]]*)\]\((data:image\/[^;]+;base64,[A-Za-z0-9+/=]+|https?:\/\/[^\s)]+|\/\/[^\s)]+|[^\s)]+)\)/; const match = content.match(regex); if (!match) return null; const raw = match[2].trim(); const url = raw.startsWith("data:") ? raw : raw.split(/\s+/)[0]; return { alt: match[1], url, type: url.startsWith("data:image") ? "base64" : "url", }; } // gemini 图片请求适配 function geminiImageAdaptor(imageConfig: ImageConfig, imageModel: ImageModel) { const images = []; if (imageConfig.imageBase64 && imageConfig.imageBase64.length) { images.push({ role: "user", content: imageConfig.imageBase64.map((i) => ({ type: "image_url", image_url: { url: i, }, })), }); } const imageConfigGoogle = { aspect_ratio: imageConfig.aspectRatio, }; // if(imageModel.ModelName == 'gemini-3-pro-image-preview-vt'){ imageConfigGoogle.image_size = imageConfig.size; // } const body = { model: imageModel.modelName, messages: [{ role: "user", content: imageConfig.prompt + `请直接输出图片` }, ...images], extra_body: { google: { image_config: { ...imageConfigGoogle, }, }, }, }; return { body, url: `${vendor.inputValues.baseUrl}/chat/completions`, processFn: (data: any) => { return extractFirstImageFromMd(data.choices[0].message.content).url; }, }; } function commonAdaptor(imageConfig: ImageConfig, imageModel: ImageModel) { const defaultImageFn = [ ["doubao", doubaoAdaptor], ["nano", geminiImageAdaptor], ["gemini", geminiImageAdaptor], ["seedream", doubaoAdaptor], ]; const modelName = imageModel.modelName; const lowerName = modelName.toLowerCase(); const match = defaultImageFn.find(([key]) => lowerName.includes(key)); return match ? match[1](imageConfig, imageModel) : {}; } const imageRequest = async (imageConfig: ImageConfig, imageModel: ImageModel) => { if (!vendor.inputValues.apiKey) throw new Error("缺少API Key"); const apiKey = vendor.inputValues.apiKey.replace("Bearer ", ""); const adaptor = commonAdaptor(imageConfig, imageModel); const requestUrl = adaptor?.url ? `${vendor.inputValues.baseUrl}/chat/completions` : vendor.inputValues.baseUrl + "/images/generations"; const response = await fetch(requestUrl, { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, body: JSON.stringify(adaptor.body), }); if (!response.ok) { const errorText = await response.text(); // 获取错误信息 console.error("请求失败,状态码:", response.status, ", 错误信息:", errorText); throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`); } const data = await response.json(); return adaptor.processFn(data); }; exports.imageRequest = imageRequest; interface VideoConfig { duration: number; //视频时长,单位秒 resolution: string; //视频分辨率,如"720p"、"1080p" aspectRatio: "16:9" | "9:16"; //视频长宽比 prompt: string; //视频提示词 fileBase64?: string[]; // 文件base64 包含图片base64、视频base64、音频base64 audio?: boolean; mode: | "singleImage" // 单图 | "multiImage" // 多图模式 | "gridImage" // 网格单图(传入一张图片,但该图片是网格图) | "startEndRequired" // 首尾帧(两张都得有) | "endFrameOptional" // 首尾帧(尾帧可选) | "startFrameOptional" // 首尾帧(首帧可选) | "text" // 文本生视频 | ("videoReference" | "imageReference" | "audioReference" | "textReference")[]; // 混合参考 } // 豆包视频 const buildDoubaoMetadata = (videoConfig: VideoConfig) => { const metaData = { ...(typeof videoConfig.audio == "boolean" && { generate_audio: videoConfig.audio ?? false }), ratio: videoConfig.aspectRatio, image_roles: [], references: [], }; if (videoConfig.imageBase64 && videoConfig.imageBase64.length) { videoConfig.imageBase64.forEach((i, index) => { if (Array.isArray(videoConfig.mode)) { metaData.references.push(i); } else { if (videoConfig.mode == "startEndRequired" || videoConfig.mode == "endFrameOptional" || videoConfig.mode == "startFrameOptional") { (metaData.image_roles as string[]).push(index == 0 ? "first_frame" : "last_frame"); } if (videoConfig.mode == "singleImage") { (metaData.image_roles as string[]).push("reference_image"); } } }); } return metaData; }; // 万象 const buildWanMetadata = (videoConfig: VideoConfig) => { const images = videoConfig.imageBase64 ?? []; const metaData: Record = {}; if ( (videoConfig.mode === "startEndRequired" || videoConfig.mode == "endFrameOptional" || videoConfig.mode == "startFrameOptional") && images.length == 2 ) { if (images[0]) metaData.first_frame_url = images[0]; if (images[1]) metaData.last_frame_url = images[1]; } else if (images.length) { metaData.img_url = images[0]!; } if (typeof videoConfig.audio == "boolean") { metaData.audio = videoConfig.audio; } return metaData; }; // 千问视频 const buildViduMetadata = (videoConfig: VideoConfig) => ({ aspect_ratio: videoConfig.aspectRatio, audio: videoConfig.audio ?? false, off_peak: false, }); // 可灵 const buildKlingAdaptor = (videoConfig: VideoConfig) => { const metaData: any = { aspect_ratio: videoConfig.aspectRatio, }; if (videoConfig.imageBase64 && videoConfig.imageBase64.length) { if (Array.isArray(videoConfig.mode)) { metaData.reference = videoConfig.imageBase64; } if (videoConfig.mode == "endFrameOptional") { metaData.image_tail = videoConfig.imageBase64[0]; } if (videoConfig.mode == "startEndRequired") { metaData.image_list = [ { image_url: videoConfig.imageBase64[0], type: "first_frame", }, { image_url: videoConfig.imageBase64[1], type: "last_frame", }, ]; } if (videoConfig.mode == "singleImage") { metaData.image = videoConfig.imageBase64[0]; } } return metaData; }; type MetadataBuilder = (config: VideoConfig) => Record; const METADATA_BUILDERS: Array<[string, MetadataBuilder]> = [ ["doubao", buildDoubaoMetadata], ["wan", buildWanMetadata], ["vidu", buildViduMetadata], ["seedance", buildDoubaoMetadata], ["kling", buildKlingAdaptor], ]; const buildModelMetadata = (modelName: string, videoConfig: VideoConfig) => { const lowerName = modelName.toLowerCase(); const match = METADATA_BUILDERS.find(([key]) => lowerName.includes(key)); return match ? match[1](videoConfig) : {}; }; const videoRequest = async (videoConfig: VideoConfig, videoModel: VideoModel) => { if (!vendor.inputValues.apiKey) throw new Error("缺少API Key"); const apiKey = vendor.inputValues.apiKey.replace("Bearer ", ""); try { videoConfig.mode = JSON.parse(videoConfig.mode); } catch (e) { videoConfig.mode = videoConfig.mode as any; } // 构建每个模型对应的附加参数 const metadata = buildModelMetadata(videoModel.modelName, videoConfig); //公共请求参数 const publicBody = { model: videoModel.modelName, ...(videoConfig.imageBase64 && videoConfig.imageBase64.length && !Array.isArray(videoConfig.mode) ? { images: videoConfig.imageBase64 } : {}), prompt: videoConfig.prompt, duration: videoConfig.duration, metadata: metadata, }; if (videoModel.modelName.toLocaleLowerCase().includes("wan")) { const sizeMap: Record> = { "480p": { "16:9": "832*480", "9:16": "480*832", }, "720p": { "16:9": "1280*720", "9:16": "720*1280", }, "1080p": { "16:9": "1920*1080", "9:16": "1080*1920", }, }; const size = sizeMap[videoConfig.resolution]?.[videoConfig.aspectRatio]; publicBody.size = size; } const requestUrl = vendor.inputValues.baseUrl + "/video/generations"; const queryUrl = vendor.inputValues.baseUrl + "/video/generations/{id}"; const response = await fetch(requestUrl, { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, body: JSON.stringify(publicBody), }); if (!response.ok) { const errorText = await response.text(); // 获取错误信息 console.error("请求失败,状态码:", response.status, ", 错误信息:", errorText); throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`); } const data = await response.json(); const taskId = data.id; const res = await pollTask(async () => { const queryResponse = await fetch(queryUrl.replace("{id}", taskId), { method: "GET", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, }); if (!queryResponse.ok) { const errorText = await queryResponse.text(); // 获取错误信息 console.error("请求失败,状态码:", queryResponse.status, ", 错误信息:", errorText); throw new Error(`请求失败,状态码: ${queryResponse.status}, 错误信息: ${errorText}`); } const queryData = await queryResponse.json(); const status = queryData?.status ?? queryData?.data?.status; const fail_reason = queryData?.data?.fail_reason ?? queryData?.data; switch (status) { case "completed": case "SUCCESS": case "success": return { completed: true, data: queryData.data.result_url }; case "FAILURE": return { completed: false, error: fail_reason || "视频生成失败" }; default: return { completed: false }; } }); if (res.error) throw new Error(res.error); return res.data; }; exports.videoRequest = videoRequest; interface TTSConfig { text: string; voice: string; speechRate: number; pitchRate: number; volume: number; } const ttsRequest = async (ttsConfig: TTSConfig, ttsModel: TTSModel) => { return null; }; exports.ttsRequest = ttsRequest;