diff --git a/data/vendor/minimax.ts b/data/vendor/minimax.ts index 86bd710..ea5c3df 100644 --- a/data/vendor/minimax.ts +++ b/data/vendor/minimax.ts @@ -134,7 +134,7 @@ declare const exports: { const vendor: VendorConfig = { id: "minimax", - version: "2.0", + version: "2.1", author: "Toonflow", name: "MiniMax(海螺AI)", description: "MiniMax官方接口适配,支持M系列推理文本模型、文生图/图生图、视频生成(文生视频、图生视频、首尾帧生成)能力 \n [前往平台](https://minimaxi.com/)", @@ -228,11 +228,8 @@ const extractBase64WithHead = (ref: ReferenceList): string => { const textRequest = (model: TextModel, think: boolean, thinkLevel: 0 | 1 | 2 | 3) => { if (!vendor.inputValues.apiKey) throw new Error("缺少API Key"); const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, ""); - const baseUrl = getBaseUrl(); - - const openaiBaseUrl = `${baseUrl}/v1`; const extraBody = model.think ? { reasoning_split: true } : {}; - return createOpenAI({ baseURL: openaiBaseUrl, apiKey, extraBody }).chat(model.modelName); + return createOpenAI({ baseURL: getBaseUrl(), apiKey, extraBody }).chat(model.modelName); }; const uploadReference = async (base64: string, fileType: "image" | "audio" | "video"): Promise => { diff --git a/data/vendor/volcengine.ts b/data/vendor/volcengine.ts index 718f638..86eaa56 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.0", + version: "2.1", author: "leeqi", name: "火山引擎(豆包)", description: @@ -326,43 +326,120 @@ const imageRequest = async (config: ImageConfig, model: ImageModel): Promise 0) { - for (const ref of config.referenceList) { - content.push({ - type: "image_url", - image_url: { url: ref.base64 }, - }); + // 参考图片:单图为 string,多图为 array(seedream-3.0-t2i 不支持 image 参数) + if (!isOldModel && config.referenceList && config.referenceList.length > 0) { + const images = config.referenceList.map((ref) => ref.base64); + body.image = images.length === 1 ? images[0] : images; + } + + // 尺寸处理:优先使用推荐像素值,未匹配则直接传分辨率字符串让模型自行决定 + const [w, h] = config.aspectRatio.split(":").map(Number); + const sizeTable: Record> = { + "1K": { + "1:1": "1024x1024", + "4:3": "1152x864", + "3:4": "864x1152", + "16:9": "1280x720", + "9:16": "720x1280", + "3:2": "1248x832", + "2:3": "832x1248", + "21:9": "1512x648", + }, + "2K": { + "1:1": "2048x2048", + "4:3": "2304x1728", + "3:4": "1728x2304", + "16:9": "2848x1600", + "9:16": "1600x2848", + "3:2": "2496x1664", + "2:3": "1664x2496", + "21:9": "3136x1344", + }, + "4K": { + "1:1": "4096x4096", + "4:3": "4704x3520", + "3:4": "3520x4704", + "16:9": "5504x3040", + "9:16": "3040x5504", + "3:2": "4992x3328", + "2:3": "3328x4992", + "21:9": "6240x2656", + }, + }; + + const sizeKey = config.size || "2K"; + const ratioKey = config.aspectRatio; + const table = sizeTable[sizeKey]; + + if (table && table[ratioKey]) { + // 推荐像素值匹配到了,但需要检查是否满足模型最低像素要求 + const [pw, ph] = table[ratioKey].split("x").map(Number); + const totalPixels = pw * ph; + if (isOldModel) { + // seedream-3.0-t2i: 像素范围 [512x512, 2048x2048] + body.size = table[ratioKey]; + } else if (totalPixels < 3686400) { + // 1K 像素值不满足新模型最低要求,直接传 "2K" 让模型自行决定 + body.size = "2K"; + } else if (is5Lite && totalPixels > 10404496) { + // seedream-5.0-lite 最高 10404496,4K 超限,回退传 "2K" + body.size = "2K"; + } else { + body.size = table[ratioKey]; + } + } else if (isOldModel) { + // seedream-3.0-t2i: 像素范围 [512x512, 2048x2048],直接按比例计算 + const base = sizeKey === "1K" ? 1024 : 2048; + const calcW = Math.min(2048, Math.round(base * Math.sqrt(w / h))); + const calcH = Math.min(2048, Math.round(base * Math.sqrt(h / w))); + body.size = `${Math.max(512, calcW)}x${Math.max(512, calcH)}`; + } else { + // 新模型未匹配推荐值时,直接传分辨率字符串(方式1),由模型根据 prompt 自行决定尺寸 + // seedream 5.0-lite 支持 "2K"/"3K",seedream 4.5 支持 "2K"/"4K",seedream 4.0 支持 "1K"/"2K"/"4K" + if (is5Lite) { + body.size = sizeKey === "4K" ? "3K" : sizeKey === "1K" ? "2K" : sizeKey; + } else { + body.size = sizeKey === "1K" ? "2K" : sizeKey; } } - const [w, h] = config.aspectRatio.split(":").map(Number); - const sizeMap: Record = { - "1K": { width: 1024, height: Math.round(1024 * (h / w)) }, - "2K": { width: 2048, height: Math.round(2048 * (h / w)) }, - "4K": { width: 4096, height: Math.round(4096 * (h / w)) }, - }; - const size = sizeMap[config.size] || sizeMap["1K"]; - - const body = { - model: model.modelName, - content, - size: `${size.width}x${size.height}`, - response_format: "url", - }; - - logger(`[图片生成] 请求模型: ${model.modelName}`); + logger(`[图片生成] 请求模型: ${model.modelName}, 尺寸: ${body.size}`); const response = await axios.post(`${baseUrl}/images/generations`, body, { headers }); const data = response.data; - if (data?.data?.[0]?.url) { - return await urlToBase64(data.data[0].url); + if (data?.error) { + throw new Error(`图片生成失败:${data.error.message || data.error.code}`); + } + + // 从 data 数组中提取第一张成功的图片 + if (data?.data && data.data.length > 0) { + for (const item of data.data) { + if (item.url) { + return await urlToBase64(item.url); + } + if (item.b64_json) { + return item.b64_json; + } + if (item.error) { + throw new Error(`图片生成失败:${item.error.message || item.error.code}`); + } + } } throw new Error("图片生成失败:未返回有效结果"); diff --git a/package.json b/package.json index b27d355..c4f62f4 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "dist:mac": "yarn build && electron-builder --mac", "dist:linux": "yarn build && electron-builder --linux", "debug:ai": "npx @ai-sdk/devtools", - "license": "bun run scripts/license.ts" + "license": "node scripts/license.ts", + "vendor2json": "node scripts/vendor2json.ts" }, "dependencies": { "@ai-sdk/anthropic": "^3.0.35", @@ -98,4 +99,4 @@ "better-sqlite3": "^12.8.0" } } -} +} \ No newline at end of file diff --git a/scripts/vendor2json.ts b/scripts/vendor2json.ts new file mode 100644 index 0000000..5da4492 --- /dev/null +++ b/scripts/vendor2json.ts @@ -0,0 +1,11 @@ +import fs from "fs"; +import path from "path"; + +const vendorDir = path.join("data", "vendor"); +const files = fs.readdirSync(vendorDir).filter((f) => f.endsWith(".ts")); +const result: Record = {}; +for (const file of files) { + result[file] = fs.readFileSync(path.join(vendorDir, file), "utf-8"); +} +fs.writeFileSync(path.join(vendorDir, "vendor.json"), JSON.stringify(result, null, 2), "utf-8"); +console.log("Done, saved vendor.json"); diff --git a/src/agents/productionAgent/index copy.ts b/src/agents/productionAgent/index copy.ts deleted file mode 100644 index 4744625..0000000 --- a/src/agents/productionAgent/index copy.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { Socket } from "socket.io"; -import { tool } from "ai"; -import { z } from "zod"; -import u from "@/utils"; -import Memory from "@/utils/agent/memory"; -import { createSkillTools, parseFrontmatter, scanSkills, useSkill } from "@/utils/agent/skillsTools"; -import useTools from "@/agents/productionAgent/tools"; -import ResTool from "@/socket/resTool"; -import * as fs from "fs"; -import path from "path"; - -export interface AgentContext { - socket: Socket; - isolationKey: string; - text: string; - userMessageTime?: number; - abortSignal?: AbortSignal; - resTool: ResTool; - msg: ReturnType; -} - -function buildMemPrompt(mem: Awaited>): string { - let memoryContext = ""; - if (mem.rag.length) { - memoryContext += `[相关记忆]\n${mem.rag.map((r) => r.content).join("\n")}`; - } - if (mem.summaries.length) { - if (memoryContext) memoryContext += "\n\n"; - memoryContext += `[历史摘要]\n${mem.summaries.map((s, i) => `${i + 1}. ${s.content}`).join("\n")}`; - } - if (mem.shortTerm.length) { - if (memoryContext) memoryContext += "\n\n"; - memoryContext += `[近期对话]\n${mem.shortTerm.map((m) => `${m.role}: ${m.content}`).join("\n")}`; - } - return `## Memory\n以下是你对用户的记忆,可作为参考但不要主动提及:\n${memoryContext}`; -} - -export async function decisionAI(ctx: AgentContext) { - const { isolationKey, text, abortSignal } = ctx; - const memory = new Memory("productionAgent", isolationKey); - await memory.add("user", text); - - const skill = path.join(u.getPath("skills"), "production_agent_decision.md"); - const prompt = await fs.promises.readFile(skill, "utf-8"); - - 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 data = await u.db("o_vendorConfig").where("id", id).select("models").first(); - const models = JSON.parse(data!.models!); - 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({ - messages: [ - { role: "system", content: prompt }, - { role: "assistant", content: mem + "\n" + modelInfo }, - { role: "user", content: text }, - ], - abortSignal, - tools: { - ...memory.getTools(), - ...useTools({ resTool: ctx.resTool, msg: ctx.msg }), - ...createSubAgent(ctx), - }, - onFinish: async (completion) => { - await memory.add("assistant:decision", removeAllXmlTags(completion.text)); - }, - }); - - return textStream; -} - -function createSubAgent(parentCtx: AgentContext) { - const { resTool, abortSignal } = parentCtx; - const memory = new Memory("productionAgent", parentCtx.isolationKey); - async function runAgent({ - prompt, - system, - name, - memoryKey, - tools: extraTools, - messages, - }: { - prompt: string; - system: string; - name: string; - memoryKey: string; - tools?: Record; - messages?: { role: "user" | "assistant" | "system"; content: string }[]; - }) { - parentCtx.msg.complete(); - const subMsg = resTool.newMessage("assistant", name); - const text = subMsg.text(); - let fullResponse = ""; - - const { textStream } = await u.Ai.Text("scriptAgent").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; - } - - if (fullResponse.trim()) { - await memory.add(memoryKey, removeAllXmlTags(fullResponse), { - name, - createTime: new Date(subMsg.datetime).getTime(), - }); - } - - parentCtx.msg = resTool.newMessage("assistant", "视频策划"); - return fullResponse; - } - - const promptInput = z.object({ - prompt: z.string().describe("交给子Agent的任务简约描述,100字以内"), - }); - - const run_sub_agent_execution = tool({ - description: "执行层子Agent,负责衍生资产、", - inputSchema: promptInput, - execute: async ({ prompt }) => { - const skill = path.join(u.getPath("skills"), "production_agent_execution.md"); - const systemPrompt = await fs.promises.readFile(skill, "utf-8"); - const addPrompt = - "\n" + - [ - "你必须使用如下XML格式写入工作区:\n```", - "拍摄计划:内容", - "分镜表:内容", - "分镜面板:", - "```", - ].join("\n"); - - const projectInfo = await u.db("o_project").where("id", resTool.data.projectId).first(); - 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 data = await u.db("o_vendorConfig").where("id", id).select("models").first(); - const models = JSON.parse(data!.models!); - 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 ? "是" : "否"}`; - - return runAgent({ - prompt, - system: systemPrompt + addPrompt, - name: "执行导演", - memoryKey: "assistant:execution", - messages: [ - { role: "assistant", content: artSkills.prompt + `\n${modelInfo}` }, - { role: "user", content: prompt + addPrompt }, - ], - tools: { ...artSkills.tools }, - }); - }, - }); - - const run_sub_agent_supervision = tool({ - description: "监制层子Agent,负责审核执行结果", - inputSchema: promptInput, - execute: async ({ prompt }) => { - const skill = path.join(u.getPath("skills"), "production_agent_supervision.md"); - const systemPrompt = await fs.promises.readFile(skill, "utf-8"); - return runAgent({ - prompt, - system: systemPrompt, - name: "监制", - memoryKey: "assistant:supervision", - }); - }, - }); - - return { run_sub_agent_execution, run_sub_agent_supervision }; -} - -async function createArtSkills(artName: string, storyName: string) { - const artWorkerPath = u.getPath(["skills", "art_skills", artName, "driector_skills"]); - const storyWorkerPath = u.getPath(["skills", "story_skills", storyName, "driector_skills"]); - const skillList = [...(await scanSkills(artWorkerPath + "/*.md")), ...(await scanSkills(storyWorkerPath + "/*.md"))]; - const mainSkills: { path: string; name: string; description: string }[] = []; - for (const skillPath of skillList) { - if (!fs.existsSync(skillPath)) throw new Error(`主技能文件不存在: ${skillPath}`); - const content = await fs.promises.readFile(skillPath, "utf-8"); - const parsed = parseFrontmatter(content); - mainSkills.push({ path: skillPath, ...parsed }); - } - const res = { - prompt: `## Skills -以下技能提供了专业任务的专用指令。 -当任务与某个技能的描述匹配时,调用 activate_skill 工具并传入技能名称来加载完整指令。 -加载后遵循技能指令执行任务,需要时调用 read_skill_file 读取资源文件内容。 -${buildSkillPrompt(mainSkills)}`, - tools: createSkillTools(mainSkills, { mainSkill: mainSkills, secondarySkills: [], tertiarySkills: [] }), - }; - return res; -} - -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, ""); - text = text.replace(/<\/?[a-zA-Z][\w-]*(\s+[^>]*)?>/g, ""); - return text.trim(); -} - -export function buildSkillPrompt(skills: { name: string; description: string }[]): string { - const skillEntries = skills - .map((s) => ` \n ${s.name}\n ${s.description}\n `) - .join("\n"); - return ` - -${skillEntries} -`; -} diff --git a/src/agents/productionAgent/index.ts b/src/agents/productionAgent/index.ts index 2d304b7..4c54335 100644 --- a/src/agents/productionAgent/index.ts +++ b/src/agents/productionAgent/index.ts @@ -47,8 +47,8 @@ export async function decisionAI(ctx: AgentContext) { if (!projectInfo) throw new Error(`项目不存在,ID: ${ctx.resTool.data.projectId}`); const [_, imageModelName] = projectInfo.imageModel!.split(":"); const [id, videoModelName] = projectInfo.videoModel!.split(":"); - const data = await u.db("o_vendorConfig").where("id", id).select("models").first(); - const models = JSON.parse(data!.models!); + const models = await u.vendor.getModelList(id); + 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 ? "是" : "否"}`; @@ -140,8 +140,8 @@ async function createSubAgent(parentCtx: AgentContext) { const [_, imageModelName] = projectInfo.imageModel!.split(":"); const [id, videoModelName] = projectInfo.videoModel!.split(":"); - const data = await u.db("o_vendorConfig").where("id", id).select("models").first(); - const models = JSON.parse(data!.models!); + const models = await u.vendor.getModelList(id); + 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 ? "是" : "否"}`; diff --git a/src/agents/scriptAgent/tools.ts b/src/agents/scriptAgent/tools.ts index 58c191a..adeece6 100644 --- a/src/agents/scriptAgent/tools.ts +++ b/src/agents/scriptAgent/tools.ts @@ -93,7 +93,7 @@ export default (toolCpnfig: ToolConfig) => { const thinking = msg.thinking(`正在获取脚本内容...`); const data = await u.db("o_script").whereIn("id", ids).select("content", "name"); const text = data && data.length ? data.map((d) => `${d.content}`).join("\n") : ""; - thinking.appendText(`获取到脚本内容:\n` + text); + thinking.appendText(`获取到脚本内容:\n` + JSON.stringify(data, null, 2)); thinking.updateTitle(`获取脚本内容完成`); thinking.complete(); return text ?? "无数据"; diff --git a/src/lib/fixDB.ts b/src/lib/fixDB.ts index d127d26..cb78388 100644 --- a/src/lib/fixDB.ts +++ b/src/lib/fixDB.ts @@ -81,7 +81,7 @@ export default async (knex: Knex): Promise => { "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.0",\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 content: any[] = [];\r\n\r\n if (config.prompt) {\r\n content.push({ type: "text", text: config.prompt });\r\n }\r\n\r\n if (config.referenceList && config.referenceList.length > 0) {\r\n for (const ref of config.referenceList) {\r\n content.push({\r\n type: "image_url",\r\n image_url: { url: ref.base64 },\r\n });\r\n }\r\n }\r\n\r\n const [w, h] = config.aspectRatio.split(":").map(Number);\r\n const sizeMap: Record = {\r\n "1K": { width: 1024, height: Math.round(1024 * (h / w)) },\r\n "2K": { width: 2048, height: Math.round(2048 * (h / w)) },\r\n "4K": { width: 4096, height: Math.round(4096 * (h / w)) },\r\n };\r\n const size = sizeMap[config.size] || sizeMap["1K"];\r\n\r\n const body = {\r\n model: model.modelName,\r\n content,\r\n size: `${size.width}x${size.height}`,\r\n response_format: "url",\r\n };\r\n\r\n logger(`[图片生成] 请求模型: ${model.modelName}`);\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?.data?.[0]?.url) {\r\n return await urlToBase64(data.data[0].url);\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', + '/**\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.0",\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', }; //迁移供应商函数 @@ -90,7 +90,7 @@ export default async (knex: Knex): Promise => { let { id, code } = item; const filename = `${id}.ts`; const rootDir = u.getPath("vendor"); - if (!code && fs.existsSync(path.join(rootDir,filename))) continue; + if (!code && fs.existsSync(path.join(rootDir, filename))) continue; if (!fs.existsSync(rootDir)) fs.mkdirSync(rootDir, { recursive: true }); if (!fs.existsSync(path.join(rootDir, filename))) { code = vendorData[filename] || code; @@ -112,6 +112,15 @@ export default async (knex: Knex): Promise => { await dropColumn("o_vendorConfig", "icon"); await dropColumn("o_vendorConfig", "inputs"); await dropColumn("o_vendorConfig", "createTime"); + + const volcengineVer = await u.vendor.getVendor("volcengine").version; + if (Number(volcengineVer) < 2.1) { + u.vendor.writeCode("volcengine", vendorData["volcengine.ts"]); + } + const minimaxVer = await u.vendor.getVendor("minimax").version; + if (Number(minimaxVer) < 2.1) { + u.vendor.writeCode("minimax", vendorData["minimax.ts"]); + } }; async function tempOnsert(tsCode: string) { diff --git a/src/routes/setting/vendorConfig/updateCode.ts b/src/routes/setting/vendorConfig/updateCode.ts index 3c88614..a67d17e 100644 --- a/src/routes/setting/vendorConfig/updateCode.ts +++ b/src/routes/setting/vendorConfig/updateCode.ts @@ -85,7 +85,6 @@ export default router.post( .db("o_vendorConfig") .where("id", id) .update({ - inputValues: JSON.stringify(vendor.inputValues ?? {}), models: JSON.stringify(vendor.models ?? []), }); u.vendor.writeCode(id, tsCode);