diff --git a/data/vendor/volcengine.ts b/data/vendor/volcengine.ts index 86eaa56..3a696c1 100644 --- a/data/vendor/volcengine.ts +++ b/data/vendor/volcengine.ts @@ -133,7 +133,7 @@ declare const exports: { const vendor: VendorConfig = { id: "volcengine", - version: "2.1", + version: "2.2", author: "leeqi", name: "火山引擎(豆包)", description: @@ -301,10 +301,10 @@ const textRequest = (model: TextModel, think: boolean, thinkLevel: 0 | 1 | 2 | 3 3: "high", }; - return createOpenAI({ + return createOpenAICompatible({ + name: "volcengine", baseURL: getBaseUrl(), apiKey, - compatibility: "compatible", fetch: async (url: string, options?: RequestInit) => { const rawBody = JSON.parse((options?.body as string) ?? "{}"); const modifiedBody = { @@ -319,7 +319,7 @@ const textRequest = (model: TextModel, think: boolean, thinkLevel: 0 | 1 | 2 | 3 body: JSON.stringify(modifiedBody), }); }, - }).chat(model.modelName); + }).chatModel(model.modelName); }; const imageRequest = async (config: ImageConfig, model: ImageModel): Promise => { diff --git a/data/version.txt b/data/version.txt index 9c1218c..1b87bcd 100644 --- a/data/version.txt +++ b/data/version.txt @@ -1 +1 @@ -1.1.3 \ No newline at end of file +1.1.4 \ No newline at end of file diff --git a/package.json b/package.json index 9294cf0..001e568 100644 --- a/package.json +++ b/package.json @@ -99,4 +99,4 @@ "better-sqlite3": "^12.8.0" } } -} \ No newline at end of file +} diff --git a/src/agents/productionAgent/index.ts b/src/agents/productionAgent/index.ts index 4c54335..434af18 100644 --- a/src/agents/productionAgent/index.ts +++ b/src/agents/productionAgent/index.ts @@ -17,6 +17,11 @@ export interface AgentContext { abortSignal?: AbortSignal; resTool: ResTool; msg: ReturnType; + messages?: { role: "user" | "assistant" | "system"; content: string }[]; + thinkConfig: { + think: boolean; + thinlLevel: 0 | 1 | 2 | 3; + }; } function buildMemPrompt(mem: Awaited>): string { @@ -35,7 +40,7 @@ function buildMemPrompt(mem: Awaited>): string { return `## Memory\n以下是你对用户的记忆,可作为参考但不要主动提及:\n${memoryContext}`; } -export async function decisionAI(ctx: AgentContext) { +export async function runDecisionAI(ctx: AgentContext) { const { isolationKey, text, abortSignal } = ctx; const memory = new Memory("productionAgent", isolationKey); await memory.add("user", text); @@ -45,17 +50,17 @@ export async function decisionAI(ctx: AgentContext) { const projectInfo = await u.db("o_project").where("id", ctx.resTool.data.projectId).first(); if (!projectInfo) throw new Error(`项目不存在,ID: ${ctx.resTool.data.projectId}`); - const [_, imageModelName] = projectInfo.imageModel!.split(":"); - const [id, videoModelName] = projectInfo.videoModel!.split(":"); + const [_, imageModelName] = projectInfo.imageModel!.split(/:(.+)/) + const [id, videoModelName] = projectInfo.videoModel!.split(/:(.+)/) const models = await u.vendor.getModelList(id); - if(!models.length) throw new Error(`项目使用的模型不存在,ID: ${projectInfo.videoModel}`); + if (!models.length) throw new Error(`项目使用的模型不存在,ID: ${projectInfo.videoModel}`); const findData = models.find((i: any) => i.modelName == videoModelName); const isRef = findData.mode.every((i: any) => Array.isArray(i)); const modelInfo = `项目使用的模型如下:\n图像模型:${imageModelName}\n视频模型:${videoModelName}\n多参:${isRef ? "是" : "否"}`; const mem = buildMemPrompt(await memory.get(text)); - const { textStream } = await u.Ai.Text("productionAgent").stream({ + const { fullStream } = await u.Ai.Text("productionAgent", ctx.thinkConfig.think, ctx.thinkConfig.thinlLevel).stream({ messages: [ { role: "system", content: prompt }, { role: "assistant", content: mem + "\n" + modelInfo }, @@ -72,7 +77,13 @@ export async function decisionAI(ctx: AgentContext) { }, }); - return textStream; + let currentMsg = ctx.msg; + await consumeFullStream(fullStream, currentMsg, () => { + if (ctx.msg === currentMsg) return currentMsg; + currentMsg.complete(); + currentMsg = ctx.msg; + return currentMsg; + }); } async function createSubAgent(parentCtx: AgentContext) { @@ -95,29 +106,15 @@ async function createSubAgent(parentCtx: AgentContext) { }) { parentCtx.msg.complete(); const subMsg = resTool.newMessage("assistant", name); - const text = subMsg.text(); - let fullResponse = ""; - const { textStream } = await u.Ai.Text("productionAgent").stream({ + const { fullStream } = await u.Ai.Text("productionAgent", parentCtx.thinkConfig.think, parentCtx.thinkConfig.thinlLevel).stream({ system, messages: messages ?? [{ role: "user", content: prompt }], abortSignal, tools: { ...extraTools, ...useTools({ resTool, msg: subMsg }) }, }); - try { - for await (const chunk of textStream) { - await new Promise((resolve) => setTimeout(() => resolve(), 1)); - text.append(chunk); - fullResponse += chunk; - } - text.complete(); - subMsg.complete(); - } catch (err: any) { - text.complete(); - subMsg.stop(); - throw err; - } + const fullResponse = await consumeFullStream(fullStream, subMsg); if (fullResponse.trim()) { await memory.add(memoryKey, removeAllXmlTags(fullResponse), { @@ -138,10 +135,10 @@ async function createSubAgent(parentCtx: AgentContext) { if (!projectInfo) throw new Error(`项目不存在,ID: ${resTool.data.projectId}`); const artSkills = await createArtSkills(projectInfo?.artStyle!, projectInfo?.directorManual!); - const [_, imageModelName] = projectInfo.imageModel!.split(":"); - const [id, videoModelName] = projectInfo.videoModel!.split(":"); + const [_, imageModelName] = projectInfo.imageModel!.split(/:(.+)/) + const [id, videoModelName] = projectInfo.videoModel!.split(/:(.+)/) const models = await u.vendor.getModelList(id); - if(!models.length) throw new Error(`项目使用的模型不存在,ID: ${projectInfo.videoModel}`); + if (!models.length) throw new Error(`项目使用的模型不存在,ID: ${projectInfo.videoModel}`); const findData = models.find((i: any) => i.modelName == videoModelName); const isRef = findData.mode.every((i: any) => Array.isArray(i)); const modelInfo = `项目使用的模型如下:\n图像模型:${imageModelName}\n视频模型:${videoModelName}\n多参:${isRef ? "是" : "否"}`; @@ -370,7 +367,53 @@ ${buildSkillPrompt(mainSkills)}`, }; return res; } +async function consumeFullStream( + fullStream: AsyncIterable, + initialMsg: ReturnType, + syncMsg?: () => ReturnType, +): Promise { + let msg = initialMsg; + let text = msg.text(); + let thinking: ReturnType | null = null; + let thinkTime = 0; + let fullResponse = ""; + try { + for await (const chunk of fullStream) { + await new Promise((resolve) => setTimeout(() => resolve(), 1)); + if (syncMsg) { + const newMsg = syncMsg(); + if (newMsg !== msg) { + msg = newMsg; + text = msg.text(); + } + } + if (chunk.type === "reasoning-start") { + thinkTime = Date.now(); + thinking = msg.thinking("思考中..."); + } else if (chunk.type === "reasoning-delta") { + thinking?.append(chunk.text); + } else if (chunk.type === "reasoning-end") { + thinkTime = Date.now() - thinkTime; + thinking?.updateTitle(`思考完毕(${(thinkTime / 1000).toFixed(1)} 秒)`); + thinking?.complete(); + thinking = null; + } else if (chunk.type === "text-delta") { + text.append(chunk.text); + fullResponse += chunk.text; + } + } + text.complete(); + msg.complete(); + } catch (err: any) { + thinking?.complete(); + text.complete(); + msg.stop(); + throw err; + } + + return fullResponse; +} function removeAllXmlTags(text: string): string { text = text.replace(/<([a-zA-Z][\w-]*)(\s+[^>]*)?>([\s\S]*?)<\/\1>/g, ""); text = text.replace(/<([a-zA-Z][\w-]*)(\s+[^>]*)?\/>/g, ""); diff --git a/src/agents/scriptAgent/index.ts b/src/agents/scriptAgent/index.ts index 39c602d..b4a0c76 100644 --- a/src/agents/scriptAgent/index.ts +++ b/src/agents/scriptAgent/index.ts @@ -16,6 +16,10 @@ export interface AgentContext { abortSignal?: AbortSignal; resTool: ResTool; msg: ReturnType; + thinkConfig: { + think: boolean; + thinlLevel: 0 | 1 | 2 | 3; + }; } function buildMemPrompt(mem: Awaited>): string { @@ -34,7 +38,7 @@ function buildMemPrompt(mem: Awaited>): string { return `## Memory\n以下是你对用户的记忆,可作为参考但不要主动提及:\n${memoryContext}`; } -export async function decisionAI(ctx: AgentContext) { +export async function runDecisionAI(ctx: AgentContext) { const { isolationKey, text, userMessageTime, abortSignal, resTool } = ctx; const memory = new Memory("scriptAgent", isolationKey); @@ -59,7 +63,7 @@ export async function decisionAI(ctx: AgentContext) { `章节数量:${novelData.length}章`, ].join("\n"); - const { textStream } = await u.Ai.Text("scriptAgent").stream({ + const { fullStream } = await u.Ai.Text("scriptAgent", ctx.thinkConfig.think, ctx.thinkConfig.thinlLevel).stream({ messages: [ { role: "system", content: prompt }, { role: "assistant", content: projectInfo + "\n" + mem }, @@ -76,7 +80,13 @@ export async function decisionAI(ctx: AgentContext) { }, }); - return textStream; + let currentMsg = ctx.msg; + await consumeFullStream(fullStream, currentMsg, () => { + if (ctx.msg === currentMsg) return currentMsg; + currentMsg.complete(); + currentMsg = ctx.msg; + return currentMsg; + }); } function createSubAgent(parentCtx: AgentContext) { @@ -100,29 +110,15 @@ function createSubAgent(parentCtx: AgentContext) { }) { parentCtx.msg.complete(); const subMsg = resTool.newMessage("assistant", name); - const text = subMsg.text(); - let fullResponse = ""; - const { textStream } = await u.Ai.Text("scriptAgent").stream({ + const { fullStream } = await u.Ai.Text("scriptAgent", parentCtx.thinkConfig.think, parentCtx.thinkConfig.thinlLevel ).stream({ system, messages: messages ?? [{ role: "user", content: prompt }], abortSignal, tools: { ...extraTools, ...useTools({ resTool, msg: subMsg }) }, }); - try { - for await (const chunk of textStream) { - await new Promise((resolve) => setTimeout(() => resolve(), 1)); - text.append(chunk); - fullResponse += chunk; - } - text.complete(); - subMsg.complete(); - } catch (err: any) { - text.complete(); - subMsg.stop(); - throw err; - } + const fullResponse = await consumeFullStream(fullStream, subMsg); if (fullResponse.trim()) { await memory.add(memoryKey, removeAllXmlTags(fullResponse), { @@ -230,6 +226,54 @@ function createSubAgent(parentCtx: AgentContext) { }; } +async function consumeFullStream( + fullStream: AsyncIterable, + initialMsg: ReturnType, + syncMsg?: () => ReturnType, +): Promise { + let msg = initialMsg; + let text = msg.text(); + let thinking: ReturnType | null = null; + let thinkTime = 0; + let fullResponse = ""; + + try { + for await (const chunk of fullStream) { + await new Promise((resolve) => setTimeout(() => resolve(), 1)); + if (syncMsg) { + const newMsg = syncMsg(); + if (newMsg !== msg) { + msg = newMsg; + text = msg.text(); + } + } + if (chunk.type === "reasoning-start") { + thinkTime = Date.now(); + thinking = msg.thinking("思考中..."); + } else if (chunk.type === "reasoning-delta") { + thinking?.append(chunk.text); + } else if (chunk.type === "reasoning-end") { + thinkTime = Date.now() - thinkTime; + thinking?.updateTitle(`思考完毕(${(thinkTime / 1000).toFixed(1)} 秒)`); + thinking?.complete(); + thinking = null; + } else if (chunk.type === "text-delta") { + text.append(chunk.text); + fullResponse += chunk.text; + } + } + text.complete(); + msg.complete(); + } catch (err: any) { + thinking?.complete(); + text.complete(); + msg.stop(); + throw err; + } + + return fullResponse; +} + function removeAllXmlTags(text: string): string { text = text.replace(/<([a-zA-Z][\w-]*)(\s+[^>]*)?>([\s\S]*?)<\/\1>/g, ""); text = text.replace(/<([a-zA-Z][\w-]*)(\s+[^>]*)?\/>/g, ""); diff --git a/src/lib/fixDB.ts b/src/lib/fixDB.ts index 1f34722..6ece49f 100644 --- a/src/lib/fixDB.ts +++ b/src/lib/fixDB.ts @@ -66,23 +66,15 @@ export default async (knex: Knex): Promise => { }); const vendorData: any = { - "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.1",\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 createOpenAI({\r\n baseURL: getBaseUrl(),\r\n apiKey,\r\n compatibility: "compatible",\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 }).chat(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', - }; + "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" +}; //迁移供应商函数 const data = await knex("o_vendorConfig").select("*"); @@ -114,7 +106,7 @@ export default async (knex: Knex): Promise => { await dropColumn("o_vendorConfig", "createTime"); const volcengineVer = await u.vendor.getVendor("volcengine").version; - if (Number(volcengineVer) < 2.1) { + if (Number(volcengineVer) < 2.2) { u.vendor.writeCode("volcengine", vendorData["volcengine.ts"]); } const minimaxVer = await u.vendor.getVendor("minimax").version; diff --git a/src/lib/initDB.ts b/src/lib/initDB.ts index 2749c39..207f524 100644 --- a/src/lib/initDB.ts +++ b/src/lib/initDB.ts @@ -261,29 +261,6 @@ export default async (knex: Knex, forceInit: boolean = false): Promise => table.unique(["id"]); }, }, - //大纲表 - { - name: "o_outline", - builder: (table) => { - table.integer("id").notNullable(); - table.integer("episode"); - table.text("data"); - table.integer("projectId"); - table.primary(["id"]); - table.unique(["id"]); - }, - }, - //大纲-原文表 - { - name: "o_outlineNovel", - builder: (table) => { - table.integer("id").notNullable(); - table.integer("outlineId").unsigned().references("id").inTable("o_outline"); - table.integer("novelId").unsigned().references("id").inTable("o_novel"); - table.primary(["id"]); - table.unique(["id"]); - }, - }, //剧本 { name: "o_script", diff --git a/src/router.ts b/src/router.ts index 731c1e7..dc0f410 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,4 +1,4 @@ -// @routes-hash 62534cff632db5d31442f1bca1932925 +// @routes-hash 4d6cbfaad479bdfafe13bc61e7550f55 import { Express } from "express"; import route1 from "./routes/agents/clearMemory"; @@ -34,22 +34,22 @@ import route30 from "./routes/general/generalStatistics"; import route31 from "./routes/general/getSingleProject"; import route32 from "./routes/general/updateProject"; import route33 from "./routes/login/login"; -import route34 from "./routes/migrate/migrateData"; -import route35 from "./routes/modelSelect/getModelDetail"; -import route36 from "./routes/modelSelect/getModelList"; -import route37 from "./routes/novel/addNovel"; -import route38 from "./routes/novel/batchDeleteNovel"; -import route39 from "./routes/novel/delNovel"; -import route40 from "./routes/novel/event/batchDeleteEvent"; -import route41 from "./routes/novel/event/deletEvent"; -import route42 from "./routes/novel/event/generateEvents"; -import route43 from "./routes/novel/event/getEvent"; -import route44 from "./routes/novel/getNovel"; -import route45 from "./routes/novel/getNovelData"; -import route46 from "./routes/novel/getNovelEventState"; -import route47 from "./routes/novel/getNovelIndex"; -import route48 from "./routes/novel/updateNovel"; -import route49 from "./routes/other/deleteAllData"; +import route34 from "./routes/modelSelect/getModelDetail"; +import route35 from "./routes/modelSelect/getModelList"; +import route36 from "./routes/novel/addNovel"; +import route37 from "./routes/novel/batchDeleteNovel"; +import route38 from "./routes/novel/delNovel"; +import route39 from "./routes/novel/event/batchDeleteEvent"; +import route40 from "./routes/novel/event/deletEvent"; +import route41 from "./routes/novel/event/generateEvents"; +import route42 from "./routes/novel/event/getEvent"; +import route43 from "./routes/novel/getNovel"; +import route44 from "./routes/novel/getNovelData"; +import route45 from "./routes/novel/getNovelEventState"; +import route46 from "./routes/novel/getNovelIndex"; +import route47 from "./routes/novel/updateNovel"; +import route48 from "./routes/other/deleteAllData"; +import route49 from "./routes/other/getModelDetails"; import route50 from "./routes/other/getVersion"; import route51 from "./routes/production/assets/batchGenerateAssetsImage"; import route52 from "./routes/production/assets/deleteAssetsDireve"; @@ -179,22 +179,22 @@ export default async (app: Express) => { app.use("/api/general/getSingleProject", route31); app.use("/api/general/updateProject", route32); app.use("/api/login/login", route33); - app.use("/api/migrate/migrateData", route34); - app.use("/api/modelSelect/getModelDetail", route35); - app.use("/api/modelSelect/getModelList", route36); - app.use("/api/novel/addNovel", route37); - app.use("/api/novel/batchDeleteNovel", route38); - app.use("/api/novel/delNovel", route39); - app.use("/api/novel/event/batchDeleteEvent", route40); - app.use("/api/novel/event/deletEvent", route41); - app.use("/api/novel/event/generateEvents", route42); - app.use("/api/novel/event/getEvent", route43); - app.use("/api/novel/getNovel", route44); - app.use("/api/novel/getNovelData", route45); - app.use("/api/novel/getNovelEventState", route46); - app.use("/api/novel/getNovelIndex", route47); - app.use("/api/novel/updateNovel", route48); - app.use("/api/other/deleteAllData", route49); + app.use("/api/modelSelect/getModelDetail", route34); + app.use("/api/modelSelect/getModelList", route35); + app.use("/api/novel/addNovel", route36); + app.use("/api/novel/batchDeleteNovel", route37); + app.use("/api/novel/delNovel", route38); + app.use("/api/novel/event/batchDeleteEvent", route39); + app.use("/api/novel/event/deletEvent", route40); + app.use("/api/novel/event/generateEvents", route41); + app.use("/api/novel/event/getEvent", route42); + app.use("/api/novel/getNovel", route43); + app.use("/api/novel/getNovelData", route44); + app.use("/api/novel/getNovelEventState", route45); + app.use("/api/novel/getNovelIndex", route46); + app.use("/api/novel/updateNovel", route47); + app.use("/api/other/deleteAllData", route48); + app.use("/api/other/getModelDetails", route49); app.use("/api/other/getVersion", route50); app.use("/api/production/assets/batchGenerateAssetsImage", route51); app.use("/api/production/assets/deleteAssetsDireve", route52); diff --git a/src/routes/assetsGenerate/batchPolishAssetsPrompt.ts b/src/routes/assetsGenerate/batchPolishAssetsPrompt.ts index 04db8ff..8187f66 100644 --- a/src/routes/assetsGenerate/batchPolishAssetsPrompt.ts +++ b/src/routes/assetsGenerate/batchPolishAssetsPrompt.ts @@ -27,22 +27,6 @@ interface NovelChapter { type ItemType = "characters" | "props" | "scenes"; -interface ResultItem { - type: ItemType; - name: string; - chapterRange: number[]; -} -function findItemByName(items: ResultItem[], name: string, type?: ItemType): ResultItem | undefined { - return items.find((item) => (!type || item.type === type) && item.name === name); -} -function mergeNovelText(novelData: NovelChapter[]): string { - if (!Array.isArray(novelData)) return ""; - return novelData - .map((chap) => { - return `${chap.chapter.trim()}\n\n${chap.chapterData.trim().replace(/\r?\n/g, "\n")}\n`; - }) - .join("\n"); -} //润色提示词 export default router.post( "/", @@ -66,23 +50,6 @@ export default router.post( if (!project) return res.status(500).send(success({ message: "项目为空" })); // 预加载公共数据 - const allOutlineDataList: { data: string }[] = await u.db("o_outline").where("projectId", projectId).select("data"); - const itemMap: Record = {}; - if (allOutlineDataList.length > 0) - allOutlineDataList.forEach((row) => { - const data: OutlineData = JSON.parse(row?.data || "{}"); - (["characters", "props", "scenes"] as ItemType[]).forEach((type) => { - (data[type] || []).forEach((item) => { - const key = `${type}-${item.name}`; - if (!itemMap[key]) { - itemMap[key] = { type, name: item.name, chapterRange: [...(data.chapterRange || [])] }; - } else { - itemMap[key].chapterRange = Array.from(new Set([...itemMap[key].chapterRange, ...(data.chapterRange || [])])); - } - }); - }); - }); - const result: ResultItem[] = Object.values(itemMap); const assetsIds = items.map((item: { assetsId: number }) => item.assetsId); //查询所有资产,用于判断每个资产是否是衍生资产 const assetsDataList = await u.db("o_assets").whereIn("id", assetsIds).select("id", "assetsId"); @@ -132,7 +99,6 @@ export default router.post( await u.db("o_assets").where("id", item.assetsId).update({ promptState: "生成失败", promptErrorReason: "视觉手册未定义" }); return; } - findItemByName(result, item.name, config.itemType); const systemPrompt = visualManual; try { const { _output } = (await u.Ai.Text("universalAi").invoke({ diff --git a/src/routes/assetsGenerate/polishAssetsPrompt.ts b/src/routes/assetsGenerate/polishAssetsPrompt.ts index e1914bb..eed51b6 100644 --- a/src/routes/assetsGenerate/polishAssetsPrompt.ts +++ b/src/routes/assetsGenerate/polishAssetsPrompt.ts @@ -4,36 +4,10 @@ import * as zod from "zod"; import { error, success } from "@/lib/responseFormat"; import { validateFields } from "@/middleware/middleware"; const router = express.Router(); -interface OutlineItem { - description: string; - name: string; -} -interface OutlineData { - chapterRange: number[]; - characters?: OutlineItem[]; - props?: OutlineItem[]; - scenes?: OutlineItem[]; -} - -interface NovelChapter { - id: number; - reel: string; - chapter: string; - chapterData: string; - projectId: number; -} type ItemType = "characters" | "props" | "scenes"; -interface ResultItem { - type: ItemType; - name: string; - chapterRange: number[]; -} -function findItemByName(items: ResultItem[], name: string, type?: ItemType): ResultItem | undefined { - return items.find((item) => (!type || item.type === type) && item.name === name); -} //润色提示词 export default router.post( "/", @@ -51,31 +25,8 @@ export default router.post( //如果没有找到对应的项目,返回错误 if (!project) return res.status(500).send(success({ message: "项目为空" })); - const allOutlineDataList: { data: string }[] = await u.db("o_outline").where("projectId", projectId).select("data"); await u.db("o_assets").where("id", assetsId).update({ promptState: "生成中" }); - const itemMap: Record = {}; - - if (allOutlineDataList.length > 0) - allOutlineDataList.forEach((row) => { - const data: OutlineData = JSON.parse(row?.data || "{}"); - (["characters", "props", "scenes"] as ItemType[]).forEach((type) => { - (data[type] || []).forEach((item) => { - const key = `${type}-${item.name}`; - if (!itemMap[key]) { - itemMap[key] = { - type, - name: item.name, - chapterRange: [...(data.chapterRange || [])], - }; - } else { - itemMap[key].chapterRange = Array.from(new Set([...itemMap[key].chapterRange, ...(data.chapterRange || [])])); - } - }); - }); - }); - - const result: ResultItem[] = Object.values(itemMap); //查询资产是否是衍生资产 const assetsData = await u.db("o_assets").where("id", assetsId).select("assetsId").first(); if (!assetsData) return { code: 500, message: "资产不存在" }; @@ -109,7 +60,6 @@ export default router.post( //获取到视觉手册 const visualManual = await u.getArtPrompt(project.artStyle as string, "art_skills", config.visualManual); if (!visualManual) return res.status(500).send(error("视觉手册未定义")); - findItemByName(result, name, config.itemType); const systemPrompt = visualManual; try { const { _output } = (await u.Ai.Text("universalAi").invoke({ diff --git a/src/routes/migrate/migrateData.ts b/src/routes/migrate/migrateData.ts deleted file mode 100644 index f9261e7..0000000 --- a/src/routes/migrate/migrateData.ts +++ /dev/null @@ -1,133 +0,0 @@ -import express from "express"; -import { success } from "@/lib/responseFormat"; -import db from "@/utils/db"; -import type { DB } from "@/types/database"; -import knex from "knex"; -import path from "path"; -import fs from "fs"; -import { tr } from "zod/locales"; - -const router = express.Router(); - -// 迁移数据 -export default router.post( - "/", - async (req, res) => { - // return res.status(200).send({ - // success: true, - // message: '数据迁移功能已关闭,建议手动迁移数据后删除旧数据库文件' - // }); - //连接旧数据库,读取数据 - try { - let db2: knex.Knex | null = null; - //读取旧数据库路径 - let db2Path: string; - if (typeof process.versions?.electron !== "undefined") { - const { app } = require("electron"); - const userDataDir: string = app.getPath("userData"); - db2Path = path.join(userDataDir, "db2.sqlite"); - } else { - db2Path = path.join(process.cwd(), "db2.sqlite"); - } - const dbDir = path.dirname(db2Path); - // 确保数据库目录存在 - if (!fs.existsSync(dbDir)) { - fs.mkdirSync(dbDir, { recursive: true }); - } - if (!fs.existsSync(db2Path)) { - return res.status(404).send({ - success: false, - message: `源数据库文件不存在: ${db2Path}` - }); - } - //连接旧数据库 - db2 = knex({ - client: "better-sqlite3", - connection: { - filename: db2Path, - }, - useNullAsDefault: true, - }); - //需要迁移的旧数据表 - const db2TableNames = [ - 't_project', - 't_assets', - 't_event', - 't_image', - 't_novel', - 't_outline', - 't_script', - 't_storyboard', - 't_video', - ] - //新数据库的表 - const dbTableNames = [ - 'o_project', - 'o_assets', - 'o_event', - 'o_eventChapter', - 'o_image', - 'o_novel', - 'o_outline', - 'o_outlineNovel', - 'o_script', - 'o_scriptAssets', - 'o_scriptOutline', - 'o_storyboard', - 'o_storyboardScript', - 'o_video', - ] - - for (const tableName of db2TableNames) { - try { - // 从 db2 读取数据 - const sourceData = await db2(tableName).select('*'); - for (const item of sourceData) { - //迁移项目表 - if (tableName === 't_project') { - // await db("o_project").insert({ - // name: item.name, - // intro: item.intro, - // type: item.type, - // artStyle: item.artStyle, - // videoRatio: item.videoRatio, - // createTime: item.createTime, - // userId: item.userId, - // projectType: "基于小说原文" - // }) - } - //迁移资产表 - if (tableName === 't_assets') { - } - //迁移事件表 - if (tableName === 't_event') { } - //迁移图片表 - if (tableName === 't_image') { } - //迁移小说表 - if (tableName === 't_novel') { } - //迁移大纲表 - if (tableName === 't_outline') { } - //迁移脚本表 - if (tableName === 't_script') { } - //迁移分镜面板 - if (tableName === 't_storyboard') { } - //迁移视频表 - if (tableName === 't_video') { } - } - // // 将数据插入到 db 中 - // const targetTableName = dbTableNames[db2TableNames.indexOf(tableName)]; - // await db(targetTableName).insert(sourceData); - // console.log(`成功迁移表 ${tableName} 的数据到 ${targetTableName}`); - } catch (error) { - console.error(`连接旧数据库失败: ${error instanceof Error ? error.message : String(error)}`); - } - } - } catch (error) { - console.error('连接旧数据库失败:', error); - } - return res.status(200).send({ - success: true, - message: '数据迁移功能已关闭,建议手动迁移数据后删除旧数据库文件' - }); - } -); diff --git a/src/routes/modelSelect/getModelDetail.ts b/src/routes/modelSelect/getModelDetail.ts index 4e3d73b..fdef5b8 100644 --- a/src/routes/modelSelect/getModelDetail.ts +++ b/src/routes/modelSelect/getModelDetail.ts @@ -12,7 +12,7 @@ export default router.post( }), async (req, res) => { const { modelId } = req.body; - const [id, name] = modelId.split(":"); + const [id, name] = modelId.split(/:(.+)/); const models = await u.vendor.getModelList(id); const findData = models.find((i: any) => i.modelName == name); res.status(200).send(success(findData)); diff --git a/src/routes/other/getModelDetails.ts b/src/routes/other/getModelDetails.ts new file mode 100644 index 0000000..d411cf4 --- /dev/null +++ b/src/routes/other/getModelDetails.ts @@ -0,0 +1,21 @@ +import express from "express"; +import { success, error } from "@/lib/responseFormat"; +import u from "@/utils"; +import { z } from "zod"; +import { validateFields } from "@/middleware/middleware"; +const router = express.Router(); + +export default router.post( + "/", + validateFields({ + key: z.string().optional(), + }), + async (req, res) => { + const { key } = req.body; + const [id, modelName] = key ? key.split(":") : []; + const models = await u.vendor.getModelList(id); + const model = models.find((m) => m.modelName === modelName); + if (!model) return res.status(400).send(error("未找到模型")); + res.status(200).send(success(model)); + }, +); diff --git a/src/routes/production/workbench/generateVideoPrompt.ts b/src/routes/production/workbench/generateVideoPrompt.ts index 7f5dce7..643cc23 100644 --- a/src/routes/production/workbench/generateVideoPrompt.ts +++ b/src/routes/production/workbench/generateVideoPrompt.ts @@ -73,7 +73,7 @@ export default router.post( shouldGenerateImage: item.shouldGenerateImage, }); } - const [id, modelData] = model.split(":"); + const [id, modelData] = model.split(/:(.+)/); const projectData = await u.db("o_project").select("*").where({ id: projectId }).first(); const videoPrompt = await u.db("o_prompt").where("type", "videoPromptGeneration").first(); let videoPromptGeneration = "" as string | undefined; diff --git a/src/routes/project/delProject.ts b/src/routes/project/delProject.ts index 3f45939..e9360ca 100644 --- a/src/routes/project/delProject.ts +++ b/src/routes/project/delProject.ts @@ -18,9 +18,6 @@ export default router.post( await u.db("o_agentWorkData").where("projectId", id).delete(); const novelData = await u.db("o_novel").where("projectId", id).select("id"); const novelId = novelData.map((item: any) => item.id); - if (novelId.length > 0) { - await u.db("o_outlineNovel").whereIn("novelId", novelId).delete(); - } //删除项目下的原文 await u.db("o_novel").where("projectId", id).delete(); // 删除项目下的剧本信息 diff --git a/src/socket/routes/productionAgent.ts b/src/socket/routes/productionAgent.ts index 6a69741..06a92ee 100644 --- a/src/socket/routes/productionAgent.ts +++ b/src/socket/routes/productionAgent.ts @@ -41,6 +41,11 @@ export default (nsp: Namespace) => { }); let abortController: AbortController | null = null; + const thinkConfig: agent.AgentContext["thinkConfig"] = { + think: false, + thinlLevel: 0, + }; + socket.on("updateContext", (data: { isolationKey: string; projectId: number; scriptId: number }, callback) => { isolationKey = data.isolationKey; resTool = new ResTool(socket, { @@ -66,46 +71,11 @@ export default (nsp: Namespace) => { abortSignal: currentController.signal, resTool, msg, + thinkConfig, }; try { - const textStream = await agent.decisionAI(ctx); - - let currentMsg = ctx.msg; - let text = currentMsg.text(); - - const syncCurrentMessage = () => { - if (ctx.msg === currentMsg) return; - text.complete(); - currentMsg.complete(); - currentMsg = ctx.msg; - text = currentMsg.text(); - }; - - let aborted = false; - try { - for await (const chunk of textStream) { - await new Promise((resolve) => setTimeout(() => resolve(), 1)); - syncCurrentMessage(); - text.append(chunk); - } - } catch (err: any) { - if (err.name === "AbortError" || currentController.signal.aborted) { - aborted = true; - } else { - throw err; - } - } finally { - syncCurrentMessage(); - if (aborted) { - text.append("[已停止]"); - text.complete(); - currentMsg.stop(); - } else { - text.complete(); - currentMsg.complete(); - } - } + await agent.runDecisionAI(ctx); } catch (err: any) { if (err.name !== "AbortError" && !currentController.signal.aborted) { const errorMsg = u.error(err).message; @@ -120,6 +90,12 @@ export default (nsp: Namespace) => { } }); + socket.on("updateThinkConfig", (data: { think: boolean; thinlLevel: 0 | 1 | 2 | 3 }) => { + thinkConfig.think = data.think; + thinkConfig.thinlLevel = data.thinlLevel; + console.log("[productionAgent] 更新思考配置:", thinkConfig); + }); + socket.on("stop", () => { abortController?.abort(); abortController = null; diff --git a/src/socket/routes/scriptAgent.ts b/src/socket/routes/scriptAgent.ts index cc2b5e2..feec101 100644 --- a/src/socket/routes/scriptAgent.ts +++ b/src/socket/routes/scriptAgent.ts @@ -40,6 +40,11 @@ export default (nsp: Namespace) => { }); let abortController: AbortController | null = null; + const thinkConfig: agent.AgentContext["thinkConfig"] = { + think:false, + thinlLevel: 0, + } + socket.on("chat", async (data: { content: string }) => { const { content } = data; abortController?.abort(); @@ -55,45 +60,11 @@ export default (nsp: Namespace) => { abortSignal: currentController.signal, resTool, msg, + thinkConfig, }; try { - const textStream = await agent.decisionAI(ctx); - - let currentMsg = ctx.msg; - let text = currentMsg.text(); - - const syncCurrentMessage = () => { - if (ctx.msg === currentMsg) return; - text.complete(); - currentMsg.complete(); - currentMsg = ctx.msg; - text = currentMsg.text(); - }; - - let aborted = false; - try { - for await (const chunk of textStream) { - await new Promise((resolve) => setTimeout(() => resolve(), 1)); - syncCurrentMessage(); - text.append(chunk); - } - } catch (err: any) { - if (err.name === "AbortError" || currentController.signal.aborted) { - aborted = true; - } else { - throw err; - } - } finally { - syncCurrentMessage(); - if (aborted) { - text.complete(); - currentMsg.stop(); - } else { - text.complete(); - currentMsg.complete(); - } - } + await agent.runDecisionAI(ctx); } catch (err: any) { if (err.name !== "AbortError" && !currentController.signal.aborted) { const errorMsg = u.error(err).message; @@ -108,6 +79,12 @@ export default (nsp: Namespace) => { } }); + socket.on("updateThinkConfig", (data: { think: boolean; thinlLevel: 0 | 1 | 2 | 3 }) => { + thinkConfig.think = data.think; + thinkConfig.thinlLevel = data.thinlLevel; + console.log("[scriptAgent] 更新思考配置:", thinkConfig); + }); + socket.on("stop", () => { abortController?.abort(); abortController = null; diff --git a/src/types/database.d.ts b/src/types/database.d.ts index 0c85ca3..551db81 100644 --- a/src/types/database.d.ts +++ b/src/types/database.d.ts @@ -1,4 +1,4 @@ -// @db-hash 3296433eb24314b094ac5d3839c049c5 +// @db-hash 9248d7bcfe0a1bc57e5b9bc33d8c7d83 //该文件由脚本自动生成,请勿手动修改 export interface memories { diff --git a/src/utils/ai.ts b/src/utils/ai.ts index 5539cba..e452fec 100644 --- a/src/utils/ai.ts +++ b/src/utils/ai.ts @@ -23,7 +23,7 @@ async function getVendorTemplateFn( ): Promise<(think?: boolean, thinkLevel?: 0 | 1 | 2 | 3) => any>; async function getVendorTemplateFn(fnName: Exclude, modelName: `${string}:${string}`): Promise<(input: any) => any>; async function getVendorTemplateFn(fnName: FnName, modelName: `${string}:${string}`): Promise { - const [id, name] = modelName.split(":"); + const [id, name] = modelName.split(/:(.+)/); const vendorConfigData = await u.db("o_vendorConfig").where("id", id).first(); if (!vendorConfigData) throw new Error(`未找到供应商配置 id=${id}`); const modelList = await u.vendor.getModelList(id); @@ -55,7 +55,7 @@ async function withTaskRecord( fn: (modelName: `${string}:${string}`, think: Boolean, thinkLevel: 0 | 1 | 2 | 3) => Promise, ): Promise { const modelName = await resolveModelName(modelKey); - const [id, model] = modelName.split(":"); + const [_, model] = modelName.split(/:(.+)/); const taskRecord = await u.task(projectId, taskClass, model, { describe: describe, content: relatedObjects }); try { const result = await fn(modelName, false, 0); @@ -89,46 +89,29 @@ class AiText { this.think = think; this.thinkLevel = thinkLevel; } - async invoke(input: Omit[0], "model">) { + private async resolveModel(middleware?: any | any[]) { const switchAiDevTool = await u.db("o_setting").where("key", "switchAiDevTool").first(); const modelName = await resolveModelName(this.AiType); const sdkFn = await getVendorTemplateFn("textRequest", modelName); + const baseModel = await sdkFn(this.think, this.thinkLevel); + const mws = [ + ...(switchAiDevTool?.value === "1" ? [devToolsMiddleware()] : []), + ...(middleware ? (Array.isArray(middleware) ? middleware : [middleware]) : []), + ]; + return mws.length > 0 ? wrapLanguageModel({ model: baseModel, middleware: mws.length === 1 ? mws[0] : mws }) : baseModel; + } + async invoke(input: Omit[0], "model">) { return generateText({ ...(input.tools && { stopWhen: stepCountIs(Object.keys(input.tools).length * 50) }), ...input, - model: - switchAiDevTool?.value === "1" - ? wrapLanguageModel({ - model: await sdkFn(this.think, this.thinkLevel), - middleware: devToolsMiddleware(), - }) - : await sdkFn(this.think, this.thinkLevel), + model: await this.resolveModel(), } as Parameters[0]); } async stream(input: Omit[0], "model">) { - const switchAiDevTool = await u.db("o_setting").where("key", "switchAiDevTool").first(); - const modelName = await resolveModelName(this.AiType); - const sdkFn = await getVendorTemplateFn("textRequest", modelName); return streamText({ ...(input.tools && { stopWhen: stepCountIs(Object.keys(input.tools).length * 50) }), ...input, - model: - switchAiDevTool?.value == "1" - ? wrapLanguageModel({ - model: sdkFn(this.think, this.thinkLevel), - middleware: [ - devToolsMiddleware(), - extractReasoningMiddleware({ - tagName: "reasoning_content", - }), - ], - }) - : wrapLanguageModel({ - model: sdkFn(this.think, this.thinkLevel), - middleware: extractReasoningMiddleware({ - tagName: "reasoning_content", - }), - }), + model: await this.resolveModel(extractReasoningMiddleware({ tagName: "reasoning_content", separator: "\n" })), } as Parameters[0]); } }