From bc3c5e722cb67a5c5238522d8b63c9b8ff5f9494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ACT=E4=B8=B6=E6=B5=81=E6=98=9F=E9=9B=A8?= <1340145680@qq.com> Date: Fri, 3 Apr 2026 07:41:08 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8F=90=E7=A4=BA=E8=AF=8D?= =?UTF-8?q?=E9=97=AE=E9=A2=98=EF=BC=8C=E6=96=B0=E5=A2=9Esd2.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../skills/art_skills/2D_90s_japanese_anime/prefix.md | 2 +- data/skills/art_skills/2D_chinese_guofeng/prefix.md | 1 + data/skills/art_skills/2D_flat_design/prefix.md | 2 +- .../art_skills/2D_mature_urban_romance/prefix.md | 1 + data/skills/art_skills/3D_anime_render/prefix.md | 2 +- .../art_skills/3D_chinese_traditional/prefix.md | 1 + data/skills/art_skills/3D_clay_stopmotion/prefix.md | 2 +- .../art_skills/realpeople_ancient_chinese/prefix.md | 2 +- .../art_skills/realpeople_urban_modern/prefix.md | 2 +- src/lib/initDB.ts | 11 ++++++----- 10 files changed, 15 insertions(+), 11 deletions(-) diff --git a/data/skills/art_skills/2D_90s_japanese_anime/prefix.md b/data/skills/art_skills/2D_90s_japanese_anime/prefix.md index 48c2f86..8d73a5d 100644 --- a/data/skills/art_skills/2D_90s_japanese_anime/prefix.md +++ b/data/skills/art_skills/2D_90s_japanese_anime/prefix.md @@ -1,7 +1,7 @@ # 全局美学基础 · 90年代日式动画 --- -严格遵循下方风格约束与全局规则,依据提示词模板格式生成提示词。仅输出提示词本身,不附加任何解释、说明或额外文本。 +必须严格、完整遵循下方全部风格约束与全局规则,并严格按提示词模板格式生成提示词;仅输出提示词正文,不得附加任何解释、说明、注释、标题或其他额外文本。 ## 一、风格基因 | 维度 | 定义 | diff --git a/data/skills/art_skills/2D_chinese_guofeng/prefix.md b/data/skills/art_skills/2D_chinese_guofeng/prefix.md index 44e71d6..d9cd297 100644 --- a/data/skills/art_skills/2D_chinese_guofeng/prefix.md +++ b/data/skills/art_skills/2D_chinese_guofeng/prefix.md @@ -1,6 +1,7 @@ # 全局美学基础 · 国风二次元新国潮 --- +必须严格、完整遵循下方全部风格约束与全局规则,并严格按提示词模板格式生成提示词;仅输出提示词正文,不得附加任何解释、说明、注释、标题或其他额外文本。 ## 一、风格基因 diff --git a/data/skills/art_skills/2D_flat_design/prefix.md b/data/skills/art_skills/2D_flat_design/prefix.md index a48c809..f941c25 100644 --- a/data/skills/art_skills/2D_flat_design/prefix.md +++ b/data/skills/art_skills/2D_flat_design/prefix.md @@ -1,7 +1,7 @@ # 全局美学基础 · 2D扁平风(Flat Design) --- -严格遵循下方风格约束与全局规则,依据提示词模板格式生成提示词。仅输出提示词本身,不附加任何解释、说明或额外文本。 +必须严格、完整遵循下方全部风格约束与全局规则,并严格按提示词模板格式生成提示词;仅输出提示词正文,不得附加任何解释、说明、注释、标题或其他额外文本。 ## 一、风格基因 | 维度 | 定义 | diff --git a/data/skills/art_skills/2D_mature_urban_romance/prefix.md b/data/skills/art_skills/2D_mature_urban_romance/prefix.md index 07f8b1e..e66c64e 100644 --- a/data/skills/art_skills/2D_mature_urban_romance/prefix.md +++ b/data/skills/art_skills/2D_mature_urban_romance/prefix.md @@ -1,6 +1,7 @@ # 全局美学基础 · 成熟都市言情二次元动画 --- +必须严格、完整遵循下方全部风格约束与全局规则,并严格按提示词模板格式生成提示词;仅输出提示词正文,不得附加任何解释、说明、注释、标题或其他额外文本。 ## 一、风格基因 diff --git a/data/skills/art_skills/3D_anime_render/prefix.md b/data/skills/art_skills/3D_anime_render/prefix.md index ff3be92..6399fed 100644 --- a/data/skills/art_skills/3D_anime_render/prefix.md +++ b/data/skills/art_skills/3D_anime_render/prefix.md @@ -1,7 +1,7 @@ # 全局美学基础 · 3D 动画渲染 --- -严格遵循下方风格约束与全局规则,依据提示词模板格式生成提示词。仅输出提示词本身,不附加任何解释、说明或额外文本。 +必须严格、完整遵循下方全部风格约束与全局规则,并严格按提示词模板格式生成提示词;仅输出提示词正文,不得附加任何解释、说明、注释、标题或其他额外文本。 ## 一、风格基因 | 维度 | 定义 | diff --git a/data/skills/art_skills/3D_chinese_traditional/prefix.md b/data/skills/art_skills/3D_chinese_traditional/prefix.md index 40ee4dc..548e294 100644 --- a/data/skills/art_skills/3D_chinese_traditional/prefix.md +++ b/data/skills/art_skills/3D_chinese_traditional/prefix.md @@ -1,6 +1,7 @@ # 全局美学基础 · 国风3D --- +必须严格、完整遵循下方全部风格约束与全局规则,并严格按提示词模板格式生成提示词;仅输出提示词正文,不得附加任何解释、说明、注释、标题或其他额外文本。 ## 一、风格基因 diff --git a/data/skills/art_skills/3D_clay_stopmotion/prefix.md b/data/skills/art_skills/3D_clay_stopmotion/prefix.md index 2e63df6..1b180b8 100644 --- a/data/skills/art_skills/3D_clay_stopmotion/prefix.md +++ b/data/skills/art_skills/3D_clay_stopmotion/prefix.md @@ -1,7 +1,7 @@ # 全局美学基础 · 定格动画黏土 --- -严格遵循下方风格约束与全局规则,依据提示词模板格式生成提示词。仅输出提示词本身,不附加任何解释、说明或额外文本。 +必须严格、完整遵循下方全部风格约束与全局规则,并严格按提示词模板格式生成提示词;仅输出提示词正文,不得附加任何解释、说明、注释、标题或其他额外文本。 ## 一、风格基因 | 维度 | 定义 | diff --git a/data/skills/art_skills/realpeople_ancient_chinese/prefix.md b/data/skills/art_skills/realpeople_ancient_chinese/prefix.md index 673396d..ea04f11 100644 --- a/data/skills/art_skills/realpeople_ancient_chinese/prefix.md +++ b/data/skills/art_skills/realpeople_ancient_chinese/prefix.md @@ -1,7 +1,7 @@ # 全局美学基础 · 真人古风写实 --- -严格遵循下方风格约束与全局规则,依据提示词模板格式生成提示词。仅输出提示词本身,不附加任何解释、说明或额外文本。 +必须严格、完整遵循下方全部风格约束与全局规则,并严格按提示词模板格式生成提示词;仅输出提示词正文,不得附加任何解释、说明、注释、标题或其他额外文本。 ## 一、风格基因 | 维度 | 定义 | diff --git a/data/skills/art_skills/realpeople_urban_modern/prefix.md b/data/skills/art_skills/realpeople_urban_modern/prefix.md index 2da3001..48b273c 100644 --- a/data/skills/art_skills/realpeople_urban_modern/prefix.md +++ b/data/skills/art_skills/realpeople_urban_modern/prefix.md @@ -1,7 +1,7 @@ # 全局美学基础 · 真人都市写实 --- -严格遵循下方风格约束与全局规则,依据提示词模板格式生成提示词。仅输出提示词本身,不附加任何解释、说明或额外文本。 +必须严格、完整遵循下方全部风格约束与全局规则,并严格按提示词模板格式生成提示词;仅输出提示词正文,不得附加任何解释、说明、注释、标题或其他额外文本。 ## 一、风格基因 | 维度 | 定义 | diff --git a/src/lib/initDB.ts b/src/lib/initDB.ts index 1d87253..283b4cd 100644 --- a/src/lib/initDB.ts +++ b/src/lib/initDB.ts @@ -439,13 +439,14 @@ export default async (knex: Knex, forceInit: boolean = false): Promise => 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"]}]', code: '//如需遥测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\n// ==================== 供应商数据 ====================\r\nconst vendor: VendorConfig = {\r\n id: "toonflow",\r\n author: "Toonflow",\r\n description:\r\n "## Toonflow官方中转平台\\n\\nToonflow官方中转平台,提供**文本、图像、视频、音频**等多模态生成能力的中转服务,支持接入多个大模型供应商,方便用户统一管理和调用不同供应商的生成能力。\\n\\n🔗 [前往中转平台](https://api.toonflow.net/)\\n\\n如果这个项目对你有帮助,可以考虑支持一下我们的开发工作 ☕",\r\n name: "Toonflow官方中转平台",\r\n icon: "",\r\n inputs: [\r\n { key: "apiKey", label: "API密钥", type: "password", required: true },\r\n ],\r\n inputValues: {\r\n apiKey: "",\r\n baseUrl: "https://api.toonflow.net/v1"\r\n },\r\n models: [\r\n {\r\n name: "claude-sonnet-4-6",\r\n type: "text",\r\n modelName: "claude-sonnet-4-6",\r\n think: false,\r\n },\r\n {\r\n name: "claude-opus-4-6",\r\n type: "text",\r\n modelName: "claude-opus-4-6",\r\n think: false,\r\n },\r\n {\r\n name: "claude-sonnet-4-5-20250929",\r\n type: "text",\r\n modelName: "claude-sonnet-4-5-20250929",\r\n think: false,\r\n },\r\n {\r\n name: "claude-opus-4-5-20251101",\r\n type: "text",\r\n modelName: "claude-opus-4-5-20251101",\r\n think: false,\r\n },\r\n {\r\n name: "claude-haiku-4-5-20251001",\r\n type: "text",\r\n modelName: "claude-haiku-4-5-20251001",\r\n think: false,\r\n },\r\n {\r\n name: "gpt-5.4",\r\n type: "text",\r\n modelName: "gpt-5.4",\r\n think: false,\r\n },\r\n {\r\n name: "gpt-5.2",\r\n type: "text",\r\n modelName: "gpt-5.2",\r\n think: false,\r\n },\r\n {\r\n name: "MiniMax-M2.7",\r\n type: "text",\r\n modelName: "MiniMax-M2.7",\r\n think: true,\r\n },\r\n {\r\n name: "MiniMax-M2.5",\r\n type: "text",\r\n modelName: "MiniMax-M2.5",\r\n think: true,\r\n },\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 durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }],\r\n mode: ["text", "endFrameOptional"],\r\n audio: true,\r\n },\r\n {\r\n name: "vidu2 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: false,\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"],\r\n audio: false,\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: false,\r\n },\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\nexports.vendor = vendor;\r\n\r\n// ==================== 适配器函数 ====================\r\n\r\n// 文本请求函数\r\nconst textRequest: (textModel: TextModel) => { url: string; model: string } = (textModel) => {\r\n if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");\r\n const apiKey = vendor.inputValues.apiKey.replace("Bearer ", "");\r\n\r\n return createOpenAI({\r\n baseURL: vendor.inputValues.baseUrl,\r\n apiKey: apiKey,\r\n }).chat(textModel.modelName);\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\n//豆包格式适配\r\nfunction doubaoAdaptor(imageConfig: ImageConfig, imageModel: ImageModel) {\r\n const size = imageConfig.size === "1K" ? "2K" : imageConfig.size;\r\n const sizeMap: Record> = {\r\n "16:9": {\r\n "2k": "2848x1600",\r\n "2K": "2848x1600",\r\n "4K": "4096x2304",\r\n "4k": "4096x2304",\r\n },\r\n "9:16": {\r\n "4k": "2304x4096",\r\n "2k": "1600x2848",\r\n "2K": "1600x2848",\r\n "4K": "2304x4096",\r\n },\r\n };\r\n const body = {\r\n model: imageModel.modelName,\r\n prompt: imageConfig.prompt,\r\n size: sizeMap[imageConfig.aspectRatio][size],\r\n response_format: "url",\r\n sequential_image_generation: "disabled",\r\n stream: false,\r\n watermark: false,\r\n ...(imageConfig.imageBase64 && { image: imageConfig.imageBase64 }),\r\n };\r\n return {\r\n body,\r\n processFn: (data) => {\r\n return data.data[0].url;\r\n },\r\n };\r\n}\r\n\r\n// 提取图片内容\r\nfunction extractFirstImageFromMd(content) {\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 {\r\n alt: match[1],\r\n url,\r\n type: url.startsWith("data:image") ? "base64" : "url",\r\n };\r\n}\r\n// gemini 图片请求适配\r\nfunction geminiImageAdaptor(imageConfig: ImageConfig, imageModel: ImageModel) {\r\n const images = [];\r\n if (imageConfig.imageBase64 && imageConfig.imageBase64.length) {\r\n images.push({\r\n role: "user",\r\n content: imageConfig.imageBase64.map((i) => ({\r\n type: "image_url",\r\n image_url: {\r\n url: i,\r\n },\r\n })),\r\n });\r\n }\r\n const imageConfigGoogle = {\r\n aspect_ratio: imageConfig.aspectRatio,\r\n };\r\n // if(imageModel.ModelName == \'gemini-3-pro-image-preview-vt\'){\r\n imageConfigGoogle.image_size = imageConfig.size;\r\n // }\r\n const body = {\r\n model: imageModel.modelName,\r\n messages: [{ role: "user", content: imageConfig.prompt + `请直接输出图片` }, ...images],\r\n extra_body: {\r\n google: {\r\n image_config: {\r\n ...imageConfigGoogle,\r\n },\r\n },\r\n },\r\n };\r\n return {\r\n body,\r\n url: `${vendor.inputValues.baseUrl}/chat/completions`,\r\n processFn: (data: any) => {\r\n return extractFirstImageFromMd(data.choices[0].message.content).url;\r\n },\r\n };\r\n}\r\nfunction commonAdaptor(imageConfig: ImageConfig, imageModel: ImageModel) {\r\n const defaultImageFn = [\r\n ["doubao", doubaoAdaptor],\r\n ["nano", geminiImageAdaptor],\r\n ["gemini", geminiImageAdaptor],\r\n ["seedream", doubaoAdaptor],\r\n ];\r\n const modelName = imageModel.modelName;\r\n const lowerName = modelName.toLowerCase();\r\n const match = defaultImageFn.find(([key]) => lowerName.includes(key));\r\n return match ? match[1](imageConfig, imageModel) : {};\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("Bearer ", "");\r\n const adaptor = commonAdaptor(imageConfig, imageModel);\r\n\r\n const requestUrl = adaptor?.url ? `${vendor.inputValues.baseUrl}/chat/completions` : vendor.inputValues.baseUrl + "/images/generations";\r\n const response = await fetch(requestUrl, {\r\n method: "POST",\r\n headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },\r\n body: JSON.stringify(adaptor.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 return adaptor.processFn(data);\r\n};\r\nexports.imageRequest = imageRequest;\r\n\r\ninterface VideoConfig {\r\n duration: number; //视频时长,单位秒\r\n resolution: string; //视频分辨率,如"720p"、"1080p"\r\n aspectRatio: "16:9" | "9:16"; //视频长宽比\r\n prompt: string; //视频提示词\r\n fileBase64?: string[]; // 文件base64 包含图片base64、视频base64、音频base64\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 | ("videoReference" | "imageReference" | "audioReference" | "textReference")[]; // 混合参考\r\n}\r\n// 豆包视频\r\nconst buildDoubaoMetadata = (videoConfig: VideoConfig) => {\r\n const metaData = {\r\n ...(typeof videoConfig.audio == "boolean" && { generate_audio: videoConfig.audio ?? false }),\r\n ratio: videoConfig.aspectRatio,\r\n image_roles: [],\r\n references: [],\r\n };\r\n if (videoConfig.imageBase64 && videoConfig.imageBase64.length) {\r\n videoConfig.imageBase64.forEach((i, index) => {\r\n if (Array.isArray(videoConfig.mode)) {\r\n metaData.references.push(i);\r\n } else {\r\n if (videoConfig.mode == "startEndRequired" || videoConfig.mode == "endFrameOptional" || videoConfig.mode == "startFrameOptional") {\r\n (metaData.image_roles as string[]).push(index == 0 ? "first_frame" : "last_frame");\r\n }\r\n if (videoConfig.mode == "singleImage") {\r\n (metaData.image_roles as string[]).push("reference_image");\r\n }\r\n }\r\n });\r\n }\r\n\r\n return metaData;\r\n};\r\n\r\n// 万象\r\nconst buildWanMetadata = (videoConfig: VideoConfig) => {\r\n const images = videoConfig.imageBase64 ?? [];\r\n const metaData: Record = {};\r\n if (\r\n (videoConfig.mode === "startEndRequired" || videoConfig.mode == "endFrameOptional" || videoConfig.mode == "startFrameOptional") &&\r\n images.length == 2\r\n ) {\r\n if (images[0]) metaData.first_frame_url = images[0];\r\n if (images[1]) metaData.last_frame_url = images[1];\r\n } else if (images.length) {\r\n metaData.img_url = images[0]!;\r\n }\r\n if (typeof videoConfig.audio == "boolean") {\r\n metaData.audio = videoConfig.audio;\r\n }\r\n return metaData;\r\n};\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\nconst buildKlingAdaptor = (videoConfig: VideoConfig) => {\r\n const metaData: any = {\r\n aspect_ratio: videoConfig.aspectRatio,\r\n };\r\n\r\n if (videoConfig.imageBase64 && videoConfig.imageBase64.length) {\r\n if (Array.isArray(videoConfig.mode)) {\r\n metaData.reference = videoConfig.imageBase64;\r\n }\r\n if (videoConfig.mode == "endFrameOptional") {\r\n metaData.image_tail = videoConfig.imageBase64[0];\r\n }\r\n if (videoConfig.mode == "startEndRequired") {\r\n metaData.image_list = [\r\n {\r\n image_url: videoConfig.imageBase64[0],\r\n type: "first_frame",\r\n },\r\n {\r\n image_url: videoConfig.imageBase64[1],\r\n type: "last_frame",\r\n },\r\n ];\r\n }\r\n if (videoConfig.mode == "singleImage") {\r\n metaData.image = videoConfig.imageBase64[0];\r\n }\r\n }\r\n\r\n return metaData;\r\n};\r\ntype MetadataBuilder = (config: VideoConfig) => Record;\r\nconst METADATA_BUILDERS: Array<[string, MetadataBuilder]> = [\r\n ["doubao", buildDoubaoMetadata],\r\n ["wan", buildWanMetadata],\r\n ["vidu", buildViduMetadata],\r\n ["seedance", buildDoubaoMetadata],\r\n ["kling", buildKlingAdaptor],\r\n];\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\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("Bearer ", "");\r\n try {\r\n videoConfig.mode = JSON.parse(videoConfig.mode);\r\n } catch (e) {\r\n videoConfig.mode = videoConfig.mode as any;\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 && !Array.isArray(videoConfig.mode) ? { images: videoConfig.imageBase64 } : {}),\r\n prompt: videoConfig.prompt,\r\n duration: videoConfig.duration,\r\n metadata: metadata,\r\n };\r\n\r\n if (videoModel.modelName.toLocaleLowerCase().includes("wan")) {\r\n const sizeMap: Record> = {\r\n "480p": {\r\n "16:9": "832*480",\r\n "9:16": "480*832",\r\n },\r\n "720p": {\r\n "16:9": "1280*720",\r\n "9:16": "720*1280",\r\n },\r\n "1080p": {\r\n "16:9": "1920*1080",\r\n "9:16": "1080*1920",\r\n },\r\n };\r\n const size = sizeMap[videoConfig.resolution]?.[videoConfig.aspectRatio];\r\n publicBody.size = size;\r\n }\r\n const requestUrl = vendor.inputValues.baseUrl + "/video/generations";\r\n const queryUrl = vendor.inputValues.baseUrl + "/video/generations/{id}";\r\n const response = await fetch(requestUrl, {\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 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 res = await pollTask(async () => {\r\n const queryResponse = await fetch(queryUrl.replace("{id}", 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 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?.status ?? queryData?.data?.status;\r\n const fail_reason = queryData?.data?.fail_reason ?? 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.data.result_url };\r\n case "FAILURE":\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.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 return null;\r\n};\r\nexports.ttsRequest = ttsRequest;\r\n', - enable: 1, + enable: 0, createTime: 1775164020756, }, { id: "volcengine", author: "leeqi", - description: "火山引擎方舟官方直连模板,接入 Ark 的文本、图片、视频生成 API,支持 Doubao、DeepSeek、GLM 等模型。", + description: + "火山引擎方舟官方直连模板,接入 Ark 的文本、图片、视频生成 API,支持 Doubao、DeepSeek、GLM 等模型。\n[](https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey)", name: "火山引擎", icon: "", inputs: @@ -453,10 +454,10 @@ export default async (knex: Knex, forceInit: boolean = false): Promise => inputValues: '{"apiKey":"","text":"https://ark.cn-beijing.volces.com/api/v3","baseUrl":"https://ark.cn-beijing.volces.com/api/v3","image":"https://ark.cn-beijing.volces.com/api/v3/images/generations","videoCreate":"https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks","videoQuery":"https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks/{id}"}', models: - '[{"name":"Doubao-Seed-2.0-pro","type":"text","modelName":"doubao-seed-2-0-pro-260215","think":false},{"name":"Doubao-Seed-2.0-lite","type":"text","modelName":"doubao-seed-2-0-lite-260215","think":false},{"name":"Doubao-Seed-2.0-mini","type":"text","modelName":"doubao-seed-2-0-mini-260215","think":false},{"name":"Doubao-Seed-2.0-Code","type":"text","modelName":"doubao-seed-2-0-code-preview-260215","think":false},{"name":"Doubao-1.5-pro-32k","type":"text","modelName":"doubao-1-5-pro-32k-250115","think":false},{"name":"deepseek-v3-250324","type":"text","modelName":"deepseek-v3-250324","think":false},{"name":"glm-4-7-251222","type":"text","modelName":"glm-4-7-251222","think":false},{"name":"Doubao-Seedream-5.0-lite","type":"image","modelName":"doubao-seedream-5-0-260128","mode":["text","singleImage","multiReference"]},{"name":"Doubao-Seedream-4.5","type":"image","modelName":"doubao-seedream-4-5-251128","mode":["text","singleImage","multiReference"]},{"name":"Doubao-Seedream-4.0","type":"image","modelName":"doubao-seedream-4-0-250828","mode":["text","singleImage","multiReference"]},{"name":"Doubao-Seedance-1.5-pro","type":"video","modelName":"doubao-seedance-1-5-pro-251215","mode":["text","singleImage","endFrameOptional"],"audio":true,"durationResolutionMap":[{"duration":[5],"resolution":["480p","720p","1080p"]}]},{"name":"Doubao-Seedance-1.0-pro-fast","type":"video","modelName":"doubao-seedance-1-0-pro-fast-251015","mode":["text","singleImage"],"audio":false,"durationResolutionMap":[{"duration":[2,3,4,5,6,7,8,9,10,11,12],"resolution":["480p","720p","1080p"]}]}]', - code: '//如需遥测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 ARK_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";\r\nconst SUCCESS_TASK_STATUS = ["succeeded", "completed", "success"];\r\nconst FAILED_TASK_STATUS = ["failed", "failure", "error", "canceled", "cancelled"];\r\n\r\nconst vendor: VendorConfig = {\r\n id: "volcengine",\r\n version: 1,\r\n author: "leeqi",\r\n description: "火山引擎方舟官方直连模板,接入 Ark 的文本、图片、视频生成 API,支持 Doubao、DeepSeek、GLM 等模型。",\r\n name: "火山引擎",\r\n inputs: [\r\n { key: "apiKey", label: "ARK API Key", type: "password", required: true },\r\n { key: "text", label: "文本生成接口", type: "url", required: false, placeholder: "如非必要请勿更改" },\r\n { key: "baseUrl", label: "Ark Base URL", type: "url", required: false, placeholder: "如非必要请勿更改" },\r\n { key: "image", label: "图片生成接口", type: "url", required: false, placeholder: "如非必要请勿更改" },\r\n { key: "videoCreate", label: "视频任务创建接口", type: "url", required: false, placeholder: "如非必要请勿更改" },\r\n { key: "videoQuery", label: "视频任务查询接口", type: "url", required: false, placeholder: "如非必要请勿更改" },\r\n ],\r\n inputValues: {\r\n apiKey: "",\r\n text: ARK_BASE_URL,\r\n baseUrl: ARK_BASE_URL,\r\n image: `${ARK_BASE_URL}/images/generations`,\r\n videoCreate: `${ARK_BASE_URL}/contents/generations/tasks`,\r\n videoQuery: `${ARK_BASE_URL}/contents/generations/tasks/{id}`,\r\n },\r\n models: [\r\n {\r\n name: "Doubao-Seed-2.0-pro",\r\n type: "text",\r\n modelName: "doubao-seed-2-0-pro-260215",\r\n think: false,\r\n },\r\n {\r\n name: "Doubao-Seed-2.0-lite",\r\n type: "text",\r\n modelName: "doubao-seed-2-0-lite-260215",\r\n think: false,\r\n },\r\n {\r\n name: "Doubao-Seed-2.0-mini",\r\n type: "text",\r\n modelName: "doubao-seed-2-0-mini-260215",\r\n think: false,\r\n },\r\n {\r\n name: "Doubao-Seed-2.0-Code",\r\n type: "text",\r\n modelName: "doubao-seed-2-0-code-preview-260215",\r\n think: false,\r\n },\r\n {\r\n name: "Doubao-1.5-pro-32k",\r\n type: "text",\r\n modelName: "doubao-1-5-pro-32k-250115",\r\n think: false,\r\n },\r\n {\r\n name: "deepseek-v3-250324",\r\n type: "text",\r\n modelName: "deepseek-v3-250324",\r\n think: false,\r\n },\r\n {\r\n name: "glm-4-7-251222",\r\n type: "text",\r\n modelName: "glm-4-7-251222",\r\n think: false,\r\n },\r\n {\r\n name: "Doubao-Seedream-5.0-lite",\r\n type: "image",\r\n modelName: "doubao-seedream-5-0-260128",\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 name: "Doubao-Seedream-4.0",\r\n type: "image",\r\n modelName: "doubao-seedream-4-0-250828",\r\n mode: ["text", "singleImage", "multiReference"],\r\n },\r\n {\r\n name: "Doubao-Seedance-1.5-pro",\r\n type: "video",\r\n modelName: "doubao-seedance-1-5-pro-251215",\r\n mode: ["text", "singleImage", "endFrameOptional"],\r\n audio: true,\r\n durationResolutionMap: [{ duration: [5], resolution: ["480p", "720p", "1080p"] }],\r\n },\r\n {\r\n name: "Doubao-Seedance-1.0-pro-fast",\r\n type: "video",\r\n modelName: "doubao-seedance-1-0-pro-fast-251015",\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};\r\nexports.vendor = vendor;\r\n\r\n// ==================== 适配器函数 ====================\r\nconst getApiKey = () => {\r\n if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");\r\n return vendor.inputValues.apiKey.replace(/^Bearer\\s+/i, "");\r\n};\r\n\r\nconst getBaseUrl = () => vendor.inputValues.baseUrl || ARK_BASE_URL;\r\nconst buildUrl = (overrideUrl: string | undefined, fallbackPath: string) => overrideUrl || `${getBaseUrl().replace(/\\/$/, "")}${fallbackPath}`;\r\n\r\nconst getHeaders = () => ({\r\n Authorization: `Bearer ${getApiKey()}`,\r\n "Content-Type": "application/json",\r\n});\r\n\r\nconst getOpenAIBaseUrl = () =>\r\n (vendor.inputValues.text || getBaseUrl())\r\n .trim()\r\n .replace(/\\/$/, "")\r\n .replace(/\\/chat\\/completions$/i, "");\r\n\r\nconst readJson = async (response: Response, action: string) => {\r\n if (!response.ok) {\r\n const errorText = await response.text();\r\n throw new Error(`${action}失败,状态码: ${response.status}, 错误信息: ${errorText}`);\r\n }\r\n\r\n return response.json();\r\n};\r\n\r\n//补齐图片 data url 前缀\r\nconst normalizeImageInput = (value: string) => {\r\n if (!value) return value;\r\n if (/^(https?:\\/\\/|data:|volc:)/i.test(value)) return value;\r\n return `data:image/png;base64,${value}`;\r\n};\r\n\r\nconst normalizeImageList = (imageBase64?: string[]) => (imageBase64 || []).filter(Boolean).map(normalizeImageInput);\r\n\r\nconst extractImageResult = (data: any) => {\r\n const first = data?.data?.[0] ?? data?.images?.[0] ?? data?.output?.[0];\r\n return first?.url ?? first?.image_url ?? first?.b64_json ?? first?.base64;\r\n};\r\n\r\nconst extractTaskId = (data: any) => data?.id ?? data?.task_id ?? data?.taskId ?? data?.data?.id ?? data?.data?.task_id ?? data?.data;\r\n\r\nconst extractVideoUrl = (data: any) =>\r\n data?.content?.video_url ??\r\n data?.content?.video_urls?.[0] ??\r\n data?.data?.content?.video_url ??\r\n data?.data?.content?.video_urls?.[0] ??\r\n data?.data?.video_url ??\r\n data?.result?.video_url ??\r\n data?.result_url ??\r\n data?.data?.result_url;\r\n\r\nconst getTaskStatus = (data: any) => (data?.status ?? data?.data?.status ?? "").toString().toLowerCase();\r\n\r\nconst getTaskError = (data: any) =>\r\n data?.error?.message ?? data?.message ?? data?.data?.message ?? data?.data?.fail_reason ?? data?.fail_reason ?? "任务执行失败";\r\n\r\n// 文本请求函数\r\nconst textRequest: (textModel: TextModel) => { url: string; model: string } = (textModel) => {\r\n return createOpenAI({\r\n baseURL: getOpenAIBaseUrl(),\r\n apiKey: getApiKey(),\r\n }).chat(textModel.modelName);\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\n\r\nconst normalizeImageSize = (imageConfig: ImageConfig) => {\r\n const normalizedSize = imageConfig.size.toUpperCase();\r\n const normalizedAspectRatio = imageConfig.aspectRatio;\r\n\r\n if (normalizedSize === "1K") {\r\n return "2k";\r\n }\r\n\r\n const sizeMap: Record<"16:9" | "9:16", Record<"2K" | "4K", string>> = {\r\n "16:9": {\r\n "2K": "2848x1600",\r\n "4K": "4096x2304",\r\n },\r\n "9:16": {\r\n "2K": "1600x2848",\r\n "4K": "2304x4096",\r\n },\r\n };\r\n\r\n if (normalizedAspectRatio === "16:9" || normalizedAspectRatio === "9:16") {\r\n return sizeMap[normalizedAspectRatio][normalizedSize as "2K" | "4K"];\r\n }\r\n\r\n return normalizedSize === "4K" ? "3k" : "2k";\r\n};\r\n\r\nconst imageRequest = async (imageConfig: ImageConfig, imageModel: ImageModel) => {\r\n const images = normalizeImageList(imageConfig.imageBase64);\r\n const body = {\r\n model: imageModel.modelName,\r\n prompt: imageConfig.prompt,\r\n size: normalizeImageSize(imageConfig),\r\n response_format: "url",\r\n sequential_image_generation: "disabled",\r\n stream: false,\r\n watermark: false,\r\n ...(images.length ? { image: images.length === 1 ? images[0] : images } : {}),\r\n };\r\n\r\n const data = await readJson(\r\n await fetch(buildUrl(vendor.inputValues.image, "/images/generations"), {\r\n method: "POST",\r\n headers: getHeaders(),\r\n body: JSON.stringify(body),\r\n }),\r\n "图片生成",\r\n );\r\n\r\n const result = extractImageResult(data);\r\n if (!result) throw new Error(`图片生成返回格式异常: ${JSON.stringify(data)}`);\r\n return result;\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\nconst isSeedanceModel = (modelName: string) => /^doubao-seedance-/i.test(modelName);\r\nconst isSeedance15ProModel = (modelName: string) => /^doubao-seedance-1-5-pro/i.test(modelName);\r\n\r\nconst appendPromptFlag = (prompt: string, flag: string, value: string | number | boolean | undefined) => {\r\n if (value === undefined || value === null || value === "") return prompt;\r\n const normalizedPrompt = prompt.trim();\r\n const flagPattern = new RegExp(`(^|\\\\s)--${flag}(?:\\\\s+\\\\S+)?(?=\\\\s|$)`, "i");\r\n if (flagPattern.test(normalizedPrompt)) return normalizedPrompt;\r\n return `${normalizedPrompt} --${flag} ${value}`.trim();\r\n};\r\n\r\nconst buildVideoPrompt = (videoConfig: VideoConfig, videoModel: VideoModel) => {\r\n if (!isSeedanceModel(videoModel.modelName)) return videoConfig.prompt || "";\r\n\r\n let prompt = videoConfig.prompt || "";\r\n prompt = appendPromptFlag(prompt, "resolution", videoConfig.resolution);\r\n prompt = appendPromptFlag(prompt, "duration", videoConfig.duration);\r\n prompt = appendPromptFlag(prompt, "camerafixed", false);\r\n prompt = appendPromptFlag(prompt, "watermark", false);\r\n return prompt;\r\n};\r\n\r\nconst buildVideoContent = (videoConfig: VideoConfig, videoModel: VideoModel) => [\r\n { type: "text", text: buildVideoPrompt(videoConfig, videoModel) },\r\n ...normalizeImageList(videoConfig.imageBase64).map((image) => ({\r\n type: "image_url",\r\n image_url: { url: image },\r\n })),\r\n];\r\n\r\nconst buildVideoBody = (videoConfig: VideoConfig, videoModel: VideoModel) => {\r\n const isSeedance = isSeedanceModel(videoModel.modelName);\r\n const isSeedance15Pro = isSeedance15ProModel(videoModel.modelName);\r\n\r\n return {\r\n model: videoModel.modelName,\r\n content: buildVideoContent(videoConfig, videoModel),\r\n ...(!isSeedance15Pro && !isSeedance\r\n ? {\r\n duration: videoConfig.duration,\r\n resolution: videoConfig.resolution,\r\n ratio: videoConfig.aspectRatio,\r\n }\r\n : {}),\r\n ...(videoModel.audio === true && typeof videoConfig.audio === "boolean" ? { generate_audio: videoConfig.audio } : {}),\r\n };\r\n};\r\n\r\nconst queryVideoResult = async (taskId: string) => {\r\n const queryData = await readJson(\r\n await fetch(buildUrl(vendor.inputValues.videoQuery, "/contents/generations/tasks/{id}").replace("{id}", taskId), {\r\n method: "GET",\r\n headers: getHeaders(),\r\n }),\r\n "视频任务查询",\r\n );\r\n\r\n const status = getTaskStatus(queryData);\r\n if (SUCCESS_TASK_STATUS.includes(status)) {\r\n const videoUrl = extractVideoUrl(queryData);\r\n if (!videoUrl) return { completed: true, error: `视频任务成功但未返回结果: ${JSON.stringify(queryData)}` };\r\n return { completed: true, data: videoUrl };\r\n }\r\n\r\n if (FAILED_TASK_STATUS.includes(status)) {\r\n return { completed: false, error: getTaskError(queryData) };\r\n }\r\n\r\n return { completed: false };\r\n};\r\n\r\nconst videoRequest = async (videoConfig: VideoConfig, videoModel: VideoModel) => {\r\n const createData = await readJson(\r\n await fetch(buildUrl(vendor.inputValues.videoCreate, "/contents/generations/tasks"), {\r\n method: "POST",\r\n headers: getHeaders(),\r\n body: JSON.stringify(buildVideoBody(videoConfig, videoModel)),\r\n }),\r\n "视频任务创建",\r\n );\r\n\r\n const taskId = extractTaskId(createData);\r\n if (!taskId) throw new Error(`视频任务创建返回格式异常: ${JSON.stringify(createData)}`);\r\n\r\n const result = await pollTask(() => queryVideoResult(taskId));\r\n if (result.error) throw new Error(result.error);\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\n\r\nconst ttsRequest = async (ttsConfig: TTSConfig, ttsModel: TTSModel) => {\r\n return null;\r\n};\r\nexports.ttsRequest = ttsRequest;\r\n', + '[{"name":"Doubao-Seed-2.0-pro","type":"text","modelName":"doubao-seed-2-0-pro-260215","think":false},{"name":"Doubao-Seed-2.0-lite","type":"text","modelName":"doubao-seed-2-0-lite-260215","think":false},{"name":"Doubao-Seed-2.0-mini","type":"text","modelName":"doubao-seed-2-0-mini-260215","think":false},{"name":"Doubao-Seed-2.0-Code","type":"text","modelName":"doubao-seed-2-0-code-preview-260215","think":false},{"name":"Doubao-1.5-pro-32k","type":"text","modelName":"doubao-1-5-pro-32k-250115","think":false},{"name":"deepseek-v3-250324","type":"text","modelName":"deepseek-v3-250324","think":false},{"name":"glm-4-7-251222","type":"text","modelName":"glm-4-7-251222","think":false},{"name":"Doubao-Seedream-5.0-lite","type":"image","modelName":"doubao-seedream-5-0-260128","mode":["text","singleImage","multiReference"]},{"name":"Doubao-Seedream-4.5","type":"image","modelName":"doubao-seedream-4-5-251128","mode":["text","singleImage","multiReference"]},{"name":"Doubao-Seedream-4.0","type":"image","modelName":"doubao-seedream-4-0-250828","mode":["text","singleImage","multiReference"]},{"name":"Doubao-Seedance-1.5-pro","type":"video","modelName":"doubao-seedance-1-5-pro-251215","mode":["text","singleImage","endFrameOptional"],"audio":true,"durationResolutionMap":[{"duration":[5],"resolution":["480p","720p","1080p"]}]},{"name":"Doubao-Seedance-1.5-pro","type":"video","modelName":"doubao-seedance-1-5-pro-251215","mode":["text","singleImage","endFrameOptional"],"audio":true,"durationResolutionMap":[{"duration":[5],"resolution":["480p","720p","1080p"]}]},{"name":"Doubao-Seedance-1.0-pro-fast","type":"video","modelName":"doubao-seedance-1-0-pro-fast-251015","mode":["text","singleImage"],"audio":false,"durationResolutionMap":[{"duration":[2,3,4,5,6,7,8,9,10,11,12],"resolution":["480p","720p","1080p"]}]},{"name":"Doubao-Seedance-2.0","modelName":"doubao-seedance-2-0-260128","type":"video","mode":[["textReference","videoReference","imageReference","audioReference"]],"audio":"optional","durationResolutionMap":[{"duration":[4,5,6,7,8,9,10,11,12,13,14,15],"resolution":["480p","720p"]}],"associationSkills":""}]', + code: '//如需遥测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 ARK_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";\r\nconst SUCCESS_TASK_STATUS = ["succeeded", "completed", "success"];\r\nconst FAILED_TASK_STATUS = ["failed", "failure", "error", "canceled", "cancelled"];\r\n\r\nconst vendor: VendorConfig = {\r\n id: "volcengine",\r\n author: "leeqi",\r\n description: "火山引擎方舟官方直连模板,接入 Ark 的文本、图片、视频生成 API,支持 Doubao、DeepSeek、GLM 等模型。\\n[](https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey)",\r\n name: "火山引擎",\r\n inputs: [\r\n { key: "apiKey", label: "ARK API Key", type: "password", required: true },\r\n { key: "text", label: "文本生成接口", type: "url", required: false, placeholder: "如非必要请勿更改" },\r\n { key: "baseUrl", label: "Ark Base URL", type: "url", required: false, placeholder: "如非必要请勿更改" },\r\n { key: "image", label: "图片生成接口", type: "url", required: false, placeholder: "如非必要请勿更改" },\r\n { key: "videoCreate", label: "视频任务创建接口", type: "url", required: false, placeholder: "如非必要请勿更改" },\r\n { key: "videoQuery", label: "视频任务查询接口", type: "url", required: false, placeholder: "如非必要请勿更改" },\r\n ],\r\n inputValues: {\r\n apiKey: "",\r\n text: ARK_BASE_URL,\r\n baseUrl: ARK_BASE_URL,\r\n image: `${ARK_BASE_URL}/images/generations`,\r\n videoCreate: `${ARK_BASE_URL}/contents/generations/tasks`,\r\n videoQuery: `${ARK_BASE_URL}/contents/generations/tasks/{id}`,\r\n },\r\n models: [\r\n {\r\n name: "Doubao-Seed-2.0-pro",\r\n type: "text",\r\n modelName: "doubao-seed-2-0-pro-260215",\r\n think: false,\r\n },\r\n {\r\n name: "Doubao-Seed-2.0-lite",\r\n type: "text",\r\n modelName: "doubao-seed-2-0-lite-260215",\r\n think: false,\r\n },\r\n {\r\n name: "Doubao-Seed-2.0-mini",\r\n type: "text",\r\n modelName: "doubao-seed-2-0-mini-260215",\r\n think: false,\r\n },\r\n {\r\n name: "Doubao-Seed-2.0-Code",\r\n type: "text",\r\n modelName: "doubao-seed-2-0-code-preview-260215",\r\n think: false,\r\n },\r\n {\r\n name: "Doubao-1.5-pro-32k",\r\n type: "text",\r\n modelName: "doubao-1-5-pro-32k-250115",\r\n think: false,\r\n },\r\n {\r\n name: "deepseek-v3-250324",\r\n type: "text",\r\n modelName: "deepseek-v3-250324",\r\n think: false,\r\n },\r\n {\r\n name: "glm-4-7-251222",\r\n type: "text",\r\n modelName: "glm-4-7-251222",\r\n think: false,\r\n },\r\n {\r\n name: "Doubao-Seedream-5.0-lite",\r\n type: "image",\r\n modelName: "doubao-seedream-5-0-260128",\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 name: "Doubao-Seedream-4.0",\r\n type: "image",\r\n modelName: "doubao-seedream-4-0-250828",\r\n mode: ["text", "singleImage", "multiReference"],\r\n },\r\n {\r\n name: "Doubao-Seedance-1.5-pro",\r\n type: "video",\r\n modelName: "doubao-seedance-1-5-pro-251215",\r\n mode: ["text", "singleImage", "endFrameOptional"],\r\n audio: true,\r\n durationResolutionMap: [{ duration: [5], resolution: ["480p", "720p", "1080p"] }],\r\n },\r\n {\r\n name: "Doubao-Seedance-1.5-pro",\r\n type: "video",\r\n modelName: "doubao-seedance-1-5-pro-251215",\r\n mode: ["text", "singleImage", "endFrameOptional"],\r\n audio: true,\r\n durationResolutionMap: [{ duration: [5], resolution: ["480p", "720p", "1080p"] }],\r\n },\r\n {\r\n name: "Doubao-Seedance-1.0-pro-fast",\r\n type: "video",\r\n modelName: "doubao-seedance-1-0-pro-fast-251015",\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: "Doubao-Seedance-2.0",\r\n modelName: "doubao-seedance-2-0-260128",\r\n type: "video",\r\n mode: [["textReference", "videoReference", "imageReference", "audioReference"]],\r\n audio: "optional",\r\n durationResolutionMap: [\r\n {\r\n duration: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],\r\n resolution: ["480p", "720p"],\r\n },\r\n ],\r\n associationSkills: "",\r\n },\r\n ],\r\n};\r\nexports.vendor = vendor;\r\n\r\n// ==================== 适配器函数 ====================\r\nconst getApiKey = () => {\r\n if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");\r\n return vendor.inputValues.apiKey.replace(/^Bearer\\s+/i, "");\r\n};\r\n\r\nconst getBaseUrl = () => vendor.inputValues.baseUrl || ARK_BASE_URL;\r\nconst buildUrl = (overrideUrl: string | undefined, fallbackPath: string) => overrideUrl || `${getBaseUrl().replace(/\\/$/, "")}${fallbackPath}`;\r\n\r\nconst getHeaders = () => ({\r\n Authorization: `Bearer ${getApiKey()}`,\r\n "Content-Type": "application/json",\r\n});\r\n\r\nconst getOpenAIBaseUrl = () =>\r\n (vendor.inputValues.text || getBaseUrl())\r\n .trim()\r\n .replace(/\\/$/, "")\r\n .replace(/\\/chat\\/completions$/i, "");\r\n\r\nconst readJson = async (response: Response, action: string) => {\r\n if (!response.ok) {\r\n const errorText = await response.text();\r\n throw new Error(`${action}失败,状态码: ${response.status}, 错误信息: ${errorText}`);\r\n }\r\n\r\n return response.json();\r\n};\r\n\r\n//补齐图片 data url 前缀\r\nconst normalizeImageInput = (value: string) => {\r\n if (!value) return value;\r\n if (/^(https?:\\/\\/|data:|volc:)/i.test(value)) return value;\r\n return `data:image/png;base64,${value}`;\r\n};\r\n\r\nconst normalizeImageList = (imageBase64?: string[]) => (imageBase64 || []).filter(Boolean).map(normalizeImageInput);\r\n\r\nconst extractImageResult = (data: any) => {\r\n const first = data?.data?.[0] ?? data?.images?.[0] ?? data?.output?.[0];\r\n return first?.url ?? first?.image_url ?? first?.b64_json ?? first?.base64;\r\n};\r\n\r\nconst extractTaskId = (data: any) => data?.id ?? data?.task_id ?? data?.taskId ?? data?.data?.id ?? data?.data?.task_id ?? data?.data;\r\n\r\nconst extractVideoUrl = (data: any) =>\r\n data?.content?.video_url ??\r\n data?.content?.video_urls?.[0] ??\r\n data?.data?.content?.video_url ??\r\n data?.data?.content?.video_urls?.[0] ??\r\n data?.data?.video_url ??\r\n data?.result?.video_url ??\r\n data?.result_url ??\r\n data?.data?.result_url;\r\n\r\nconst getTaskStatus = (data: any) => (data?.status ?? data?.data?.status ?? "").toString().toLowerCase();\r\n\r\nconst getTaskError = (data: any) =>\r\n data?.error?.message ?? data?.message ?? data?.data?.message ?? data?.data?.fail_reason ?? data?.fail_reason ?? "任务执行失败";\r\n\r\n// 文本请求函数\r\nconst textRequest: (textModel: TextModel) => { url: string; model: string } = (textModel) => {\r\n return createOpenAI({\r\n baseURL: getOpenAIBaseUrl(),\r\n apiKey: getApiKey(),\r\n }).chat(textModel.modelName);\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\n\r\nconst normalizeImageSize = (imageConfig: ImageConfig) => {\r\n const normalizedSize = imageConfig.size.toUpperCase();\r\n const normalizedAspectRatio = imageConfig.aspectRatio;\r\n\r\n if (normalizedSize === "1K") {\r\n return "2k";\r\n }\r\n\r\n const sizeMap: Record<"16:9" | "9:16", Record<"2K" | "4K", string>> = {\r\n "16:9": {\r\n "2K": "2848x1600",\r\n "4K": "4096x2304",\r\n },\r\n "9:16": {\r\n "2K": "1600x2848",\r\n "4K": "2304x4096",\r\n },\r\n };\r\n\r\n if (normalizedAspectRatio === "16:9" || normalizedAspectRatio === "9:16") {\r\n return sizeMap[normalizedAspectRatio][normalizedSize as "2K" | "4K"];\r\n }\r\n\r\n return normalizedSize === "4K" ? "3k" : "2k";\r\n};\r\n\r\nconst imageRequest = async (imageConfig: ImageConfig, imageModel: ImageModel) => {\r\n const images = normalizeImageList(imageConfig.imageBase64);\r\n const body = {\r\n model: imageModel.modelName,\r\n prompt: imageConfig.prompt,\r\n size: normalizeImageSize(imageConfig),\r\n response_format: "url",\r\n sequential_image_generation: "disabled",\r\n stream: false,\r\n watermark: false,\r\n ...(images.length ? { image: images.length === 1 ? images[0] : images } : {}),\r\n };\r\n\r\n const data = await readJson(\r\n await fetch(buildUrl(vendor.inputValues.image, "/images/generations"), {\r\n method: "POST",\r\n headers: getHeaders(),\r\n body: JSON.stringify(body),\r\n }),\r\n "图片生成",\r\n );\r\n\r\n const result = extractImageResult(data);\r\n if (!result) throw new Error(`图片生成返回格式异常: ${JSON.stringify(data)}`);\r\n return result;\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\nconst isSeedanceModel = (modelName: string) => /^doubao-seedance-/i.test(modelName);\r\nconst isSeedance15ProModel = (modelName: string) => /^doubao-seedance-1-5-pro/i.test(modelName);\r\n\r\nconst appendPromptFlag = (prompt: string, flag: string, value: string | number | boolean | undefined) => {\r\n if (value === undefined || value === null || value === "") return prompt;\r\n const normalizedPrompt = prompt.trim();\r\n const flagPattern = new RegExp(`(^|\\\\s)--${flag}(?:\\\\s+\\\\S+)?(?=\\\\s|$)`, "i");\r\n if (flagPattern.test(normalizedPrompt)) return normalizedPrompt;\r\n return `${normalizedPrompt} --${flag} ${value}`.trim();\r\n};\r\n\r\nconst buildVideoPrompt = (videoConfig: VideoConfig, videoModel: VideoModel) => {\r\n if (!isSeedanceModel(videoModel.modelName)) return videoConfig.prompt || "";\r\n\r\n let prompt = videoConfig.prompt || "";\r\n prompt = appendPromptFlag(prompt, "resolution", videoConfig.resolution);\r\n prompt = appendPromptFlag(prompt, "duration", videoConfig.duration);\r\n prompt = appendPromptFlag(prompt, "camerafixed", false);\r\n prompt = appendPromptFlag(prompt, "watermark", false);\r\n return prompt;\r\n};\r\n\r\nconst buildVideoContent = (videoConfig: VideoConfig, videoModel: VideoModel) => [\r\n { type: "text", text: buildVideoPrompt(videoConfig, videoModel) },\r\n ...normalizeImageList(videoConfig.imageBase64).map((image) => ({\r\n type: "image_url",\r\n image_url: { url: image },\r\n })),\r\n];\r\n\r\nconst buildVideoBody = (videoConfig: VideoConfig, videoModel: VideoModel) => {\r\n const isSeedance = isSeedanceModel(videoModel.modelName);\r\n const isSeedance15Pro = isSeedance15ProModel(videoModel.modelName);\r\n\r\n return {\r\n model: videoModel.modelName,\r\n content: buildVideoContent(videoConfig, videoModel),\r\n ...(!isSeedance15Pro && !isSeedance\r\n ? {\r\n duration: videoConfig.duration,\r\n resolution: videoConfig.resolution,\r\n ratio: videoConfig.aspectRatio,\r\n }\r\n : {}),\r\n ...(videoModel.audio === true && typeof videoConfig.audio === "boolean" ? { generate_audio: videoConfig.audio } : {}),\r\n };\r\n};\r\n\r\nconst queryVideoResult = async (taskId: string) => {\r\n const queryData = await readJson(\r\n await fetch(buildUrl(vendor.inputValues.videoQuery, "/contents/generations/tasks/{id}").replace("{id}", taskId), {\r\n method: "GET",\r\n headers: getHeaders(),\r\n }),\r\n "视频任务查询",\r\n );\r\n\r\n const status = getTaskStatus(queryData);\r\n if (SUCCESS_TASK_STATUS.includes(status)) {\r\n const videoUrl = extractVideoUrl(queryData);\r\n if (!videoUrl) return { completed: true, error: `视频任务成功但未返回结果: ${JSON.stringify(queryData)}` };\r\n return { completed: true, data: videoUrl };\r\n }\r\n\r\n if (FAILED_TASK_STATUS.includes(status)) {\r\n return { completed: false, error: getTaskError(queryData) };\r\n }\r\n\r\n return { completed: false };\r\n};\r\n\r\nconst videoRequest = async (videoConfig: VideoConfig, videoModel: VideoModel) => {\r\n const createData = await readJson(\r\n await fetch(buildUrl(vendor.inputValues.videoCreate, "/contents/generations/tasks"), {\r\n method: "POST",\r\n headers: getHeaders(),\r\n body: JSON.stringify(buildVideoBody(videoConfig, videoModel)),\r\n }),\r\n "视频任务创建",\r\n );\r\n\r\n const taskId = extractTaskId(createData);\r\n if (!taskId) throw new Error(`视频任务创建返回格式异常: ${JSON.stringify(createData)}`);\r\n\r\n const result = await pollTask(() => queryVideoResult(taskId));\r\n if (result.error) throw new Error(result.error);\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\n\r\nconst ttsRequest = async (ttsConfig: TTSConfig, ttsModel: TTSModel) => {\r\n return null;\r\n};\r\nexports.ttsRequest = ttsRequest;\r\n', enable: 0, - createTime: 1775155204210, + createTime: 1775172727809, }, { id: "minimax",