diff --git a/NOTICES.txt b/NOTICES.txt index 00b6afe..9be9d1c 100644 --- a/NOTICES.txt +++ b/NOTICES.txt @@ -70,6 +70,12 @@ Repository: https://github.com/DefinitelyTyped/DefinitelyTyped ----------------------------- +Name: @types/graphlib +License: MIT +Repository: https://github.com/DefinitelyTyped/DefinitelyTyped + +----------------------------- + Name: @types/jsonwebtoken License: MIT Repository: https://github.com/DefinitelyTyped/DefinitelyTyped @@ -142,18 +148,18 @@ Repository: https://github.com/motdotla/dotenv ----------------------------- -Name: dotenv -License: BSD-2-Clause -Repository: https://github.com/motdotla/dotenv - ------------------------------ - Name: electron-builder License: MIT Repository: https://github.com/electron-userland/electron-builder ----------------------------- +Name: electron-rebuild +License: MIT +Repository: https://github.com/electron/electron-rebuild + +----------------------------- + Name: electron License: MIT Repository: https://github.com/electron/electron @@ -244,15 +250,15 @@ Repository: https://github.com/remy/nodemon ----------------------------- -Name: qwen-ai-provider-v5 -License: Apache-2.0 -Repository: https://github.com/bolechen/qwen-ai-provider-v5 +Name: p-limit +License: MIT +Repository: https://github.com/sindresorhus/p-limit ----------------------------- -Name: serialize-error -License: MIT -Repository: https://github.com/sindresorhus/serialize-error +Name: qwen-ai-provider-v5 +License: Apache-2.0 +Repository: https://github.com/bolechen/qwen-ai-provider-v5 ----------------------------- @@ -304,6 +310,12 @@ Repository: https://github.com/uuidjs/uuid ----------------------------- +Name: vercel-minimax-ai-provider +License: Apache-2.0 +Repository: https://github.com/MiniMax-AI/vercel-minimax-ai-provider + +----------------------------- + Name: vm2 License: MIT Repository: https://github.com/patriksimek/vm2 diff --git a/data/skills/production_execution_generate_assets.md b/data/skills/production_execution_generate_assets.md index 68921b0..801f73c 100644 --- a/data/skills/production_execution_generate_assets.md +++ b/data/skills/production_execution_generate_assets.md @@ -23,12 +23,12 @@ description: >- | 操作 | 调用 | |------|------| | 读取资产列表 | `get_flowData("assets")` | -| 生成资产图片 | `generate_assets_images({ ids: [资产id列表] })` | +| 生成资产图片 | `generate_deriveAsset({ ids: [资产id列表] })` | ### 执行流程 1. 获取 `assets`,收集所有需要生成图片的资产 id -2. 调用 `generate_assets_images({ ids: [资产id列表] })` 生成图片(异步,发起即返回) +2. 调用 `generate_deriveAsset({ ids: [资产id列表] })` 生成图片(异步,发起即返回) ### 约束 diff --git a/data/vendor/grsai.ts b/data/vendor/grsai.ts new file mode 100644 index 0000000..6e541e7 --- /dev/null +++ b/data/vendor/grsai.ts @@ -0,0 +1,318 @@ +/** + * Toonflow AI供应商模板 + * @version 2.0 + */ + +// ============================================================ +// 类型定义 +// ============================================================ + +type VideoMode = + | "singleImage" //单图参考 + | "startEndRequired" //首尾帧(两张都得有) + | "endFrameOptional" //首尾帧(尾帧可选) + | "startFrameOptional" //首尾帧(首帧可选) + | "text" //文本 + | (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[]; //多参考(数字代表限制数量) + +interface TextModel { + name: string; + modelName: string; + type: "text"; + think: boolean; +} + +interface ImageModel { + name: string; + modelName: string; + type: "image"; + mode: ("text" | "singleImage" | "multiReference")[]; + associationSkills?: string; +} + +interface VideoModel { + name: string; + modelName: string; + type: "video"; + mode: VideoMode[]; + associationSkills?: string; + audio: "optional" | false | true; + durationResolutionMap: { duration: number[]; resolution: string[] }[]; +} + +interface TTSModel { + name: string; + modelName: string; + type: "tts"; + voices: { title: string; voice: string }[]; +} + +interface VendorConfig { + id: string; //唯一ID,作为文件名存储用户磁盘上,禁止符号 + version: string; //版本号,格式为x.y,需遵守语义化版本控制 + name: string; //供应商名称 + author: string; //作者 + description?: string; //描述,支持Markdown格式 + icon?: string; //图标,仅支持Base64格式,建议尺寸为128x128像素 + inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[]; + inputValues: Record; + models: (TextModel | ImageModel | VideoModel | TTSModel)[]; +} + +type ReferenceList = + | { type: "image"; sourceType: "base64"; base64: string } + | { type: "audio"; sourceType: "base64"; base64: string } + | { type: "video"; sourceType: "base64"; base64: string }; + +interface ImageConfig { + prompt: string; + referenceList?: Extract[]; + size: "1K" | "2K" | "4K"; + aspectRatio: `${number}:${number}`; +} + +interface VideoConfig { + duration: number; + resolution: string; + aspectRatio: "16:9" | "9:16"; + prompt: string; + referenceList?: ReferenceList[]; + audio?: boolean; + mode: VideoMode[]; +} + +interface TTSConfig { + text: string; + voice: string; + speechRate: number; + pitchRate: number; + volume: number; + referenceList?: Extract[]; +} + +interface PollResult { + completed: boolean; + data?: string; + error?: string; +} + +// ============================================================ +// 全局声明 +// ============================================================ + +declare const axios: any; // HTTP请求库 +declare const logger: (msg: string) => void; // 日志函数 +declare const jsonwebtoken: any; // JWT处理库 +declare const zipImage: (base64: string, size: number) => Promise; // 图片压缩函数,返回有头base64字符串 +declare const zipImageResolution: (base64: string, w: number, h: number) => Promise; // 图片分辨率调整函数,返回有头base64字符串 +declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise; // 图片合成函数,返回有头base64字符串 +declare const urlToBase64: (url: string) => Promise; // URL转Base64函数,返回有头base64字符串 +declare const pollTask: (fn: () => Promise, interval?: number, timeout?: number) => Promise; // 轮询函数,fn为异步函数,interval为轮询间隔,timeout为超时时间,返回fn的结果 +declare const createOpenAI: any; +declare const createDeepSeek: any; +declare const createZhipu: any; +declare const createQwen: any; +declare const createAnthropic: any; +declare const createOpenAICompatible: any; +declare const createXai: any; +declare const createMinimax: any; +declare const createGoogleGenerativeAI: any; +declare const exports: { + vendor: VendorConfig; + textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => any; //文本模型 + imageRequest: (c: ImageConfig, m: ImageModel) => Promise; //图片模型,返回有头base64字符串 + videoRequest: (c: VideoConfig, m: VideoModel) => Promise; //视频模型,返回有头base64字符串 + ttsRequest: (c: TTSConfig, m: TTSModel) => Promise; //(暂未开放)语音模型,返回有头base64字符串 + checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>; //检查更新函数,返回是否有更新和最新版本号和更公告(支持Markdown格式) + updateVendor?: () => Promise; //更新函数,返回最新的代码文本 +}; + +// ============================================================ +// 供应商配置 +// ============================================================ + +const vendor: VendorConfig = { + id: "grsai", + version: "2.0", + author: "Toonflow", + name: "Grsai", + description: "Grsai AI平台适配,支持文生图、图生图、文生视频、Gemini兼容文本模型 \n [前往中转平台](https://tf.grsai.ai/zh)", + inputs: [ + { key: "apiKey", label: "API密钥", type: "password", required: true }, + { key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "示例:https://grsai.dakka.com.cn" }, + ], + inputValues: { apiKey: "", baseUrl: "https://grsai.dakka.com.cn" }, + models: [ + { name: "Nano Banana Fast", modelName: "nano-banana-fast", type: "image", mode: ["text", "singleImage", "multiReference"] }, + { name: "Nano Banana 2", modelName: "nano-banana-2", type: "image", mode: ["text", "singleImage", "multiReference"] }, + { name: "Nano Banana Pro", modelName: "nano-banana-pro", type: "image", mode: ["text", "singleImage", "multiReference"] }, + ], +}; + +// ============================================================ +// 辅助工具 +// ============================================================ + +const getHeaders = () => { + const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, ""); + return { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }; +}; + +// ============================================================ +// 适配器函数 +// ============================================================ + +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, ""); + return createGoogleGenerativeAI({ + baseURL: `${vendor.inputValues.baseUrl}/v1beta`, + apiKey, + }).chat(model.modelName); +}; + +const imageRequest = async (config: ImageConfig, model: ImageModel): Promise => { + if (!vendor.inputValues.apiKey) throw new Error("缺少API Key"); + const baseUrl = vendor.inputValues.baseUrl; + const headers = getHeaders(); + + // 构造请求参数 + const requestBody: any = { + model: model.modelName, + prompt: config.prompt, + aspectRatio: config.aspectRatio, + webHook: "-1", + shutProgress: true, + }; + + // 补充模型专属参数 + if (model.modelName.startsWith("nano-banana")) { + requestBody.imageSize = config.size; + } else { + requestBody.size = config.aspectRatio; + requestBody.variants = 1; + } + + // 处理参考图 + if (config.referenceList && config.referenceList.length > 0) { + requestBody.urls = config.referenceList.map((img) => img.base64); + } + + // 选择接口路径 + const apiPath = model.modelName.startsWith("nano-banana") ? "/v1/draw/nano-banana" : "/v1/draw/completions"; + + logger(`开始提交图片生成任务,模型:${model.modelName}`); + const submitResp = await axios.post(`${baseUrl}${apiPath}`, requestBody, { headers }); + if (submitResp.data.code !== 0) throw new Error(`任务提交失败:${submitResp.data.msg}`); + + const taskId = submitResp.data.data.id; + logger(`图片任务提交成功,任务ID:${taskId}`); + + // 轮询结果 + const pollResult = await pollTask( + async () => { + const resp = await axios.post(`${baseUrl}/v1/draw/result`, { id: taskId }, { headers }); + if (resp.data.code !== 0) return { completed: true, error: resp.data.msg }; + + const taskData = resp.data.data; + if (taskData.status === "failed") return { completed: true, error: taskData.failure_reason || taskData.error }; + if (taskData.status === "succeeded") { + const imgUrl = taskData.results?.[0]?.url || taskData.url; + return { completed: true, data: imgUrl }; + } + logger(`图片任务生成中,进度:${taskData.progress}%`); + return { completed: false }; + }, + 3000, + 600000, + ); + + if (pollResult.error) throw new Error(pollResult.error); + logger(`图片生成完成,开始转换Base64`); + return await urlToBase64(pollResult.data!); +}; + +const videoRequest = async (config: VideoConfig, model: VideoModel): Promise => { + if (!vendor.inputValues.apiKey) throw new Error("缺少API Key"); + const baseUrl = vendor.inputValues.baseUrl; + const headers = getHeaders(); + + // 构造请求参数 + const requestBody: any = { + model: model.modelName, + prompt: config.prompt, + aspectRatio: config.aspectRatio, + webHook: "-1", + shutProgress: true, + }; + + // 处理参考资源 + if (config.referenceList && config.referenceList.length > 0) { + const imageRefs = config.referenceList.filter((item) => item.type === "image") as Extract[]; + if (config.mode.includes("endFrameOptional") && imageRefs.length >= 1) { + requestBody.firstFrameUrl = imageRefs[0].base64; + if (imageRefs.length >= 2) requestBody.lastFrameUrl = imageRefs[1].base64; + } else if (config.mode.some((m) => Array.isArray(m) && m.includes("imageReference:3"))) { + requestBody.urls = imageRefs.map((img) => img.base64); + } + } + + logger(`开始提交视频生成任务,模型:${model.modelName}`); + const submitResp = await axios.post(`${baseUrl}/v1/video/veo`, requestBody, { headers }); + if (submitResp.data.code !== 0) throw new Error(`任务提交失败:${submitResp.data.msg}`); + + const taskId = submitResp.data.data.id; + logger(`视频任务提交成功,任务ID:${taskId}`); + + // 轮询结果 + const pollResult = await pollTask( + async () => { + const resp = await axios.post(`${baseUrl}/v1/draw/result`, { id: taskId }, { headers }); + if (resp.data.code !== 0) return { completed: true, error: resp.data.msg }; + + const taskData = resp.data.data; + if (taskData.status === "failed") return { completed: true, error: taskData.failure_reason || taskData.error }; + if (taskData.status === "succeeded") { + return { completed: true, data: taskData.url }; + } + logger(`视频任务生成中,进度:${taskData.progress}%`); + return { completed: false }; + }, + 5000, + 1800000, + ); + + if (pollResult.error) throw new Error(pollResult.error); + logger(`视频生成完成,开始转换Base64`); + return await urlToBase64(pollResult.data!); +}; + +const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise => { + return ""; +}; + +const checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => { + return { hasUpdate: false, latestVersion: "1.0", notice: "## 新版本更新公告" }; +}; + +const updateVendor = async (): Promise => { + return ""; +}; + +// ============================================================ +// 导出 +// ============================================================ + +exports.vendor = vendor; +exports.textRequest = textRequest; +exports.imageRequest = imageRequest; +exports.videoRequest = videoRequest; +exports.ttsRequest = ttsRequest; +exports.checkForUpdates = checkForUpdates; +exports.updateVendor = updateVendor; + +// 这行代码用于确保当前文件被识别为模块,避免全局变量冲突 +export {}; diff --git a/data/vendor/klingai.ts b/data/vendor/klingai.ts new file mode 100644 index 0000000..103eb55 --- /dev/null +++ b/data/vendor/klingai.ts @@ -0,0 +1,638 @@ +/** + * Toonflow AI供应商模板 - 可灵AI + * @version 2.0 + */ + +// ============================================================ +// 类型定义 +// ============================================================ + +type VideoMode = + | "singleImage" + | "startEndRequired" + | "endFrameOptional" + | "startFrameOptional" + | "text" + | (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[]; + +interface TextModel { + name: string; + modelName: string; + type: "text"; + think: boolean; +} + +interface ImageModel { + name: string; + modelName: string; + type: "image"; + mode: ("text" | "singleImage" | "multiReference")[]; + associationSkills?: string; +} + +interface VideoModel { + name: string; + modelName: string; + type: "video"; + mode: VideoMode[]; + associationSkills?: string; + audio: "optional" | false | true; + durationResolutionMap: { duration: number[]; resolution: string[] }[]; +} + +interface TTSModel { + name: string; + modelName: string; + type: "tts"; + voices: { title: string; voice: string }[]; +} + +interface VendorConfig { + id: string; + version: string; + name: string; + author: string; + description?: string; + icon?: string; + inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[]; + inputValues: Record; + models: (TextModel | ImageModel | VideoModel | TTSModel)[]; +} + +type ReferenceList = + | { type: "image"; sourceType: "base64"; base64: string } + | { type: "audio"; sourceType: "base64"; base64: string } + | { type: "video"; sourceType: "base64"; base64: string }; + +interface ImageConfig { + prompt: string; + referenceList?: Extract[]; + size: "1K" | "2K" | "4K"; + aspectRatio: `${number}:${number}`; +} + +interface VideoConfig { + duration: number; + resolution: string; + aspectRatio: "16:9" | "9:16"; + prompt: string; + referenceList?: ReferenceList[]; + audio?: boolean; + mode: VideoMode[]; +} + +interface TTSConfig { + text: string; + voice: string; + speechRate: number; + pitchRate: number; + volume: number; + referenceList?: Extract[]; +} + +interface PollResult { + completed: boolean; + data?: string; + error?: string; +} + +// ============================================================ +// 全局声明 +// ============================================================ + +declare const axios: any; +declare const logger: (msg: string) => void; +declare const jsonwebtoken: any; +declare const zipImage: (base64: string, size: number) => Promise; +declare const zipImageResolution: (base64: string, w: number, h: number) => Promise; +declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise; +declare const urlToBase64: (url: string) => Promise; +declare const pollTask: (fn: () => Promise, interval?: number, timeout?: number) => Promise; +declare const createOpenAI: any; +declare const createDeepSeek: any; +declare const createZhipu: any; +declare const createQwen: any; +declare const createAnthropic: any; +declare const createOpenAICompatible: any; +declare const createXai: any; +declare const createMinimax: any; +declare const createGoogleGenerativeAI: any; +declare const exports: { + vendor: VendorConfig; + textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => any; + imageRequest: (c: ImageConfig, m: ImageModel) => Promise; + videoRequest: (c: VideoConfig, m: VideoModel) => Promise; + ttsRequest: (c: TTSConfig, m: TTSModel) => Promise; + checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>; + updateVendor?: () => Promise; +}; + +// ============================================================ +// 供应商配置 +// ============================================================ + +const vendor: VendorConfig = { + id: "klingai", + version: "2.0", + author: "Toonflow", + name: "可灵AI", + description: + "可灵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。", + inputs: [ + { key: "accessKey", label: "Access Key", type: "password", required: true, placeholder: "请输入可灵AI的Access Key" }, + { key: "secretKey", label: "Secret Key", type: "password", required: true, placeholder: "请输入可灵AI的Secret Key" }, + { key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "默认:https://api-beijing.klingai.com" }, + ], + inputValues: { accessKey: "", secretKey: "", baseUrl: "https://api-beijing.klingai.com" }, + models: [ + // kling-video-o1 (Omni) + { + name: "kling-video-o1 标准", + modelName: "kling-video-o1:std", + type: "video", + mode: ["text", "singleImage", "startEndRequired", ["imageReference:7", "videoReference:1"]], + audio: false, + durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }], + }, + { + name: "kling-video-o1 专家", + modelName: "kling-video-o1:pro", + type: "video", + mode: ["text", "singleImage", "startEndRequired", ["imageReference:7", "videoReference:1"]], + audio: false, + durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }], + }, + // kling-v3-omni (Omni) + { + name: "kling-v3-omni 标准", + modelName: "kling-v3-omni:std", + type: "video", + mode: ["text", "singleImage", "startEndRequired", ["imageReference:7", "videoReference:1"]], + audio: false, + durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["720p"] }], + }, + { + name: "kling-v3-omni 专家", + modelName: "kling-v3-omni:pro", + type: "video", + mode: ["text", "singleImage", "startEndRequired", ["imageReference:7", "videoReference:1"]], + audio: false, + durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["720p"] }], + }, + // kling-v3 + { + name: "kling-v3 标准", + modelName: "kling-v3:std", + type: "video", + mode: ["text", "singleImage", "startEndRequired"], + audio: false, + durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["720p"] }], + }, + { + name: "kling-v3 专家", + modelName: "kling-v3:pro", + type: "video", + mode: ["text", "singleImage", "startEndRequired"], + audio: false, + durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["720p"] }], + }, + // kling-v2-6 + { + name: "kling-v2-6 标准", + modelName: "kling-v2-6:std", + type: "video", + mode: ["text", "singleImage"], + audio: false, + durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }], + }, + { + name: "kling-v2-6 专家", + modelName: "kling-v2-6:pro", + type: "video", + mode: ["text", "singleImage", "startEndRequired"], + audio: "optional", + durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }], + }, + // kling-v2-5-turbo + { + name: "kling-v2-5-turbo 标准", + modelName: "kling-v2-5-turbo:std", + type: "video", + mode: ["text", "singleImage"], + audio: false, + durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }], + }, + { + name: "kling-v2-5-turbo 专家", + modelName: "kling-v2-5-turbo:pro", + type: "video", + mode: ["text", "singleImage", "startEndRequired"], + audio: false, + durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }], + }, + // kling-v2-1 + { + name: "kling-v2-1 标准", + modelName: "kling-v2-1:std", + type: "video", + mode: ["singleImage"], + audio: false, + durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }], + }, + { + name: "kling-v2-1 专家", + modelName: "kling-v2-1:pro", + type: "video", + mode: ["singleImage", "startEndRequired"], + audio: false, + durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }], + }, + // kling-v2-1-master + { + name: "kling-v2-1 Master", + modelName: "kling-v2-1-master:pro", + type: "video", + mode: ["text", "singleImage"], + audio: false, + durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }], + }, + // kling-v2-master + { + name: "kling-v2 Master", + modelName: "kling-v2-master:pro", + type: "video", + mode: ["text", "singleImage"], + audio: false, + durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }], + }, + // kling-v1-6 + { + name: "kling-v1-6 标准", + modelName: "kling-v1-6:std", + type: "video", + mode: ["text", "singleImage", ["imageReference:4"]], + audio: false, + durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }], + }, + { + name: "kling-v1-6 专家", + modelName: "kling-v1-6:pro", + type: "video", + mode: ["text", "singleImage", "endFrameOptional", ["imageReference:4"]], + audio: false, + durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }], + }, + // kling-v1-5 + { + name: "kling-v1-5 标准", + modelName: "kling-v1-5:std", + type: "video", + mode: ["singleImage"], + audio: false, + durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }], + }, + { + name: "kling-v1-5 专家", + modelName: "kling-v1-5:pro", + type: "video", + mode: ["singleImage", "endFrameOptional"], + audio: false, + durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }], + }, + // kling-v1 + { + name: "kling-v1 标准", + modelName: "kling-v1:std", + type: "video", + mode: ["text", "singleImage", "startEndRequired"], + audio: false, + durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }], + }, + { + name: "kling-v1 专家", + modelName: "kling-v1:pro", + type: "video", + mode: ["text", "singleImage", "startEndRequired"], + audio: false, + durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }], + }, + ], +}; + +// ============================================================ +// 辅助工具 +// ============================================================ + +/** + * 生成可灵AI的JWT鉴权Token + */ +const generateAuthToken = (): string => { + const now = Math.floor(Date.now() / 1000); + const payload = { + iss: vendor.inputValues.accessKey, + exp: now + 1800, + nbf: now - 5, + }; + return jsonwebtoken.sign(payload, vendor.inputValues.secretKey, { + algorithm: "HS256", + header: { alg: "HS256", typ: "JWT" }, + }); +}; + +/** + * 获取基础请求地址 + */ +const getBaseUrl = (): string => { + return vendor.inputValues.baseUrl || "https://api-beijing.klingai.com"; +}; + +/** + * 从 ReferenceList 条目中提取可用的数据字符串 + * 对于 url 类型返回 url,对于 base64 类型返回纯 base64(去掉 data: 前缀) + */ +const extractRawBase64 = (ref: ReferenceList): string => { + return ref.base64.replace(/^data:[^;]+;base64,/, ""); +}; + +/** + * 从 ReferenceList 条目中提取带头的 base64 或 url + * 用于 omni-video 接口,该接口的 image_url 支持带前缀的 base64 和 url + */ +const extractImageUrl = (ref: ReferenceList): string => { + return ref.base64.startsWith("data:") ? ref.base64 : `data:image/jpeg;base64,${ref.base64}`; +}; + +/** + * 提交任务并轮询获取结果的通用函数 + */ +const submitAndPoll = async (submitUrl: string, queryUrlBase: string, requestBody: any): Promise => { + const token = generateAuthToken(); + + logger(`开始提交可灵AI视频生成任务: ${submitUrl}`); + logger( + `请求参数: ${JSON.stringify({ + ...requestBody, + image: requestBody.image ? "[BASE64]" : undefined, + image_tail: requestBody.image_tail ? "[BASE64]" : undefined, + image_list: requestBody.image_list ? "[IMAGES]" : undefined, + })}`, + ); + + const submitResp = await axios.post(submitUrl, requestBody, { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + + if (submitResp.data.code !== 0) { + throw new Error(`提交任务失败: ${submitResp.data.message || JSON.stringify(submitResp.data)}`); + } + + const taskId = submitResp.data.data.task_id; + logger(`任务已提交,任务ID: ${taskId}`); + + const result = await pollTask( + async () => { + const freshToken = generateAuthToken(); + const queryResp = await axios.get(`${queryUrlBase}/${taskId}`, { + headers: { + Authorization: `Bearer ${freshToken}`, + }, + }); + + if (queryResp.data.code !== 0) { + return { completed: true, error: `查询任务失败: ${queryResp.data.message}` }; + } + + const taskData = queryResp.data.data; + const status = taskData.task_status; + logger(`轮询中... 任务状态: ${status}`); + + if (status === "succeed") { + const videoUrl = taskData.task_result?.videos?.[0]?.url; + if (!videoUrl) { + return { completed: true, error: "任务完成但未获取到视频URL" }; + } + return { completed: true, data: videoUrl }; + } + + if (status === "failed") { + return { completed: true, error: `视频生成失败: ${taskData.task_status_msg || "未知错误"}` }; + } + + return { completed: false }; + }, + 5000, + 600000, + ); + + if (result.error) throw new Error(result.error); + logger(`视频生成完成,正在转换为Base64...`); + return await urlToBase64(result.data!); +}; + +// ============================================================ +// 适配器函数 +// ============================================================ + +const textRequest = (model: TextModel, think: boolean, thinkLevel: 0 | 1 | 2 | 3) => { + throw new Error("可灵AI不支持文本模型"); +}; + +const imageRequest = async (config: ImageConfig, model: ImageModel): Promise => { + throw new Error("可灵AI不支持图片模型"); +}; + +const videoRequest = async (config: VideoConfig, model: VideoModel): Promise => { + if (!vendor.inputValues.accessKey) throw new Error("缺少Access Key"); + if (!vendor.inputValues.secretKey) throw new Error("缺少Secret Key"); + + const baseUrl = getBaseUrl(); + + // 解析 modelName,格式:kling-video-o1:pro => modelName=kling-video-o1, mode=pro + const colonIdx = model.modelName.indexOf(":"); + const modelName = colonIdx > -1 ? model.modelName.substring(0, colonIdx) : model.modelName; + const mode = colonIdx > -1 ? model.modelName.substring(colonIdx + 1) : "pro"; + + // 判断是否为 Omni 模型 + const isOmniModel = modelName === "kling-video-o1" || modelName === "kling-v3-omni"; + + // 判断当前选中的视频生成模式 + const currentMode = config.mode; + const isText = currentMode.includes("text"); + const isSingleImage = currentMode.includes("singleImage"); + const isStartEndRequired = currentMode.includes("startEndRequired"); + const isEndFrameOptional = currentMode.includes("endFrameOptional"); + const isStartFrameOptional = currentMode.includes("startFrameOptional"); + const hasMultiRef = currentMode.some((m) => Array.isArray(m)); + + // 提取不同类型的引用 + const imageRefs = (config.referenceList || []).filter((r) => r.type === "image"); + const videoRefs = (config.referenceList || []).filter((r) => r.type === "video"); + + // ===================================================== + // Omni 模型 —— 使用 /v1/videos/omni-video 接口 + // ===================================================== + if (isOmniModel) { + const requestBody: any = { + model_name: modelName, + mode: mode, + duration: String(config.duration), + sound: config.audio === true ? "on" : "off", + }; + + if (config.prompt) { + requestBody.prompt = config.prompt; + } + + if (isSingleImage && imageRefs.length > 0) { + const imageUrl = extractImageUrl(imageRefs[0]); + requestBody.image_list = [{ image_url: imageUrl, type: "first_frame" }]; + if (!requestBody.prompt) requestBody.prompt = "根据图片生成视频"; + } else if (isStartEndRequired && imageRefs.length >= 2) { + const firstUrl = extractImageUrl(imageRefs[0]); + const endUrl = extractImageUrl(imageRefs[1]); + requestBody.image_list = [ + { image_url: firstUrl, type: "first_frame" }, + { image_url: endUrl, type: "end_frame" }, + ]; + if (!requestBody.prompt) requestBody.prompt = "根据首尾帧图片生成过渡视频"; + } else if (isEndFrameOptional && imageRefs.length >= 1) { + const firstUrl = extractImageUrl(imageRefs[0]); + requestBody.image_list = [{ image_url: firstUrl, type: "first_frame" }]; + if (imageRefs.length >= 2) { + const endUrl = extractImageUrl(imageRefs[1]); + requestBody.image_list.push({ image_url: endUrl, type: "end_frame" }); + } + if (!requestBody.prompt) requestBody.prompt = "根据图片生成视频"; + } else if (isStartFrameOptional && imageRefs.length >= 1) { + if (imageRefs.length >= 2) { + const firstUrl = extractImageUrl(imageRefs[0]); + const endUrl = extractImageUrl(imageRefs[1]); + requestBody.image_list = [ + { image_url: firstUrl, type: "first_frame" }, + { image_url: endUrl, type: "end_frame" }, + ]; + } else { + const endUrl = extractImageUrl(imageRefs[0]); + requestBody.image_list = [{ image_url: endUrl, type: "end_frame" }]; + } + if (!requestBody.prompt) requestBody.prompt = "根据图片生成视频"; + } else if (hasMultiRef && (imageRefs.length > 0 || videoRefs.length > 0)) { + requestBody.image_list = []; + for (let i = 0; i < imageRefs.length; i++) { + const imageUrl = extractImageUrl(imageRefs[i]); + requestBody.image_list.push({ image_url: imageUrl }); + } + if (!requestBody.prompt) { + const refs = imageRefs.map((_, idx) => `<<>>`).join("、"); + requestBody.prompt = `参考${refs}生成视频`; + } + } + + // 文生视频或无图片输入时需要设置宽高比 + const hasImageInput = requestBody.image_list && requestBody.image_list.length > 0; + if (!hasImageInput) { + requestBody.aspect_ratio = config.aspectRatio || "16:9"; + if (!requestBody.prompt) throw new Error("文生视频模式需要提供提示词"); + } + + const apiPath = "/v1/videos/omni-video"; + return await submitAndPoll(`${baseUrl}${apiPath}`, `${baseUrl}${apiPath}`, requestBody); + } + + // ===================================================== + // 非 Omni 模型 —— 根据模式选择不同接口 + // ===================================================== + + // 多图参考模式 —— 使用 /v1/videos/multi-image2video 接口(仅 kling-v1-6 支持) + if (hasMultiRef && imageRefs.length > 0) { + const imageList = []; + for (let i = 0; i < imageRefs.length; i++) { + const rawBase64 = extractRawBase64(imageRefs[i]); + imageList.push({ image: rawBase64 }); + } + + const requestBody: any = { + model_name: modelName, + image_list: imageList, + prompt: config.prompt || "根据参考图片生成视频", + mode: mode, + duration: String(config.duration), + aspect_ratio: config.aspectRatio || "16:9", + }; + + const apiPath = "/v1/videos/multi-image2video"; + return await submitAndPoll(`${baseUrl}${apiPath}`, `${baseUrl}${apiPath}`, requestBody); + } + + // 文生视频模式 —— 使用 /v1/videos/text2video 接口 + if (isText) { + if (!config.prompt) throw new Error("文生视频模式需要提供提示词"); + + const requestBody: any = { + model_name: modelName, + prompt: config.prompt, + mode: mode, + duration: String(config.duration), + aspect_ratio: config.aspectRatio || "16:9", + sound: config.audio === true ? "on" : "off", + }; + + const apiPath = "/v1/videos/text2video"; + return await submitAndPoll(`${baseUrl}${apiPath}`, `${baseUrl}${apiPath}`, requestBody); + } + + // 图生视频模式(单图 / 首尾帧 / 尾帧可选等)—— 使用 /v1/videos/image2video 接口 + if ((isSingleImage || isStartEndRequired || isEndFrameOptional || isStartFrameOptional) && imageRefs.length > 0) { + const requestBody: any = { + model_name: modelName, + prompt: config.prompt || "根据图片生成视频", + mode: mode, + duration: String(config.duration), + sound: config.audio === true ? "on" : "off", + }; + + if (isSingleImage) { + requestBody.image = extractRawBase64(imageRefs[0]); + } else if (isStartEndRequired && imageRefs.length >= 2) { + requestBody.image = extractRawBase64(imageRefs[0]); + requestBody.image_tail = extractRawBase64(imageRefs[1]); + } else if (isEndFrameOptional) { + requestBody.image = extractRawBase64(imageRefs[0]); + if (imageRefs.length >= 2) { + requestBody.image_tail = extractRawBase64(imageRefs[1]); + } + } else if (isStartFrameOptional) { + if (imageRefs.length >= 2) { + requestBody.image = extractRawBase64(imageRefs[0]); + requestBody.image_tail = extractRawBase64(imageRefs[1]); + } else { + requestBody.image = extractRawBase64(imageRefs[0]); + } + } + + const apiPath = "/v1/videos/image2video"; + return await submitAndPoll(`${baseUrl}${apiPath}`, `${baseUrl}${apiPath}`, requestBody); + } + + throw new Error("不支持的视频生成模式或缺少必要的输入参数"); +}; + +const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise => { + return ""; +}; + +// ============================================================ +// 导出 +// ============================================================ + +exports.vendor = vendor; +exports.textRequest = textRequest; +exports.imageRequest = imageRequest; +exports.videoRequest = videoRequest; +exports.ttsRequest = ttsRequest; + +// 这行代码用于确保当前文件被识别为模块,避免全局变量冲突 +export {}; \ No newline at end of file diff --git a/data/vendor/minimax.ts b/data/vendor/minimax.ts new file mode 100644 index 0000000..86bd710 --- /dev/null +++ b/data/vendor/minimax.ts @@ -0,0 +1,399 @@ +/** + * Toonflow AI供应商模板 - MiniMax(海螺AI) + * @version 2.0 + */ + +// ============================================================ +// 类型定义 +// ============================================================ + +type VideoMode = + | "singleImage" + | "startEndRequired" + | "endFrameOptional" + | "startFrameOptional" + | "text" + | (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[]; + +interface TextModel { + name: string; + modelName: string; + type: "text"; + think: boolean; +} + +interface ImageModel { + name: string; + modelName: string; + type: "image"; + mode: ("text" | "singleImage" | "multiReference")[]; + associationSkills?: string; +} + +interface VideoModel { + name: string; + modelName: string; + type: "video"; + mode: VideoMode[]; + associationSkills?: string; + audio: "optional" | false | true; + durationResolutionMap: { duration: number[]; resolution: string[] }[]; +} + +interface TTSModel { + name: string; + modelName: string; + type: "tts"; + voices: { title: string; voice: string }[]; +} + +interface VendorConfig { + id: string; + version: string; + name: string; + author: string; + description?: string; + icon?: string; + inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[]; + inputValues: Record; + models: (TextModel | ImageModel | VideoModel | TTSModel)[]; +} + +type ReferenceList = + | { type: "image"; sourceType: "base64"; base64: string } + | { type: "audio"; sourceType: "base64"; base64: string } + | { type: "video"; sourceType: "base64"; base64: string }; + +interface ImageConfig { + prompt: string; + referenceList?: Extract[]; + size: "1K" | "2K" | "4K"; + aspectRatio: `${number}:${number}`; +} + +interface VideoConfig { + duration: number; + resolution: string; + aspectRatio: "16:9" | "9:16"; + prompt: string; + referenceList?: ReferenceList[]; + audio?: boolean; + mode: VideoMode[]; +} + +interface TTSConfig { + text: string; + voice: string; + speechRate: number; + pitchRate: number; + volume: number; + referenceList?: Extract[]; +} + +interface PollResult { + completed: boolean; + data?: string; + error?: string; +} + +// ============================================================ +// 全局声明 +// ============================================================ + +declare const axios: any; +declare const logger: (msg: string) => void; +declare const jsonwebtoken: any; +declare const zipImage: (base64: string, size: number) => Promise; +declare const zipImageResolution: (base64: string, w: number, h: number) => Promise; +declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise; +declare const urlToBase64: (url: string) => Promise; +declare const pollTask: (fn: () => Promise, interval?: number, timeout?: number) => Promise; +declare const createOpenAI: any; +declare const createDeepSeek: any; +declare const createZhipu: any; +declare const createQwen: any; +declare const createAnthropic: any; +declare const createOpenAICompatible: any; +declare const createXai: any; +declare const createMinimax: any; +declare const createGoogleGenerativeAI: any; +declare const exports: { + vendor: VendorConfig; + textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => any; + uploadReference: (base64: string, fileType: "image" | "audio" | "video") => Promise; + imageRequest: (c: ImageConfig, m: ImageModel) => Promise; + videoRequest: (c: VideoConfig, m: VideoModel) => Promise; + ttsRequest: (c: TTSConfig, m: TTSModel) => Promise; + checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>; + updateVendor?: () => Promise; +}; + +// ============================================================ +// 供应商配置 +// ============================================================ + +const vendor: VendorConfig = { + id: "minimax", + version: "2.0", + author: "Toonflow", + name: "MiniMax(海螺AI)", + description: "MiniMax官方接口适配,支持M系列推理文本模型、文生图/图生图、视频生成(文生视频、图生视频、首尾帧生成)能力 \n [前往平台](https://minimaxi.com/)", + inputs: [ + { key: "apiKey", label: "API密钥", type: "password", required: true }, + { key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "示例:https://api.minimaxi.com" }, + ], + inputValues: { apiKey: "", baseUrl: "https://api.minimaxi.com" }, + models: [ + // 文本模型 + { name: "MiniMax-M2.7 (推理版)", modelName: "MiniMax-M2.7", type: "text", think: true }, + { name: "MiniMax-M2.7 极速版 (推理版)", modelName: "MiniMax-M2.7-highspeed", type: "text", think: true }, + { name: "MiniMax-M2.5 (推理版)", modelName: "MiniMax-M2.5", type: "text", think: true }, + { name: "MiniMax-M2.5 极速版 (推理版)", modelName: "MiniMax-M2.5-highspeed", type: "text", think: true }, + { name: "MiniMax-M2.1 (编程版)", modelName: "MiniMax-M2.1", type: "text", think: true }, + { name: "MiniMax-M2.1 极速版 (编程版)", modelName: "MiniMax-M2.1-highspeed", type: "text", think: true }, + { name: "MiniMax-M2 (Agent版)", modelName: "MiniMax-M2", type: "text", think: false }, + // 图片模型 + { name: "海螺图像V1", modelName: "image-01", type: "image", mode: ["text", "singleImage"] }, + { name: "海螺图像V1 Live版", modelName: "image-01-live", type: "image", mode: ["text", "singleImage"], associationSkills: "支持自定义画风" }, + // 视频模型 + { + name: "海螺2.3", + modelName: "MiniMax-Hailuo-2.3", + type: "video", + mode: ["text", "singleImage"], + audio: false, + durationResolutionMap: [ + { duration: [6], resolution: ["768P", "1080P"] }, + { duration: [10], resolution: ["768P"] }, + ], + }, + { + name: "海螺2.3极速版", + modelName: "MiniMax-Hailuo-2.3-Fast", + type: "video", + mode: ["text", "singleImage"], + audio: false, + durationResolutionMap: [ + { duration: [6], resolution: ["768P", "1080P"] }, + { duration: [10], resolution: ["768P"] }, + ], + }, + { + name: "海螺02", + modelName: "MiniMax-Hailuo-02", + type: "video", + mode: ["text", "singleImage", "startEndRequired"], + audio: false, + durationResolutionMap: [ + { duration: [6], resolution: ["512P", "768P", "1080P"] }, + { duration: [10], resolution: ["512P", "768P"] }, + ], + }, + ], +}; + +// ============================================================ +// 辅助工具 +// ============================================================ + +/** + * 获取请求头 + */ +const getHeaders = (): Record => { + const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, ""); + return { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }; +}; + +/** + * 获取基础请求地址 + */ +const getBaseUrl = (): string => { + return vendor.inputValues.baseUrl.replace(/\/$/, ""); +}; + +/** + * 从 ReferenceList 条目中提取有头 base64 字符串 + */ +const extractBase64WithHead = (ref: ReferenceList): string => { + return ref.base64.startsWith("data:") ? ref.base64 : `data:image/png;base64,${ref.base64}`; +}; + +// ============================================================ +// 适配器函数 +// ============================================================ + +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); +}; + +const uploadReference = async (base64: string, fileType: "image" | "audio" | "video"): Promise => { + // MiniMax的图片接口直接接受 base64,压缩后原样返回 + if (fileType === "image") { + const compressed = await zipImage(base64, 10 * 1024); + return { type: "image", sourceType: "base64", base64: compressed }; + } + // 视频接口的图片参数也是 base64,压缩到20MB + return { type: fileType, sourceType: "base64", base64 } as ReferenceList; +}; + +const imageRequest = async (config: ImageConfig, model: ImageModel): Promise => { + if (!vendor.inputValues.apiKey) throw new Error("缺少API Key"); + const baseUrl = getBaseUrl(); + const headers = getHeaders(); + + const reqBody: any = { + model: model.modelName, + prompt: config.prompt, + aspect_ratio: config.aspectRatio, + response_format: "base64", + n: 1, + prompt_optimizer: true, + aigc_watermark: false, + }; + + // 处理图生图参考 + const imageRefs = config.referenceList || []; + if (imageRefs.length > 0) { + const refBase64 = extractBase64WithHead(imageRefs[0]); + reqBody.subject_reference = [{ type: "character", image_file: refBase64 }]; + } + + logger("开始提交MiniMax图像生成任务"); + const resp = await axios.post(`${baseUrl}/v1/image_generation`, reqBody, { headers }); + if (resp.data.base_resp.status_code !== 0) { + throw new Error(`图像生成失败:${resp.data.base_resp.status_msg}`); + } + if (resp.data.metadata.success_count === 0) { + throw new Error("图像生成被安全策略拦截,请调整prompt或参考图"); + } + + const imgBase64 = resp.data.data.image_base64[0]; + return imgBase64.startsWith("data:") ? imgBase64 : `data:image/png;base64,${imgBase64}`; +}; + +const videoRequest = async (config: VideoConfig, model: VideoModel): Promise => { + if (!vendor.inputValues.apiKey) throw new Error("缺少API Key"); + const baseUrl = getBaseUrl(); + const headers = getHeaders(); + + const reqBody: any = { + model: model.modelName, + prompt: config.prompt, + duration: config.duration, + resolution: config.resolution, + aigc_watermark: false, + prompt_optimizer: true, + }; + + // 提取图片类型的引用 + const imageRefs = (config.referenceList || []).filter((r) => r.type === "image"); + + if (imageRefs.length > 0) { + // 压缩图片到20MB以内 + const compressedImages: string[] = []; + for (const ref of imageRefs) { + const base64 = extractBase64WithHead(ref); + const compressed = await zipImage(base64, 20 * 1024); + compressedImages.push(compressed); + } + + if (config.mode.includes("startEndRequired")) { + if (compressedImages.length < 2) throw new Error("首尾帧模式需要上传两张图片"); + reqBody.first_frame_image = compressedImages[0]; + reqBody.last_frame_image = compressedImages[1]; + } else if (config.mode.includes("singleImage")) { + reqBody.first_frame_image = compressedImages[0]; + } + } + + logger("开始提交MiniMax视频生成任务"); + const submitResp = await axios.post(`${baseUrl}/v1/video_generation`, reqBody, { headers }); + if (submitResp.data.base_resp.status_code !== 0) { + throw new Error(`任务提交失败:${submitResp.data.base_resp.status_msg}`); + } + const taskId = submitResp.data.task_id; + logger(`视频任务提交成功,任务ID: ${taskId}`); + + // 轮询任务状态 + const pollResult = await pollTask( + async () => { + const queryResp = await axios.get(`${baseUrl}/v1/query/video_generation`, { + headers: getHeaders(), + params: { task_id: taskId }, + }); + if (queryResp.data.base_resp.status_code !== 0) { + return { completed: true, error: queryResp.data.base_resp.status_msg }; + } + const status = queryResp.data.status; + if (status === "Success") { + return { completed: true, data: queryResp.data.file_id }; + } + if (status === "Fail") { + return { completed: true, error: "视频生成失败" }; + } + logger(`视频任务生成中,当前状态:${status}`); + return { completed: false }; + }, + 5000, + 600000, + ); + + if (pollResult.error) throw new Error(pollResult.error); + const fileId = pollResult.data!; + logger(`视频任务生成成功,文件ID: ${fileId}`); + + // 获取下载地址 + const fileResp = await axios.get(`${baseUrl}/v1/files/retrieve`, { + headers: getHeaders(), + params: { file_id: fileId }, + }); + if (fileResp.data.base_resp.status_code !== 0) { + throw new Error(`获取文件地址失败:${fileResp.data.base_resp.status_msg}`); + } + const downloadUrl = fileResp.data.file.download_url; + logger(`视频下载地址获取成功,开始转Base64`); + + return await urlToBase64(downloadUrl); +}; + +const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise => { + return ""; +}; + +const checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => { + return { + hasUpdate: false, + latestVersion: "2.0", + notice: + "## 新版本更新公告\n1. 适配新版模板架构,支持 ReferenceList 统一引用类型\n2. 新增 uploadReference 前置处理器\n3. 优化图片压缩和引用提取逻辑", + }; +}; + +const updateVendor = async (): Promise => { + return ""; +}; + +// ============================================================ +// 导出 +// ============================================================ + +exports.vendor = vendor; +exports.textRequest = textRequest; +exports.uploadReference = uploadReference; +exports.imageRequest = imageRequest; +exports.videoRequest = videoRequest; +exports.ttsRequest = ttsRequest; +exports.checkForUpdates = checkForUpdates; +exports.updateVendor = updateVendor; + +// 这行代码用于确保当前文件被识别为模块,避免全局变量冲突 +export {}; \ No newline at end of file diff --git a/data/vendor/null.ts b/data/vendor/null.ts new file mode 100644 index 0000000..96ba3c2 --- /dev/null +++ b/data/vendor/null.ts @@ -0,0 +1,334 @@ +/** + * Toonflow AI供应商模板 + * @version 2.0 + */ + +// ============================================================ +// 类型定义 +// ============================================================ + +type VideoMode = + | "singleImage" //单图参考 + | "startEndRequired" //首尾帧(两张都得有) + | "endFrameOptional" //首尾帧(尾帧可选) + | "startFrameOptional" //首尾帧(首帧可选) + | "text" //文本 + | (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[]; //多参考(数字代表限制数量) + +interface TextModel { + name: string; + modelName: string; + type: "text"; + think: boolean; +} + +interface ImageModel { + name: string; + modelName: string; + type: "image"; + mode: ("text" | "singleImage" | "multiReference")[]; + associationSkills?: string; +} + +interface VideoModel { + name: string; + modelName: string; + type: "video"; + mode: VideoMode[]; + associationSkills?: string; + audio: "optional" | false | true; + durationResolutionMap: { duration: number[]; resolution: string[] }[]; +} + +interface TTSModel { + name: string; + modelName: string; + type: "tts"; + voices: { title: string; voice: string }[]; +} + +interface VendorConfig { + id: string; //唯一ID,作为文件名存储用户磁盘上,禁止符号 + version: string; //版本号,格式为x.y,需遵守语义化版本控制 + name: string; //供应商名称 + author: string; //作者 + description?: string; //描述,支持Markdown格式 + icon?: string; //图标,仅支持Base64格式,建议尺寸为128x128像素 + inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[]; + inputValues: Record; + models: (TextModel | ImageModel | VideoModel | TTSModel)[]; +} + +type ReferenceList = + | { type: "image"; sourceType: "base64"; base64: string } + | { type: "audio"; sourceType: "base64"; base64: string } + | { type: "video"; sourceType: "base64"; base64: string }; + +interface ImageConfig { + prompt: string; + referenceList?: Extract[]; + size: "1K" | "2K" | "4K"; + aspectRatio: `${number}:${number}`; +} + +interface VideoConfig { + duration: number; + resolution: string; + aspectRatio: "16:9" | "9:16"; + prompt: string; + referenceList?: ReferenceList[]; + audio?: boolean; + mode: VideoMode[]; +} + +interface TTSConfig { + text: string; + voice: string; + speechRate: number; + pitchRate: number; + volume: number; + referenceList?: Extract[]; +} + +interface PollResult { + completed: boolean; + data?: string; + error?: string; +} + +// ============================================================ +// 全局声明 +// ============================================================ + +declare const axios: any; // HTTP请求库 +declare const logger: (msg: string) => void; // 日志函数 +declare const jsonwebtoken: any; // JWT处理库 +declare const zipImage: (base64: string, size: number) => Promise; // 图片压缩函数,返回有头base64字符串 +declare const zipImageResolution: (base64: string, w: number, h: number) => Promise; // 图片分辨率调整函数,返回有头base64字符串 +declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise; // 图片合成函数,返回有头base64字符串 +declare const urlToBase64: (url: string) => Promise; // URL转Base64函数,返回有头base64字符串 +declare const pollTask: (fn: () => Promise, interval?: number, timeout?: number) => Promise; // 轮询函数,fn为异步函数,interval为轮询间隔,timeout为超时时间,返回fn的结果 +declare const createOpenAI: any; +declare const createDeepSeek: any; +declare const createZhipu: any; +declare const createQwen: any; +declare const createAnthropic: any; +declare const createOpenAICompatible: any; +declare const createXai: any; +declare const createMinimax: any; +declare const createGoogleGenerativeAI: any; +declare const exports: { + vendor: VendorConfig; + textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => any; //文本模型 + imageRequest: (c: ImageConfig, m: ImageModel) => Promise; //图片模型,返回有头base64字符串 + videoRequest: (c: VideoConfig, m: VideoModel) => Promise; //视频模型,返回有头base64字符串 + ttsRequest: (c: TTSConfig, m: TTSModel) => Promise; //(暂未开放)语音模型,返回有头base64字符串 + checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>; //检查更新函数,返回是否有更新和最新版本号和更公告(支持Markdown格式) + updateVendor?: () => Promise; //更新函数,返回最新的代码文本 +}; + +// ============================================================ +// 供应商配置 +// ============================================================ + +const vendor: VendorConfig = { + id: "null", + version: "2.0", + author: "Toonflow", + name: "空模板", + description: "## 开发模板,您可以使用此模板进行Vibe Coding", + inputs: [ + { key: "apiKey", label: "API密钥", type: "password", required: true }, + { key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "示例:https://api.openai.com/v1" }, + ], + inputValues: { apiKey: "", baseUrl: "https://api.openai.com/v1" }, + models: [{ name: "GPT-4o", modelName: "gpt-4o", type: "text", think: false }], +}; + +// ============================================================ +// 适配器函数 +// ============================================================ + +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, ""); + return createOpenAI({ baseURL: vendor.inputValues.baseUrl, apiKey }).chat(model.modelName); +}; + +const imageRequest = async (config: ImageConfig, model: ImageModel): Promise => { + return ""; +}; + +const videoRequest = async (config: VideoConfig, model: VideoModel): Promise => { + return ""; +}; + +const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise => { + return ""; +}; + +const checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => { + return { hasUpdate: false, latestVersion: "2.0", notice: "## 新版本更新公告" }; +}; + +const updateVendor = async (): Promise => { + return ""; +}; + +// ============================================================ +// 导出 +// ============================================================ + +exports.vendor = vendor; +exports.textRequest = textRequest; +exports.imageRequest = imageRequest; +exports.videoRequest = videoRequest; +exports.ttsRequest = ttsRequest; +exports.checkForUpdates = checkForUpdates; +exports.updateVendor = updateVendor; + +// 这行代码用于确保当前文件被识别为模块,避免全局变量冲突 +export {}; + +/** + * ============================================================ + * AI 代码生成指南 + * ============================================================ + * + * 【概述】 + * 本文件是 Toonflow AI 供应商适配模板。AI 在生成新供应商适配代码时, + * 必须严格遵守以下规则,并要求用户提供目标平台的 curl 调用示例或 API 文档信息作为输入依据。 + * + * 【前置要求】 + * 在生成代码前,请向用户索要以下信息(至少其一): + * 1. 目标 API 的 curl 请求示例(包含请求地址、Headers、Body 结构、响应结构) + * 2. 目标 API 的官方文档链接或文档截图/文本内容 + * 3. 需要适配的模型类型(text / image / video / tts)及其能力说明 + * 没有足够信息时,应主动追问,不要凭空编造 API 结构。 + * + * 【代码规则】 + * + * 1. 禁止引入任何外部包 + * 不可使用 import / require,仅能使用本文件「全局声明」区域中已声明的方法和对象, + * 包括:axios、logger、jsonwebtoken、zipImage、zipImageResolution、mergeImages、 + * urlToBase64、pollTask,以及 createOpenAI、createDeepSeek、createZhipu、createQwen、 + * createAnthropic、createOpenAICompatible、createXai、createMinimax、 + * createGoogleGenerativeAI 等 AI SDK 工厂函数。 + * + * 2. 禁止在 exports.* 函数外部声明离散的全大写常量 + * 错误示例:const API_URL = "https://..."; const MAX_RETRY = 3; + * 如果确实需要可配置的常量值,必须将其声明在 vendor.inputValues 中, + * 通过 vendor.inputValues.xxx 访问,让用户可在界面上配置。 + * 如果是纯逻辑内部使用的临时变量,应内联在对应的 exports.* 函数体内部,使用小驼峰命名。 + * + * 3. 逻辑尽量聚合在 exports.* 对应的函数内部 + * 每个适配函数(textRequest / imageRequest / videoRequest / ttsRequest) + * 应自包含,将请求构造、发送、轮询、结果解析等逻辑写在函数体内,避免拆分出大量外部辅助函数。 + * 如果多个函数确实存在公共逻辑(如签名计算、Token 生成、请求头构造), + * 可提取为文件内的小驼峰命名函数,放在「适配器函数」区块之前的「辅助工具」区块中, + * 且不可使用全大写命名。 + * + * 4. 命名规范 + * 所有变量、函数一律使用小驼峰命名(camelCase),禁止使用 UPPER_SNAKE_CASE。 + * + * 5. 不需要重新声明类型 + * 本文件顶部已完整定义了所有接口和类型(VendorConfig、ImageConfig、VideoConfig、 + * TTSConfig、TextModel、ImageModel、VideoModel、TTSModel、ReferenceList、PollResult 等), + * AI 生成代码时直接使用即可,不要重复声明。 + * + * 6. 返回值规范 + * - textRequest(model):返回 AI SDK 的 chat model 实例(通过 createOpenAI 等工厂函数创建)。 + * - imageRequest(config, model):返回有头 base64 字符串(如 "data:image/png;base64,...")。 + * config.referenceList 为 Extract[] 类型, + * 每个引用条目均为 base64 形式(sourceType 固定为 "base64")。 + * - videoRequest(config, model):返回有头 base64 字符串(如 "data:video/mp4;base64,...")。 + * config.referenceList 为 ReferenceList[] 类型,可包含 image / video / audio 三种引用, + * 每个引用条目均为 base64 形式(sourceType 固定为 "base64")。 + * config.mode 为当前激活的视频模式数组,需根据 mode 决定如何使用 referenceList。 + * - ttsRequest(config, model):返回有头 base64 字符串(如 "data:audio/mp3;base64,...")。 + * config.referenceList 为 Extract[] 类型(音频参考)。 + * 当 API 返回的是 URL 而非二进制数据时,使用 urlToBase64(url) 转换。 + * + * 7. ReferenceList 与 VideoMode 说明 + * ReferenceList 是统一的多媒体引用类型,每个条目包含: + * - type: "image" | "audio" | "video"(媒体类型) + * - sourceType: "base64"(当前模板固定为 base64) + * - base64(对应的数据) + * + * VideoMode 定义了视频模型支持的输入模式: + * - "text":纯文本生成视频 + * - "singleImage":单张首帧图片 + * - "startEndRequired":首尾帧(两张都必须提供) + * - "endFrameOptional":首尾帧(尾帧可选) + * - "startFrameOptional":首尾帧(首帧可选) + * - 数组形式如 ["imageReference:9", "videoReference:3", "audioReference:3"]: + * 多模态参考模式,数字表示该类型的最大数量限制。 + * + * 在 videoRequest 中,config.mode 表示当前选择的模式,需根据其值决定: + * - 如何从 config.referenceList 中提取对应类型的引用 + * - 如何构造 API 请求体中的图片/视频/音频参数 + * + * 8. 异步任务处理 + * 对于视频生成等需要轮询的异步任务,使用全局的 pollTask 函数: + * const result = await pollTask(async () => { + * const resp = await axios.get(...); + * if (resp.data.status === "SUCCESS") return { completed: true, data: resp.data.url }; + * if (resp.data.status === "FAILED") return { completed: true, error: resp.data.message }; + * return { completed: false }; + * }, 5000, 600000); // 每5秒轮询,10分钟超时 + * if (result.error) throw new Error(result.error); + * return await urlToBase64(result.data!); + * + * 9. 错误处理 + * 在每个函数开头校验必需参数(如 API Key),缺失时使用 throw new Error("...") 抛出。 + * API 请求失败时,从响应中提取有意义的错误信息抛出,不要吞掉异常。 + * + * 10. 日志输出 + * 在关键步骤使用 logger("...") 输出日志(如"开始提交任务"、"任务ID: xxx"、"轮询中..."), + * 便于调试。 + * + * 11. vendor 配置填写 + * - id:纯英文小写,作为文件名使用,禁止特殊符号和空格。 + * - version:语义化版本格式 "x.y"。 + * - inputs:根据目标 API 所需的认证信息配置(API Key、Secret、请求地址等)。 + * - models:根据目标平台支持的模型列表填写,注意正确设置 type 和各模型特有字段。 + * - VideoModel 的 mode 对应 API 支持的输入模式(参见规则 7 的 VideoMode 说明)。 + * - VideoModel 的 audio 字段:true(始终生成音频)、false(不生成)、"optional"(用户可选)。 + * - VideoModel 的 durationResolutionMap 对应各时长下可选的分辨率。 + * - VideoModel 的 associationSkills 可选,用于描述模型的特殊能力。 + * - ImageModel 的 mode 对应 API 支持的生图模式("text" 纯文本、"singleImage" 单图参考、"multiReference" 多图参考)。 + * - TTSModel 的 voices 对应可选的音色列表。 + * + * 12. 图片处理 + * - 需要压缩图片体积时使用 zipImage(base64, maxSizeKB)。 + * - 需要调整图片分辨率时使用 zipImageResolution(base64, width, height)。 + * - 需要将多张图片拼合为一张时使用 mergeImages(base64Arr, maxSize)。 + * - 以上函数均接收和返回有头 base64 字符串。 + * + * 13. 文件结构 + * 生成的代码必须保持本模板的整体结构: + * 类型定义区 → 全局声明区 → 供应商配置区 → [辅助工具区(可选)] → 适配器函数区 → 导出区 + * 不要打乱顺序,不要删除已有的结构注释分隔线。 + * 辅助工具区用于放置多个适配器函数共享的小驼峰命名辅助函数(如 getHeaders、getBaseUrl)。 + * + * 14. 导出规范 + * 必须导出以下字段(通过 exports.xxx = xxx 赋值): + * - exports.vendor(必须) + * - exports.textRequest(必须) + * - exports.imageRequest(必须) + * - exports.videoRequest(必须) + * - exports.ttsRequest(必须) + * - exports.checkForUpdates(可选) + * - exports.updateVendor(可选) + * 未实现的适配器函数保留空实现(return ""),不可省略导出。 + * 文件末尾必须包含 export {}; 以确保文件被识别为模块。 + * + * 【生成流程】 + * 当用户请求生成新的供应商适配时: + * 1. 确认用户已提供 curl 示例或 API 文档。 + * 2. 分析 API 的认证方式、端点地址、请求/响应结构。 + * 3. 基于本模板结构,填充 vendor 配置和对应的适配器函数。 + * 4. 根据当前模板的 ReferenceList 定义,按 base64 形式构造和消费 referenceList。 + * 5. 仅实现用户需要的模型类型,未用到的函数保留空实现(return "")。 + * 6. 生成完整可用的代码,确保无语法错误、无遗漏导出。 + */ diff --git a/data/vendor/openai.ts b/data/vendor/openai.ts new file mode 100644 index 0000000..f0030d0 --- /dev/null +++ b/data/vendor/openai.ts @@ -0,0 +1,169 @@ +/** + * Toonflow AI供应商模板 + * @version 2.0 + */ +// ============================================================ +// 类型定义 +// ============================================================ +type VideoMode = + | "singleImage" + | "startEndRequired" + | "endFrameOptional" + | "startFrameOptional" + | "text" + | (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[]; +interface TextModel { + name: string; + modelName: string; + type: "text"; + think: boolean; +} +interface ImageModel { + name: string; + modelName: string; + type: "image"; + mode: ("text" | "singleImage" | "multiReference")[]; + associationSkills?: string; +} +interface VideoModel { + name: string; + modelName: string; + type: "video"; + mode: VideoMode[]; + associationSkills?: string; + audio: "optional" | false | true; + durationResolutionMap: { duration: number[]; resolution: string[] }[]; +} +interface TTSModel { + name: string; + modelName: string; + type: "tts"; + voices: { title: string; voice: string }[]; +} +interface VendorConfig { + id: string; + version: string; + name: string; + author: string; + description?: string; + icon?: string; + inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[]; + inputValues: Record; + models: (TextModel | ImageModel | VideoModel | TTSModel)[]; +} +interface ImageConfig { + prompt: string; + imageBase64: string[]; + size: "1K" | "2K" | "4K"; + aspectRatio: `${number}:${number}`; +} +interface VideoConfig { + duration: number; + resolution: string; + aspectRatio: "16:9" | "9:16"; + prompt: string; + imageBase64?: string[]; + audio?: boolean; + mode: VideoMode[]; +} +interface TTSConfig { + text: string; + voice: string; + speechRate: number; + pitchRate: number; + volume: number; +} +interface PollResult { + completed: boolean; + data?: string; + error?: string; +} +// ============================================================ +// 全局声明 +// ============================================================ +declare const axios: any; +declare const logger: (msg: string) => void; +declare const jsonwebtoken: any; +declare const zipImage: (base64: string, size: number) => Promise; +declare const zipImageResolution: (base64: string, w: number, h: number) => Promise; +declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise; +declare const urlToBase64: (url: string) => Promise; +declare const pollTask: (fn: () => Promise, interval?: number, timeout?: number) => Promise; +declare const createOpenAI: any; +declare const createDeepSeek: any; +declare const createZhipu: any; +declare const createQwen: any; +declare const createAnthropic: any; +declare const createOpenAICompatible: any; +declare const createXai: any; +declare const createMinimax: any; +declare const createGoogleGenerativeAI: any; +declare const exports: { + vendor: VendorConfig; + textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => any; + imageRequest: (c: ImageConfig, m: ImageModel) => Promise; + videoRequest: (c: VideoConfig, m: VideoModel) => Promise; + ttsRequest: (c: TTSConfig, m: TTSModel) => Promise; + checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>; + updateVendor?: () => Promise; +}; +// ============================================================ +// 供应商配置 +// ============================================================ +const vendor: VendorConfig = { + id: "openai", + version: "2.0", + author: "Toonflow", + name: "OpenAI标准接口", + description: "OpenAI标准格式接口,可修改请求地址并手动添加模型。", + icon: "", + inputs: [ + { key: "apiKey", label: "API密钥", type: "password", required: true }, + { key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "以v1结束,示例:https://api.openai.com/v1" }, + ], + inputValues: { + apiKey: "", + baseUrl: "https://api.openai.com/v1", + }, + models: [ + { name: "GPT-4o", modelName: "gpt-4o", type: "text", think: false }, + { name: "GPT-4.1", modelName: "gpt-4.1", type: "text", think: false }, + { name: "GPT-5.1", modelName: "gpt-5.1", type: "text", think: false }, + { name: "GPT-5.2", modelName: "gpt-5.2", type: "text", think: false }, + { name: "GPT-5.4", modelName: "gpt-5.4", type: "text", think: false }, + ], +}; +// ============================================================ +// 适配器函数 +// ============================================================ +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, ""); + return createOpenAI({ baseURL: vendor.inputValues.baseUrl, apiKey }).chat(model.modelName); +}; +const imageRequest = async (config: ImageConfig, model: ImageModel): Promise => { + return ""; +}; +const videoRequest = async (config: VideoConfig, model: VideoModel): Promise => { + return ""; +}; +const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise => { + return ""; +}; +const checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => { + return { hasUpdate: false, latestVersion: "2.0", notice: "" }; +}; +const updateVendor = async (): Promise => { + return ""; +}; +// ============================================================ +// 导出 +// ============================================================ +exports.vendor = vendor; +exports.textRequest = textRequest; +exports.imageRequest = imageRequest; +exports.videoRequest = videoRequest; +exports.ttsRequest = ttsRequest; +exports.checkForUpdates = checkForUpdates; +exports.updateVendor = updateVendor; +export {}; \ No newline at end of file diff --git a/data/vendor/toonflow.ts b/data/vendor/toonflow.ts new file mode 100644 index 0000000..a002957 --- /dev/null +++ b/data/vendor/toonflow.ts @@ -0,0 +1,522 @@ +/** + * Toonflow官方中转平台 供应商适配 + * @version 2.0 + */ + +// ============================================================ +// 类型定义 +// ============================================================ + +type VideoMode = + | "singleImage" + | "startEndRequired" + | "endFrameOptional" + | "startFrameOptional" + | "text" + | (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[]; + +interface TextModel { + name: string; + modelName: string; + type: "text"; + think: boolean; +} + +interface ImageModel { + name: string; + modelName: string; + type: "image"; + mode: ("text" | "singleImage" | "multiReference")[]; + associationSkills?: string; +} + +interface VideoModel { + name: string; + modelName: string; + type: "video"; + mode: VideoMode[]; + associationSkills?: string; + audio: "optional" | false | true; + durationResolutionMap: { duration: number[]; resolution: string[] }[]; +} + +interface TTSModel { + name: string; + modelName: string; + type: "tts"; + voices: { title: string; voice: string }[]; +} + +interface VendorConfig { + id: string; + version: string; + name: string; + author: string; + description?: string; + icon?: string; + inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[]; + inputValues: Record; + models: (TextModel | ImageModel | VideoModel | TTSModel)[]; +} + +type ReferenceList = + | { type: "image"; sourceType: "base64"; base64: string } + | { type: "audio"; sourceType: "base64"; base64: string } + | { type: "video"; sourceType: "base64"; base64: string }; + +interface ImageConfig { + prompt: string; + referenceList?: Extract[]; + size: "1K" | "2K" | "4K"; + aspectRatio: `${number}:${number}`; +} + +interface VideoConfig { + duration: number; + resolution: string; + aspectRatio: "16:9" | "9:16"; + prompt: string; + referenceList?: ReferenceList[]; + audio?: boolean; + mode: VideoMode[]; +} + +interface TTSConfig { + text: string; + voice: string; + speechRate: number; + pitchRate: number; + volume: number; + referenceList?: Extract[]; +} + +interface PollResult { + completed: boolean; + data?: string; + error?: string; +} + +// ============================================================ +// 全局声明 +// ============================================================ + +declare const axios: any; +declare const logger: (msg: string) => void; +declare const jsonwebtoken: any; +declare const zipImage: (base64: string, size: number) => Promise; +declare const zipImageResolution: (base64: string, w: number, h: number) => Promise; +declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise; +declare const urlToBase64: (url: string) => Promise; +declare const pollTask: (fn: () => Promise, interval?: number, timeout?: number) => Promise; +declare const createOpenAI: any; +declare const createDeepSeek: any; +declare const createZhipu: any; +declare const createQwen: any; +declare const createAnthropic: any; +declare const createOpenAICompatible: any; +declare const createXai: any; +declare const createMinimax: any; +declare const createGoogleGenerativeAI: any; +declare const exports: { + vendor: VendorConfig; + textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => any; + imageRequest: (c: ImageConfig, m: ImageModel) => Promise; + videoRequest: (c: VideoConfig, m: VideoModel) => Promise; + ttsRequest: (c: TTSConfig, m: TTSModel) => Promise; + checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>; + updateVendor?: () => Promise; +}; + +// ============================================================ +// 供应商配置 +// ============================================================ + +const vendor: VendorConfig = { + id: "toonflow", + version: "2.0", + author: "Toonflow", + name: "Toonflow官方中转平台", + description: + "## Toonflow官方中转平台\n\nToonflow官方中转平台,提供**文本、图像、视频、音频**等多模态生成能力的中转服务,支持接入多个大模型供应商,方便用户统一管理和调用不同供应商的生成能力。\n\n🔗 [前往中转平台](https://api.toonflow.net/)\n\n如果这个项目对你有帮助,可以考虑支持一下我们的开发工作 ☕", + icon: "", + inputs: [{ key: "apiKey", label: "API密钥", type: "password", required: true }], + inputValues: { + apiKey: "", + baseUrl: "https://api.toonflow.net/v1", + }, + models: [ + { name: "claude-sonnet-4-6", type: "text", modelName: "claude-sonnet-4-6", think: false }, + { name: "claude-opus-4-6", type: "text", modelName: "claude-opus-4-6", think: false }, + { name: "claude-sonnet-4-5-20250929", type: "text", modelName: "claude-sonnet-4-5-20250929", think: false }, + { name: "claude-opus-4-5-20251101", type: "text", modelName: "claude-opus-4-5-20251101", think: false }, + { name: "claude-haiku-4-5-20251001", type: "text", modelName: "claude-haiku-4-5-20251001", think: false }, + { name: "gpt-5.4", type: "text", modelName: "gpt-5.4", think: false }, + { name: "gpt-5.2", type: "text", modelName: "gpt-5.2", think: false }, + { name: "MiniMax-M2.7", type: "text", modelName: "MiniMax-M2.7", think: true }, + { name: "MiniMax-M2.5", type: "text", modelName: "MiniMax-M2.5", think: true }, + { + name: "Wan2.6 I2V 1080P (支持真人)", + type: "video", + modelName: "Wan2.6-I2V-1080P", + mode: ["text", "startEndRequired"], + durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["1080p"] }], + audio: true, + }, + { + name: "Wan2.6 I2V 720P (支持真人)", + type: "video", + modelName: "Wan2.6-I2V-720P", + mode: ["text", "startEndRequired"], + durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["720p"] }], + audio: true, + }, + { + name: "Seedance 1.5 Pro", + type: "video", + modelName: "doubao-seedance-1-5-pro-251215", + mode: ["text", "endFrameOptional"], + durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }], + audio: true, + }, + { + name: "vidu2 turbo", + type: "video", + modelName: "ViduQ2-turbo", + mode: ["singleImage", "startEndRequired"], + durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["540p", "720p", "1080p"] }], + audio: false, + }, + { + name: "ViduQ3 pro", + type: "video", + modelName: "ViduQ3-pro", + mode: ["singleImage", "startEndRequired"], + durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], resolution: ["540p", "720p", "1080p"] }], + audio: false, + }, + { + name: "ViduQ2 pro", + type: "video", + modelName: "ViduQ2-pro", + mode: ["singleImage", "startEndRequired"], + durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["540p", "720p", "1080p"] }], + audio: false, + }, + { + name: "Doubao Seedream 5.0 Lite", + type: "image", + modelName: "Doubao-Seedream-5.0-Lite", + mode: ["text", "singleImage", "multiReference"], + }, + { + name: "Doubao Seedream 4.5", + type: "image", + modelName: "doubao-seedream-4-5-251128", + mode: ["text", "singleImage", "multiReference"], + }, + ], +}; + +// ============================================================ +// 辅助工具 +// ============================================================ + +// 从 markdown 内容中提取第一张图片 +function extractFirstImageFromMd(content: string) { + const regex = /!\[([^\]]*)\]\((data:image\/[^;]+;base64,[A-Za-z0-9+/=]+|https?:\/\/[^\s)]+|\/\/[^\s)]+|[^\s)]+)\)/; + const match = content.match(regex); + if (!match) return null; + const raw = match[2].trim(); + const url = raw.startsWith("data:") ? raw : raw.split(/\s+/)[0]; + return { alt: match[1], url, type: url.startsWith("data:image") ? "base64" : "url" }; +} + +// ============================================================ +// 适配器函数 +// ============================================================ + +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, ""); + return createOpenAI({ baseURL: vendor.inputValues.baseUrl, apiKey }).chat(model.modelName); +}; + +const imageRequest = async (config: ImageConfig, model: ImageModel): Promise => { + if (!vendor.inputValues.apiKey) throw new Error("缺少API Key"); + const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, ""); + const baseUrl = vendor.inputValues.baseUrl; + const lowerName = model.modelName.toLowerCase(); + const imageBase64List = (config.referenceList ?? []).map((r) => r.base64); + + // Gemini / nano 系模型:走 chat/completions 接口,从返回的 markdown 中提取图片 + if (lowerName.includes("gemini") || lowerName.includes("nano")) { + const imageConfigGoogle: Record = { + aspect_ratio: config.aspectRatio, + image_size: config.size, + }; + const messages: any[] = []; + if (imageBase64List.length) { + messages.push({ + role: "user", + content: imageBase64List.map((b) => ({ type: "image_url", image_url: { url: b } })), + }); + } + messages.push({ role: "user", content: config.prompt + "请直接输出图片" }); + const body = { + model: model.modelName, + messages, + extra_body: { google: { image_config: imageConfigGoogle } }, + }; + logger(`[imageRequest] 使用 gemini 适配器,模型: ${model.modelName}`); + const response = await fetch(`${baseUrl}/chat/completions`, { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`); + } + const data = await response.json(); + const imageResult = extractFirstImageFromMd(data.choices[0].message.content); + if (!imageResult) throw new Error("未能从响应中提取图片"); + if (imageResult.type === "base64") return imageResult.url; + return await urlToBase64(imageResult.url); + } + + // 豆包 / seedream 系模型:走 images/generations 接口 + if (lowerName.includes("doubao") || lowerName.includes("seedream")) { + const effectiveSize = config.size === "1K" ? "2K" : config.size; + const sizeMap: Record> = { + "16:9": { "2K": "2848x1600", "4K": "4096x2304" }, + "9:16": { "2K": "1600x2848", "4K": "2304x4096" }, + }; + const resolvedSize = sizeMap[config.aspectRatio]?.[effectiveSize]; + const body: Record = { + model: model.modelName, + prompt: config.prompt, + size: resolvedSize, + response_format: "url", + sequential_image_generation: "disabled", + stream: false, + watermark: false, + ...(imageBase64List.length && { image: imageBase64List }), + }; + logger(`[imageRequest] 使用 doubao 适配器,模型: ${model.modelName}`); + const response = await fetch(`${baseUrl}/images/generations`, { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`); + } + const data = await response.json(); + const resultUrl = data.data[0].url; + return await urlToBase64(resultUrl); + } + + throw new Error(`不支持的图像模型: ${model.modelName}`); +}; + +const videoRequest = async (config: VideoConfig, model: VideoModel): Promise => { + if (!vendor.inputValues.apiKey) throw new Error("缺少API Key"); + const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, ""); + const baseUrl = vendor.inputValues.baseUrl; + const lowerName = model.modelName.toLowerCase(); + + // 当前激活的单一 VideoMode(取第一个非数组模式,或数组模式) + const activeMode = config.mode[0]; + const imageRefs = (config.referenceList ?? []).filter((r) => r.type === "image").map((r) => r.base64); + const videoRefs = (config.referenceList ?? []).filter((r) => r.type === "video").map((r) => r.base64); + const audioRefs = (config.referenceList ?? []).filter((r) => r.type === "audio").map((r) => r.base64); + + // 构建模型专属 metadata + let metadata: Record = {}; + + if (lowerName.includes("wan")) { + // 万象系列 + if ( + (activeMode === "startEndRequired" || activeMode === "endFrameOptional" || activeMode === "startFrameOptional") && + imageRefs.length >= 2 + ) { + if (imageRefs[0]) metadata.first_frame_url = imageRefs[0]; + if (imageRefs[1]) metadata.last_frame_url = imageRefs[1]; + } else if (imageRefs.length) { + metadata.img_url = imageRefs[0]; + } + if (typeof config.audio === "boolean") metadata.audio = config.audio; + + // 万象需要额外传 size 字段 + const wanSizeMap: Record> = { + "480p": { "16:9": "832*480", "9:16": "480*832" }, + "720p": { "16:9": "1280*720", "9:16": "720*1280" }, + "1080p": { "16:9": "1920*1080", "9:16": "1080*1920" }, + }; + const wanSize = wanSizeMap[config.resolution]?.[config.aspectRatio]; + const body: Record = { + model: model.modelName, + prompt: config.prompt, + duration: config.duration, + size: wanSize, + metadata, + }; + logger(`[videoRequest] 提交万象视频任务,模型: ${model.modelName}`); + const response = await fetch(`${baseUrl}/video/generations`, { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`); + } + const data = await response.json(); + const taskId = data.id; + logger(`[videoRequest] 万象任务ID: ${taskId}`); + const res = await pollTask(async () => { + const queryResponse = await fetch(`${baseUrl}/video/generations/${taskId}`, { + method: "GET", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + }); + if (!queryResponse.ok) { + const errorText = await queryResponse.text(); + throw new Error(`轮询失败,状态码: ${queryResponse.status}, 错误信息: ${errorText}`); + } + const queryData = await queryResponse.json(); + const status = queryData?.status ?? queryData?.data?.status; + switch (status) { + case "completed": + case "SUCCESS": + case "success": + return { completed: true, data: queryData.data.result_url }; + case "FAILURE": + case "failed": + return { completed: true, error: queryData?.data?.fail_reason ?? "视频生成失败" }; + default: + return { completed: false }; + } + }); + if (res.error) throw new Error(res.error); + return await urlToBase64(res.data!); + } + + if (lowerName.includes("doubao") || lowerName.includes("seedance")) { + // 豆包/Seedance 系列 + metadata = { + ...(typeof config.audio === "boolean" && { generate_audio: config.audio }), + ratio: config.aspectRatio, + image_roles: [] as string[], + references: [] as string[], + }; + if (Array.isArray(activeMode)) { + // 多参考模式 + imageRefs.forEach((b) => metadata.references.push(b)); + videoRefs.forEach((b) => metadata.references.push(b)); + audioRefs.forEach((b) => metadata.references.push(b)); + } else if (activeMode === "startEndRequired" || activeMode === "endFrameOptional" || activeMode === "startFrameOptional") { + imageRefs.forEach((_, i) => (metadata.image_roles as string[]).push(i === 0 ? "first_frame" : "last_frame")); + } else if (activeMode === "singleImage") { + imageRefs.forEach(() => (metadata.image_roles as string[]).push("reference_image")); + } + } else if (lowerName.includes("vidu")) { + // Vidu 系列 + metadata = { + aspect_ratio: config.aspectRatio, + audio: config.audio ?? false, + off_peak: false, + }; + } else if (lowerName.includes("kling")) { + // 可灵系列 + metadata = { aspect_ratio: config.aspectRatio }; + if (Array.isArray(activeMode)) { + metadata.reference = [...imageRefs, ...videoRefs, ...audioRefs]; + } else if (activeMode === "endFrameOptional" && imageRefs.length) { + metadata.image_tail = imageRefs[0]; + } else if (activeMode === "startEndRequired" && imageRefs.length >= 2) { + metadata.image_list = [ + { image_url: imageRefs[0], type: "first_frame" }, + { image_url: imageRefs[1], type: "last_frame" }, + ]; + } else if (activeMode === "singleImage" && imageRefs.length) { + metadata.image = imageRefs[0]; + } + } + + // 公共请求体(非万象通用路径) + const publicBody: Record = { + model: model.modelName, + ...(!Array.isArray(activeMode) && imageRefs.length ? { images: imageRefs } : {}), + prompt: config.prompt, + duration: config.duration, + metadata, + }; + + logger(`[videoRequest] 提交视频任务,模型: ${model.modelName}`); + const response = await fetch(`${baseUrl}/video/generations`, { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify(publicBody), + }); + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`); + } + const data = await response.json(); + const taskId = data.id; + logger(`[videoRequest] 任务ID: ${taskId}`); + + const res = await pollTask(async () => { + const queryResponse = await fetch(`${baseUrl}/video/generations/${taskId}`, { + method: "GET", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + }); + if (!queryResponse.ok) { + const errorText = await queryResponse.text(); + throw new Error(`轮询失败,状态码: ${queryResponse.status}, 错误信息: ${errorText}`); + } + const queryData = await queryResponse.json(); + const status = queryData?.status ?? queryData?.data?.status; + switch (status) { + case "completed": + case "SUCCESS": + case "success": + return { completed: true, data: queryData.data.result_url }; + case "FAILURE": + case "failed": + return { completed: true, error: queryData?.data?.fail_reason ?? "视频生成失败" }; + default: + return { completed: false }; + } + }); + + if (res.error) throw new Error(res.error); + return await urlToBase64(res.data!); +}; + +const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise => { + return ""; +}; + +const checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => { + return { hasUpdate: false, latestVersion: "2.0", notice: "" }; +}; + +const updateVendor = async (): Promise => { + return ""; +}; + +// ============================================================ +// 导出 +// ============================================================ + +exports.vendor = vendor; +exports.textRequest = textRequest; +exports.imageRequest = imageRequest; +exports.videoRequest = videoRequest; +exports.ttsRequest = ttsRequest; +exports.checkForUpdates = checkForUpdates; +exports.updateVendor = updateVendor; + +export {}; \ No newline at end of file diff --git a/data/vendor/vidu.ts b/data/vendor/vidu.ts new file mode 100644 index 0000000..e4395f4 --- /dev/null +++ b/data/vendor/vidu.ts @@ -0,0 +1,368 @@ +//如需遥测AI请使用在toonflow安装目录运行npx @ai-sdk/devtools (要求在其他设置中打开遥测功能,且toonflow有权限在安装目录创建.devtools文件夹) +// ==================== 类型定义 ==================== +// 文本模型 +interface TextModel { + name: string; // 显示名称 + modelName: string; + type: "text"; + think: boolean; // 前端显示用 +} + +// 图像模型 +interface ImageModel { + name: string; // 显示名称 + modelName: string; + type: "image"; + mode: ("text" | "singleImage" | "multiReference")[]; + associationSkills?: string; // 关联技能,多个技能用逗号分隔 +} +// 视频模型 +interface VideoModel { + name: string; // 显示名称 + modelName: string; //全局唯一 + type: "video"; + mode: ( + | "singleImage" // 单图 + | "startEndRequired" // 首尾帧(两张都得有) + | "endFrameOptional" // 首尾帧(尾帧可选) + | "startFrameOptional" // 首尾帧(首帧可选) + | "text" // 文本生视频 + | ("videoReference" | "imageReference" | "audioReference" | "textReference")[] // 混合参考 + )[]; + associationSkills?: string; // 关联技能,多个技能用逗号分隔 + audio: "optional" | false | true; // 音频配置 + durationResolutionMap: { duration: number[]; resolution: string[] }[]; +} + +interface TTSModel { + name: string; // 显示名称 + modelName: string; + type: "tts"; + voices: { + title: string; //显示名称 + voice: string; //说话人 + }[]; +} +// 供应商配置 +interface VendorConfig { + id: string; //供应商唯一标识,必须全局唯一 + author: string; + description?: string; //md5格式 + name: string; + icon?: string; //仅支持base64格式 + inputs: { + key: string; + label: string; + type: "text" | "password" | "url"; + required: boolean; + placeholder?: string; + }[]; + inputValues: Record; + models: (TextModel | ImageModel | VideoModel)[]; +} +// ==================== 全局工具函数 ==================== +//Axios实例 +//压缩图片大小(1MB = 1 * 1024 * 1024) +declare const zipImage: (completeBase64: string, size: number) => Promise; +//压缩图片分辨率 +declare const zipImageResolution: (completeBase64: string, width: number, height: number) => Promise; +//多图拼接乘单图 maxSize 最大输出大小,默认为 10mb +declare const mergeImages: (completeBase64: string[], maxSize?: string) => Promise; +//Url转Base64 +declare const urlToBase64: (url: string) => Promise; +//轮询函数 +declare const pollTask: ( + fn: () => Promise<{ completed: boolean; data?: string; error?: string }>, + interval?: number, + timeout?: number, +) => Promise<{ completed: boolean; data?: string; error?: string }>; +declare const axios: any; +declare const createOpenAI: any; +declare const createDeepSeek: any; +declare const createZhipu: any; +declare const createQwen: any; +declare const createAnthropic: any; +declare const createOpenAICompatible: any; +declare const createXai: any; +declare const createMinimax: any; +declare const createGoogleGenerativeAI: any; +declare const logger: (logstring: string) => void; +declare const jsonwebtoken: any; +// ==================== 供应商数据 ==================== +const vendor: VendorConfig = { + id: "vidu", + author: "搬砖的Coder", + description: + "Vidu 官方视频生成平台。 [前往平台](https://platform.vidu.cn/login/)", + name: "Vidu 开放平台", + inputs: [ + { key: "apiKey", label: "API密钥", type: "password", required: true, placeholder: "请到Vidu官方申请" }, + { key: "baseUrl", label: "接口路径", type: "url", required: true, placeholder: "https://api.vidu.cn/ent/v2" }, + ], + inputValues: { + apiKey: "", + baseUrl: "https://api.vidu.cn/ent/v2", + }, + models: [ + { + name: "ViduQ3 turbo", + type: "video", + modelName: "ViduQ3-turbo", + durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], resolution: ["540p", "720p", "1080p"] }], + mode: ["singleImage", "startEndRequired", "text"], + audio: true, + }, + { + name: "ViduQ3 pro", + type: "video", + modelName: "ViduQ3-pro", + durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], resolution: ["540p", "720p", "1080p"] }], + mode: ["singleImage", "startEndRequired", "text"], + audio: true, + }, + { + name: "ViduQ2 pro fast", + type: "video", + modelName: "ViduQ2-pro-fast", + durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["720p", "1080p"] }], + mode: ["singleImage", "startEndRequired"], + audio: true, + }, + { + name: "viduQ2 turbo", + type: "video", + modelName: "ViduQ2-turbo", + durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["540p", "720p", "1080p"] }], + mode: ["singleImage", "startEndRequired"], + audio: true, + }, + { + name: "ViduQ2 pro", + type: "video", + modelName: "ViduQ2-pro", + durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["540p", "720p", "1080p"] }], + mode: ["singleImage", "startEndRequired"], //参考生视频无有效设置值 + audio: true, + }, + { + name: "ViduQ2", + type: "video", + modelName: "ViduQ2", + durationResolutionMap: [{ duration: [5], resolution: ["1080p"] }], + mode: ["text"], + audio: true, + }, + { + name: "ViduQ1", + type: "video", + modelName: "ViduQ1", + durationResolutionMap: [{ duration: [5], resolution: ["1080p"] }], + mode: ["singleImage", "startEndRequired", "text"], + audio: true, + }, + { + name: "ViduQ1 classic", + type: "video", + modelName: "viduQ1-classic", + durationResolutionMap: [{ duration: [5], resolution: ["1080p"] }], + mode: ["singleImage", "startEndRequired"], + audio: true, + }, + { + name: "Vidu2.0", + type: "video", + modelName: "vidu2.0", + durationResolutionMap: [{ duration: [4, 8], resolution: ["360p", "720p", "1080p"] }], + mode: ["singleImage", "startEndRequired"], + audio: true, + }, + { + name: "viduq1 for image", + type: "image", + modelName: "viduq1", + mode: ["text"], + }, + { + name: "viduq2 for image", + type: "image", + modelName: "viduq2", + mode: ["text", "singleImage", "multiReference"], + }, + ], +}; +exports.vendor = vendor; + +// ==================== 适配器函数 ==================== + +// 文本请求函数 +const textRequest: (textModel: TextModel) => { url: string; model: string } = (textModel) => { + throw new Error("当前供应商仅支持视频大模型,谢谢!"); +}; +exports.textRequest = textRequest; + +//图片请求函数 +interface ImageConfig { + prompt: string; //图片提示词 + imageBase64: string[]; //输入的图片提示词 + size: "1K" | "2K" | "4K"; // 图片尺寸 + aspectRatio: `${number}:${number}`; // 长宽比 +} +const imageRequest = async (imageConfig: ImageConfig, imageModel: ImageModel) => { + if (!vendor.inputValues.apiKey) throw new Error("缺少API Key"); + const apiKey = vendor.inputValues.apiKey.replace("Token ", ""); + + const size = imageConfig.size === "1K" ? "2K" : imageConfig.size; + const sizeMap: Record> = { + "16:9": { + "1k": "1920x1080", + "2K": "2848x1600", + "4K": "4096x2304", + }, + "9:16": { + "1k": "1920x1080", + "2K": "1600x2848", + "4K": "2304x4096", + }, + }; + + const body: Record = { + model: imageModel.modelName, + prompt: imageConfig.prompt, + aspect_ratio: sizeMap[imageConfig.aspectRatio][size], + seed: 0, + resolution: size, + ...(imageConfig.imageBase64 && { image: imageConfig.imageBase64 }), + }; + + const createImageUrl = vendor.inputValues.baseUrl + "/reference2image"; + const response = await fetch(createImageUrl, { + method: "POST", + headers: { Authorization: `Token ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!response.ok) { + const errorText = await response.text(); // 获取错误信息 + console.error("请求失败,状态码:", response.status, ", 错误信息:", errorText); + throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`); + } + const data = await response.json(); + const res = await checkTaskResult(data.task_id); + if (!res.data) { + throw new Error("图片未能生成"); + } + const list = JSON.parse(JSON.stringify(res.data)); + return list[0].url; +}; +exports.imageRequest = imageRequest; + +interface VideoConfig { + duration: number; + resolution: string; + aspectRatio: "16:9" | "9:16"; + prompt: string; + imageBase64?: string[]; + audio?: boolean; + mode: + | "singleImage" // 单图 + | "multiImage" // 多图模式 + | "gridImage" // 网格单图(传入一张图片,但该图片是网格图) + | "startEndRequired" // 首尾帧(两张都得有) + | "endFrameOptional" // 首尾帧(尾帧可选) + | "startFrameOptional" // 首尾帧(首帧可选) + | "text" // 文本生视频 + | ("video" | "image" | "audio" | "text")[]; // 混合参考 +} + +// 构建 各个平台的metadata参数 + +const buildViduMetadata = (videoConfig: VideoConfig) => ({ + aspect_ratio: videoConfig.aspectRatio, + audio: videoConfig.audio ?? false, + off_peak: false, +}); + +type MetadataBuilder = (config: VideoConfig) => Record; +const METADATA_BUILDERS: Array<[string, MetadataBuilder]> = [["vidu", buildViduMetadata]]; +const buildModelMetadata = (modelName: string, videoConfig: VideoConfig) => { + const lowerName = modelName.toLowerCase(); + const match = METADATA_BUILDERS.find(([key]) => lowerName.includes(key)); + return match ? match[1](videoConfig) : {}; +}; +// 检查生成物结果 +const checkTaskResult = async (taskId: string) => { + const queryUrl = vendor.inputValues.baseUrl + "/tasks/{id}/creations"; + const apiKey = vendor.inputValues.apiKey; + const res = await pollTask(async () => { + const queryResponse = await fetch(queryUrl.replace("{id}", taskId), { + method: "GET", + headers: { Authorization: `Token ${apiKey}`, "Content-Type": "application/json" }, + }); + if (!queryResponse.ok) { + const errorText = await queryResponse.text(); // 获取错误信息 + console.error("请求失败,状态码:", queryResponse.status, ", 错误信息:", errorText); + throw new Error(`请求失败,状态码: ${queryResponse.status}, 错误信息: ${errorText}`); + } + const queryData = await queryResponse.json(); + const status = queryData?.state ?? queryData?.data?.state; + const fail_reason = queryData?.data?.err_code ?? queryData?.data; + switch (status) { + case "completed": + case "SUCCESS": + case "success": + return { completed: true, data: queryData.creations }; + case "FAILURE": + case "failed": + return { completed: false, error: fail_reason || "生成失败" }; + default: + return { completed: false }; + } + }); + if (res.error) throw new Error(res.error); + return res; +}; + +const videoRequest = async (videoConfig: VideoConfig, videoModel: VideoModel) => { + if (!vendor.inputValues.apiKey) throw new Error("缺少API Key"); + const apiKey = vendor.inputValues.apiKey.replace("Token ", ""); + + // 构建每个模型对应的附加参数 + const metadata = buildModelMetadata(videoModel.modelName, videoConfig); + + //公共请求参数 + const publicBody = { + model: videoModel.modelName, + ...(videoConfig.imageBase64 && videoConfig.imageBase64.length ? { images: videoConfig.imageBase64 } : {}), + prompt: videoConfig.prompt, + size: videoConfig.resolution, + duration: videoConfig.duration, + metadata: metadata, + }; + + const requestUrl = vendor.inputValues.baseUrl + "/start-end2video"; + const response = await fetch(requestUrl, { + method: "POST", + headers: { Authorization: `Token ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify(publicBody), + }); + if (!response.ok) { + const errorText = await response.text(); // 获取错误信息 + console.error("请求失败,状态码:", response.status, ", 错误信息:", errorText); + throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`); + } + const data = await response.json(); + const taskId = data.id; + const result = await checkTaskResult(taskId); + return result.data; +}; +exports.videoRequest = videoRequest; + +interface TTSConfig { + text: string; + voice: string; + speechRate: number; + pitchRate: number; + volume: number; +} +const ttsRequest = async (ttsConfig: TTSConfig, ttsModel: TTSModel) => { + throw new Error("Vidu 暂不支持语音合成(TTS)"); +}; diff --git a/data/vendor/volcengine.ts b/data/vendor/volcengine.ts new file mode 100644 index 0000000..718f638 --- /dev/null +++ b/data/vendor/volcengine.ts @@ -0,0 +1,578 @@ +/** + * Toonflow AI供应商模板 - 火山引擎(豆包) + * @version 2.0 + */ + +// ============================================================ +// 类型定义 +// ============================================================ + +type VideoMode = + | "singleImage" + | "startEndRequired" + | "endFrameOptional" + | "startFrameOptional" + | "text" + | (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[]; + +interface TextModel { + name: string; + modelName: string; + type: "text"; + think: boolean; +} + +interface ImageModel { + name: string; + modelName: string; + type: "image"; + mode: ("text" | "singleImage" | "multiReference")[]; + associationSkills?: string; +} + +interface VideoModel { + name: string; + modelName: string; + type: "video"; + mode: VideoMode[]; + associationSkills?: string; + audio: "optional" | false | true; + durationResolutionMap: { duration: number[]; resolution: string[] }[]; +} + +interface TTSModel { + name: string; + modelName: string; + type: "tts"; + voices: { title: string; voice: string }[]; +} + +interface VendorConfig { + id: string; + version: string; + name: string; + author: string; + description?: string; + icon?: string; + inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[]; + inputValues: Record; + models: (TextModel | ImageModel | VideoModel | TTSModel)[]; +} + +type ReferenceList = + | { type: "image"; sourceType: "base64"; base64: string } + | { type: "audio"; sourceType: "base64"; base64: string } + | { type: "video"; sourceType: "base64"; base64: string }; + +interface ImageConfig { + prompt: string; + referenceList?: Extract[]; + size: "1K" | "2K" | "4K"; + aspectRatio: `${number}:${number}`; +} + +interface VideoConfig { + duration: number; + resolution: string; + aspectRatio: "16:9" | "9:16"; + prompt: string; + referenceList?: ReferenceList[]; + audio?: boolean; + mode: VideoMode[]; +} + +interface TTSConfig { + text: string; + voice: string; + speechRate: number; + pitchRate: number; + volume: number; + referenceList?: Extract[]; +} + +interface PollResult { + completed: boolean; + data?: string; + error?: string; +} + +// ============================================================ +// 全局声明 +// ============================================================ + +declare const axios: any; +declare const logger: (msg: string) => void; +declare const jsonwebtoken: any; +declare const zipImage: (base64: string, size: number) => Promise; +declare const zipImageResolution: (base64: string, w: number, h: number) => Promise; +declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise; +declare const urlToBase64: (url: string) => Promise; +declare const pollTask: (fn: () => Promise, interval?: number, timeout?: number) => Promise; +declare const createOpenAI: any; +declare const createDeepSeek: any; +declare const createZhipu: any; +declare const createQwen: any; +declare const createAnthropic: any; +declare const createOpenAICompatible: any; +declare const createXai: any; +declare const createMinimax: any; +declare const createGoogleGenerativeAI: any; +declare const exports: { + vendor: VendorConfig; + textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => any; + imageRequest: (c: ImageConfig, m: ImageModel) => Promise; + videoRequest: (c: VideoConfig, m: VideoModel) => Promise; + ttsRequest: (c: TTSConfig, m: TTSModel) => Promise; + checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>; + updateVendor?: () => Promise; +}; + +// ============================================================ +// 供应商配置 +// ============================================================ + +const vendor: VendorConfig = { + id: "volcengine", + version: "2.0", + author: "leeqi", + name: "火山引擎(豆包)", + description: + "火山引擎豆包大模型,支持文本、图片生成、视频生成等能力。\n\n需要在[火山引擎控制台](https://console.volcengine.com/ark)获取API密钥。", + icon: "", + inputs: [ + { key: "apiKey", label: "API密钥", type: "password", required: true, placeholder: "火山引擎API Key" }, + { key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "以v3结束,示例:https://ark.cn-beijing.volces.com/api/v3" }, + ], + inputValues: { + apiKey: "", + baseUrl: "https://ark.cn-beijing.volces.com/api/v3", + }, + models: [ + // ===================== 文本模型 - 推荐 ===================== + { name: "Doubao-Seed-2.0-Pro", modelName: "doubao-seed-2-0-pro-260215", type: "text", think: true }, + { name: "Doubao-Seed-2.0-Lite", modelName: "doubao-seed-2-0-lite-260215", type: "text", think: true }, + { name: "Doubao-Seed-2.0-Mini", modelName: "doubao-seed-2-0-mini-260215", type: "text", think: true }, + { name: "Doubao-Seed-2.0-Code-Preview", modelName: "doubao-seed-2-0-code-preview-260215", type: "text", think: true }, + { name: "Doubao-Seed-Character", modelName: "doubao-seed-character-251128", type: "text", think: false }, + // ===================== 文本模型 - 往期 ===================== + { name: "Doubao-Seed-1.8", modelName: "doubao-seed-1-8-251228", type: "text", think: true }, + { name: "Doubao-Seed-Code-Preview", modelName: "doubao-seed-code-preview-251028", type: "text", think: true }, + { name: "Doubao-Seed-1.6-Lite", modelName: "doubao-seed-1-6-lite-251015", type: "text", think: true }, + { name: "Doubao-Seed-1.6-Flash(0828)", modelName: "doubao-seed-1-6-flash-250828", type: "text", think: true }, + { name: "Doubao-Seed-1.6-Vision", modelName: "doubao-seed-1-6-vision-250815", type: "text", think: true }, + { name: "Doubao-Seed-1.6(1015)", modelName: "doubao-seed-1-6-251015", type: "text", think: true }, + { name: "Doubao-Seed-1.6(0615)", modelName: "doubao-seed-1-6-250615", type: "text", think: true }, + { name: "Doubao-Seed-1.6-Flash(0615)", modelName: "doubao-seed-1-6-flash-250615", type: "text", think: true }, + { name: "Doubao-Seed-Translation", modelName: "doubao-seed-translation-250915", type: "text", think: false }, + { name: "Doubao-1.5-Pro-32K", modelName: "doubao-1-5-pro-32k-250115", type: "text", think: false }, + { name: "Doubao-1.5-Pro-32K-Character(0715)", modelName: "doubao-1-5-pro-32k-character-250715", type: "text", think: false }, + { name: "Doubao-1.5-Pro-32K-Character(0228)", modelName: "doubao-1-5-pro-32k-character-250228", type: "text", think: false }, + { name: "Doubao-1.5-Lite-32K", modelName: "doubao-1-5-lite-32k-250115", type: "text", think: false }, + { name: "Doubao-1.5-Vision-Pro-32K", modelName: "doubao-1-5-vision-pro-32k-250115", type: "text", think: false }, + // ===================== 文本模型 - 第三方(火山引擎托管) ===================== + { name: "GLM-4-7", modelName: "glm-4-7-251222", type: "text", think: true }, + { name: "DeepSeek-V3-2", modelName: "deepseek-v3-2-251201", type: "text", think: true }, + { name: "DeepSeek-V3-1-Terminus", modelName: "deepseek-v3-1-terminus", type: "text", think: true }, + { name: "DeepSeek-V3(0324)", modelName: "deepseek-v3-250324", type: "text", think: false }, + { name: "DeepSeek-R1(0528)", modelName: "deepseek-r1-250528", type: "text", think: true }, + { name: "Qwen3-32B", modelName: "qwen3-32b-20250429", type: "text", think: false }, + { name: "Qwen3-14B", modelName: "qwen3-14b-20250429", type: "text", think: false }, + { name: "Qwen3-8B", modelName: "qwen3-8b-20250429", type: "text", think: false }, + { name: "Qwen3-0.6B", modelName: "qwen3-0-6b-20250429", type: "text", think: false }, + { name: "Qwen2.5-72B", modelName: "qwen2-5-72b-20240919", type: "text", think: false }, + { name: "GLM-4.5-Air", modelName: "glm-4-5-air", type: "text", think: false }, + // ===================== 图片生成模型 ===================== + { + name: "Seedream-5.0", + modelName: "doubao-seedream-5-0-260128", + type: "image", + mode: ["text", "singleImage", "multiReference"], + }, + { + name: "Seedream-5.0-Lite", + modelName: "doubao-seedream-5-0-lite-260128", + type: "image", + mode: ["text", "singleImage", "multiReference"], + }, + { + name: "Seedream-4.5", + modelName: "doubao-seedream-4-5-251128", + type: "image", + mode: ["text", "singleImage", "multiReference"], + }, + { + name: "Seedream-4.0", + modelName: "doubao-seedream-4-0-250828", + type: "image", + mode: ["text", "singleImage", "multiReference"], + }, + { + name: "Seedream-3.0-T2I", + modelName: "doubao-seedream-3-0-t2i-250415", + type: "image", + mode: ["text"], + }, + // ===================== 视频生成模型 ===================== + { + name: "Seedance-2.0(音画同生)", + modelName: "doubao-seedance-2-0-260128", + type: "video", + mode: ["text", "startFrameOptional", ["imageReference:9", "videoReference:3", "audioReference:3"]], + audio: "optional", + durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["480p", "720p"] }], + }, + { + name: "Seedance-2.0-Fast(音画同生)", + modelName: "doubao-seedance-2-0-fast-260128", + type: "video", + mode: ["text", "startFrameOptional", ["imageReference:9", "videoReference:3", "audioReference:3"]], + audio: "optional", + durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["480p", "720p"] }], + }, + { + name: "Seedance-1.5-Pro(音画同生)", + modelName: "doubao-seedance-1-5-pro-251215", + type: "video", + mode: ["text", "startFrameOptional"], + audio: "optional", + durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }], + }, + { + name: "Seedance-1.0-Pro", + modelName: "doubao-seedance-1-0-pro-250528", + type: "video", + mode: ["text", "startFrameOptional"], + audio: false, + durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }], + }, + { + name: "Seedance-1.0-Pro-Fast", + modelName: "doubao-seedance-1-0-pro-fast-251015", + type: "video", + mode: ["text", "singleImage"], + audio: false, + durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }], + }, + { + name: "Seedance-1.0-Lite-T2V", + modelName: "doubao-seedance-1-0-lite-t2v-250428", + type: "video", + mode: ["text"], + audio: false, + durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }], + }, + { + name: "Seedance-1.0-Lite-I2V", + modelName: "doubao-seedance-1-0-lite-i2v-250428", + type: "video", + mode: ["startFrameOptional", ["imageReference:4"]], + audio: false, + durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }], + }, + ], +}; + +// ============================================================ +// 辅助工具 +// ============================================================ + +const getHeaders = () => { + if (!vendor.inputValues.apiKey) throw new Error("缺少API Key"); + return { + "Content-Type": "application/json", + Authorization: `Bearer ${vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "")}`, + }; +}; + +const getBaseUrl = () => vendor.inputValues.baseUrl.replace(/\/+$/, ""); + +// ============================================================ +// 适配器函数 +// ============================================================ + +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 effortMap: Record = { + 0: "minimal", + 1: "low", + 2: "medium", + 3: "high", + }; + + return createOpenAI({ + baseURL: getBaseUrl(), + apiKey, + compatibility: "compatible", + fetch: async (url: string, options?: RequestInit) => { + const rawBody = JSON.parse((options?.body as string) ?? "{}"); + const modifiedBody = { + ...rawBody, + thinking: { + type: "enabled", + }, + reasoning_effort: effortMap[thinkLevel], + }; + return await fetch(url, { + ...options, + body: JSON.stringify(modifiedBody), + }); + }, + }).chat(model.modelName); +}; + +const imageRequest = async (config: ImageConfig, model: ImageModel): Promise => { + const baseUrl = getBaseUrl(); + const headers = getHeaders(); + + const content: any[] = []; + + if (config.prompt) { + content.push({ type: "text", text: config.prompt }); + } + + if (config.referenceList && config.referenceList.length > 0) { + for (const ref of config.referenceList) { + content.push({ + type: "image_url", + image_url: { url: ref.base64 }, + }); + } + } + + 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}`); + + 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); + } + + throw new Error("图片生成失败:未返回有效结果"); +}; + +const videoRequest = async (config: VideoConfig, model: VideoModel): Promise => { + const baseUrl = getBaseUrl(); + const headers = getHeaders(); + + const content: any[] = []; + + if (config.prompt) { + content.push({ type: "text", text: config.prompt }); + } + + const activeMode = config.mode && config.mode.length > 0 ? config.mode[0] : "text"; + + if (typeof activeMode === "string") { + switch (activeMode) { + case "singleImage": { + const firstImage = config.referenceList?.find((r) => r.type === "image"); + if (firstImage) { + content.push({ + type: "image_url", + image_url: { url: firstImage.base64 }, + role: "first_frame", + }); + } + break; + } + case "startFrameOptional": { + const images = config.referenceList?.filter((r) => r.type === "image") ?? []; + if (images.length > 0) { + content.push({ + type: "image_url", + image_url: { url: images[0].base64 }, + role: "first_frame", + }); + if (images.length > 1) { + content.push({ + type: "image_url", + image_url: { url: images[1].base64 }, + role: "last_frame", + }); + } + } + break; + } + case "startEndRequired": { + const images = config.referenceList?.filter((r) => r.type === "image") ?? []; + if (images.length >= 2) { + content.push({ + type: "image_url", + image_url: { url: images[0].base64 }, + role: "first_frame", + }); + content.push({ + type: "image_url", + image_url: { url: images[1].base64 }, + role: "last_frame", + }); + } + break; + } + case "endFrameOptional": { + const images = config.referenceList?.filter((r) => r.type === "image") ?? []; + if (images.length > 0) { + content.push({ + type: "image_url", + image_url: { url: images[0].base64 }, + role: "first_frame", + }); + if (images.length > 1) { + content.push({ + type: "image_url", + image_url: { url: images[1].base64 }, + role: "last_frame", + }); + } + } + break; + } + case "text": + default: + break; + } + } else if (Array.isArray(activeMode)) { + // 多模态参考模式:按类型分别提取并添加 + const imageRefs = config.referenceList?.filter((r) => r.type === "image") ?? []; + const videoRefs = config.referenceList?.filter((r) => r.type === "video") ?? []; + const audioRefs = config.referenceList?.filter((r) => r.type === "audio") ?? []; + + for (const refDef of activeMode) { + if (typeof refDef === "string") { + if (refDef.startsWith("imageReference:")) { + const maxCount = parseInt(refDef.split(":")[1], 10); + for (const ref of imageRefs.slice(0, maxCount)) { + content.push({ + type: "image_url", + image_url: { url: ref.base64 }, + role: "reference_image", + }); + } + } else if (refDef.startsWith("videoReference:")) { + const maxCount = parseInt(refDef.split(":")[1], 10); + for (const ref of videoRefs.slice(0, maxCount)) { + content.push({ + type: "video_url", + video_url: { url: ref.base64 }, + role: "reference_video", + }); + } + } else if (refDef.startsWith("audioReference:")) { + const maxCount = parseInt(refDef.split(":")[1], 10); + for (const ref of audioRefs.slice(0, maxCount)) { + content.push({ + type: "audio_url", + audio_url: { url: ref.base64 }, + role: "reference_audio", + }); + } + } + } + } + } + + const body: any = { + model: model.modelName, + content, + ratio: config.aspectRatio, + duration: config.duration, + resolution: config.resolution || "720p", + watermark: false, + }; + + if (model.audio === "optional") { + body.generate_audio = config.audio !== false; + } else if (model.audio === true) { + body.generate_audio = true; + } else { + body.generate_audio = false; + } + + logger(`[视频生成] 提交任务, 模型: ${model.modelName}, 时长: ${config.duration}s, 分辨率: ${config.resolution}`); + + const createResponse = await axios.post(`${baseUrl}/contents/generations/tasks`, body, { headers }); + const taskId = createResponse.data?.id; + + if (!taskId) { + throw new Error("视频生成任务创建失败:未返回任务ID"); + } + + logger(`[视频生成] 任务已创建, ID: ${taskId}`); + + const result = await pollTask( + async (): Promise => { + const queryResponse = await axios.get(`${baseUrl}/contents/generations/tasks/${taskId}`, { headers }); + const task = queryResponse.data; + + logger(`[视频生成] 任务状态: ${task.status}`); + + switch (task.status) { + case "succeeded": + if (task.content?.video_url) { + return { completed: true, data: task.content.video_url }; + } + return { completed: true, error: "任务成功但未返回视频URL" }; + case "failed": + return { completed: true, error: task.error?.message || "视频生成失败" }; + case "expired": + return { completed: true, error: "视频生成任务超时" }; + case "cancelled": + return { completed: true, error: "视频生成任务已取消" }; + default: + return { completed: false }; + } + }, + 10000, + 600000, + ); + + if (result.error) { + throw new Error(result.error); + } + + return await urlToBase64(result.data!); +}; + +const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise => { + return ""; +}; + +const checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => { + return { hasUpdate: false, latestVersion: "2.0", notice: "" }; +}; + +const updateVendor = async (): Promise => { + return ""; +}; + +// ============================================================ +// 导出 +// ============================================================ + +exports.vendor = vendor; +exports.textRequest = textRequest; +exports.imageRequest = imageRequest; +exports.videoRequest = videoRequest; +exports.ttsRequest = ttsRequest; +exports.checkForUpdates = checkForUpdates; +exports.updateVendor = updateVendor; + +export {}; diff --git a/data/version.txt b/data/version.txt index 8428158..9c1218c 100644 --- a/data/version.txt +++ b/data/version.txt @@ -1 +1 @@ -1.1.2 \ No newline at end of file +1.1.3 \ No newline at end of file diff --git a/data/web/index.html b/data/web/index.html index aeb382d..4ad8c33 100644 --- a/data/web/index.html +++ b/data/web/index.html @@ -5,812 +5,812 @@ Toonflow - - + `),QUs=/enable|requires|diagnostic/,tmn=new RegExp("[_\\p{XID_Start}]\\p{XID_Continue}*","u"),mM="variable.predefined",BUs={tokenPostfix:".wgsl",defaultToken:"invalid",unicode:!0,atoms:wUs,keywords:yUs,reserved:CUs,predeclared_enums:xUs,predeclared_types:LUs,predeclared_type_generators:SUs,predeclared_type_aliases:TUs,predeclared_intrinsics:EUs,operators:DUs,symbols:/[!%&*+\-\.\/:;<=>^|_~,]+/,tokenizer:{root:[[QUs,"keyword","@directive"],[tmn,{cases:{"@atoms":mM,"@keywords":"keyword","@reserved":"invalid","@predeclared_enums":mM,"@predeclared_types":mM,"@predeclared_type_generators":mM,"@predeclared_type_aliases":mM,"@predeclared_intrinsics":mM,"@default":"identifier"}}],{include:"@commentOrSpace"},{include:"@numbers"},[/[{}()\[\]]/,"@brackets"],["@","annotation","@attribute"],[/@symbols/,{cases:{"@operators":"operator","@default":"delimiter"}}],[/./,"invalid"]],commentOrSpace:[[/\s+/,"white"],[/\/\*/,"comment","@blockComment"],[/\/\/.*$/,"comment"]],blockComment:[[/[^\/*]+/,"comment"],[/\/\*/,"comment","@push"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],attribute:[{include:"@commentOrSpace"},[/\w+/,"annotation","@pop"]],directive:[{include:"@commentOrSpace"},[/[()]/,"@brackets"],[/,/,"delimiter"],[tmn,"meta.content"],[/;/,"delimiter","@pop"]],numbers:[[/0[fh]/,"number.float"],[/[1-9][0-9]*[fh]/,"number.float"],[/[0-9]*\.[0-9]+([eE][+-]?[0-9]+)?[fh]?/,"number.float"],[/[0-9]+\.[0-9]*([eE][+-]?[0-9]+)?[fh]?/,"number.float"],[/[0-9]+[eE][+-]?[0-9]+[fh]?/,"number.float"],[/0[xX][0-9a-fA-F]*\.[0-9a-fA-F]+(?:[pP][+-]?[0-9]+[fh]?)?/,"number.hex"],[/0[xX][0-9a-fA-F]+\.[0-9a-fA-F]*(?:[pP][+-]?[0-9]+[fh]?)?/,"number.hex"],[/0[xX][0-9a-fA-F]+[pP][+-]?[0-9]+[fh]?/,"number.hex"],[/0[xX][0-9a-fA-F]+[iu]?/,"number.hex"],[/[1-9][0-9]*[iu]?/,"number"],[/0[iu]?/,"number"]]}},FUs=Object.freeze(Object.defineProperty({__proto__:null,conf:OUs,language:BUs},Symbol.toStringTag,{value:"Module"})),PUs={comments:{blockComment:["\x3C!--","-->"]},brackets:[["<",">"]],autoClosingPairs:[{open:"<",close:">"},{open:"'",close:"'"},{open:'"',close:'"'}],surroundingPairs:[{open:"<",close:">"},{open:"'",close:"'"},{open:'"',close:'"'}],onEnterRules:[{beforeText:new RegExp("<([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$","i"),afterText:/^<\/([_:\w][_:\w-.\d]*)\s*>$/i,action:{indentAction:Et.IndentAction.IndentOutdent}},{beforeText:new RegExp("<(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$","i"),action:{indentAction:Et.IndentAction.Indent}}]},MUs={defaultToken:"",tokenPostfix:".xml",ignoreCase:!0,qualifiedName:/(?:[\w\.\-]+:)?[\w\.\-]+/,tokenizer:{root:[[/[^<&]+/,""],{include:"@whitespace"},[/(<)(@qualifiedName)/,[{token:"delimiter"},{token:"tag",next:"@tag"}]],[/(<\/)(@qualifiedName)(\s*)(>)/,[{token:"delimiter"},{token:"tag"},"",{token:"delimiter"}]],[/(<\?)(@qualifiedName)/,[{token:"delimiter"},{token:"metatag",next:"@tag"}]],[/(<\!)(@qualifiedName)/,[{token:"delimiter"},{token:"metatag",next:"@tag"}]],[/<\!\[CDATA\[/,{token:"delimiter.cdata",next:"@cdata"}],[/&\w+;/,"string.escape"]],cdata:[[/[^\]]+/,""],[/\]\]>/,{token:"delimiter.cdata",next:"@pop"}],[/\]/,""]],tag:[[/[ \t\r\n]+/,""],[/(@qualifiedName)(\s*=\s*)("[^"]*"|'[^']*')/,["attribute.name","","attribute.value"]],[/(@qualifiedName)(\s*=\s*)("[^">?\/]*|'[^'>?\/]*)(?=[\?\/]\>)/,["attribute.name","","attribute.value"]],[/(@qualifiedName)(\s*=\s*)("[^">]*|'[^'>]*)/,["attribute.name","","attribute.value"]],[/@qualifiedName/,"attribute.name"],[/\?>/,{token:"delimiter",next:"@pop"}],[/(\/)(>)/,[{token:"tag"},{token:"delimiter",next:"@pop"}]],[/>/,{token:"delimiter",next:"@pop"}]],whitespace:[[/[ \t\r\n]+/,""],[/\x3C!--/,{token:"comment",next:"@comment"}]],comment:[[/[^<\-]+/,"comment.content"],[/-->/,{token:"comment",next:"@pop"}],[/\x3C!--/,"comment.content.invalid"],[/[<\-]/,"comment.content"]]}},RUs=Object.freeze(Object.defineProperty({__proto__:null,conf:PUs,language:MUs},Symbol.toStringTag,{value:"Module"})),NUs={comments:{lineComment:"#"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],folding:{offSide:!0},onEnterRules:[{beforeText:/:\s*$/,action:{indentAction:Et.IndentAction.Indent}}]},IUs={tokenPostfix:".yaml",brackets:[{token:"delimiter.bracket",open:"{",close:"}"},{token:"delimiter.square",open:"[",close:"]"}],keywords:["true","True","TRUE","false","False","FALSE","null","Null","Null","~"],numberInteger:/(?:0|[+-]?[0-9]+)/,numberFloat:/(?:0|[+-]?[0-9]+)(?:\.[0-9]+)?(?:e[-+][1-9][0-9]*)?/,numberOctal:/0o[0-7]+/,numberHex:/0x[0-9a-fA-F]+/,numberInfinity:/[+-]?\.(?:inf|Inf|INF)/,numberNaN:/\.(?:nan|Nan|NAN)/,numberDate:/\d{4}-\d\d-\d\d([Tt ]\d\d:\d\d:\d\d(\.\d+)?(( ?[+-]\d\d?(:\d\d)?)|Z)?)?/,escapes:/\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/,tokenizer:{root:[{include:"@whitespace"},{include:"@comment"},[/%[^ ]+.*$/,"meta.directive"],[/---/,"operators.directivesEnd"],[/\.{3}/,"operators.documentEnd"],[/[-?:](?= )/,"operators"],{include:"@anchor"},{include:"@tagHandle"},{include:"@flowCollections"},{include:"@blockStyle"},[/@numberInteger(?![ \t]*\S+)/,"number"],[/@numberFloat(?![ \t]*\S+)/,"number.float"],[/@numberOctal(?![ \t]*\S+)/,"number.octal"],[/@numberHex(?![ \t]*\S+)/,"number.hex"],[/@numberInfinity(?![ \t]*\S+)/,"number.infinity"],[/@numberNaN(?![ \t]*\S+)/,"number.nan"],[/@numberDate(?![ \t]*\S+)/,"number.date"],[/(".*?"|'.*?'|[^#'"]*?)([ \t]*)(:)( |$)/,["type","white","operators","white"]],{include:"@flowScalars"},[/.+?(?=(\s+#|$))/,{cases:{"@keywords":"keyword","@default":"string"}}]],object:[{include:"@whitespace"},{include:"@comment"},[/\}/,"@brackets","@pop"],[/,/,"delimiter.comma"],[/:(?= )/,"operators"],[/(?:".*?"|'.*?'|[^,\{\[]+?)(?=: )/,"type"],{include:"@flowCollections"},{include:"@flowScalars"},{include:"@tagHandle"},{include:"@anchor"},{include:"@flowNumber"},[/[^\},]+/,{cases:{"@keywords":"keyword","@default":"string"}}]],array:[{include:"@whitespace"},{include:"@comment"},[/\]/,"@brackets","@pop"],[/,/,"delimiter.comma"],{include:"@flowCollections"},{include:"@flowScalars"},{include:"@tagHandle"},{include:"@anchor"},{include:"@flowNumber"},[/[^\],]+/,{cases:{"@keywords":"keyword","@default":"string"}}]],multiString:[[/^( +).+$/,"string","@multiStringContinued.$1"]],multiStringContinued:[[/^( *).+$/,{cases:{"$1==$S2":"string","@default":{token:"@rematch",next:"@popall"}}}]],whitespace:[[/[ \t\r\n]+/,"white"]],comment:[[/#.*$/,"comment"]],flowCollections:[[/\[/,"@brackets","@array"],[/\{/,"@brackets","@object"]],flowScalars:[[/"([^"\\]|\\.)*$/,"string.invalid"],[/'([^'\\]|\\.)*$/,"string.invalid"],[/'[^']*'/,"string"],[/"/,"string","@doubleQuotedString"]],doubleQuotedString:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"]],blockStyle:[[/[>|][0-9]*[+-]?$/,"operators","@multiString"]],flowNumber:[[/@numberInteger(?=[ \t]*[,\]\}])/,"number"],[/@numberFloat(?=[ \t]*[,\]\}])/,"number.float"],[/@numberOctal(?=[ \t]*[,\]\}])/,"number.octal"],[/@numberHex(?=[ \t]*[,\]\}])/,"number.hex"],[/@numberInfinity(?=[ \t]*[,\]\}])/,"number.infinity"],[/@numberNaN(?=[ \t]*[,\]\}])/,"number.nan"],[/@numberDate(?=[ \t]*[,\]\}])/,"number.date"]],tagHandle:[[/\![^ ]*/,"tag"]],anchor:[[/[&*][^ ]+/,"namespace"]]}},UUs=Object.freeze(Object.defineProperty({__proto__:null,conf:NUs,language:IUs},Symbol.toStringTag,{value:"Module"})); +
diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..620babf --- /dev/null +++ b/nodemon.json @@ -0,0 +1,14 @@ +{ + "ignore": [ + "node_modules", + "data/*", + "build/*", + "dist/*", + "router.ts", + "database.d.ts" + ], + "events": { + "restart": "" + }, + "delay": 0 +} \ No newline at end of file diff --git a/package.json b/package.json index 7e84ac7..b27d355 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "toonflow", - "version": "1.1.2", + "version": "1.1.3", "description": "Toonflow 是一款 AI 短剧漫剧工具,能够利用 AI 技术将小说自动转化为剧本,并结合 AI 生成的图片和视频,实现高效的短剧创作。", "author": "HBAI-Ltd ", "license": "Apache-2.0", @@ -49,7 +49,6 @@ "better-sqlite3": "^12.8.0", "compressing": "^2.1.0", "cors": "^2.8.5", - "custom-electron-titlebar": "^4.2.8", "dotenv": "^17.2.3", "express": "^5.2.1", "express-ws": "^5.0.2", diff --git a/scripts/license.ts b/scripts/license.ts index 36d4ce1..93cee24 100644 --- a/scripts/license.ts +++ b/scripts/license.ts @@ -61,7 +61,34 @@ checker.init({ start: process.cwd() }, (err: Error, packages: Record pkg.name && !excludeNames.some((exName) => pkg.name.startsWith(exName))); - const content = filteredDeclare + + // 去重:同一个 name@version 只保留一条,并合并 licenses + const dedupedDeclare = Array.from( + filteredDeclare + .reduce((acc, pkg) => { + const key = `${pkg.name}@${pkg.version}`; + const licenseList = Array.isArray(pkg.licenses) ? pkg.licenses : [pkg.licenses]; + const existing = acc.get(key); + + if (!existing) { + acc.set(key, { + ...pkg, + licenses: [...new Set(licenseList.filter(Boolean))], + }); + return acc; + } + + const existingLicenses = Array.isArray(existing.licenses) ? existing.licenses : [existing.licenses]; + existing.licenses = [...new Set([...existingLicenses, ...licenseList].filter(Boolean))]; + if (!existing.repository && pkg.repository) { + existing.repository = pkg.repository; + } + return acc; + }, new Map()) + .values() + ); + + const content = dedupedDeclare .map( (pkg) => `Name: ${pkg.name}\nLicense: ${Array.isArray(pkg.licenses) ? pkg.licenses.join(", ") : pkg.licenses}\nRepository: ${pkg.repository ?? "N/A"}` diff --git a/scripts/main.ts b/scripts/main.ts index b793b20..52d36da 100644 --- a/scripts/main.ts +++ b/scripts/main.ts @@ -7,7 +7,7 @@ import Module from "module"; app.commandLine.appendSwitch("disable-gpu-shader-disk-cache"); app.commandLine.appendSwitch("disable-features", "CalculateNativeWinOcclusion"); -const TARGET_ENTRIES = new Set(["assets", "models", "serve", "skills", "web"]); +const TARGET_ENTRIES = new Set(["assets", "models", "serve", "skills", "web", "vendor"]); function copyDir(src: string, dest: string): void { if (!fs.existsSync(src)) return; diff --git a/src/agents/productionAgent/tools.ts b/src/agents/productionAgent/tools.ts index ac27ff0..bbf28d4 100644 --- a/src/agents/productionAgent/tools.ts +++ b/src/agents/productionAgent/tools.ts @@ -145,7 +145,7 @@ export default (toolCpnfig: ToolConfig) => { }, }), generate_deriveAsset: tool({ - description: "生成衍生资产", + description: "生成衍生资产图片", inputSchema: z.object({ ids: z.array(z.number()).describe("需要生成的 衍生资产ID"), }), diff --git a/src/app.ts b/src/app.ts index 6e1c996..25bf495 100644 --- a/src/app.ts +++ b/src/app.ts @@ -12,19 +12,6 @@ import fs from "fs"; import u from "@/utils"; import jwt from "jsonwebtoken"; import socketInit from "@/socket/index"; -import path from "path"; - -declare const __APP_VERSION__: string; - -const APP_VERSION: string = (() => { - if (typeof __APP_VERSION__ !== "undefined") { - return __APP_VERSION__; - } - // 开发环境回退:从 package.json 读取 - const pkgPath = path.resolve(process.cwd(), "package.json"); - const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")); - return pkg.version; -})(); const app = express(); const server = http.createServer(app); @@ -49,7 +36,7 @@ export default async function startServe(randomPort: Boolean = false) { fs.mkdirSync(ossDir, { recursive: true }); } console.log("文件目录:", ossDir); - app.use("/oss", express.static(ossDir)); + app.use("/oss", express.static(ossDir, { acceptRanges: false })); // skills 静态资源 const skillsDir = u.getPath("skills"); if (!fs.existsSync(skillsDir)) { @@ -62,7 +49,7 @@ export default async function startServe(randomPort: Boolean = false) { (req, res, next) => { /\.(jpe?g|png|gif|webp|svg|ico|bmp)$/i.test(req.path) ? next() : res.status(403).end(); }, - express.static(skillsDir), + express.static(skillsDir, { acceptRanges: false }), ); // assets 静态资源 @@ -71,13 +58,13 @@ export default async function startServe(randomPort: Boolean = false) { fs.mkdirSync(assetsDir, { recursive: true }); } console.log("文件目录:", assetsDir); - app.use("/assets", express.static(assetsDir)); + app.use("/assets", express.static(assetsDir, { acceptRanges: false })); // data/web 静态网站 const webDir = u.getPath("web"); if (fs.existsSync(webDir)) { console.log("静态网站目录:", webDir); - app.use(express.static(webDir)); + app.use(express.static(webDir, { acceptRanges: false })); } else { console.warn("静态网站目录不存在:", webDir); } diff --git a/src/lib/fixDB.ts b/src/lib/fixDB.ts index 90dd83a..d127d26 100644 --- a/src/lib/fixDB.ts +++ b/src/lib/fixDB.ts @@ -1,5 +1,10 @@ +import u from "@/utils"; +import path from "path"; +import fs from "fs"; import { Knex } from "knex"; import db from "@/utils/db"; +import { transform } from "sucrase"; + export default async (knex: Knex): Promise => { const addColumn = async (table: string, column: string, type: string) => { if (!(await knex.schema.hasTable(table))) return; @@ -23,6 +28,7 @@ export default async (knex: Knex): Promise => { }); } }; + //矫正因软件异常退出导致的状态不一致问题 await db("o_novel").where("eventState", 0).update({ eventState: -1, errorReason: "软件退出导致失败", @@ -47,14 +53,78 @@ export default async (knex: Knex): Promise => { state: "生成失败", errorReason: "软件退出导致失败", }); + + // 添加新字段 await addColumn("o_prompt", "useData", "text"); + //矫正提示词 await db("o_prompt").where("type", "scriptAssetExtraction").update({ data: `---\nname: universal_agent\ndescription: 专注于从剧本内容中提取所使用的资产(角色、场景、道具)并生成结构化资产列表的助手。\n---\n\n# Script Assets Extract\n\n你是一个专业的剧本内容分析助手,专注于从剧本文本中识别和提取所有涉及的资产(角色、场景、道具),并为每项资产生成可供下游制作流程使用的结构化描述和提示词。\n\n## 何时使用\n\n用户提供剧本内容,你需要逐段阅读并提取其中涉及的所有资产(人物角色、场景地点、道具物件),输出为结构化的资产列表。产出的资产描述将用于后续 AI 图片生成和制作流程。\n\n## 与系统的对应关系\n\n- 资产类型:\n - \`role\` — 角色(对应 \`o_assets.type = "role"\`)\n - \`scene\` — 场景(对应 \`o_assets.type = "scene"\`)\n - \`tool\` — 道具(对应 \`o_assets.type = "tool"\`)\n- 下游用途:资产提示词生成 → AI 资产图生成 → 分镜制作\n\n## 输出要求\n\n**必须通过调用 \`resultTool\` 工具返回结果**,禁止以纯文本、Markdown 表格或 JSON 代码块等形式直接输出资产列表。\n\`resultTool\` 的 schema 会对字段类型和枚举值做强校验,调用时请严格按照下方字段定义填写,确保数据结构正确、字段完整、类型匹配。\n\n每个资产对象包含以下字段:\n\n| 字段 | 类型 | 必填 | 说明 |\n| ---- | ---- | ---- | ---- |\n| \`name\` | string | 是 | 资产名称,使用剧本中的原始称呼,不做其他多余描述 |\n| \`desc\` | string | 是 | 资产描述,30-80 字的视觉化描述 |\n| \`prompt\` | string | 是 | 生成提示词,英文,用于 AI 图片生成 |\n| \`type\` | enum | 是 | 资产类型:\`role\` / \`scene\` / \`tool\` |\n\n## 提取规则\n\n### 角色(role)\n\n- 提取剧本中出现的所有有名字的角色\n- \`desc\`:包含性别、外貌特征、服饰风格、体态气质等视觉要素,需在描述开头明确标注角色性别(如"男性,……"或"女性,……")\n- \`prompt\`:英文提示词,描述角色的外观特征,需以性别词开头(如 \`a young man, ...\` 或 \`a young woman, ...\`),适用于 AI 角色图生成\n- 同一角色有多个称呼时,取最常用的作为 \`name\`\n- 无名龙套(如"路人甲"、"士兵")可跳过,除非其造型对剧情有重要视觉意义\n\n### 场景(scene)\n\n- 提取剧本中出现的所有场景/地点\n- \`desc\`:包含空间结构、光照氛围、关键陈设、色调基调等视觉要素\n- \`prompt\`:英文提示词,描述场景的整体视觉风格,适用于 AI 场景图生成\n- 同一场景的不同状态(如白天/夜晚)不重复提取,在 \`desc\` 中注明即可\n\n### 道具(tool)\n\n- 提取剧本中出现的重要道具/物品\n- \`desc\`:包含外观形状、颜色材质、尺寸参考、特殊效果等视觉要素\n- \`prompt\`:英文提示词,描述道具的外观细节,适用于 AI 道具图生成\n- 仅提取有独立视觉意义或剧情功能的道具,通用物品可跳过\n\n\n## 提示词(prompt)生成规范\n\n- 采用逗号分隔的关键词/短语格式\n- 优先描述**视觉特征**,避免抽象概念\n- 包含风格关键词(如 anime style, manga style 等,根据项目风格决定)\n- 角色 prompt 示例:\`a young man, sharp eyebrows, black hair, pale skin, wearing a gray Taoist robe, slender build, cold expression\`\n- 场景 prompt 示例:\`dark cave interior, glowing crystals on walls, misty atmosphere, dim blue lighting, stone altar in center\`\n- 道具 prompt 示例:\`ancient jade pendant, oval shape, translucent green, carved dragon pattern, glowing faintly\`\n\n## 提取流程\n\n1. 通读剧本全文,识别所有出现的角色、场景、道具\n2. 对每个资产生成结构化的 \`name\`、\`desc\`、\`prompt\`、\`type\`\n3. 去重:同一资产不重复提取\n4. **必须通过调用 \`resultTool\` 工具输出完整资产列表**,不要分多次调用,一次性将所有资产放入 \`assetsList\` 数组中提交\n\n## 提取原则\n\n1. **忠于剧本**:所有提取基于剧本中的实际内容,不臆造未出现的资产\n2. **视觉优先**:描述和提示词聚焦视觉特征,便于 AI 图片生成\n3. **精简实用**:只提取对制作有实际意义的资产,避免过度提取\n4. **分类准确**:严格按照 role/scene/tool 分类,不混淆\n5. **提示词质量**:英文提示词应具体、可执行,能直接用于 AI 图片生成\n\n## 注意事项\n\n- 资产列表中**不要包含剧本内容本身**,仅提取所使用到的资产\n- 角色的随身物品如果有独立剧情功能,应单独作为道具提取\n- 场景中的固定陈设不需要单独提取为道具,除非该物件有独立剧情作用`, }); - await db("o_prompt") - .where("type", "videoPromptGeneration") - .update({ - data: `# 视频提示词生成 Skill\n\n你是**视频提示词生成 Agent**,专门负责根据指定的 AI 视频模型,读取分镜信息并输出该模型对应格式的视频提示词。\n\n---\n\n## 输入格式\n\n### 1. 模型与模式(必选)\n\n\n#### 模式路由规则\n\n| 条件 | 匹配模式 | 说明 |\n|------|----------|------|\n| 模型名为 \`seedance-2-0\` + \`多参:是\` / \`seedance 2.0\` + \`多参:是\` / \`即梦2.0\` + \`多参:是\` | **seedance-2-0*,不包含其他版本比如seedance-1-5/seedance-1-0 | 支持角色/场景/分镜图多参引用 |\n| 模型名为 \`Wan2.6\` / \`wan 2.6\` / \`万象2.6\` | **Wan 2.6** | 固定模式,单图(首帧)+ 叙事文本,无尾帧 |\n| 其他任何模型 + \`多参:是\` | **通用多参模式** | 支持角色/场景/分镜图多参引用 |\n| 其他任何模型/seedance-1-5/seedance-1-0 + \`多参:否\` | **通用首尾帧模式** | 首帧/首尾帧 + 纯文本描述 |\n\n> 模型名仅用于记录,实际提示词格式由匹配到的模式决定。Seedance 2.0 和 Wan 2.6 是指定模型名即确定模式的特例。\n\n### 2. 资产信息\n\n\`\`\`\n资产信息[id, type, name], [id, type, name], ...\n\`\`\`\n\n- \`id\`:资产唯一标识(如 \`A001\`)\n- \`type\`:资产类型,取值 \`role\`(角色)/ \`scene\`(场景)/ \`prop\`(道具)\n- \`name\`:资产名称(如 \`沈辞\`、\`城楼\`、\`长剑\`)\n\n### 3. 分镜信息\n\n分镜以 \`\` XML 标签列表的形式传入,每条分镜结构如下:\n\n\`\`\`xml\n\n\`\`\`\n\n#### 输入字段说明\n\n| 属性 | 说明 | 来源 |\n|------|------|------|\n| \`videoDesc\` | **核心输入**:分镜的结构化画面描述,包含画面描述、场景、关联资产名称、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效、关联资产ID | 用户/上游系统填写 |\n| \`prompt\` | **已有字段**:上游生成的分镜图提示词,作为辅助参考上下文,**不修改** | 上游系统已填写 |\n| \`track\` | 分镜分组标识 | 用户/上游系统填写 |\n| \`duration\` | 视频推荐时长(秒) | 用户/上游系统填写 |\n| \`associateAssetsIds\` | 该分镜关联的资产ID列表 | 用户/上游系统填写 |\n| \`shouldGenerateImage\` | 是否需要生成分镜图片,默认 \`true\` | 用户/上游系统填写 |\n\n---\n\n## 任务目标\n\n读取所有 \`\` 的属性,结合资产信息,根据指定模型的提示词格式,将全部分镜整合为一个完整的视频提示词。\n\n---\n\n## 输出格式\n\n将所有分镜整合为**一个完整的视频提示词**输出(非逐条独立):\n\n| 模式 | 整合方式 |\n|------|----------|\n| **通用多参模式** | \`[References]\` 汇总所有 \`@图N \` 引用;\`[Instruction]\` 按时间顺序描述完整叙事 |\n| **通用首尾帧模式** | 纯文本五维度(Visual / Motion / Camera / Audio / Narrative),不使用任何 \`@图N \` 引用,按时间轴连续编排(\`[Motion]\` 0s → 总时长,每段最低 1 秒),全程单一连贯镜头,不切镜 |\n| **Seedance 2.0** | \`生成一个由以下 N 个分镜组成的视频\`,每条对应 \`分镜N{N}s\` 段落 |\n| **Wan 2.6** | 单图首帧模式,每次仅输入一条分镜,输出一段叙事式英文提示词(三段式:风格基调 → 主体动作+场景环境+光线氛围 → 镜头收尾),不使用 \`@图N \` 引用 |\n\n- 仅输出视频提示词文本,不输出 XML 标签,不附加解释\n\n---\n\n## videoDesc 解析规则\n\n从 \`videoDesc\` 括号内按顿号分隔提取以下结构化字段:\n\n\`\`\`\n({画面描述}、{场景}、{关联资产名称}、{时长}、{景别}、{运镜}、{角色动作}、{情绪}、{光影氛围}、{台词}、{音效}、{关联资产ID})\n\`\`\`\n\n| 序号 | 字段 | 用途 | 示例 |\n|------|------|------|------|\n| 1 | 画面描述 | prompt 的叙事主干 | 沈辞独立城楼远眺苍茫大地 |\n| 2 | 场景 | 匹配场景资产 | 城楼 |\n| 3 | 关联资产名称 | 匹配角色/道具资产 | 沈辞/城楼 |\n| 4 | 时长 | 控制时长参数 | 4s |\n| 5 | 景别 | 控制镜头景别 | 全景 |\n| 6 | 运镜 | 控制运镜方式 | 静止 |\n| 7 | 角色动作 | prompt 动作描写 | 负手而立衣袂随风飘扬 |\n| 8 | 情绪 | prompt 情绪氛围 | 坚定决绝 |\n| 9 | 光影氛围 | prompt 光影描写 | 黄昏冷调侧逆光 |\n| 10 | 台词 | prompt 台词/音频段 | 无台词 / 具体台词内容 |\n| 11 | 音效 | prompt 音效描写 | 风声衣袂声 |\n| 12 | 关联资产ID | 用于资产ID↔角色标签映射 | A001/A002 |\n\n---\n\n## 资产引用编号规则\n\n所有模型统一使用 \`@图N \` 格式引用资产和分镜图,编号按输入顺序连续递增:\n\n1. **资产**:按资产信息中 \`[id, type, name]\` 的出现顺序,从 \`@图1 \` 开始编号(不区分 role / scene / prop)。**资产类型的出现顺序不固定**——可能先 scene 后 character,也可能 prop 在前、character 在后,或任意交替出现,编号严格按输入位置分配,不按类型归组\n2. **分镜图**:每条 \`\` 对应一张分镜图,编号接续资产之后\n3. **跳过无分镜图的条目**:当 \`shouldGenerateImage="false"\` 时,该分镜未生成图片,**不分配**分镜图编号,后续编号顺延\n\n#### 示例\n\n输入 3 个资产 + 2 条分镜:\n\`\`\`\n资产信息[A001, role, 沈辞], [A002, role, 苏锦], [A003, scene, 城楼]\n\`\`\`\n\`\`\`xml\n \n \n\`\`\`\n\n编号结果:\n\n| 输入项 | 引用标签 | 说明 |\n|--------|----------|------|\n| [A001, role, 沈辞] | \`@图1 \` | 角色·沈辞 参考图 |\n| [A002, role, 苏锦] | \`@图2 \` | 角色·苏锦 参考图 |\n| [A003, scene, 城楼] | \`@图3 \` | 场景·城楼 参考图 |\n| storyboardItem 第1条 | \`@图4 \` | 分镜图1 |\n| storyboardItem 第2条 | \`@图5 \` | 分镜图2 |\n\n**混合顺序示例**\n\n输入 3 个资产(场景在前)+ 2 条分镜:\n\`\`\`\n资产信息[A003, scene, 城楼], [A001, role, 沈辞], [A002, role, 苏锦]\n\`\`\`\n\`\`\`xml\n \n \n\`\`\`\n\n编号结果:\n\n| 输入项 | 引用标签 | 说明 |\n|--------|----------|------|\n| [A003, scene, 城楼] | \`@图1 \` | 场景·城楼 参考图 |\n| [A001, role, 沈辞] | \`@图2 \` | 角色·沈辞 参考图 |\n| [A002, role, 苏锦] | \`@图3 \` | 角色·苏锦 参考图 |\n| storyboardItem 第1条 | \`@图4 \` | 分镜图1 |\n| storyboardItem 第2条 | \`@图5 \` | 分镜图2 |\n\n> **关键**:此例中 \`@图1 \` 是场景而非角色,\`@图2 \` \`@图3 \` 才是角色。生成提示词时,必须根据资产的实际 \`type\` 字段确定引用方式,而非根据编号大小假定类型。\n\n---\n\n## 模型提示词生成规则\n\n### 一、通用多参模式\n\n#### 核心原则\n- MVL 多模态融合:自然语言 + 图像引用在同一语义空间\n- 分镜图序列负责动作/时间轴/构图,场景参考图负责环境一致性\n- 所有资产和分镜图统一用 \`@图N \` 引用\n- **严格遵循 videoDesc**:提示词内容严格基于 videoDesc 中的画面描述、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效字段生成,不编造额外内容\n- **台词不可缺失**:videoDesc 中有台词的分镜,必须在 Instruction 中体现台词相关描述\n- **台词类型标注**:区分普通对白(dialogue)、内心独白(inner monologue OS)、画外音(voiceover VO),在 Instruction 中用括号标注\n\n#### prompt 生成模板\n\n> **注意**:\`[References]\` 中的 \`@图N\` 编号严格按资产输入顺序分配,角色/场景/道具可能出现在任意编号位置。生成时需根据每个资产的 \`type\` 字段确定其引用方式,不可假定固定的类型-编号对应关系。\n\n\`\`\`\n[References]\n@图{资产1编号} : [{资产1名称}参考图] ← 可能是 role/scene/prop 中的任意类型\n@图{资产2编号} : [{资产2名称}参考图]\n@图{资产3编号} : [{资产3名称}参考图]\n...\n@图{分镜图编号} : [分镜图1] ← 分镜图编号接续资产之后\n\n[Instruction]\nBased on the storyboard @图{分镜图编号} :\n@图{角色资产编号} {动作/状态描述(英文)},\nset in the {场景描述(英文)} of @图{场景资产编号} ,\n{镜头/运镜描述(英文)},\n{情感基调(英文)},\n{台词描述(英文,含 dialogue/OS/VO 标注)/ No dialogue},\n{音效描述(英文)}.\n\`\`\`\n\n#### 生成约束\n1. **Instruction 必须用英文**\n2. **严格遵循 videoDesc**:提示词内容严格基于 videoDesc 的画面描述、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效字段,不编造额外信息\n3. **角色动作**从 videoDesc 的「角色动作」字段提取,翻译为简洁英文动作描述\n4. **台词不可缺失**:videoDesc 中有台词的分镜,必须在 Instruction 中体现台词内容(保持原始语言,不翻译)\n5. **台词类型标注**:普通对白标注 \`(dialogue)\`;内心独白标注 \`(inner monologue, OS)\`;画外音标注 \`(voiceover, VO)\`\n6. **镜头风格**使用标准标签:\`cinematic\` / \`wide-angle\` / \`close-up\` / \`slow motion\` / \`surround shooting\` / \`handheld\`\n7. **空间关系**使用标准动词:\`wearing\` / \`holding\` / \`standing on\` / \`following behind\` / \`sitting in\`\n8. 单条分镜对应单个 \`@图N \`,不做多帧跨镜描述\n9. 无需描述角色外观(由参考图负责)\n10. 无时长标注(由模型推断)\n11. **无分镜图时**:当 \`shouldGenerateImage="false"\` 时,该分镜无分镜图,\`[References]\` 中不列出该分镜图,\`[Instruction]\` 中不使用 \`@图N \` 引用该分镜图,改为纯文本描述画面内容\n\n#### KlingOmni 完整示例\n\n输入:\n\`\`\`\n模型:KlingOmni\n资产信息[A001, role, 沈辞], [A002, role, 苏锦], [A003, scene, 城楼]\n\`\`\`\n\`\`\`xml\n\n\n\`\`\`\n\n输出:\n\`\`\`\n[References]\n@图1 : [沈辞参考图]\n@图2 : [苏锦参考图]\n@图3 : [城楼参考图]\n@图4 : [分镜图1]\n@图5 : [分镜图2]\n\n[Instruction]\nBased on the storyboard from @图4 to @图5 :\n@图1 standing alone atop the city wall, hands clasped behind back, robes billowing in the wind, gazing across the vast land,\n@图2 ascending the steps toward @图1 , expression worried,\nset in the ancient city wall environment of @图3 ,\nwide shot transitioning to medium tracking shot, cinematic,\nresolute determination shifting to concerned anticipation, dusk cold-toned side-backlit atmosphere fading,\nno dialogue,\nwind howling, fabric flapping, footsteps on stone.\n\`\`\`\n\n---\n\n### 二、通用首尾帧模式\n\n#### 核心原则\n- **纯文本提示词**:提示词内**不使用任何 \`@图N \` 引用**(不引用角色资产、场景资产、也不引用分镜图),全部内容用纯文本描述\n- **五维度结构**:Visual / Motion / Camera / Audio / Narrative\n- **严格遵循 videoDesc**:提示词内容严格基于 videoDesc 中的画面描述、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效字段生成,不编造额外内容\n- **台词不可缺失**:videoDesc 中有台词的分镜,必须在 \`[Audio]\` 中完整输出台词内容\n- **台词类型标注**:区分普通对白(dialogue, lip-sync active)、内心独白(inner monologue OS, silent lips)、画外音(voiceover VO, silent lips),并在 \`[Audio]\` 中明确标注\n- **不说话的主体标注 \`silent\`** — 防止误生口型\n- **全程单一连贯镜头**:从头到尾一个镜头,不存在切镜\n- **时间轴分段**:每段最低 1 秒,用 \`0s-Xs\` 标注\n\n#### prompt 生成模板\n\n\`\`\`\n[Visual]\n{主体A名}: {外观简述}, {站位/姿态}, {说话状态 speaking/silent}.\n{主体B名}: {外观简述}, {站位/姿态}, {说话状态}.\n{场景描述}, {道具描述}.\n{视觉风格标签}.\n\n[Motion]\n0s-{X}s: {主体A名} {动作描述段1}.\n{X}s-{Y}s: {主体B名} {动作描述段2}.\n\n[Camera]\n{镜头类型}, {运镜方式}, {全程单一连贯镜头描述}.\n\n[Audio]\n{Xs-Ys}: "{台词内容}" — {说话者名} ({dialogue / inner monologue OS / voiceover VO}), {lip-sync active / silent lips}.\n{音效描述}.\n\n[Narrative]\n{情节点概述}, {叙事位置}.\n\`\`\`\n\n#### 生成约束\n1. **全部用英文**\n2. **不使用任何 \`@图N \` 引用**:提示词内不引用角色资产、场景资产、分镜图,全部内容用纯文本描述\n3. **主体用文字描述**:在 [Visual] 中简要描述主体外观特征(如服饰、发型等关键辨识特征)\n4. **严格遵循 videoDesc**:提示词内容严格基于 videoDesc 中的画面描述、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效字段,不编造额外信息\n5. **每个主体必须标注说话状态**:\`speaking\` / \`silent\` / \`speaking simultaneously\`\n6. **台词不可缺失**:videoDesc 中有台词的分镜,必须在 \`[Audio]\` 中完整输出台词内容(保持原始语言,不翻译)\n7. **台词类型标注**:普通对白标注 \`dialogue, lip-sync active\`;内心独白标注 \`inner monologue (OS), silent lips\`;画外音标注 \`voiceover (VO), silent lips\`\n8. **Motion 时间轴**每段最低 1 秒,不超过总时长\n9. **全程单一连贯镜头**:Camera 段落描述从头到尾的一个镜头,绝不切镜\n10. **视觉风格**参考 Assistant 中的「视觉风格约束」部分内容\n11. **镜头类型**从以下选取:\`Wide establishing shot / Over-the-shoulder / Medium shot / Close-up / Wide shot / POV / Dutch angle / Crane up / Dolly right / Whip pan / Handheld / Slow motion\`\n\n#### Seedance 1.5 Pro 完整示例\n\n输入:\n\`\`\`\n模型:Seedance1.5\n资产信息[A001, role, 沈辞], [A002, role, 苏锦], [A003, scene, 城楼]\n\`\`\`\n\`\`\`xml\n\n\n\`\`\`\n\n输出:\n\`\`\`\n[Visual]\nShen Ci: male, dark flowing robes, hair tied up, standing alone atop city wall, hands clasped behind back, robes billowing, silent.\nSu Jin: female, light-colored dress, hair partially down, ascending steps toward Shen Ci, expression worried, silent.\nAncient city wall, vast open land beyond, dusk sky fading.\nCinematic, photorealistic, 4K, high contrast, desaturated tones, shallow depth of field.\n\n[Motion]\n0s-4s: Shen Ci stands still on city wall edge, robes flutter in wind, hair sways gently. Gaze fixed on distant horizon.\n4s-8s: Su Jin climbs the last few steps onto the wall, walks toward Shen Ci. Shen Ci remains still, unaware. Su Jin slows as she approaches.\n\n[Camera]\nWide establishing shot, static for first 4 seconds capturing the lone figure. Then smooth transition to medium tracking shot following the woman ascending steps, single continuous take throughout, no cuts.\n\n[Audio]\n0s-4s: Wind howling across wall, fabric flapping rhythmically. No dialogue.\n4s-8s: Footsteps on stone, robes rustling. No dialogue.\nShen Ci — silent. Su Jin — silent.\n\n[Narrative]\nLone figure on city wall, then arrival of a companion. Tension between determination and concern. Single continuous take.\n\`\`\`\n\n---\n\n### 三、Seedance 2.0\n\n#### 核心原则\n- **结构化12维编码**:统一用 \`@图N \` 引用资产和分镜图,时长 \`{N}s\`\n- **最前面先定义图片映射**:先输出“图片定义”段,集中声明 \`@图N : 主体名字/场景名字,简述\`;后续分镜正文只使用主体名字,不再写 \`@图N \`\n- **音色参数9维度精细描述**(有台词时必填)\n- **秒级时长控制**:单分镜时长最低 1s\n- **中文提示词**\n- **严格遵循 videoDesc**:每条分镜的描述内容严格基于 videoDesc 中的画面描述、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效字段生成,不编造额外内容\n- **台词不可缺失**:videoDesc 中有台词的分镜,必须完整输出台词和音色描述\n- **台词类型标注**:区分普通对白(直接使用「说:」)、内心独白(使用「内心OS:」)、画外音(使用「画外音VO:」),并匹配对应的嘴型状态描述\n\n#### prompt 生成模板\n\n> **注意**:\`@图{编号}\` 仅用于最前面的“图片定义”段。分镜正文中禁止再写 \`@图{编号}\`,统一改用主体名字/场景名字。\n\n**单分镜模板:**\n\`\`\`\n画面风格和类型: {风格}, {色调}, {类型}\n\n图片定义:\n@图1: {资产1名字},{简述}\n@图2: {资产2名字},{简述}\n@图N: {资产N名字},{简述}\n...\n\n生成一个由以下 1 个分镜组成的视频:\n\n场景:\n分镜过渡: 无\n\n分镜1 {N}s: 时间:{日/夜/晨/黄昏},场景:{场景名字},镜头:{景别},{角度},{运镜},{角色名字} {动作/表情/视线朝向/站位描述}。{台词与音色描述(如有)}。{背景环境补充}。{光影氛围}。{运镜补充}。\n\`\`\`\n\n**多分镜模板:**\n\`\`\`\n画面风格和类型: {风格}, {色调}, {类型}\n\n图片定义:\n@图1: {资产1名字},{简述}\n@图2: {资产2名字},{简述}\n@图N: {资产N名字},{简述}\n...\n\n生成一个由以下 {N} 个分镜组成的视频:\n\n场景:\n分镜过渡: {全局过渡描述}\n\n分镜1 {N}s: 时间:{...},场景:{场景名字},镜头:{...},{角色名字} {...}。{...}。\n分镜2{N}s: ...\n...\n\`\`\`\n\n#### 音色生成规则(有台词时必填)\n\n台词格式:\`{角色名字} 说:「{台词内容}」音色:{9维度描述}\`\n\n9维度按顺序填写:\n\`\`\`\n{性别},{年龄音色},{音调},{音色质感},{声音厚度},{发音方式},{气息},{语速},{特殊质感}\n\`\`\`\n\n> 当 desc 中未明确音色信息时,根据角色类型从以下参考表推断:\n\n| 角色类型特征 | 默认音色 |\n|------------|---------|\n| 男性权威/霸气角色 | 男声,中年音色,音调低沉,音色浑厚有力,声音厚重,发音标准,气息极其沉稳,语速偏慢 |\n| 女性温柔/甜美角色 | 女声,青年音色,音调中等偏高,音色质感明亮清脆,声音清亮柔和,气息充沛平稳,带温婉真诚感 |\n| 男性年轻/普通角色 | 男声,青年音色,音调中等,音色干净,声音厚度适中,发音清晰,气息平稳,语速适中 |\n| 女性活泼/外向角色 | 女声,青年音色,音调偏高,音色清脆活泼,声音轻盈,气息充沛,语速偏快,带笑意和感染力 |\n| 反派/冷酷角色 | 男声,中年音色,音调低沉,音色质感干燥偏暗,声音带沙砾感,气息平稳,语速极慢,有威胁感 |\n\n#### 无台词分镜处理\n- 不写 \`说:\` 和音色段落\n- 在动作描述后标注 \`无台词\`\n\n#### 台词类型格式\n\n| 台词类型 | 格式 | 嘴型描述 |\n|----------|------|----------|\n| 普通对白 | \`{角色名字} 说:「{台词}」音色:{9维度}\` | 角色嘴部开合说话 |\n| 内心独白 | \`{角色名字} 内心OS:「{台词}」音色:{9维度}\` | 角色嘴部紧闭不动 |\n| 画外音 | \`{角色名字} 画外音VO:「{台词}」音色:{9维度}\` | 角色嘴部紧闭不动(或角色不在画面中) |\n\n#### 生成约束\n1. **中文提示词**\n2. **直接输出视频提示词**:禁止输出任何分析过程、推理步骤、模型匹配说明、资产编号表、分隔线等非提示词内容。第一行必须是 \`画面风格和类型:\`\n3. **严格遵循 videoDesc**:每条分镜内容严格基于 videoDesc 的画面描述、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效字段,不编造额外信息\n4. **台词不可缺失**:videoDesc 中有台词的分镜,必须完整输出台词和音色\n5. **台词类型正确标注**:普通对白用「说:」,内心独白用「内心OS:」,画外音用「画外音VO:」\n6. **先图片定义,后写分镜**:最前面必须先输出"图片定义"段,列出 \`@图N : 名字,描述\`\n7. **分镜正文禁用 \`@图N \`**:正文统一使用角色名/场景名,不写 \`@图1/@图2\` 等编号\n8. **单分镜时长最低 1s**\n9. **时长单位**:直接使用 videoDesc 中的秒数,格式为 \`{N}s\`(如 \`4s\`),最低 1s\n\n#### Seedance 2.0 完整示例\n\n输入:\n\`\`\`\n模型:Seedance2.0\n资产信息[A001, role, 沈辞], [A002, role, 苏锦], [A003, scene, 城楼]\n\`\`\`\n\`\`\`xml\n\n\n\`\`\`\n\n输出:\n\`\`\`\n画面风格和类型: 真人写实, 电影风格, 冷调, 古风\n\n参考定义:\n@图1: 沈辞,黑色长袍,气质冷峻的青年男性\n@图2: 苏锦,浅色衣裙,神情细腻的青年女性\n@图3: 城楼,古代砖石城楼与台阶场景\n\n生成一个由以下 2 个分镜组成的视频:\n\n场景:\n分镜过渡: 镜头平滑切换,从全景过渡到中景跟踪,焦点从沈辞独处转向苏锦到来。\n\n分镜1 4s: 时间:黄昏,场景:城楼,镜头:全景,平视略仰,静止镜头,沈辞独立城楼之上,负手而立,衣袂随风飘扬,目光远眺苍茫大地,神情肃然面容沉着,眼神坚定目光清冽,眉眼沉静气质凛然。无台词。背景是古城楼砖石纹理清晰,远方大地苍茫辽阔,天际线冷暖交替。黄昏斜射余晖侧逆光,冷调为主,长影拉伸,轮廓光微勾勒人物边缘,光感诗意。镜头静止。\n\n分镜2 4s: 时间:黄昏,场景:城楼,镜头:中景,平视,跟踪拍摄,苏锦拾级而上,走向城楼上的沈辞,面部朝向沈辞方向,神情微愣面色微变,眼神中带着担忧,苏锦说:「你又一个人在这里。」音色:女声,青年音色,音调中等偏高,音色质感明亮清脆,声音清亮柔和,发音方式干净,气息充沛平稳,语速适中,带温婉真诚感。背景城楼台阶纹理清晰,余晖渐暗,天际线冷暖交替加深。镜头跟踪苏锦移动。\n\`\`\`\n\n---\n\n### 四、Wan 2.6\n\n#### 核心原则\n- **单图首帧模式**:归类为首尾帧模式,但仅有首帧(分镜图),无尾帧\n- **单条分镜输入/输出**:每次仅输入一条 \`\` 及其关联资产信息,输出也仅为一段完整的叙事式提示词\n- **叙事式英文提示词**:像写小说一样描写画面,不使用标签罗列(不写 \`4K, cinematic, high quality\` 这类堆砌)\n- **三段式结构**:风格基调 → 主体动作 + 场景环境 + 光线氛围 → 镜头收尾\n- **纯文本提示词**:提示词内**不使用任何 \`@图N \` 引用**,全部内容用纯文本描述\n- **严格遵循 videoDesc**:提示词内容严格基于 videoDesc 中的画面描述、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效字段生成,不编造额外内容\n- **台词不可缺失**:videoDesc 中有台词的分镜,必须在提示词中体现台词相关描述\n- **台词类型标注**:区分普通对白(dialogue)、内心独白(inner monologue OS)、画外音(voiceover VO),在提示词中用括号标注\n\n#### prompt 生成模板\n\n每次输入一条分镜,输出一段完整提示词(无编号前缀),格式如下:\n\n\`\`\`\n{风格基调一句话定性},\n{主体名} {外观简述}, {具体动作/姿态描述}, {情绪/表情用动作暗示}.\n{场景背景主体}, {具体环境物件}, {空间感}, {时间/天气}.\n{光线方向/色温} {质感描述}, {情绪暗示光影}.\n{台词描述(如有,含 dialogue/OS/VO 标注)/ No dialogue}.\n{音效描述}.\n{拍摄方式}, {景别}, {视角}, {运镜方式}.\n\`\`\`\n\n#### 叙事式写法要点\n\n| 原则 | 说明 | 示例 |\n|------|------|------|\n| 风格基调放最前 | 一句话定性整体气质 | \`A cinematic epic scene\` / \`A melancholic cinematic scene\` |\n| 主体+动作紧密绑定 | 主体后面直接跟动作,外观细节嵌入主体描述 | \`A young man in dark flowing robes stands alone atop the city wall, hands clasped behind back\` |\n| 情绪用动作暗示 | 不直接陈述「他很悲伤」 | ❌ \`He is sad.\` → ✅ \`head drops slowly, shoulders slumped\` |\n| 环境融入叙事 | 不罗列环境属性 | ❌ \`The sky is blue. The grass is green.\` → ✅ \`hazy blue sky stretches over the emerald valley\` |\n| 光线单独成句 | 光线方向+色温+质感+情绪 | \`Warm golden hour light streams from behind, casting long shadows across the stone floor\` |\n| 镜头语言收尾 | 一句话点睛 | \`Captured in a wide establishing shot from a low-angle perspective, static camera\` |\n| 禁止标签堆砌 | 不写 \`4K, cinematic, high quality\` | \`cinematic\` 融入风格基调即可 |\n\n#### 生成约束\n1. **全部用英文**\n2. **不使用任何 \`@图N \` 引用**:提示词内不引用角色资产、场景资产、分镜图,全部内容用纯文本描述\n3. **叙事式描写**:像写小说一样构建画面,禁止标签罗列和配置清单式写法\n4. **主体用文字描述**:简要描述主体外观特征(如服饰、发型等关键辨识特征),嵌入主体描述中\n5. **严格遵循 videoDesc**:提示词内容严格基于 videoDesc 中的画面描述、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效字段,不编造额外信息\n6. **台词不可缺失**:videoDesc 中有台词的分镜,必须在提示词中完整输出台词内容(保持原始语言,不翻译)\n7. **台词类型标注**:普通对白标注 \`(dialogue)\`;内心独白标注 \`(inner monologue, OS)\`;画外音标注 \`(voiceover, VO)\`\n8. **单条输入/输出**:每次仅处理一条分镜,输出一段提示词,无编号前缀\n9. **无需标注时长**:时长由模型侧控制,提示词中不写时长参数\n10. **镜头描述融入叙事**:不用方括号标签,用完整句子描述镜头\n11. **视觉风格**参考 Assistant 中的「视觉风格约束」部分内容\n\n#### Wan 2.6 完整示例\n\n**示例1:无台词分镜**\n\n输入:\n\`\`\`\n模型:Wan2.6\n资产信息[A001, role, 沈辞], [A003, scene, 城楼]\n\`\`\`\n\`\`\`xml\n\n\`\`\`\n\n输出:\n\`\`\`\nA cinematic epic scene with a cold, desaturn\`ated palette,\nA lone man in dark flowing robes stands atop an ancient city wall, hands clasped behind his back, robes and hair billowing in the wind, gaze fixed on the vast land stretching to the horizon, jaw set firm, eyes unwavering.\nThe weathered stone battlements frame the endless expanse below, rolling terrain fading into haze beneath a heavy dusk sky, clouds layered in muted golds and slate greys.\nCold side-backlight from the setting sun carves a sharp silhouette, long shadows stretching across the stone floor, a faint warm rim outlining the figure against the cool atmosphere.\nNo dialogue.\nWind howling across the open wall, fabric flapping rhythmically.\nCaptured in a wide establishing shot from a slightly low angle, static camera, single continuous take.\n\`\`\`\n\n**示例2:有台词分镜**\n\n输入:\n\`\`\`\n模型:Wan2.6\n资产信息[A001, role, 沈辞], [A002, role, 苏锦], [A003, scene, 城楼]\n\`\`\`\n\`\`\`xml\n\n\`\`\`\n\n输出:\n\`\`\`\nA melancholic cinematic scene, dusk tones deepening,\nA young woman in a light-colored dress ascends the final stone steps onto the city wall, her gaze locked on the lone figure ahead, brow slightly furrowed, pace slowing as she approaches, lips parting softly.\nThe ancient city wall stretches behind her, weathered stairs leading up from below, the distant skyline dimming as the last traces of golden hour fade into twilight.\nFading warm light mingles with rising cool blue tones, the contrast between the two figures softened by the diffused remnants of sunset.\n"你又一个人在这里。" — Su Jin (dialogue).\nFootsteps on stone, wind sweeping across the battlements, fabric rustling.\nA medium tracking shot follows the woman from behind as she ascends and approaches, handheld camera with subtle movement, single continuous take.\n\`\`\`\n\n---\n\n## 景别 → 镜头标签映射\n\n| videoDesc 中的景别 | KlingOmni(英文标签) | Seedance 1.5(英文标签) | Seedance 2.0(中文描述) | Wan 2.6(英文叙事式) |\n|------|------|------|------|------|\n| 远景 | extreme wide shot | Extreme wide shot | 远景 | an extreme wide shot capturing the vast expanse |\n| 全景 | wide shot | Wide establishing shot | 全景 | a wide establishing shot |\n| 中景 | medium shot | Medium shot | 中景 | a medium shot |\n| 近景 | close-up | Close-up | 近景 | a close-up shot |\n| 特写 | close-up | Close-up | 特写 | a close-up capturing fine detail |\n| 大特写 | extreme close-up | Extreme close-up | 大特写 | an extreme close-up |\n\n## 运镜 → 镜头标签映射\n\n| videoDesc 中的运镜 | KlingOmni(英文标签) | Seedance 1.5(英文标签) | Seedance 2.0(中文描述) | Wan 2.6(英文叙事式) |\n|------|------|------|------|------|\n| 静止 | static camera | Static, no camera movement | 镜头静止 | static camera, locked off |\n| 推进 | dolly in / push in | Slow dolly forward | 镜头缓慢向前推进 | camera slowly pushing in |\n| 拉远 | dolly out / pull back | Slow dolly backward pull | 镜头缓慢向后拉远 | camera gently pulling back |\n| 跟踪 | tracking shot | Tracking shot, handheld | 跟踪拍摄 | tracking shot following the subject |\n| 摇镜 | pan left/right | Slow pan | 镜头缓慢摇移 | smooth pan across the scene |\n| 甩镜 | whip pan | Whip pan | 快速甩镜 | whip pan |\n| 升降 | crane up/down | Crane up/down | 镜头升降 | crane rising / descending |\n| 环绕 | surround shooting | Orbiting shot | 环绕拍摄 | orbiting around the subject |\n\n---\n\n## 执行流程\n\n1. **解析输入**:提取模型名和多参标志,按路由规则匹配模式;提取资产列表\n2. **构建 @图N 编号表**:资产按输入顺序从 \`@图1 \` 起编号,分镜图接续编号;\`shouldGenerateImage="false"\` 的分镜不分配分镜图编号\n3. **逐条解析 \`\`**:按 videoDesc 解析规则提取12个字段,结合 \`duration\`、\`associateAssetsIds\` 建立标签映射\n4. **整合为一个完整的视频提示词**:按目标模型格式编排全部分镜\n5. **输出视频提示词**\n\n---\n\n## 约束\n\n- **仅输出视频提示词**:不附加任何解释、注释、分析过程、推理步骤、模型匹配说明、资产编号表、分隔线(\`---\`)或额外说明,只输出视频提示词文本。禁止在提示词前后输出任何非提示词内容\n- **严格遵循 videoDesc**(全模式通用):提示词内容严格基于 videoDesc 中的画面描述、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效字段生成,不编造额外内容\n- **台词不可缺失**(全模式通用):videoDesc 中有台词的分镜,必须在提示词中完整体现台词内容,不得遗漏\n- **台词保持原始输入**(全模式通用):台词内容严禁翻译,必须保持 videoDesc 中的原始语言原样输出\n- **台词类型标注**(全模式通用):必须区分普通对白(dialogue / 说)、内心独白(OS / 内心OS)、画外音(VO / 画外音VO),并在提示词中正确标注\n- **时间跨度最低 1 秒**(全模式通用):所有模式中涉及时间分段(Motion 时间轴 / Seedance 2.0 分镜时长 {N}s)的最小粒度为 1 秒(1s),禁止出现 0.5 秒等低于 1 秒的间隔\n- **视觉风格**:风格相关描述参考 Assistant 中的「视觉风格约束」部分内容,不在本 Skill 内自行定义风格\n- **严格按匹配到的模式格式**,不混用不同模式的格式\n- **不修改原始输入**:不改写 \`\` 的任何字段;\`prompt\` 已有的分镜图提示词仅作画面参考\n- **不编造资产或台词**:只使用输入中的资产信息;无台词则标注「无台词」/ \`No dialogue\`\n- **时长单位**:Seedance 2.0 的分镜时长直接使用秒,格式为 \`{N}s\`(如 \`4s\`),最低 1s\n`, - }); + await db("o_prompt").where("type", "videoPromptGeneration").update({ + data: `# 视频提示词生成 Skill\n\n你是**视频提示词生成 Agent**,专门负责根据指定的 AI 视频模型,读取分镜信息并输出该模型对应格式的视频提示词。\n\n---\n\n## 输入格式\n\n### 1. 模型与模式(必选)\n\n\n#### 模式路由规则\n\n| 条件 | 匹配模式 | 说明 |\n|------|----------|------|\n| 模型名为 \`seedance-2-0\` + \`多参:是\` / \`seedance 2.0\` + \`多参:是\` / \`即梦2.0\` + \`多参:是\` | **seedance-2-0*,不包含其他版本比如seedance-1-5/seedance-1-0 | 支持角色/场景/分镜图多参引用 |\n| 模型名为 \`Wan2.6\` / \`wan 2.6\` / \`万象2.6\` | **Wan 2.6** | 固定模式,单图(首帧)+ 叙事文本,无尾帧 |\n| 其他任何模型 + \`多参:是\` | **通用多参模式** | 支持角色/场景/分镜图多参引用 |\n| 其他任何模型/seedance-1-5/seedance-1-0 + \`多参:否\` | **通用首尾帧模式** | 首帧/首尾帧 + 纯文本描述 |\n\n> 模型名仅用于记录,实际提示词格式由匹配到的模式决定。Seedance 2.0 和 Wan 2.6 是指定模型名即确定模式的特例。\n\n### 2. 资产信息\n\n\`\`\`\n资产信息[id, type, name], [id, type, name], ...\n\`\`\`\n\n- \`id\`:资产唯一标识(如 \`A001\`)\n- \`type\`:资产类型,取值 \`role\`(角色)/ \`scene\`(场景)/ \`prop\`(道具)\n- \`name\`:资产名称(如 \`沈辞\`、\`城楼\`、\`长剑\`)\n\n### 3. 分镜信息\n\n分镜以 \`\` XML 标签列表的形式传入,每条分镜结构如下:\n\n\`\`\`xml\n\n\`\`\`\n\n#### 输入字段说明\n\n| 属性 | 说明 | 来源 |\n|------|------|------|\n| \`videoDesc\` | **核心输入**:分镜的结构化画面描述,包含画面描述、场景、关联资产名称、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效、关联资产ID | 用户/上游系统填写 |\n| \`prompt\` | **已有字段**:上游生成的分镜图提示词,作为辅助参考上下文,**不修改** | 上游系统已填写 |\n| \`track\` | 分镜分组标识 | 用户/上游系统填写 |\n| \`duration\` | 视频推荐时长(秒) | 用户/上游系统填写 |\n| \`associateAssetsIds\` | 该分镜关联的资产ID列表 | 用户/上游系统填写 |\n| \`shouldGenerateImage\` | 是否需要生成分镜图片,默认 \`true\` | 用户/上游系统填写 |\n\n---\n\n## 任务目标\n\n读取所有 \`\` 的属性,结合资产信息,根据指定模型的提示词格式,将全部分镜整合为一个完整的视频提示词。\n\n---\n\n## 输出格式\n\n将所有分镜整合为**一个完整的视频提示词**输出(非逐条独立):\n\n| 模式 | 整合方式 |\n|------|----------|\n| **通用多参模式** | \`[References]\` 汇总所有 \`@图N \` 引用;\`[Instruction]\` 按时间顺序描述完整叙事 |\n| **通用首尾帧模式** | 纯文本五维度(Visual / Motion / Camera / Audio / Narrative),不使用任何 \`@图N \` 引用,按时间轴连续编排(\`[Motion]\` 0s → 总时长,每段最低 1 秒),全程单一连贯镜头,不切镜 |\n| **Seedance 2.0** | \`生成一个由以下 N 个分镜组成的视频\`,每条对应 \`分镜N{N}s\` 段落 |\n| **Wan 2.6** | 单图首帧模式,每次仅输入一条分镜,输出一段叙事式英文提示词(三段式:风格基调 → 主体动作+场景环境+光线氛围 → 镜头收尾),不使用 \`@图N \` 引用 |\n\n- 仅输出视频提示词文本,不输出 XML 标签,不附加解释\n\n---\n\n## videoDesc 解析规则\n\n从 \`videoDesc\` 括号内按顿号分隔提取以下结构化字段:\n\n\`\`\`\n({画面描述}、{场景}、{关联资产名称}、{时长}、{景别}、{运镜}、{角色动作}、{情绪}、{光影氛围}、{台词}、{音效}、{关联资产ID})\n\`\`\`\n\n| 序号 | 字段 | 用途 | 示例 |\n|------|------|------|------|\n| 1 | 画面描述 | prompt 的叙事主干 | 沈辞独立城楼远眺苍茫大地 |\n| 2 | 场景 | 匹配场景资产 | 城楼 |\n| 3 | 关联资产名称 | 匹配角色/道具资产 | 沈辞/城楼 |\n| 4 | 时长 | 控制时长参数 | 4s |\n| 5 | 景别 | 控制镜头景别 | 全景 |\n| 6 | 运镜 | 控制运镜方式 | 静止 |\n| 7 | 角色动作 | prompt 动作描写 | 负手而立衣袂随风飘扬 |\n| 8 | 情绪 | prompt 情绪氛围 | 坚定决绝 |\n| 9 | 光影氛围 | prompt 光影描写 | 黄昏冷调侧逆光 |\n| 10 | 台词 | prompt 台词/音频段 | 无台词 / 具体台词内容 |\n| 11 | 音效 | prompt 音效描写 | 风声衣袂声 |\n| 12 | 关联资产ID | 用于资产ID↔角色标签映射 | A001/A002 |\n\n---\n\n## 资产引用编号规则\n\n所有模型统一使用 \`@图N \` 格式引用资产和分镜图,编号按输入顺序连续递增:\n\n1. **资产**:按资产信息中 \`[id, type, name]\` 的出现顺序,从 \`@图1 \` 开始编号(不区分 role / scene / prop)。**资产类型的出现顺序不固定**——可能先 scene 后 character,也可能 prop 在前、character 在后,或任意交替出现,编号严格按输入位置分配,不按类型归组\n2. **分镜图**:每条 \`\` 对应一张分镜图,编号接续资产之后\n3. **跳过无分镜图的条目**:当 \`shouldGenerateImage="false"\` 时,该分镜未生成图片,**不分配**分镜图编号,后续编号顺延\n\n#### 示例\n\n输入 3 个资产 + 2 条分镜:\n\`\`\`\n资产信息[A001, role, 沈辞], [A002, role, 苏锦], [A003, scene, 城楼]\n\`\`\`\n\`\`\`xml\n \n \n\`\`\`\n\n编号结果:\n\n| 输入项 | 引用标签 | 说明 |\n|--------|----------|------|\n| [A001, role, 沈辞] | \`@图1 \` | 角色·沈辞 参考图 |\n| [A002, role, 苏锦] | \`@图2 \` | 角色·苏锦 参考图 |\n| [A003, scene, 城楼] | \`@图3 \` | 场景·城楼 参考图 |\n| storyboardItem 第1条 | \`@图4 \` | 分镜图1 |\n| storyboardItem 第2条 | \`@图5 \` | 分镜图2 |\n\n**混合顺序示例**\n\n输入 3 个资产(场景在前)+ 2 条分镜:\n\`\`\`\n资产信息[A003, scene, 城楼], [A001, role, 沈辞], [A002, role, 苏锦]\n\`\`\`\n\`\`\`xml\n \n \n\`\`\`\n\n编号结果:\n\n| 输入项 | 引用标签 | 说明 |\n|--------|----------|------|\n| [A003, scene, 城楼] | \`@图1 \` | 场景·城楼 参考图 |\n| [A001, role, 沈辞] | \`@图2 \` | 角色·沈辞 参考图 |\n| [A002, role, 苏锦] | \`@图3 \` | 角色·苏锦 参考图 |\n| storyboardItem 第1条 | \`@图4 \` | 分镜图1 |\n| storyboardItem 第2条 | \`@图5 \` | 分镜图2 |\n\n> **关键**:此例中 \`@图1 \` 是场景而非角色,\`@图2 \` \`@图3 \` 才是角色。生成提示词时,必须根据资产的实际 \`type\` 字段确定引用方式,而非根据编号大小假定类型。\n\n---\n\n## 模型提示词生成规则\n\n### 一、通用多参模式\n\n#### 核心原则\n- MVL 多模态融合:自然语言 + 图像引用在同一语义空间\n- 分镜图序列负责动作/时间轴/构图,场景参考图负责环境一致性\n- 所有资产和分镜图统一用 \`@图N \` 引用\n- **严格遵循 videoDesc**:提示词内容严格基于 videoDesc 中的画面描述、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效字段生成,不编造额外内容\n- **台词不可缺失**:videoDesc 中有台词的分镜,必须在 Instruction 中体现台词相关描述\n- **台词类型标注**:区分普通对白(dialogue)、内心独白(inner monologue OS)、画外音(voiceover VO),在 Instruction 中用括号标注\n\n#### prompt 生成模板\n\n> **注意**:\`[References]\` 中的 \`@图N\` 编号严格按资产输入顺序分配,角色/场景/道具可能出现在任意编号位置。生成时需根据每个资产的 \`type\` 字段确定其引用方式,不可假定固定的类型-编号对应关系。\n\n\`\`\`\n[References]\n@图{资产1编号} : [{资产1名称}参考图] ← 可能是 role/scene/prop 中的任意类型\n@图{资产2编号} : [{资产2名称}参考图]\n@图{资产3编号} : [{资产3名称}参考图]\n...\n@图{分镜图编号} : [分镜图1] ← 分镜图编号接续资产之后\n\n[Instruction]\nBased on the storyboard @图{分镜图编号} :\n@图{角色资产编号} {动作/状态描述(英文)},\nset in the {场景描述(英文)} of @图{场景资产编号} ,\n{镜头/运镜描述(英文)},\n{情感基调(英文)},\n{台词描述(英文,含 dialogue/OS/VO 标注)/ No dialogue},\n{音效描述(英文)}.\n\`\`\`\n\n#### 生成约束\n1. **Instruction 必须用英文**\n2. **严格遵循 videoDesc**:提示词内容严格基于 videoDesc 的画面描述、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效字段,不编造额外信息\n3. **角色动作**从 videoDesc 的「角色动作」字段提取,翻译为简洁英文动作描述\n4. **台词不可缺失**:videoDesc 中有台词的分镜,必须在 Instruction 中体现台词内容(保持原始语言,不翻译)\n5. **台词类型标注**:普通对白标注 \`(dialogue)\`;内心独白标注 \`(inner monologue, OS)\`;画外音标注 \`(voiceover, VO)\`\n6. **镜头风格**使用标准标签:\`cinematic\` / \`wide-angle\` / \`close-up\` / \`slow motion\` / \`surround shooting\` / \`handheld\`\n7. **空间关系**使用标准动词:\`wearing\` / \`holding\` / \`standing on\` / \`following behind\` / \`sitting in\`\n8. 单条分镜对应单个 \`@图N \`,不做多帧跨镜描述\n9. 无需描述角色外观(由参考图负责)\n10. 无时长标注(由模型推断)\n11. **无分镜图时**:当 \`shouldGenerateImage="false"\` 时,该分镜无分镜图,\`[References]\` 中不列出该分镜图,\`[Instruction]\` 中不使用 \`@图N \` 引用该分镜图,改为纯文本描述画面内容\n\n#### KlingOmni 完整示例\n\n输入:\n\`\`\`\n模型:KlingOmni\n资产信息[A001, role, 沈辞], [A002, role, 苏锦], [A003, scene, 城楼]\n\`\`\`\n\`\`\`xml\n\n\n\`\`\`\n\n输出:\n\`\`\`\n[References]\n@图1 : [沈辞参考图]\n@图2 : [苏锦参考图]\n@图3 : [城楼参考图]\n@图4 : [分镜图1]\n@图5 : [分镜图2]\n\n[Instruction]\nBased on the storyboard from @图4 to @图5 :\n@图1 standing alone atop the city wall, hands clasped behind back, robes billowing in the wind, gazing across the vast land,\n@图2 ascending the steps toward @图1 , expression worried,\nset in the ancient city wall environment of @图3 ,\nwide shot transitioning to medium tracking shot, cinematic,\nresolute determination shifting to concerned anticipation, dusk cold-toned side-backlit atmosphere fading,\nno dialogue,\nwind howling, fabric flapping, footsteps on stone.\n\`\`\`\n\n---\n\n### 二、通用首尾帧模式\n\n#### 核心原则\n- **纯文本提示词**:提示词内**不使用任何 \`@图N \` 引用**(不引用角色资产、场景资产、也不引用分镜图),全部内容用纯文本描述\n- **五维度结构**:Visual / Motion / Camera / Audio / Narrative\n- **严格遵循 videoDesc**:提示词内容严格基于 videoDesc 中的画面描述、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效字段生成,不编造额外内容\n- **台词不可缺失**:videoDesc 中有台词的分镜,必须在 \`[Audio]\` 中完整输出台词内容\n- **台词类型标注**:区分普通对白(dialogue, lip-sync active)、内心独白(inner monologue OS, silent lips)、画外音(voiceover VO, silent lips),并在 \`[Audio]\` 中明确标注\n- **不说话的主体标注 \`silent\`** — 防止误生口型\n- **全程单一连贯镜头**:从头到尾一个镜头,不存在切镜\n- **时间轴分段**:每段最低 1 秒,用 \`0s-Xs\` 标注\n\n#### prompt 生成模板\n\n\`\`\`\n[Visual]\n{主体A名}: {外观简述}, {站位/姿态}, {说话状态 speaking/silent}.\n{主体B名}: {外观简述}, {站位/姿态}, {说话状态}.\n{场景描述}, {道具描述}.\n{视觉风格标签}.\n\n[Motion]\n0s-{X}s: {主体A名} {动作描述段1}.\n{X}s-{Y}s: {主体B名} {动作描述段2}.\n\n[Camera]\n{镜头类型}, {运镜方式}, {全程单一连贯镜头描述}.\n\n[Audio]\n{Xs-Ys}: "{台词内容}" — {说话者名} ({dialogue / inner monologue OS / voiceover VO}), {lip-sync active / silent lips}.\n{音效描述}.\n\n[Narrative]\n{情节点概述}, {叙事位置}.\n\`\`\`\n\n#### 生成约束\n1. **全部用英文**\n2. **不使用任何 \`@图N \` 引用**:提示词内不引用角色资产、场景资产、分镜图,全部内容用纯文本描述\n3. **主体用文字描述**:在 [Visual] 中简要描述主体外观特征(如服饰、发型等关键辨识特征)\n4. **严格遵循 videoDesc**:提示词内容严格基于 videoDesc 中的画面描述、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效字段,不编造额外信息\n5. **每个主体必须标注说话状态**:\`speaking\` / \`silent\` / \`speaking simultaneously\`\n6. **台词不可缺失**:videoDesc 中有台词的分镜,必须在 \`[Audio]\` 中完整输出台词内容(保持原始语言,不翻译)\n7. **台词类型标注**:普通对白标注 \`dialogue, lip-sync active\`;内心独白标注 \`inner monologue (OS), silent lips\`;画外音标注 \`voiceover (VO), silent lips\`\n8. **Motion 时间轴**每段最低 1 秒,不超过总时长\n9. **全程单一连贯镜头**:Camera 段落描述从头到尾的一个镜头,绝不切镜\n10. **视觉风格**参考 Assistant 中的「视觉风格约束」部分内容\n11. **镜头类型**从以下选取:\`Wide establishing shot / Over-the-shoulder / Medium shot / Close-up / Wide shot / POV / Dutch angle / Crane up / Dolly right / Whip pan / Handheld / Slow motion\`\n\n#### Seedance 1.5 Pro 完整示例\n\n输入:\n\`\`\`\n模型:Seedance1.5\n资产信息[A001, role, 沈辞], [A002, role, 苏锦], [A003, scene, 城楼]\n\`\`\`\n\`\`\`xml\n\n\n\`\`\`\n\n输出:\n\`\`\`\n[Visual]\nShen Ci: male, dark flowing robes, hair tied up, standing alone atop city wall, hands clasped behind back, robes billowing, silent.\nSu Jin: female, light-colored dress, hair partially down, ascending steps toward Shen Ci, expression worried, silent.\nAncient city wall, vast open land beyond, dusk sky fading.\nCinematic, photorealistic, 4K, high contrast, desaturated tones, shallow depth of field.\n\n[Motion]\n0s-4s: Shen Ci stands still on city wall edge, robes flutter in wind, hair sways gently. Gaze fixed on distant horizon.\n4s-8s: Su Jin climbs the last few steps onto the wall, walks toward Shen Ci. Shen Ci remains still, unaware. Su Jin slows as she approaches.\n\n[Camera]\nWide establishing shot, static for first 4 seconds capturing the lone figure. Then smooth transition to medium tracking shot following the woman ascending steps, single continuous take throughout, no cuts.\n\n[Audio]\n0s-4s: Wind howling across wall, fabric flapping rhythmically. No dialogue.\n4s-8s: Footsteps on stone, robes rustling. No dialogue.\nShen Ci — silent. Su Jin — silent.\n\n[Narrative]\nLone figure on city wall, then arrival of a companion. Tension between determination and concern. Single continuous take.\n\`\`\`\n\n---\n\n### 三、Seedance 2.0\n\n#### 核心原则\n- **结构化12维编码**:统一用 \`@图N \` 引用资产和分镜图,时长 \`{N}s\`\n- **最前面先定义图片映射**:先输出“图片定义”段,集中声明 \`@图N : 主体名字/场景名字,简述\`;后续分镜正文只使用主体名字,不再写 \`@图N \`\n- **音色参数9维度精细描述**(有台词时必填)\n- **秒级时长控制**:单分镜时长最低 1s\n- **中文提示词**\n- **严格遵循 videoDesc**:每条分镜的描述内容严格基于 videoDesc 中的画面描述、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效字段生成,不编造额外内容\n- **台词不可缺失**:videoDesc 中有台词的分镜,必须完整输出台词和音色描述\n- **台词类型标注**:区分普通对白(直接使用「说:」)、内心独白(使用「内心OS:」)、画外音(使用「画外音VO:」),并匹配对应的嘴型状态描述\n\n#### prompt 生成模板\n\n> **注意**:\`@图{编号}\` 仅用于最前面的“图片定义”段。分镜正文中禁止再写 \`@图{编号}\`,统一改用主体名字/场景名字。\n\n**单分镜模板:**\n\`\`\`\n画面风格和类型: {风格}, {色调}, {类型}\n\n图片定义:\n@图1: {资产1名字},{简述}\n@图2: {资产2名字},{简述}\n@图N: {资产N名字},{简述}\n...\n\n生成一个由以下 1 个分镜组成的视频:\n\n场景:\n分镜过渡: 无\n\n分镜1 {N}s: 时间:{日/夜/晨/黄昏},场景:{场景名字},镜头:{景别},{角度},{运镜},{角色名字} {动作/表情/视线朝向/站位描述}。{台词与音色描述(如有)}。{背景环境补充}。{光影氛围}。{运镜补充}。\n\`\`\`\n\n**多分镜模板:**\n\`\`\`\n画面风格和类型: {风格}, {色调}, {类型}\n\n图片定义:\n@图1: {资产1名字},{简述}\n@图2: {资产2名字},{简述}\n@图N: {资产N名字},{简述}\n...\n\n生成一个由以下 {N} 个分镜组成的视频:\n\n场景:\n分镜过渡: {全局过渡描述}\n\n分镜1 {N}s: 时间:{...},场景:{场景名字},镜头:{...},{角色名字} {...}。{...}。\n分镜2{N}s: ...\n...\n\`\`\`\n\n#### 音色生成规则(有台词时必填)\n\n台词格式:\`{角色名字} 说:「{台词内容}」音色:{9维度描述}\`\n\n9维度按顺序填写:\n\`\`\`\n{性别},{年龄音色},{音调},{音色质感},{声音厚度},{发音方式},{气息},{语速},{特殊质感}\n\`\`\`\n\n> 当 desc 中未明确音色信息时,根据角色类型从以下参考表推断:\n\n| 角色类型特征 | 默认音色 |\n|------------|---------|\n| 男性权威/霸气角色 | 男声,中年音色,音调低沉,音色浑厚有力,声音厚重,发音标准,气息极其沉稳,语速偏慢 |\n| 女性温柔/甜美角色 | 女声,青年音色,音调中等偏高,音色质感明亮清脆,声音清亮柔和,气息充沛平稳,带温婉真诚感 |\n| 男性年轻/普通角色 | 男声,青年音色,音调中等,音色干净,声音厚度适中,发音清晰,气息平稳,语速适中 |\n| 女性活泼/外向角色 | 女声,青年音色,音调偏高,音色清脆活泼,声音轻盈,气息充沛,语速偏快,带笑意和感染力 |\n| 反派/冷酷角色 | 男声,中年音色,音调低沉,音色质感干燥偏暗,声音带沙砾感,气息平稳,语速极慢,有威胁感 |\n\n#### 无台词分镜处理\n- 不写 \`说:\` 和音色段落\n- 在动作描述后标注 \`无台词\`\n\n#### 台词类型格式\n\n| 台词类型 | 格式 | 嘴型描述 |\n|----------|------|----------|\n| 普通对白 | \`{角色名字} 说:「{台词}」音色:{9维度}\` | 角色嘴部开合说话 |\n| 内心独白 | \`{角色名字} 内心OS:「{台词}」音色:{9维度}\` | 角色嘴部紧闭不动 |\n| 画外音 | \`{角色名字} 画外音VO:「{台词}」音色:{9维度}\` | 角色嘴部紧闭不动(或角色不在画面中) |\n\n#### 生成约束\n1. **中文提示词**\n2. **直接输出视频提示词**:禁止输出任何分析过程、推理步骤、模型匹配说明、资产编号表、分隔线等非提示词内容。第一行必须是 \`画面风格和类型:\`\n3. **严格遵循 videoDesc**:每条分镜内容严格基于 videoDesc 的画面描述、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效字段,不编造额外信息\n4. **台词不可缺失**:videoDesc 中有台词的分镜,必须完整输出台词和音色\n5. **台词类型正确标注**:普通对白用「说:」,内心独白用「内心OS:」,画外音用「画外音VO:」\n6. **先图片定义,后写分镜**:最前面必须先输出"图片定义"段,列出 \`@图N : 名字,描述\`\n7. **分镜正文禁用 \`@图N \`**:正文统一使用角色名/场景名,不写 \`@图1/@图2\` 等编号\n8. **单分镜时长最低 1s**\n9. **时长单位**:直接使用 videoDesc 中的秒数,格式为 \`{N}s\`(如 \`4s\`),最低 1s\n\n#### Seedance 2.0 完整示例\n\n输入:\n\`\`\`\n模型:Seedance2.0\n资产信息[A001, role, 沈辞], [A002, role, 苏锦], [A003, scene, 城楼]\n\`\`\`\n\`\`\`xml\n\n\n\`\`\`\n\n输出:\n\`\`\`\n画面风格和类型: 真人写实, 电影风格, 冷调, 古风\n\n参考定义:\n@图1: 沈辞,黑色长袍,气质冷峻的青年男性\n@图2: 苏锦,浅色衣裙,神情细腻的青年女性\n@图3: 城楼,古代砖石城楼与台阶场景\n\n生成一个由以下 2 个分镜组成的视频:\n\n场景:\n分镜过渡: 镜头平滑切换,从全景过渡到中景跟踪,焦点从沈辞独处转向苏锦到来。\n\n分镜1 4s: 时间:黄昏,场景:城楼,镜头:全景,平视略仰,静止镜头,沈辞独立城楼之上,负手而立,衣袂随风飘扬,目光远眺苍茫大地,神情肃然面容沉着,眼神坚定目光清冽,眉眼沉静气质凛然。无台词。背景是古城楼砖石纹理清晰,远方大地苍茫辽阔,天际线冷暖交替。黄昏斜射余晖侧逆光,冷调为主,长影拉伸,轮廓光微勾勒人物边缘,光感诗意。镜头静止。\n\n分镜2 4s: 时间:黄昏,场景:城楼,镜头:中景,平视,跟踪拍摄,苏锦拾级而上,走向城楼上的沈辞,面部朝向沈辞方向,神情微愣面色微变,眼神中带着担忧,苏锦说:「你又一个人在这里。」音色:女声,青年音色,音调中等偏高,音色质感明亮清脆,声音清亮柔和,发音方式干净,气息充沛平稳,语速适中,带温婉真诚感。背景城楼台阶纹理清晰,余晖渐暗,天际线冷暖交替加深。镜头跟踪苏锦移动。\n\`\`\`\n\n---\n\n### 四、Wan 2.6\n\n#### 核心原则\n- **单图首帧模式**:归类为首尾帧模式,但仅有首帧(分镜图),无尾帧\n- **单条分镜输入/输出**:每次仅输入一条 \`\` 及其关联资产信息,输出也仅为一段完整的叙事式提示词\n- **叙事式英文提示词**:像写小说一样描写画面,不使用标签罗列(不写 \`4K, cinematic, high quality\` 这类堆砌)\n- **三段式结构**:风格基调 → 主体动作 + 场景环境 + 光线氛围 → 镜头收尾\n- **纯文本提示词**:提示词内**不使用任何 \`@图N \` 引用**,全部内容用纯文本描述\n- **严格遵循 videoDesc**:提示词内容严格基于 videoDesc 中的画面描述、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效字段生成,不编造额外内容\n- **台词不可缺失**:videoDesc 中有台词的分镜,必须在提示词中体现台词相关描述\n- **台词类型标注**:区分普通对白(dialogue)、内心独白(inner monologue OS)、画外音(voiceover VO),在提示词中用括号标注\n\n#### prompt 生成模板\n\n每次输入一条分镜,输出一段完整提示词(无编号前缀),格式如下:\n\n\`\`\`\n{风格基调一句话定性},\n{主体名} {外观简述}, {具体动作/姿态描述}, {情绪/表情用动作暗示}.\n{场景背景主体}, {具体环境物件}, {空间感}, {时间/天气}.\n{光线方向/色温} {质感描述}, {情绪暗示光影}.\n{台词描述(如有,含 dialogue/OS/VO 标注)/ No dialogue}.\n{音效描述}.\n{拍摄方式}, {景别}, {视角}, {运镜方式}.\n\`\`\`\n\n#### 叙事式写法要点\n\n| 原则 | 说明 | 示例 |\n|------|------|------|\n| 风格基调放最前 | 一句话定性整体气质 | \`A cinematic epic scene\` / \`A melancholic cinematic scene\` |\n| 主体+动作紧密绑定 | 主体后面直接跟动作,外观细节嵌入主体描述 | \`A young man in dark flowing robes stands alone atop the city wall, hands clasped behind back\` |\n| 情绪用动作暗示 | 不直接陈述「他很悲伤」 | ❌ \`He is sad.\` → ✅ \`head drops slowly, shoulders slumped\` |\n| 环境融入叙事 | 不罗列环境属性 | ❌ \`The sky is blue. The grass is green.\` → ✅ \`hazy blue sky stretches over the emerald valley\` |\n| 光线单独成句 | 光线方向+色温+质感+情绪 | \`Warm golden hour light streams from behind, casting long shadows across the stone floor\` |\n| 镜头语言收尾 | 一句话点睛 | \`Captured in a wide establishing shot from a low-angle perspective, static camera\` |\n| 禁止标签堆砌 | 不写 \`4K, cinematic, high quality\` | \`cinematic\` 融入风格基调即可 |\n\n#### 生成约束\n1. **全部用英文**\n2. **不使用任何 \`@图N \` 引用**:提示词内不引用角色资产、场景资产、分镜图,全部内容用纯文本描述\n3. **叙事式描写**:像写小说一样构建画面,禁止标签罗列和配置清单式写法\n4. **主体用文字描述**:简要描述主体外观特征(如服饰、发型等关键辨识特征),嵌入主体描述中\n5. **严格遵循 videoDesc**:提示词内容严格基于 videoDesc 中的画面描述、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效字段,不编造额外信息\n6. **台词不可缺失**:videoDesc 中有台词的分镜,必须在提示词中完整输出台词内容(保持原始语言,不翻译)\n7. **台词类型标注**:普通对白标注 \`(dialogue)\`;内心独白标注 \`(inner monologue, OS)\`;画外音标注 \`(voiceover, VO)\`\n8. **单条输入/输出**:每次仅处理一条分镜,输出一段提示词,无编号前缀\n9. **无需标注时长**:时长由模型侧控制,提示词中不写时长参数\n10. **镜头描述融入叙事**:不用方括号标签,用完整句子描述镜头\n11. **视觉风格**参考 Assistant 中的「视觉风格约束」部分内容\n\n#### Wan 2.6 完整示例\n\n**示例1:无台词分镜**\n\n输入:\n\`\`\`\n模型:Wan2.6\n资产信息[A001, role, 沈辞], [A003, scene, 城楼]\n\`\`\`\n\`\`\`xml\n\n\`\`\`\n\n输出:\n\`\`\`\nA cinematic epic scene with a cold, desaturn\`ated palette,\nA lone man in dark flowing robes stands atop an ancient city wall, hands clasped behind his back, robes and hair billowing in the wind, gaze fixed on the vast land stretching to the horizon, jaw set firm, eyes unwavering.\nThe weathered stone battlements frame the endless expanse below, rolling terrain fading into haze beneath a heavy dusk sky, clouds layered in muted golds and slate greys.\nCold side-backlight from the setting sun carves a sharp silhouette, long shadows stretching across the stone floor, a faint warm rim outlining the figure against the cool atmosphere.\nNo dialogue.\nWind howling across the open wall, fabric flapping rhythmically.\nCaptured in a wide establishing shot from a slightly low angle, static camera, single continuous take.\n\`\`\`\n\n**示例2:有台词分镜**\n\n输入:\n\`\`\`\n模型:Wan2.6\n资产信息[A001, role, 沈辞], [A002, role, 苏锦], [A003, scene, 城楼]\n\`\`\`\n\`\`\`xml\n\n\`\`\`\n\n输出:\n\`\`\`\nA melancholic cinematic scene, dusk tones deepening,\nA young woman in a light-colored dress ascends the final stone steps onto the city wall, her gaze locked on the lone figure ahead, brow slightly furrowed, pace slowing as she approaches, lips parting softly.\nThe ancient city wall stretches behind her, weathered stairs leading up from below, the distant skyline dimming as the last traces of golden hour fade into twilight.\nFading warm light mingles with rising cool blue tones, the contrast between the two figures softened by the diffused remnants of sunset.\n"你又一个人在这里。" — Su Jin (dialogue).\nFootsteps on stone, wind sweeping across the battlements, fabric rustling.\nA medium tracking shot follows the woman from behind as she ascends and approaches, handheld camera with subtle movement, single continuous take.\n\`\`\`\n\n---\n\n## 景别 → 镜头标签映射\n\n| videoDesc 中的景别 | KlingOmni(英文标签) | Seedance 1.5(英文标签) | Seedance 2.0(中文描述) | Wan 2.6(英文叙事式) |\n|------|------|------|------|------|\n| 远景 | extreme wide shot | Extreme wide shot | 远景 | an extreme wide shot capturing the vast expanse |\n| 全景 | wide shot | Wide establishing shot | 全景 | a wide establishing shot |\n| 中景 | medium shot | Medium shot | 中景 | a medium shot |\n| 近景 | close-up | Close-up | 近景 | a close-up shot |\n| 特写 | close-up | Close-up | 特写 | a close-up capturing fine detail |\n| 大特写 | extreme close-up | Extreme close-up | 大特写 | an extreme close-up |\n\n## 运镜 → 镜头标签映射\n\n| videoDesc 中的运镜 | KlingOmni(英文标签) | Seedance 1.5(英文标签) | Seedance 2.0(中文描述) | Wan 2.6(英文叙事式) |\n|------|------|------|------|------|\n| 静止 | static camera | Static, no camera movement | 镜头静止 | static camera, locked off |\n| 推进 | dolly in / push in | Slow dolly forward | 镜头缓慢向前推进 | camera slowly pushing in |\n| 拉远 | dolly out / pull back | Slow dolly backward pull | 镜头缓慢向后拉远 | camera gently pulling back |\n| 跟踪 | tracking shot | Tracking shot, handheld | 跟踪拍摄 | tracking shot following the subject |\n| 摇镜 | pan left/right | Slow pan | 镜头缓慢摇移 | smooth pan across the scene |\n| 甩镜 | whip pan | Whip pan | 快速甩镜 | whip pan |\n| 升降 | crane up/down | Crane up/down | 镜头升降 | crane rising / descending |\n| 环绕 | surround shooting | Orbiting shot | 环绕拍摄 | orbiting around the subject |\n\n---\n\n## 执行流程\n\n1. **解析输入**:提取模型名和多参标志,按路由规则匹配模式;提取资产列表\n2. **构建 @图N 编号表**:资产按输入顺序从 \`@图1 \` 起编号,分镜图接续编号;\`shouldGenerateImage="false"\` 的分镜不分配分镜图编号\n3. **逐条解析 \`\`**:按 videoDesc 解析规则提取12个字段,结合 \`duration\`、\`associateAssetsIds\` 建立标签映射\n4. **整合为一个完整的视频提示词**:按目标模型格式编排全部分镜\n5. **输出视频提示词**\n\n---\n\n## 约束\n\n- **仅输出视频提示词**:不附加任何解释、注释、分析过程、推理步骤、模型匹配说明、资产编号表、分隔线(\`---\`)或额外说明,只输出视频提示词文本。禁止在提示词前后输出任何非提示词内容\n- **严格遵循 videoDesc**(全模式通用):提示词内容严格基于 videoDesc 中的画面描述、时长、景别、运镜、角色动作、情绪、光影氛围、台词、音效字段生成,不编造额外内容\n- **台词不可缺失**(全模式通用):videoDesc 中有台词的分镜,必须在提示词中完整体现台词内容,不得遗漏\n- **台词保持原始输入**(全模式通用):台词内容严禁翻译,必须保持 videoDesc 中的原始语言原样输出\n- **台词类型标注**(全模式通用):必须区分普通对白(dialogue / 说)、内心独白(OS / 内心OS)、画外音(VO / 画外音VO),并在提示词中正确标注\n- **时间跨度最低 1 秒**(全模式通用):所有模式中涉及时间分段(Motion 时间轴 / Seedance 2.0 分镜时长 {N}s)的最小粒度为 1 秒(1s),禁止出现 0.5 秒等低于 1 秒的间隔\n- **视觉风格**:风格相关描述参考 Assistant 中的「视觉风格约束」部分内容,不在本 Skill 内自行定义风格\n- **严格按匹配到的模式格式**,不混用不同模式的格式\n- **不修改原始输入**:不改写 \`\` 的任何字段;\`prompt\` 已有的分镜图提示词仅作画面参考\n- **不编造资产或台词**:只使用输入中的资产信息;无台词则标注「无台词」/ \`No dialogue\`\n- **时长单位**:Seedance 2.0 的分镜时长直接使用秒,格式为 \`{N}s\`(如 \`4s\`),最低 1s\n`, + }); + + 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.0",\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.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', + }; + + //迁移供应商函数 + const data = await knex("o_vendorConfig").select("*"); + for (const item of data) { + let { id, code } = item; + const filename = `${id}.ts`; + const rootDir = u.getPath("vendor"); + 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; + fs.writeFileSync(path.join(rootDir, filename), code); + } + } + const defList = Object.keys(vendorData).map((filename) => filename.replace(/\.ts$/, "")); + const existingIds = data.map((i: any) => i.id); + for (const id of defList) { + if (!existingIds.includes(id)) { + const tsCode = vendorData[`${id}.ts`]; + if (tsCode) await tempOnsert(tsCode); + } + } + + await dropColumn("o_vendorConfig", "author"); + await dropColumn("o_vendorConfig", "description"); + await dropColumn("o_vendorConfig", "name"); + await dropColumn("o_vendorConfig", "icon"); + await dropColumn("o_vendorConfig", "inputs"); + await dropColumn("o_vendorConfig", "createTime"); }; + +async function tempOnsert(tsCode: string) { + const jsCode = transform(tsCode, { transforms: ["typescript"] }).code; + const exports = u.vm(jsCode); + const vendor = exports.vendor; + const data = await u.db("o_vendorConfig").where("id", vendor.id).first(); + if (data) return; + await u.db("o_vendorConfig").insert({ + id: vendor.id, + inputValues: JSON.stringify(vendor.inputValues ?? {}), + models: JSON.stringify([]), + enable: vendor.id == "toonflow" ? 1 : 0, + }); + u.vendor.writeCode(vendor.id, tsCode); +} diff --git a/src/lib/initDB.ts b/src/lib/initDB.ts index 03fb09e..2749c39 100644 --- a/src/lib/initDB.ts +++ b/src/lib/initDB.ts @@ -414,16 +414,9 @@ export default async (knex: Knex, forceInit: boolean = false): Promise => name: "o_vendorConfig", builder: (table) => { table.string("id").notNullable(); - table.text("author"); - table.text("description"); - table.text("name"); - table.text("icon"); - table.text("inputs"); // 输入项配置 JSON table.text("inputValues"); // 输入项值 JSON table.text("models"); // 模型配置 JSON - table.text("code"); // 模型配置 JSON table.integer("enable"); //是否启用供应商 - table.integer("createTime"); table.primary(["id"]); table.unique(["id"]); }, @@ -431,97 +424,39 @@ export default async (knex: Knex, forceInit: boolean = false): Promise => await knex("o_vendorConfig").insert([ { id: "toonflow", - author: "Toonflow", - description: - "## Toonflow官方中转平台\n\nToonflow官方中转平台,提供**文本、图像、视频、音频**等多模态生成能力的中转服务,支持接入多个大模型供应商,方便用户统一管理和调用不同供应商的生成能力。\n\n🔗 [前往中转平台](https://api.toonflow.net/)\n\n如果这个项目对你有帮助,可以考虑支持一下我们的开发工作 ☕", - name: "Toonflow官方中转平台", - icon: "", - inputs: '[{"key":"apiKey","label":"API密钥","type":"password","required":true}]', - inputValues: '{"apiKey":"","baseUrl":"https://api.toonflow.net/v1"}', - models: - '[{"name":"claude-sonnet-4-6","type":"text","modelName":"claude-sonnet-4-6","think":false},{"name":"claude-opus-4-6","type":"text","modelName":"claude-opus-4-6","think":false},{"name":"claude-sonnet-4-5-20250929","type":"text","modelName":"claude-sonnet-4-5-20250929","think":false},{"name":"claude-opus-4-5-20251101","type":"text","modelName":"claude-opus-4-5-20251101","think":false},{"name":"claude-haiku-4-5-20251001","type":"text","modelName":"claude-haiku-4-5-20251001","think":false},{"name":"gpt-5.4","type":"text","modelName":"gpt-5.4","think":false},{"name":"gpt-5.2","type":"text","modelName":"gpt-5.2","think":false},{"name":"MiniMax-M2.7","type":"text","modelName":"MiniMax-M2.7","think":true},{"name":"MiniMax-M2.5","type":"text","modelName":"MiniMax-M2.5","think":true},{"name":"Wan2.6 I2V 1080P (支持真人)","type":"video","modelName":"Wan2.6-I2V-1080P","mode":["text","startEndRequired"],"durationResolutionMap":[{"duration":[2,3,4,5,6,7,8,9,10,11,12,13,14,15],"resolution":["1080p"]}],"audio":true},{"name":"Wan2.6 I2V 720P (支持真人)","type":"video","modelName":"Wan2.6-I2V-720P","mode":["text","startEndRequired"],"durationResolutionMap":[{"duration":[2,3,4,5,6,7,8,9,10,11,12,13,14,15],"resolution":["720p"]}],"audio":true},{"name":"Seedance 1.5 Pro","type":"video","modelName":"doubao-seedance-1-5-pro-251215","durationResolutionMap":[{"duration":[4,5,6,7,8,9,10,11,12],"resolution":["480p","720p","1080p"]}],"mode":["text","endFrameOptional"],"audio":true},{"name":"vidu2 turbo","type":"video","modelName":"ViduQ2-turbo","durationResolutionMap":[{"duration":[1,2,3,4,5,6,7,8,9,10],"resolution":["540p","720p","1080p"]}],"mode":["singleImage","startEndRequired"],"audio":false},{"name":"ViduQ3 pro","type":"video","modelName":"ViduQ3-pro","durationResolutionMap":[{"duration":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16],"resolution":["540p","720p","1080p"]}],"mode":["singleImage","startEndRequired"],"audio":false},{"name":"ViduQ2 pro","type":"video","modelName":"ViduQ2-pro","durationResolutionMap":[{"duration":[1,2,3,4,5,6,7,8,9,10],"resolution":["540p","720p","1080p"]}],"mode":["singleImage","startEndRequired"],"audio":false},{"name":"Doubao Seedream 5.0 Lite","type":"image","modelName":"Doubao-Seedream-5.0-Lite","mode":["text","singleImage","multiReference"]},{"name":"Doubao Seedream 4.5","type":"image","modelName":"doubao-seedream-4-5-251128","mode":["text","singleImage","multiReference"]}]', - code: '//如需遥测AI请使用在toonflow安装目录运行npx @ai-sdk/devtools (要求在其他设置中打开遥测功能,且toonflow有权限在安装目录创建.devtools文件夹)\r\n// ==================== 类型定义 ====================\r\n// 文本模型\r\ninterface TextModel {\r\n name: string; // 显示名称\r\n modelName: string;\r\n type: "text";\r\n think: boolean; // 前端显示用\r\n}\r\n\r\n// 图像模型\r\ninterface ImageModel {\r\n name: string; // 显示名称\r\n modelName: string;\r\n type: "image";\r\n mode: ("text" | "singleImage" | "multiReference")[];\r\n associationSkills?: string; // 关联技能,多个技能用逗号分隔\r\n}\r\n// 视频模型\r\ninterface VideoModel {\r\n name: string; // 显示名称\r\n modelName: string; //全局唯一\r\n type: "video";\r\n mode: (\r\n | "singleImage" // 单图\r\n | "startEndRequired" // 首尾帧(两张都得有)\r\n | "endFrameOptional" // 首尾帧(尾帧可选)\r\n | "startFrameOptional" // 首尾帧(首帧可选)\r\n | "text" // 文本生视频\r\n | ("videoReference" | "imageReference" | "audioReference" | "textReference")[] // 混合参考\r\n )[];\r\n associationSkills?: string; // 关联技能,多个技能用逗号分隔\r\n audio: "optional" | false | true; // 音频配置\r\n durationResolutionMap: { duration: number[]; resolution: string[] }[];\r\n}\r\n\r\ninterface TTSModel {\r\n name: string; // 显示名称\r\n modelName: string;\r\n type: "tts";\r\n voices: {\r\n title: string; //显示名称\r\n voice: string; //说话人\r\n }[];\r\n}\r\n// 供应商配置\r\ninterface VendorConfig {\r\n id: string; //供应商唯一标识,必须全局唯一\r\n author: string;\r\n description?: string; //md5格式\r\n name: string;\r\n icon?: string; //仅支持base64格式\r\n inputs: {\r\n key: string;\r\n label: string;\r\n type: "text" | "password" | "url";\r\n required: boolean;\r\n placeholder?: string;\r\n }[];\r\n inputValues: Record;\r\n models: (TextModel | ImageModel | VideoModel)[];\r\n}\r\n// ==================== 全局工具函数 ====================\r\n//Axios实例\r\n//压缩图片大小(1MB = 1 * 1024 * 1024)\r\ndeclare const zipImage: (completeBase64: string, size: number) => Promise;\r\n//压缩图片分辨率\r\ndeclare const zipImageResolution: (completeBase64: string, width: number, height: number) => Promise;\r\n//多图拼接乘单图 maxSize 最大输出大小,默认为 10mb\r\ndeclare const mergeImages: (completeBase64: string[], maxSize?: string) => Promise;\r\n//Url转Base64\r\ndeclare const urlToBase64: (url: string) => Promise;\r\n//轮询函数\r\ndeclare const pollTask: (\r\n fn: () => Promise<{ completed: boolean; data?: string; error?: string }>,\r\n interval?: number,\r\n timeout?: number,\r\n) => Promise<{ completed: boolean; data?: string; error?: string }>;\r\ndeclare const axios: any;\r\ndeclare const createOpenAI: any;\r\ndeclare const createDeepSeek: any;\r\ndeclare const createZhipu: any;\r\ndeclare const createQwen: any;\r\ndeclare const createAnthropic: any;\r\ndeclare const createOpenAICompatible: any;\r\ndeclare const createXai: any;\r\ndeclare const createMinimax: any;\r\ndeclare const createGoogleGenerativeAI: any;\r\ndeclare const logger: (logstring: string) => void;\r\ndeclare const jsonwebtoken: any;\r\n\r\n// ==================== 供应商数据 ====================\r\nconst vendor: VendorConfig = {\r\n id: "toonflow",\r\n author: "Toonflow",\r\n description:\r\n "## Toonflow官方中转平台\\n\\nToonflow官方中转平台,提供**文本、图像、视频、音频**等多模态生成能力的中转服务,支持接入多个大模型供应商,方便用户统一管理和调用不同供应商的生成能力。\\n\\n🔗 [前往中转平台](https://api.toonflow.net/)\\n\\n如果这个项目对你有帮助,可以考虑支持一下我们的开发工作 ☕",\r\n name: "Toonflow官方中转平台",\r\n icon: "",\r\n inputs: [\r\n { key: "apiKey", label: "API密钥", type: "password", required: true },\r\n ],\r\n inputValues: {\r\n apiKey: "",\r\n baseUrl: "https://api.toonflow.net/v1"\r\n },\r\n models: [\r\n {\r\n name: "claude-sonnet-4-6",\r\n type: "text",\r\n modelName: "claude-sonnet-4-6",\r\n think: false,\r\n },\r\n {\r\n name: "claude-opus-4-6",\r\n type: "text",\r\n modelName: "claude-opus-4-6",\r\n think: false,\r\n },\r\n {\r\n name: "claude-sonnet-4-5-20250929",\r\n type: "text",\r\n modelName: "claude-sonnet-4-5-20250929",\r\n think: false,\r\n },\r\n {\r\n name: "claude-opus-4-5-20251101",\r\n type: "text",\r\n modelName: "claude-opus-4-5-20251101",\r\n think: false,\r\n },\r\n {\r\n name: "claude-haiku-4-5-20251001",\r\n type: "text",\r\n modelName: "claude-haiku-4-5-20251001",\r\n think: false,\r\n },\r\n {\r\n name: "gpt-5.4",\r\n type: "text",\r\n modelName: "gpt-5.4",\r\n think: false,\r\n },\r\n {\r\n name: "gpt-5.2",\r\n type: "text",\r\n modelName: "gpt-5.2",\r\n think: false,\r\n },\r\n {\r\n name: "MiniMax-M2.7",\r\n type: "text",\r\n modelName: "MiniMax-M2.7",\r\n think: true,\r\n },\r\n {\r\n name: "MiniMax-M2.5",\r\n type: "text",\r\n modelName: "MiniMax-M2.5",\r\n think: true,\r\n },\r\n {\r\n name: "Wan2.6 I2V 1080P (支持真人)",\r\n type: "video",\r\n modelName: "Wan2.6-I2V-1080P",\r\n mode: ["text", "startEndRequired"],\r\n durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["1080p"] }],\r\n audio: true,\r\n },\r\n {\r\n name: "Wan2.6 I2V 720P (支持真人)",\r\n type: "video",\r\n modelName: "Wan2.6-I2V-720P",\r\n mode: ["text", "startEndRequired"],\r\n durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["720p"] }],\r\n audio: true,\r\n },\r\n {\r\n name: "Seedance 1.5 Pro",\r\n type: "video",\r\n modelName: "doubao-seedance-1-5-pro-251215",\r\n durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }],\r\n mode: ["text", "endFrameOptional"],\r\n audio: true,\r\n },\r\n {\r\n name: "vidu2 turbo",\r\n type: "video",\r\n modelName: "ViduQ2-turbo",\r\n durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["540p", "720p", "1080p"] }],\r\n mode: ["singleImage", "startEndRequired"],\r\n audio: false,\r\n },\r\n {\r\n name: "ViduQ3 pro",\r\n type: "video",\r\n modelName: "ViduQ3-pro",\r\n durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], resolution: ["540p", "720p", "1080p"] }],\r\n mode: ["singleImage", "startEndRequired"],\r\n audio: false,\r\n },\r\n {\r\n name: "ViduQ2 pro",\r\n type: "video",\r\n modelName: "ViduQ2-pro",\r\n durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["540p", "720p", "1080p"] }],\r\n mode: ["singleImage", "startEndRequired"],\r\n audio: false,\r\n },\r\n\r\n {\r\n name: "Doubao Seedream 5.0 Lite",\r\n type: "image",\r\n modelName: "Doubao-Seedream-5.0-Lite",\r\n mode: ["text", "singleImage", "multiReference"],\r\n },\r\n {\r\n name: "Doubao Seedream 4.5",\r\n type: "image",\r\n modelName: "doubao-seedream-4-5-251128",\r\n mode: ["text", "singleImage", "multiReference"],\r\n },\r\n ],\r\n};\r\nexports.vendor = vendor;\r\n\r\n// ==================== 适配器函数 ====================\r\n\r\n// 文本请求函数\r\nconst textRequest: (textModel: TextModel) => { url: string; model: string } = (textModel) => {\r\n if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");\r\n const apiKey = vendor.inputValues.apiKey.replace("Bearer ", "");\r\n\r\n return createOpenAI({\r\n baseURL: vendor.inputValues.baseUrl,\r\n apiKey: apiKey,\r\n }).chat(textModel.modelName);\r\n};\r\nexports.textRequest = textRequest;\r\n\r\n//图片请求函数\r\ninterface ImageConfig {\r\n prompt: string; //图片提示词\r\n imageBase64: string[]; //输入的图片提示词\r\n size: "1K" | "2K" | "4K"; // 图片尺寸\r\n aspectRatio: `${number}:${number}`; // 长宽比\r\n}\r\n//豆包格式适配\r\nfunction doubaoAdaptor(imageConfig: ImageConfig, imageModel: ImageModel) {\r\n const size = imageConfig.size === "1K" ? "2K" : imageConfig.size;\r\n const sizeMap: Record> = {\r\n "16:9": {\r\n "2k": "2848x1600",\r\n "2K": "2848x1600",\r\n "4K": "4096x2304",\r\n "4k": "4096x2304",\r\n },\r\n "9:16": {\r\n "4k": "2304x4096",\r\n "2k": "1600x2848",\r\n "2K": "1600x2848",\r\n "4K": "2304x4096",\r\n },\r\n };\r\n const body = {\r\n model: imageModel.modelName,\r\n prompt: imageConfig.prompt,\r\n size: sizeMap[imageConfig.aspectRatio][size],\r\n response_format: "url",\r\n sequential_image_generation: "disabled",\r\n stream: false,\r\n watermark: false,\r\n ...(imageConfig.imageBase64 && { image: imageConfig.imageBase64 }),\r\n };\r\n return {\r\n body,\r\n processFn: (data) => {\r\n return data.data[0].url;\r\n },\r\n };\r\n}\r\n\r\n// 提取图片内容\r\nfunction extractFirstImageFromMd(content) {\r\n const regex = /!\\[([^\\]]*)\\]\\((data:image\\/[^;]+;base64,[A-Za-z0-9+/=]+|https?:\\/\\/[^\\s)]+|\\/\\/[^\\s)]+|[^\\s)]+)\\)/;\r\n const match = content.match(regex);\r\n if (!match) return null;\r\n const raw = match[2].trim();\r\n const url = raw.startsWith("data:") ? raw : raw.split(/\\s+/)[0];\r\n return {\r\n alt: match[1],\r\n url,\r\n type: url.startsWith("data:image") ? "base64" : "url",\r\n };\r\n}\r\n// gemini 图片请求适配\r\nfunction geminiImageAdaptor(imageConfig: ImageConfig, imageModel: ImageModel) {\r\n const images = [];\r\n if (imageConfig.imageBase64 && imageConfig.imageBase64.length) {\r\n images.push({\r\n role: "user",\r\n content: imageConfig.imageBase64.map((i) => ({\r\n type: "image_url",\r\n image_url: {\r\n url: i,\r\n },\r\n })),\r\n });\r\n }\r\n const imageConfigGoogle = {\r\n aspect_ratio: imageConfig.aspectRatio,\r\n };\r\n // if(imageModel.ModelName == \'gemini-3-pro-image-preview-vt\'){\r\n imageConfigGoogle.image_size = imageConfig.size;\r\n // }\r\n const body = {\r\n model: imageModel.modelName,\r\n messages: [{ role: "user", content: imageConfig.prompt + `请直接输出图片` }, ...images],\r\n extra_body: {\r\n google: {\r\n image_config: {\r\n ...imageConfigGoogle,\r\n },\r\n },\r\n },\r\n };\r\n return {\r\n body,\r\n url: `${vendor.inputValues.baseUrl}/chat/completions`,\r\n processFn: (data: any) => {\r\n return extractFirstImageFromMd(data.choices[0].message.content).url;\r\n },\r\n };\r\n}\r\nfunction commonAdaptor(imageConfig: ImageConfig, imageModel: ImageModel) {\r\n const defaultImageFn = [\r\n ["doubao", doubaoAdaptor],\r\n ["nano", geminiImageAdaptor],\r\n ["gemini", geminiImageAdaptor],\r\n ["seedream", doubaoAdaptor],\r\n ];\r\n const modelName = imageModel.modelName;\r\n const lowerName = modelName.toLowerCase();\r\n const match = defaultImageFn.find(([key]) => lowerName.includes(key));\r\n return match ? match[1](imageConfig, imageModel) : {};\r\n}\r\nconst imageRequest = async (imageConfig: ImageConfig, imageModel: ImageModel) => {\r\n if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");\r\n const apiKey = vendor.inputValues.apiKey.replace("Bearer ", "");\r\n const adaptor = commonAdaptor(imageConfig, imageModel);\r\n\r\n const requestUrl = adaptor?.url ? `${vendor.inputValues.baseUrl}/chat/completions` : vendor.inputValues.baseUrl + "/images/generations";\r\n const response = await fetch(requestUrl, {\r\n method: "POST",\r\n headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },\r\n body: JSON.stringify(adaptor.body),\r\n });\r\n if (!response.ok) {\r\n const errorText = await response.text(); // 获取错误信息\r\n console.error("请求失败,状态码:", response.status, ", 错误信息:", errorText);\r\n throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`);\r\n }\r\n const data = await response.json();\r\n return adaptor.processFn(data);\r\n};\r\nexports.imageRequest = imageRequest;\r\n\r\ninterface VideoConfig {\r\n duration: number; //视频时长,单位秒\r\n resolution: string; //视频分辨率,如"720p"、"1080p"\r\n aspectRatio: "16:9" | "9:16"; //视频长宽比\r\n prompt: string; //视频提示词\r\n fileBase64?: string[]; // 文件base64 包含图片base64、视频base64、音频base64\r\n audio?: boolean;\r\n mode:\r\n | "singleImage" // 单图\r\n | "multiImage" // 多图模式\r\n | "gridImage" // 网格单图(传入一张图片,但该图片是网格图)\r\n | "startEndRequired" // 首尾帧(两张都得有)\r\n | "endFrameOptional" // 首尾帧(尾帧可选)\r\n | "startFrameOptional" // 首尾帧(首帧可选)\r\n | "text" // 文本生视频\r\n | ("videoReference" | "imageReference" | "audioReference" | "textReference")[]; // 混合参考\r\n}\r\n// 豆包视频\r\nconst buildDoubaoMetadata = (videoConfig: VideoConfig) => {\r\n const metaData = {\r\n ...(typeof videoConfig.audio == "boolean" && { generate_audio: videoConfig.audio ?? false }),\r\n ratio: videoConfig.aspectRatio,\r\n image_roles: [],\r\n references: [],\r\n };\r\n if (videoConfig.imageBase64 && videoConfig.imageBase64.length) {\r\n videoConfig.imageBase64.forEach((i, index) => {\r\n if (Array.isArray(videoConfig.mode)) {\r\n metaData.references.push(i);\r\n } else {\r\n if (videoConfig.mode == "startEndRequired" || videoConfig.mode == "endFrameOptional" || videoConfig.mode == "startFrameOptional") {\r\n (metaData.image_roles as string[]).push(index == 0 ? "first_frame" : "last_frame");\r\n }\r\n if (videoConfig.mode == "singleImage") {\r\n (metaData.image_roles as string[]).push("reference_image");\r\n }\r\n }\r\n });\r\n }\r\n\r\n return metaData;\r\n};\r\n\r\n// 万象\r\nconst buildWanMetadata = (videoConfig: VideoConfig) => {\r\n const images = videoConfig.imageBase64 ?? [];\r\n const metaData: Record = {};\r\n if (\r\n (videoConfig.mode === "startEndRequired" || videoConfig.mode == "endFrameOptional" || videoConfig.mode == "startFrameOptional") &&\r\n images.length == 2\r\n ) {\r\n if (images[0]) metaData.first_frame_url = images[0];\r\n if (images[1]) metaData.last_frame_url = images[1];\r\n } else if (images.length) {\r\n metaData.img_url = images[0]!;\r\n }\r\n if (typeof videoConfig.audio == "boolean") {\r\n metaData.audio = videoConfig.audio;\r\n }\r\n return metaData;\r\n};\r\n// 千问视频\r\nconst buildViduMetadata = (videoConfig: VideoConfig) => ({\r\n aspect_ratio: videoConfig.aspectRatio,\r\n audio: videoConfig.audio ?? false,\r\n off_peak: false,\r\n});\r\n// 可灵\r\nconst buildKlingAdaptor = (videoConfig: VideoConfig) => {\r\n const metaData: any = {\r\n aspect_ratio: videoConfig.aspectRatio,\r\n };\r\n\r\n if (videoConfig.imageBase64 && videoConfig.imageBase64.length) {\r\n if (Array.isArray(videoConfig.mode)) {\r\n metaData.reference = videoConfig.imageBase64;\r\n }\r\n if (videoConfig.mode == "endFrameOptional") {\r\n metaData.image_tail = videoConfig.imageBase64[0];\r\n }\r\n if (videoConfig.mode == "startEndRequired") {\r\n metaData.image_list = [\r\n {\r\n image_url: videoConfig.imageBase64[0],\r\n type: "first_frame",\r\n },\r\n {\r\n image_url: videoConfig.imageBase64[1],\r\n type: "last_frame",\r\n },\r\n ];\r\n }\r\n if (videoConfig.mode == "singleImage") {\r\n metaData.image = videoConfig.imageBase64[0];\r\n }\r\n }\r\n\r\n return metaData;\r\n};\r\ntype MetadataBuilder = (config: VideoConfig) => Record;\r\nconst METADATA_BUILDERS: Array<[string, MetadataBuilder]> = [\r\n ["doubao", buildDoubaoMetadata],\r\n ["wan", buildWanMetadata],\r\n ["vidu", buildViduMetadata],\r\n ["seedance", buildDoubaoMetadata],\r\n ["kling", buildKlingAdaptor],\r\n];\r\nconst buildModelMetadata = (modelName: string, videoConfig: VideoConfig) => {\r\n const lowerName = modelName.toLowerCase();\r\n const match = METADATA_BUILDERS.find(([key]) => lowerName.includes(key));\r\n return match ? match[1](videoConfig) : {};\r\n};\r\nconst videoRequest = async (videoConfig: VideoConfig, videoModel: VideoModel) => {\r\n if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");\r\n const apiKey = vendor.inputValues.apiKey.replace("Bearer ", "");\r\n try {\r\n videoConfig.mode = JSON.parse(videoConfig.mode);\r\n } catch (e) {\r\n videoConfig.mode = videoConfig.mode as any;\r\n }\r\n // 构建每个模型对应的附加参数\r\n const metadata = buildModelMetadata(videoModel.modelName, videoConfig);\r\n\r\n //公共请求参数\r\n const publicBody = {\r\n model: videoModel.modelName,\r\n ...(videoConfig.imageBase64 && videoConfig.imageBase64.length && !Array.isArray(videoConfig.mode) ? { images: videoConfig.imageBase64 } : {}),\r\n prompt: videoConfig.prompt,\r\n duration: videoConfig.duration,\r\n metadata: metadata,\r\n };\r\n\r\n if (videoModel.modelName.toLocaleLowerCase().includes("wan")) {\r\n const sizeMap: Record> = {\r\n "480p": {\r\n "16:9": "832*480",\r\n "9:16": "480*832",\r\n },\r\n "720p": {\r\n "16:9": "1280*720",\r\n "9:16": "720*1280",\r\n },\r\n "1080p": {\r\n "16:9": "1920*1080",\r\n "9:16": "1080*1920",\r\n },\r\n };\r\n const size = sizeMap[videoConfig.resolution]?.[videoConfig.aspectRatio];\r\n publicBody.size = size;\r\n }\r\n const requestUrl = vendor.inputValues.baseUrl + "/video/generations";\r\n const queryUrl = vendor.inputValues.baseUrl + "/video/generations/{id}";\r\n const response = await fetch(requestUrl, {\r\n method: "POST",\r\n headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },\r\n body: JSON.stringify(publicBody),\r\n });\r\n if (!response.ok) {\r\n const errorText = await response.text(); // 获取错误信息\r\n console.error("请求失败,状态码:", response.status, ", 错误信息:", errorText);\r\n throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`);\r\n }\r\n const data = await response.json();\r\n const taskId = data.id;\r\n const res = await pollTask(async () => {\r\n const queryResponse = await fetch(queryUrl.replace("{id}", taskId), {\r\n method: "GET",\r\n headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },\r\n });\r\n if (!queryResponse.ok) {\r\n const errorText = await queryResponse.text(); // 获取错误信息\r\n console.error("请求失败,状态码:", queryResponse.status, ", 错误信息:", errorText);\r\n throw new Error(`请求失败,状态码: ${queryResponse.status}, 错误信息: ${errorText}`);\r\n }\r\n const queryData = await queryResponse.json();\r\n const status = queryData?.status ?? queryData?.data?.status;\r\n const fail_reason = queryData?.data?.fail_reason ?? queryData?.data;\r\n switch (status) {\r\n case "completed":\r\n case "SUCCESS":\r\n case "success":\r\n return { completed: true, data: queryData.data.result_url };\r\n case "FAILURE":\r\n return { completed: false, error: fail_reason || "视频生成失败" };\r\n default:\r\n return { completed: false };\r\n }\r\n });\r\n if (res.error) throw new Error(res.error);\r\n return res.data;\r\n};\r\nexports.videoRequest = videoRequest;\r\n\r\ninterface TTSConfig {\r\n text: string;\r\n voice: string;\r\n speechRate: number;\r\n pitchRate: number;\r\n volume: number;\r\n}\r\nconst ttsRequest = async (ttsConfig: TTSConfig, ttsModel: TTSModel) => {\r\n return null;\r\n};\r\nexports.ttsRequest = ttsRequest;\r\n', + inputValues: "", + models: [], enable: 0, - createTime: 1775164020756, }, { id: "volcengine", - author: "leeqi", - description: - "火山引擎方舟官方直连模板,接入 Ark 的文本、图片、视频生成 API,支持 Doubao、DeepSeek、GLM 等模型。\n[](https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey)", - name: "火山引擎", - icon: "", - inputs: - '[{"key":"apiKey","label":"ARK API Key","type":"password","required":true},{"key":"text","label":"文本生成接口","type":"url","required":false,"placeholder":"如非必要请勿更改"},{"key":"baseUrl","label":"Ark Base URL","type":"url","required":false,"placeholder":"如非必要请勿更改"},{"key":"image","label":"图片生成接口","type":"url","required":false,"placeholder":"如非必要请勿更改"},{"key":"videoCreate","label":"视频任务创建接口","type":"url","required":false,"placeholder":"如非必要请勿更改"},{"key":"videoQuery","label":"视频任务查询接口","type":"url","required":false,"placeholder":"如非必要请勿更改"}]', - inputValues: - '{"apiKey":"","text":"https://ark.cn-beijing.volces.com/api/v3","baseUrl":"https://ark.cn-beijing.volces.com/api/v3","image":"https://ark.cn-beijing.volces.com/api/v3/images/generations","videoCreate":"https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks","videoQuery":"https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks/{id}"}', - models: - '[{"name":"Doubao-Seed-2.0-pro","type":"text","modelName":"doubao-seed-2-0-pro-260215","think":false},{"name":"Doubao-Seed-2.0-lite","type":"text","modelName":"doubao-seed-2-0-lite-260215","think":false},{"name":"Doubao-Seed-2.0-mini","type":"text","modelName":"doubao-seed-2-0-mini-260215","think":false},{"name":"Doubao-Seed-2.0-Code","type":"text","modelName":"doubao-seed-2-0-code-preview-260215","think":false},{"name":"Doubao-1.5-pro-32k","type":"text","modelName":"doubao-1-5-pro-32k-250115","think":false},{"name":"deepseek-v3-250324","type":"text","modelName":"deepseek-v3-250324","think":false},{"name":"glm-4-7-251222","type":"text","modelName":"glm-4-7-251222","think":false},{"name":"Doubao-Seedream-5.0-lite","type":"image","modelName":"doubao-seedream-5-0-260128","mode":["text","singleImage","multiReference"]},{"name":"Doubao-Seedream-4.5","type":"image","modelName":"doubao-seedream-4-5-251128","mode":["text","singleImage","multiReference"]},{"name":"Doubao-Seedream-4.0","type":"image","modelName":"doubao-seedream-4-0-250828","mode":["text","singleImage","multiReference"]},{"name":"Doubao-Seedance-1.5-pro","type":"video","modelName":"doubao-seedance-1-5-pro-251215","mode":["text","singleImage","endFrameOptional"],"audio":true,"durationResolutionMap":[{"duration":[4,5,6,7,8,9,10,11,12],"resolution":["480p","720p","1080p"]}]},{"name":"Doubao-Seedance-1.0-pro-fast","type":"video","modelName":"doubao-seedance-1-0-pro-fast-251015","mode":["text","singleImage"],"audio":false,"durationResolutionMap":[{"duration":[2,3,4,5,6,7,8,9,10,11,12],"resolution":["480p","720p","1080p"]}]},{"name":"Doubao-Seedance-2.0","modelName":"doubao-seedance-2-0-260128","type":"video","mode":[["textReference","videoReference","imageReference","audioReference"]],"audio":"optional","durationResolutionMap":[{"duration":[4,5,6,7,8,9,10,11,12,13,14,15],"resolution":["480p","720p"]}],"associationSkills":""}]', - code: '//如需遥测AI请使用在toonflow安装目录运行npx @ai-sdk/devtools (要求在其他设置中打开遥测功能,且toonflow有权限在安装目录创建.devtools文件夹)\r\n// ==================== 类型定义 ====================\r\n// 文本模型\r\ninterface TextModel {\r\n name: string; // 显示名称\r\n modelName: string;\r\n type: "text";\r\n think: boolean; // 前端显示用\r\n}\r\n\r\n// 图像模型\r\ninterface ImageModel {\r\n name: string; // 显示名称\r\n modelName: string;\r\n type: "image";\r\n mode: ("text" | "singleImage" | "multiReference")[];\r\n associationSkills?: string; // 关联技能,多个技能用逗号分隔\r\n}\r\n// 视频模型\r\ninterface VideoModel {\r\n name: string; // 显示名称\r\n modelName: string; //全局唯一\r\n type: "video";\r\n mode: (\r\n | "singleImage" // 单图\r\n | "startEndRequired" // 首尾帧(两张都得有)\r\n | "endFrameOptional" // 首尾帧(尾帧可选)\r\n | "startFrameOptional" // 首尾帧(首帧可选)\r\n | "text" // 文本生视频\r\n | ("videoReference" | "imageReference" | "audioReference" | "textReference")[]\r\n )[]; // 混合参考\r\n associationSkills?: string; // 关联技能,多个技能用逗号分隔\r\n audio: "optional" | false | true; // 音频配置\r\n durationResolutionMap: { duration: number[]; resolution: string[] }[];\r\n}\r\n\r\ninterface TTSModel {\r\n name: string; // 显示名称\r\n modelName: string;\r\n type: "tts";\r\n voices: {\r\n title: string; //显示名称\r\n voice: string; //说话人\r\n }[];\r\n}\r\n// 供应商配置\r\ninterface VendorConfig {\r\n id: string; //供应商唯一标识,必须全局唯一\r\n author: string;\r\n description?: string; //md5格式\r\n name: string;\r\n icon?: string; //仅支持base64格式\r\n inputs: {\r\n key: string;\r\n label: string;\r\n type: "text" | "password" | "url";\r\n required: boolean;\r\n placeholder?: string;\r\n }[];\r\n inputValues: Record;\r\n models: (TextModel | ImageModel | VideoModel)[];\r\n}\r\n// ==================== 全局工具函数 ====================\r\n//Axios实例\r\n//压缩图片大小(1MB = 1 * 1024 * 1024)\r\ndeclare const zipImage: (completeBase64: string, size: number) => Promise;\r\n//压缩图片分辨率\r\ndeclare const zipImageResolution: (completeBase64: string, width: number, height: number) => Promise;\r\n//多图拼接乘单图 maxSize 最大输出大小,默认为 10mb\r\ndeclare const mergeImages: (completeBase64: string[], maxSize?: string) => Promise;\r\n//Url转Base64\r\ndeclare const urlToBase64: (url: string) => Promise;\r\n//轮询函数\r\ndeclare const pollTask: (\r\n fn: () => Promise<{ completed: boolean; data?: string; error?: string }>,\r\n interval?: number,\r\n timeout?: number,\r\n) => Promise<{ completed: boolean; data?: string; error?: string }>;\r\ndeclare const axios: any;\r\ndeclare const createOpenAI: any;\r\ndeclare const createDeepSeek: any;\r\ndeclare const createZhipu: any;\r\ndeclare const createQwen: any;\r\ndeclare const createAnthropic: any;\r\ndeclare const createOpenAICompatible: any;\r\ndeclare const createXai: any;\r\ndeclare const createMinimax: any;\r\ndeclare const createGoogleGenerativeAI: any;\r\ndeclare const logger: (logstring: string) => void;\r\ndeclare const jsonwebtoken: any;\r\n// ==================== 供应商数据 ====================\r\nconst ARK_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";\r\nconst SUCCESS_TASK_STATUS = ["succeeded", "completed", "success"];\r\nconst FAILED_TASK_STATUS = ["failed", "failure", "error", "canceled", "cancelled"];\r\n\r\nconst vendor: VendorConfig = {\r\n id: "volcengine",\r\n author: "leeqi",\r\n description:\r\n "火山引擎方舟官方直连模板,接入 Ark 的文本、图片、视频生成 API,支持 Doubao、DeepSeek、GLM 等模型。\\n[](https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey)",\r\n name: "火山引擎",\r\n inputs: [\r\n { key: "apiKey", label: "ARK API Key", type: "password", required: true },\r\n { key: "text", label: "文本生成接口", type: "url", required: false, placeholder: "如非必要请勿更改" },\r\n { key: "baseUrl", label: "Ark Base URL", type: "url", required: false, placeholder: "如非必要请勿更改" },\r\n { key: "image", label: "图片生成接口", type: "url", required: false, placeholder: "如非必要请勿更改" },\r\n { key: "videoCreate", label: "视频任务创建接口", type: "url", required: false, placeholder: "如非必要请勿更改" },\r\n { key: "videoQuery", label: "视频任务查询接口", type: "url", required: false, placeholder: "如非必要请勿更改" },\r\n ],\r\n inputValues: {\r\n apiKey: "",\r\n text: ARK_BASE_URL,\r\n baseUrl: ARK_BASE_URL,\r\n image: `${ARK_BASE_URL}/images/generations`,\r\n videoCreate: `${ARK_BASE_URL}/contents/generations/tasks`,\r\n videoQuery: `${ARK_BASE_URL}/contents/generations/tasks/{id}`,\r\n },\r\n models: [\r\n {\r\n name: "Doubao-Seed-2.0-pro",\r\n type: "text",\r\n modelName: "doubao-seed-2-0-pro-260215",\r\n think: false,\r\n },\r\n {\r\n name: "Doubao-Seed-2.0-lite",\r\n type: "text",\r\n modelName: "doubao-seed-2-0-lite-260215",\r\n think: false,\r\n },\r\n {\r\n name: "Doubao-Seed-2.0-mini",\r\n type: "text",\r\n modelName: "doubao-seed-2-0-mini-260215",\r\n think: false,\r\n },\r\n {\r\n name: "Doubao-Seed-2.0-Code",\r\n type: "text",\r\n modelName: "doubao-seed-2-0-code-preview-260215",\r\n think: false,\r\n },\r\n {\r\n name: "Doubao-1.5-pro-32k",\r\n type: "text",\r\n modelName: "doubao-1-5-pro-32k-250115",\r\n think: false,\r\n },\r\n {\r\n name: "deepseek-v3-250324",\r\n type: "text",\r\n modelName: "deepseek-v3-250324",\r\n think: false,\r\n },\r\n {\r\n name: "glm-4-7-251222",\r\n type: "text",\r\n modelName: "glm-4-7-251222",\r\n think: false,\r\n },\r\n {\r\n name: "Doubao-Seedream-5.0-lite",\r\n type: "image",\r\n modelName: "doubao-seedream-5-0-260128",\r\n mode: ["text", "singleImage", "multiReference"],\r\n },\r\n {\r\n name: "Doubao-Seedream-4.5",\r\n type: "image",\r\n modelName: "doubao-seedream-4-5-251128",\r\n mode: ["text", "singleImage", "multiReference"],\r\n },\r\n {\r\n name: "Doubao-Seedream-4.0",\r\n type: "image",\r\n modelName: "doubao-seedream-4-0-250828",\r\n mode: ["text", "singleImage", "multiReference"],\r\n },\r\n {\r\n name: "Doubao-Seedance-1.5-pro",\r\n type: "video",\r\n modelName: "doubao-seedance-1-5-pro-251215",\r\n mode: ["text", "singleImage", "endFrameOptional"],\r\n audio: true,\r\n durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }],\r\n },\r\n {\r\n name: "Doubao-Seedance-1.0-pro-fast",\r\n type: "video",\r\n modelName: "doubao-seedance-1-0-pro-fast-251015",\r\n mode: ["text", "singleImage"],\r\n audio: false,\r\n durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }],\r\n },\r\n {\r\n name: "Doubao-Seedance-2.0",\r\n modelName: "doubao-seedance-2-0-260128",\r\n type: "video",\r\n mode: [["textReference", "videoReference", "imageReference", "audioReference"]],\r\n audio: "optional",\r\n durationResolutionMap: [\r\n {\r\n duration: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],\r\n resolution: ["480p", "720p"],\r\n },\r\n ],\r\n associationSkills: "",\r\n },\r\n ],\r\n};\r\nexports.vendor = vendor;\r\n\r\n// ==================== 适配器函数 ====================\r\nconst getApiKey = () => {\r\n if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");\r\n return vendor.inputValues.apiKey.replace(/^Bearer\\s+/i, "");\r\n};\r\n\r\nconst getBaseUrl = () => vendor.inputValues.baseUrl || ARK_BASE_URL;\r\nconst buildUrl = (overrideUrl: string | undefined, fallbackPath: string) => overrideUrl || `${getBaseUrl().replace(/\\/$/, "")}${fallbackPath}`;\r\n\r\nconst getHeaders = () => ({\r\n Authorization: `Bearer ${getApiKey()}`,\r\n "Content-Type": "application/json",\r\n});\r\n\r\nconst getOpenAIBaseUrl = () =>\r\n (vendor.inputValues.text || getBaseUrl())\r\n .trim()\r\n .replace(/\\/$/, "")\r\n .replace(/\\/chat\\/completions$/i, "");\r\n\r\nconst readJson = async (response: Response, action: string) => {\r\n if (!response.ok) {\r\n const errorText = await response.text();\r\n throw new Error(`${action}失败,状态码: ${response.status}, 错误信息: ${errorText}`);\r\n }\r\n\r\n return response.json();\r\n};\r\n\r\n//补齐图片 data url 前缀\r\nconst normalizeImageInput = (value: string) => {\r\n if (!value) return value;\r\n if (/^(https?:\\/\\/|data:|volc:)/i.test(value)) return value;\r\n return `data:image/png;base64,${value}`;\r\n};\r\n\r\nconst normalizeImageList = (imageBase64?: string[]) => (imageBase64 || []).filter(Boolean).map(normalizeImageInput);\r\n\r\nconst extractImageResult = (data: any) => {\r\n const first = data?.data?.[0] ?? data?.images?.[0] ?? data?.output?.[0];\r\n return first?.url ?? first?.image_url ?? first?.b64_json ?? first?.base64;\r\n};\r\n\r\nconst extractTaskId = (data: any) => data?.id ?? data?.task_id ?? data?.taskId ?? data?.data?.id ?? data?.data?.task_id ?? data?.data;\r\n\r\nconst extractVideoUrl = (data: any) =>\r\n data?.content?.video_url ??\r\n data?.content?.video_urls?.[0] ??\r\n data?.data?.content?.video_url ??\r\n data?.data?.content?.video_urls?.[0] ??\r\n data?.data?.video_url ??\r\n data?.result?.video_url ??\r\n data?.result_url ??\r\n data?.data?.result_url;\r\n\r\nconst getTaskStatus = (data: any) => (data?.status ?? data?.data?.status ?? "").toString().toLowerCase();\r\n\r\nconst getTaskError = (data: any) =>\r\n data?.error?.message ?? data?.message ?? data?.data?.message ?? data?.data?.fail_reason ?? data?.fail_reason ?? "任务执行失败";\r\n\r\n// 文本请求函数\r\nconst textRequest: (textModel: TextModel) => { url: string; model: string } = (textModel) => {\r\n return createOpenAI({\r\n baseURL: getOpenAIBaseUrl(),\r\n apiKey: getApiKey(),\r\n }).chat(textModel.modelName);\r\n};\r\nexports.textRequest = textRequest;\r\n\r\n//图片请求函数\r\ninterface ImageConfig {\r\n prompt: string; //图片提示词\r\n imageBase64: string[]; //输入的图片提示词\r\n size: "1K" | "2K" | "4K"; // 图片尺寸\r\n aspectRatio: `${number}:${number}`; // 长宽比\r\n}\r\n\r\nconst normalizeImageSize = (imageConfig: ImageConfig) => {\r\n const normalizedAspectRatio = imageConfig.aspectRatio;\r\n const size = imageConfig.size === "1K" ? "2K" : imageConfig.size;\r\n\r\n const sizeMap: Record> = {\r\n "16:9": {\r\n "2K": "2848x1600",\r\n "4K": "4096x2304",\r\n },\r\n "9:16": {\r\n "2K": "1600x2848",\r\n "4K": "2304x4096",\r\n },\r\n };\r\n return sizeMap[normalizedAspectRatio][size];\r\n};\r\n\r\nconst imageRequest = async (imageConfig: ImageConfig, imageModel: ImageModel) => {\r\n const images = normalizeImageList(imageConfig.imageBase64);\r\n const body = {\r\n model: imageModel.modelName,\r\n prompt: imageConfig.prompt,\r\n size: normalizeImageSize(imageConfig),\r\n response_format: "url",\r\n sequential_image_generation: "disabled",\r\n stream: false,\r\n watermark: false,\r\n ...(images.length ? { image: images.length === 1 ? images[0] : images } : {}),\r\n };\r\n\r\n const data = await readJson(\r\n await fetch(buildUrl(vendor.inputValues.image, "/images/generations"), {\r\n method: "POST",\r\n headers: getHeaders(),\r\n body: JSON.stringify(body),\r\n }),\r\n "图片生成",\r\n );\r\n\r\n const result = extractImageResult(data);\r\n if (!result) throw new Error(`图片生成返回格式异常: ${JSON.stringify(data)}`);\r\n return result;\r\n};\r\nexports.imageRequest = imageRequest;\r\n\r\ninterface VideoConfig {\r\n duration: number;\r\n resolution: string;\r\n aspectRatio: "16:9" | "9:16";\r\n prompt: string;\r\n imageBase64?: string[];\r\n audio?: boolean;\r\n mode:\r\n | "singleImage" // 单图\r\n | "multiImage" // 多图模式\r\n | "gridImage" // 网格单图(传入一张图片,但该图片是网格图)\r\n | "startEndRequired" // 首尾帧(两张都得有)\r\n | "endFrameOptional" // 首尾帧(尾帧可选)\r\n | "startFrameOptional" // 首尾帧(首帧可选)\r\n | "text" // 文本生视频\r\n | ("video" | "image" | "audio" | "text")[]; // 混合参考\r\n}\r\n\r\nconst isSeedanceModel = (modelName: string) => /^doubao-seedance-/i.test(modelName);\r\nconst isSeedance15ProModel = (modelName: string) => /^doubao-seedance-1-5-pro/i.test(modelName);\r\n\r\nconst appendPromptFlag = (prompt: string, flag: string, value: string | number | boolean | undefined) => {\r\n if (value === undefined || value === null || value === "") return prompt;\r\n const normalizedPrompt = prompt.trim();\r\n const flagPattern = new RegExp(`(^|\\\\s)--${flag}(?:\\\\s+\\\\S+)?(?=\\\\s|$)`, "i");\r\n if (flagPattern.test(normalizedPrompt)) return normalizedPrompt;\r\n return `${normalizedPrompt} --${flag} ${value}`.trim();\r\n};\r\n\r\nconst buildVideoPrompt = (videoConfig: VideoConfig, videoModel: VideoModel) => {\r\n if (!isSeedanceModel(videoModel.modelName)) return videoConfig.prompt || "";\r\n\r\n let prompt = videoConfig.prompt || "";\r\n prompt = appendPromptFlag(prompt, "resolution", videoConfig.resolution);\r\n prompt = appendPromptFlag(prompt, "duration", videoConfig.duration);\r\n prompt = appendPromptFlag(prompt, "camerafixed", false);\r\n prompt = appendPromptFlag(prompt, "watermark", false);\r\n return prompt;\r\n};\r\n\r\n//判断base64前缀 适配多参\r\nfunction getBase64Type(base64: string) {\r\n const match = base64.match(/^data:([-\\w]+)\\/([-\\w]+);base64,/);\r\n if (!match) return "unknown";\r\n const mainType = match[1];\r\n if (mainType === "image") return "image";\r\n if (mainType === "audio") return "audio";\r\n if (mainType === "video") return "video";\r\n return "unknown";\r\n}\r\n\r\nconst buildVideoContent = (videoConfig: VideoConfig, videoModel: VideoModel) => {\r\n const images = videoConfig?.imageBase64 ?? [];\r\n const content: any[] = [{ type: "text", text: buildVideoPrompt(videoConfig, videoModel) }];\r\n if (videoConfig.mode == "startEndRequired" || videoConfig.mode == "endFrameOptional") {\r\n images[0] && content.push({ type: "image_url", image_url: { url: images[0] }, role: "first_frame" });\r\n images[1] && content.push({ type: "image_url", image_url: { url: images[1] }, role: "last_frame" });\r\n }\r\n if (Array.isArray(videoConfig.mode)) {\r\n images.forEach((item) => {\r\n const type = getBase64Type(item);\r\n if (type == "audio") {\r\n content.push({\r\n type: "audio_url",\r\n audio_url: { url: item },\r\n role: "reference_audio",\r\n });\r\n } else if (type == "video") {\r\n content.push({\r\n type: "video_url",\r\n video_url: { url: item },\r\n role: "reference_video",\r\n });\r\n } else {\r\n content.push({\r\n type: "image_url",\r\n image_url: { url: item },\r\n role: "reference_image",\r\n });\r\n }\r\n });\r\n }\r\n if (videoConfig.mode == "singleImage") {\r\n images[0] && content.push({ type: "image_url", image_url: { url: images[0] } });\r\n }\r\n return content;\r\n};\r\n\r\nconst buildVideoBody = (videoConfig: VideoConfig, videoModel: VideoModel) => {\r\n const isSeedance = isSeedanceModel(videoModel.modelName);\r\n const isSeedance15Pro = isSeedance15ProModel(videoModel.modelName);\r\n\r\n return {\r\n model: videoModel.modelName,\r\n content: buildVideoContent(videoConfig, videoModel),\r\n ...(!isSeedance15Pro && !isSeedance\r\n ? {\r\n duration: videoConfig.duration,\r\n resolution: videoConfig.resolution,\r\n ratio: videoConfig.aspectRatio,\r\n }\r\n : {}),\r\n ...(videoModel.audio === true && typeof videoConfig.audio === "boolean" ? { generate_audio: videoConfig.audio } : {}),\r\n };\r\n};\r\n\r\nconst queryVideoResult = async (taskId: string) => {\r\n const queryData = await readJson(\r\n await fetch(buildUrl(vendor.inputValues.videoQuery, "/contents/generations/tasks/{id}").replace("{id}", taskId), {\r\n method: "GET",\r\n headers: getHeaders(),\r\n }),\r\n "视频任务查询",\r\n );\r\n\r\n const status = getTaskStatus(queryData);\r\n if (SUCCESS_TASK_STATUS.includes(status)) {\r\n const videoUrl = extractVideoUrl(queryData);\r\n if (!videoUrl) return { completed: true, error: `视频任务成功但未返回结果: ${JSON.stringify(queryData)}` };\r\n return { completed: true, data: videoUrl };\r\n }\r\n\r\n if (FAILED_TASK_STATUS.includes(status)) {\r\n return { completed: false, error: getTaskError(queryData) };\r\n }\r\n\r\n return { completed: false };\r\n};\r\n\r\nconst videoRequest = async (videoConfig: VideoConfig, videoModel: VideoModel) => {\r\n const createData = await readJson(\r\n await fetch(buildUrl(vendor.inputValues.videoCreate, "/contents/generations/tasks"), {\r\n method: "POST",\r\n headers: getHeaders(),\r\n body: JSON.stringify(buildVideoBody(videoConfig, videoModel)),\r\n }),\r\n "视频任务创建",\r\n );\r\n\r\n const taskId = extractTaskId(createData);\r\n if (!taskId) throw new Error(`视频任务创建返回格式异常: ${JSON.stringify(createData)}`);\r\n\r\n const result = await pollTask(() => queryVideoResult(taskId));\r\n if (result.error) throw new Error(result.error);\r\n return result.data;\r\n};\r\nexports.videoRequest = videoRequest;\r\n\r\ninterface TTSConfig {\r\n text: string;\r\n voice: string;\r\n speechRate: number;\r\n pitchRate: number;\r\n volume: number;\r\n}\r\n\r\nconst ttsRequest = async (ttsConfig: TTSConfig, ttsModel: TTSModel) => {\r\n return null;\r\n};\r\nexports.ttsRequest = ttsRequest;\r\n', + inputValues: "", + models: [], enable: 0, - createTime: 1775407135202, }, { id: "minimax", - author: "Toonflow", - description: "MiniMax标准格式接口,如果没有你想要的模型请手动添加。", - name: "MiniMax标准接口", - icon: "", - inputs: - '[{"key":"apiKey","label":"API密钥","type":"password","required":true},{"key":"baseUrl","label":"请求地址","type":"url","required":true,"placeholder":"已默认填入官方地址,非特殊情况不需要更改"}]', - inputValues: '{"apiKey":"","baseUrl":"https://api.minimaxi.com/v1"}', - models: - '[{"name":"MiniMax-M2.7","modelName":"MiniMax-M2.7","type":"text","think":true},{"name":"MiniMax-M2.7-highspeed","modelName":"MiniMax-M2.7-highspeed","type":"text","think":true},{"name":"MiniMax-M2.5","modelName":"MiniMax-M2.5","type":"text","think":true},{"name":"MiniMax-M2.5-highspeed","modelName":"MiniMax-M2.5-highspeed","type":"text","think":true}]', - code: '//如需遥测AI请使用在toonflow安装目录运行npx @ai-sdk/devtools (要求在其他设置中打开遥测功能,且toonflow有权限在安装目录创建.devtools文件夹)\r\n// ==================== 类型定义 ====================\r\n// 文本模型\r\ninterface TextModel {\r\n name: string; // 显示名称\r\n modelName: string;\r\n type: "text";\r\n think: boolean; // 前端显示用\r\n}\r\n\r\n// 图像模型\r\ninterface ImageModel {\r\n name: string; // 显示名称\r\n modelName: string;\r\n type: "image";\r\n mode: ("text" | "singleImage" | "multiReference")[];\r\n associationSkills?: string; // 关联技能,多个技能用逗号分隔\r\n}\r\n// 视频模型\r\ninterface VideoModel {\r\n name: string; // 显示名称\r\n modelName: string; //全局唯一\r\n type: "video";\r\n mode: (\r\n | "singleImage" // 单图\r\n | "startEndRequired" // 首尾帧(两张都得有)\r\n | "endFrameOptional" // 首尾帧(尾帧可选)\r\n | "startFrameOptional" // 首尾帧(首帧可选)\r\n | "text" // 文本生视频\r\n | ("videoReference" | "imageReference" | "audioReference" | "textReference")[]\r\n )[]; // 混合参考\r\n associationSkills?: string; // 关联技能,多个技能用逗号分隔\r\n audio: "optional" | false | true; // 音频配置\r\n durationResolutionMap: { duration: number[]; resolution: string[] }[];\r\n}\r\n\r\ninterface TTSModel {\r\n name: string; // 显示名称\r\n modelName: string;\r\n type: "tts";\r\n voices: {\r\n title: string; //显示名称\r\n voice: string; //说话人\r\n }[];\r\n}\r\n// 供应商配置\r\ninterface VendorConfig {\r\n id: string; //供应商唯一标识,必须全局唯一\r\n author: string;\r\n description?: string; //md5格式\r\n name: string;\r\n icon?: string; //仅支持base64格式\r\n inputs: {\r\n key: string;\r\n label: string;\r\n type: "text" | "password" | "url";\r\n required: boolean;\r\n placeholder?: string;\r\n }[];\r\n inputValues: Record;\r\n models: (TextModel | ImageModel | VideoModel)[];\r\n}\r\n// ==================== 全局工具函数 ====================\r\n//Axios实例\r\n//压缩图片大小(1MB = 1 * 1024 * 1024)\r\ndeclare const zipImage: (completeBase64: string, size: number) => Promise;\r\n//压缩图片分辨率\r\ndeclare const zipImageResolution: (completeBase64: string, width: number, height: number) => Promise;\r\n//多图拼接乘单图 maxSize 最大输出大小,默认为 10mb\r\ndeclare const mergeImages: (completeBase64: string[], maxSize?: string) => Promise;\r\n//Url转Base64\r\ndeclare const urlToBase64: (url: string) => Promise;\r\n//轮询函数\r\ndeclare const pollTask: (\r\n fn: () => Promise<{ completed: boolean; data?: string; error?: string }>,\r\n interval?: number,\r\n timeout?: number,\r\n) => Promise<{ completed: boolean; data?: string; error?: string }>;\r\ndeclare const axios: any;\r\ndeclare const createOpenAI: any;\r\ndeclare const createDeepSeek: any;\r\ndeclare const createZhipu: any;\r\ndeclare const createQwen: any;\r\ndeclare const createAnthropic: any;\r\ndeclare const createOpenAICompatible: any;\r\ndeclare const createXai: any;\r\ndeclare const createMinimax: any;\r\ndeclare const createGoogleGenerativeAI: any;\r\ndeclare const logger: (logstring: string) => void;\r\ndeclare const jsonwebtoken: any;\r\n\r\n// ==================== 供应商数据 ====================\r\nconst vendor: VendorConfig = {\r\n id: "minimax",\r\n author: "Toonflow",\r\n description: "MiniMax标准格式接口,如果没有你想要的模型请手动添加。",\r\n name: "MiniMax标准接口",\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: "已默认填入官方地址,非特殊情况不需要更改" },\r\n ],\r\n inputValues: {\r\n apiKey: "",\r\n baseUrl: "https://api.minimaxi.com/v1",\r\n },\r\n models: [\r\n {\r\n name: "MiniMax-M2.7",\r\n modelName: "MiniMax-M2.7",\r\n type: "text",\r\n think: true,\r\n },\r\n {\r\n name: "MiniMax-M2.7-highspeed",\r\n modelName: "MiniMax-M2.7-highspeed",\r\n type: "text",\r\n think: true,\r\n },\r\n {\r\n name: "MiniMax-M2.5",\r\n modelName: "MiniMax-M2.5",\r\n type: "text",\r\n think: true,\r\n },\r\n {\r\n name: "MiniMax-M2.5-highspeed",\r\n modelName: "MiniMax-M2.5-highspeed",\r\n type: "text",\r\n think: true,\r\n },\r\n ],\r\n};\r\nexports.vendor = vendor;\r\n\r\n// ==================== 适配器函数 ====================\r\n\r\n// 文本请求函数\r\nconst textRequest: (textModel: TextModel) => { url: string; model: string } = (textModel) => {\r\n if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");\r\n const apiKey = vendor.inputValues.apiKey.replace("Bearer ", "");\r\n\r\n return createOpenAI({\r\n baseURL: vendor.inputValues.baseUrl,\r\n apiKey: apiKey,\r\n }).chat(textModel.modelName);\r\n};\r\nexports.textRequest = textRequest;\r\n\r\n//图片请求函数\r\ninterface ImageConfig {\r\n prompt: string; //图片提示词\r\n imageBase64: string[]; //输入的图片提示词\r\n size: "1K" | "2K" | "4K"; // 图片尺寸\r\n aspectRatio: `${number}:${number}`; // 长宽比\r\n}\r\nconst imageRequest = async (imageConfig: ImageConfig, imageModel: ImageModel) => {\r\n return null;\r\n};\r\nexports.imageRequest = imageRequest;\r\n\r\ninterface VideoConfig {\r\n duration: number;\r\n resolution: string;\r\n aspectRatio: "16:9" | "9:16";\r\n prompt: string;\r\n imageBase64?: string[];\r\n audio?: boolean;\r\n mode:\r\n | "singleImage" // 单图\r\n | "multiImage" // 多图模式\r\n | "gridImage" // 网格单图(传入一张图片,但该图片是网格图)\r\n | "startEndRequired" // 首尾帧(两张都得有)\r\n | "endFrameOptional" // 首尾帧(尾帧可选)\r\n | "startFrameOptional" // 首尾帧(首帧可选)\r\n | "text" // 文本生视频\r\n | ("video" | "image" | "audio" | "text")[]; // 混合参考\r\n}\r\n\r\nconst videoRequest = async (videoConfig: VideoConfig, videoModel: VideoModel) => {\r\n return null;\r\n};\r\nexports.videoRequest = videoRequest;\r\n\r\ninterface TTSConfig {\r\n text: string;\r\n voice: string;\r\n speechRate: number;\r\n pitchRate: number;\r\n volume: number;\r\n}\r\nconst ttsRequest = async (ttsConfig: TTSConfig, ttsModel: TTSModel) => {\r\n return null;\r\n};\r\nexports.ttsRequest = ttsRequest;\r\n', + inputValues: "", + models: [], enable: 0, - createTime: 1775154441614, }, { id: "openai", - author: "Toonflow", - description: "OpenAI标准格式接口,如果没有你想要的模型请手动添加。", - name: "OpenAI标准接口", - icon: "", - inputs: - '[{"key":"apiKey","label":"API密钥","type":"password","required":true},{"key":"baseUrl","label":"请求地址","type":"url","required":true,"placeholder":"以v1结束,示例:https://api.openai.com/v1"}]', - inputValues: '{"apiKey":"","baseUrl":"http://192.168.0.116:33332/v1"}', - models: - '[{"name":"GPT-4o","modelName":"gpt-4o","type":"text","think":false},{"name":"GPT-4.1","modelName":"gpt-4.1","type":"text","think":false},{"name":"GPT-5.1","modelName":"gpt-5.1","type":"text","think":false},{"name":"GPT-5.2","modelName":"gpt-5.2","type":"text","think":false},{"name":"GPT-5.4","modelName":"gpt-5.4","type":"text","think":false}]', - code: '//如需遥测AI请使用在toonflow安装目录运行npx @ai-sdk/devtools (要求在其他设置中打开遥测功能,且toonflow有权限在安装目录创建.devtools文件夹)\r\n// ==================== 类型定义 ====================\r\n// 文本模型\r\ninterface TextModel {\r\n name: string; // 显示名称\r\n modelName: string;\r\n type: "text";\r\n think: boolean; // 前端显示用\r\n}\r\n\r\n// 图像模型\r\ninterface ImageModel {\r\n name: string; // 显示名称\r\n modelName: string;\r\n type: "image";\r\n mode: ("text" | "singleImage" | "multiReference")[];\r\n associationSkills?: string; // 关联技能,多个技能用逗号分隔\r\n}\r\n// 视频模型\r\ninterface VideoModel {\r\n name: string; // 显示名称\r\n modelName: string; //全局唯一\r\n type: "video";\r\n mode: (\r\n | "singleImage" // 单图\r\n | "startEndRequired" // 首尾帧(两张都得有)\r\n | "endFrameOptional" // 首尾帧(尾帧可选)\r\n | "startFrameOptional" // 首尾帧(首帧可选)\r\n | "text" // 文本生视频\r\n | ("videoReference" | "imageReference" | "audioReference" | "textReference")[]\r\n )[]; // 混合参考\r\n associationSkills?: string; // 关联技能,多个技能用逗号分隔\r\n audio: "optional" | false | true; // 音频配置\r\n durationResolutionMap: { duration: number[]; resolution: string[] }[];\r\n}\r\n\r\ninterface TTSModel {\r\n name: string; // 显示名称\r\n modelName: string;\r\n type: "tts";\r\n voices: {\r\n title: string; //显示名称\r\n voice: string; //说话人\r\n }[];\r\n}\r\n// 供应商配置\r\ninterface VendorConfig {\r\n id: string; //供应商唯一标识,必须全局唯一\r\n author: string;\r\n description?: string; //md5格式\r\n name: string;\r\n icon?: string; //仅支持base64格式\r\n inputs: {\r\n key: string;\r\n label: string;\r\n type: "text" | "password" | "url";\r\n required: boolean;\r\n placeholder?: string;\r\n }[];\r\n inputValues: Record;\r\n models: (TextModel | ImageModel | VideoModel)[];\r\n}\r\n// ==================== 全局工具函数 ====================\r\n//Axios实例\r\n//压缩图片大小(1MB = 1 * 1024 * 1024)\r\ndeclare const zipImage: (completeBase64: string, size: number) => Promise;\r\n//压缩图片分辨率\r\ndeclare const zipImageResolution: (completeBase64: string, width: number, height: number) => Promise;\r\n//多图拼接乘单图 maxSize 最大输出大小,默认为 10mb\r\ndeclare const mergeImages: (completeBase64: string[], maxSize?: string) => Promise;\r\n//Url转Base64\r\ndeclare const urlToBase64: (url: string) => Promise;\r\n//轮询函数\r\ndeclare const pollTask: (\r\n fn: () => Promise<{ completed: boolean; data?: string; error?: string }>,\r\n interval?: number,\r\n timeout?: number,\r\n) => Promise<{ completed: boolean; data?: string; error?: string }>;\r\ndeclare const axios: any;\r\ndeclare const createOpenAI: any;\r\ndeclare const createDeepSeek: any;\r\ndeclare const createZhipu: any;\r\ndeclare const createQwen: any;\r\ndeclare const createAnthropic: any;\r\ndeclare const createOpenAICompatible: any;\r\ndeclare const createXai: any;\r\ndeclare const createMinimax: any;\r\ndeclare const createGoogleGenerativeAI: any;\r\ndeclare const logger: (logstring: string) => void;\r\ndeclare const jsonwebtoken: any;\r\n\r\n// ==================== 供应商数据 ====================\r\nconst vendor: VendorConfig = {\r\n id: "openai",\r\n author: "Toonflow",\r\n description: "OpenAI标准格式接口,如果没有你想要的模型请手动添加。",\r\n name: "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 {\r\n name: "GPT-4o",\r\n modelName: "gpt-4o",\r\n type: "text",\r\n think: false,\r\n },\r\n {\r\n name: "GPT-4.1",\r\n modelName: "gpt-4.1",\r\n type: "text",\r\n think: false,\r\n },\r\n {\r\n name: "GPT-5.1",\r\n modelName: "gpt-5.1",\r\n type: "text",\r\n think: false,\r\n },\r\n {\r\n name: "GPT-5.2",\r\n modelName: "gpt-5.2",\r\n type: "text",\r\n think: false,\r\n },\r\n {\r\n name: "GPT-5.4",\r\n modelName: "gpt-5.4",\r\n type: "text",\r\n think: false,\r\n },\r\n ],\r\n};\r\nexports.vendor = vendor;\r\n\r\n// ==================== 适配器函数 ====================\r\n\r\n// 文本请求函数\r\nconst textRequest: (textModel: TextModel) => { url: string; model: string } = (textModel) => {\r\n if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");\r\n const apiKey = vendor.inputValues.apiKey.replace("Bearer ", "");\r\n\r\n return createOpenAI({\r\n baseURL: vendor.inputValues.baseUrl,\r\n apiKey: apiKey,\r\n }).chat(textModel.modelName);\r\n};\r\nexports.textRequest = textRequest;\r\n\r\n//图片请求函数\r\ninterface ImageConfig {\r\n prompt: string; //图片提示词\r\n imageBase64: string[]; //输入的图片提示词\r\n size: "1K" | "2K" | "4K"; // 图片尺寸\r\n aspectRatio: `${number}:${number}`; // 长宽比\r\n}\r\nconst imageRequest = async (imageConfig: ImageConfig, imageModel: ImageModel) => {\r\n return null;\r\n};\r\nexports.imageRequest = imageRequest;\r\n\r\ninterface VideoConfig {\r\n duration: number;\r\n resolution: string;\r\n aspectRatio: "16:9" | "9:16";\r\n prompt: string;\r\n imageBase64?: string[];\r\n audio?: boolean;\r\n mode:\r\n | "singleImage" // 单图\r\n | "multiImage" // 多图模式\r\n | "gridImage" // 网格单图(传入一张图片,但该图片是网格图)\r\n | "startEndRequired" // 首尾帧(两张都得有)\r\n | "endFrameOptional" // 首尾帧(尾帧可选)\r\n | "startFrameOptional" // 首尾帧(首帧可选)\r\n | "text" // 文本生视频\r\n | ("video" | "image" | "audio" | "text")[]; // 混合参考\r\n}\r\n\r\nconst videoRequest = async (videoConfig: VideoConfig, videoModel: VideoModel) => {\r\n return null;\r\n};\r\nexports.videoRequest = videoRequest;\r\n\r\ninterface TTSConfig {\r\n text: string;\r\n voice: string;\r\n speechRate: number;\r\n pitchRate: number;\r\n volume: number;\r\n}\r\nconst ttsRequest = async (ttsConfig: TTSConfig, ttsModel: TTSModel) => {\r\n return null;\r\n};\r\nexports.ttsRequest = ttsRequest;\r\n', + inputValues: "", + models: [], enable: 0, - createTime: 1775154125094, }, { id: "klingai", - author: "klingai", - description: - "可灵AI新一代AI创意生产力工具,基于快手大模型团队自研的 图像生成@可图大模型 和 视频生成@可灵大模型 技术,提供丰富的AI图片、AI视频及相关可控编辑能力。https://app.klingai.com/cn/", - name: "可灵AI", - icon: "", - inputs: - '[{"key":"apiKey","label":"accsseKey","type":"password","required":true,"placeholder":"请到可灵官方申请"},{"key":"sk","label":"SecretKey","type":"password","required":true,"placeholder":"请到可灵官方申请"}]', - inputValues: '{"apiKey":"","sk":""}', - models: - '[{"name":"kling-video-o1 标准版","type":"video","modelName":"kling-video-o1-std","durationResolutionMap":[{"duration":[5,10],"resolution":["540p","720p","1080p"]}],"mode":["text","startEndRequired"],"audio":false},{"name":"kling-video-o1 pro","type":"video","modelName":"kling-video-o1-pro","durationResolutionMap":[{"duration":[5,10],"resolution":["540p","720p","1080p"]}],"mode":["singleImage","startEndRequired","text"],"audio":true},{"name":"kling-v3-omni std","type":"video","modelName":"kling-v3-omni-std","durationResolutionMap":[{"duration":[3,4,5,6,7,8,9,10,11,12,13,14,15],"resolution":["540p","720p","1080p"]}],"mode":["singleImage","startEndRequired","text"],"audio":false},{"name":"kling-v3-omni pro","type":"video","modelName":"kling-v3-omni-pro","durationResolutionMap":[{"duration":[3,4,5,6,7,8,9,10,11,12,13,14,15],"resolution":["540p","720p","1080p"]}],"mode":["singleImage","startEndRequired","text"],"audio":true},{"name":"kling-v1 std 5s","type":"video","modelName":"kling-v1-std-5s","durationResolutionMap":[{"duration":[5],"resolution":["720p"]}],"mode":["singleImage","text","startEndRequired"],"audio":true},{"name":"kling-v1 std 10s","type":"video","modelName":"kling-v1-std-10s","durationResolutionMap":[{"duration":[10],"resolution":["720p"]}],"mode":["singleImage","text"],"audio":true},{"name":"kling-v1 pro 5s","type":"video","modelName":"kling-v1-pro-5s","durationResolutionMap":[{"duration":[5],"resolution":["720p"]}],"mode":["singleImage","text","startEndRequired"],"audio":true},{"name":"kling-v1 pro 10s","type":"video","modelName":"kling-v1-pro-10s","durationResolutionMap":[{"duration":[10],"resolution":["720p"]}],"mode":["singleImage","text"],"audio":true},{"name":"kling-v1-5 std 5s","type":"video","modelName":"kling-v1-5-std-5s","durationResolutionMap":[{"duration":[5],"resolution":["720p"]}],"mode":["singleImage"],"audio":true},{"name":"kling-v1-5 std 10s","type":"video","modelName":"kling-v1-5-std-10s","durationResolutionMap":[{"duration":[10],"resolution":["720p"]}],"mode":["singleImage"],"audio":true},{"name":"kling-v1-5 pro 5s","type":"video","modelName":"kling-v1-5-pro-5s","durationResolutionMap":[{"duration":[5],"resolution":["1080p"]}],"mode":["singleImage","startEndRequired","startFrameOptional"],"audio":true},{"name":"kling-v1-5 pro 10s","type":"video","modelName":"kling-v1-5-pro-10s","durationResolutionMap":[{"duration":[10],"resolution":["1080p"]}],"mode":["singleImage","startEndRequired","startFrameOptional"],"audio":true},{"name":"kling-v1-6 std 5s","type":"video","modelName":"kling-v1-5-std-5s","durationResolutionMap":[{"duration":[5],"resolution":["720p"]}],"mode":["singleImage","text"],"audio":true},{"name":"kling-v1-6 std 10s","type":"video","modelName":"kling-v1-5-std-10s","durationResolutionMap":[{"duration":[10],"resolution":["720p"]}],"mode":["singleImage","text"],"audio":true},{"name":"kling-v1-6 pro 5s","type":"video","modelName":"kling-v1-5-pro-5s","durationResolutionMap":[{"duration":[5],"resolution":["1080p"]}],"mode":["singleImage","text","startEndRequired","startFrameOptional"],"audio":true},{"name":"kling-v1-6 pro 10s","type":"video","modelName":"kling-v1-5-pro-10s","durationResolutionMap":[{"duration":[10],"resolution":["1080p"]}],"mode":["singleImage","text","startEndRequired","startFrameOptional"],"audio":true},{"name":"kling-v2-master 5s","type":"video","modelName":"kling-v2-master-5s","durationResolutionMap":[{"duration":[5],"resolution":["720p"]}],"mode":["singleImage","text"],"audio":true},{"name":"kling-v2-master 10s","type":"video","modelName":"kling-v1-5-std-10s","durationResolutionMap":[{"duration":[10],"resolution":["720p"]}],"mode":["singleImage","text"],"audio":true},{"name":"kling-v2-1 std 5s","type":"video","modelName":"kling-v1-5-std-5s","durationResolutionMap":[{"duration":[5],"resolution":["720p"]}],"mode":["singleImage"],"audio":true},{"name":"kling-v2-1 std 10s","type":"video","modelName":"kling-v1-5-std-10s","durationResolutionMap":[{"duration":[10],"resolution":["720p"]}],"mode":["singleImage"],"audio":true},{"name":"kling-v2-1 pro 5s","type":"video","modelName":"kling-v2-1-pro-5s","durationResolutionMap":[{"duration":[5],"resolution":["1080p"]}],"mode":["singleImage","startEndRequired"],"audio":true},{"name":"kling-v2-1 pro 10s","type":"video","modelName":"kling-v2-1-pro-10s","durationResolutionMap":[{"duration":[10],"resolution":["1080p"]}],"mode":["singleImage","startEndRequired"],"audio":true},{"name":"kling-v2-1-master 5s","type":"video","modelName":"kling-v2-1-master-5s","durationResolutionMap":[{"duration":[5],"resolution":["1080p"]}],"mode":["singleImage","text"],"audio":true},{"name":"kling-v2-1-master 10s","type":"video","modelName":"kling-v2-1-master-10s","durationResolutionMap":[{"duration":[10],"resolution":["1080p"]}],"mode":["singleImage","text"],"audio":true},{"name":"kling-v2-5-turbo std 5s","type":"video","modelName":"kling-v2-5-turbo-std-5s","durationResolutionMap":[{"duration":[5],"resolution":["1080p"]}],"mode":["text","singleImage"],"audio":true},{"name":"kling-v2-5-turbo std 10s","type":"video","modelName":"kling-v2-5-turbo-std-10s","durationResolutionMap":[{"duration":[10],"resolution":["1080p"]}],"mode":["text","singleImage"],"audio":true},{"name":"kling-v2-5-turbo pro 5s","type":"video","modelName":"kling-v2-5-turbo-pro-5s","durationResolutionMap":[{"duration":[5],"resolution":["1080p"]}],"mode":["text","singleImage","startEndRequired"],"audio":true},{"name":"kling-v2-5-turbo pro 10s","type":"video","modelName":"kling-v2-5-turbo-pro-10s","durationResolutionMap":[{"duration":[10],"resolution":["1080p"]}],"mode":["text","singleImage","startEndRequired"],"audio":true},{"name":"kling-v2-6 std 5s","type":"video","modelName":"kling-v2-6-std-5s","durationResolutionMap":[{"duration":[5],"resolution":["1080p"]}],"mode":["text","singleImage"],"audio":false},{"name":"kling-v2-6 std 10s","type":"video","modelName":"kling-v2-6-std-10s","durationResolutionMap":[{"duration":[10],"resolution":["1080p"]}],"mode":["text","singleImage"],"audio":false},{"name":"kling-v2-6 pro 5s","type":"video","modelName":"kling-v2-6-pro-5s","durationResolutionMap":[{"duration":[5],"resolution":["1080p"]}],"mode":["text","singleImage","startEndRequired"],"audio":false},{"name":"kling-v2-6 pro 10s","type":"video","modelName":"kling-v2-6-pro-10s","durationResolutionMap":[{"duration":[10],"resolution":["1080p"]}],"mode":["text","singleImage","startEndRequired"],"audio":false},{"name":"kling-v3-omni std","type":"video","modelName":"kling-v3-omni-std","durationResolutionMap":[{"duration":[3,4,5,6,7,8,9,10,11,12,13,14,15],"resolution":["540p","720p","1080p"]}],"mode":["singleImage","startEndRequired","text"],"audio":false},{"name":"kling-v3-omni pro","type":"video","modelName":"kling-v3-omni-pro","durationResolutionMap":[{"duration":[3,4,5,6,7,8,9,10,11,12,13,14,15],"resolution":["540p","720p","1080p"]}],"mode":["singleImage","startEndRequired","text"],"audio":false}]', - code: '// ==================== 类型定义 ====================\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}\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 | "multiImage" // 多图模式\r\n | "gridImage" // 网格单图(传入一张图片,但该图片是网格图)\r\n | "startEndRequired" // 首尾帧(两张都得有)\r\n | "endFrameOptional" // 首尾帧(尾帧可选)\r\n | "startFrameOptional" // 首尾帧(首帧可选)\r\n | "text" // 文本生视频\r\n | ("videoReference" | "imageReference" | "audioReference" | "textReference")[]\r\n )[]; // 混合参考\r\n 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\nconst KLINGAI_API_URL = "https://api-beijing.klingai.com";\r\nconst vendor: VendorConfig = {\r\n id: "klingai",\r\n author: "klingai",\r\n description:\r\n "可灵AI新一代AI创意生产力工具,基于快手大模型团队自研的 图像生成@可图大模型 和 视频生成@可灵大模型 技术,提供丰富的AI图片、AI视频及相关可控编辑能力。https://app.klingai.com/cn/",\r\n name: "可灵AI",\r\n inputs: [\r\n { key: "apiKey", label: "accsseKey", type: "password", required: true, placeholder: "请到可灵官方申请" },\r\n { key: "sk", label: "SecretKey", type: "password", required: true, placeholder: "请到可灵官方申请" },\r\n ],\r\n inputValues: {\r\n apiKey: "",\r\n sk: "",\r\n },\r\n models: [\r\n {\r\n name: "kling-video-o1 标准版",\r\n type: "video",\r\n modelName: "kling-video-o1-std",\r\n durationResolutionMap: [{ duration: [5, 10], resolution: ["540p", "720p", "1080p"] }],\r\n mode: ["text", "startEndRequired"],\r\n audio: false,\r\n },\r\n {\r\n name: "kling-video-o1 pro",\r\n type: "video",\r\n modelName: "kling-video-o1-pro",\r\n durationResolutionMap: [{ duration: [5, 10], resolution: ["540p", "720p", "1080p"] }],\r\n mode: ["singleImage", "startEndRequired", "text"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v3-omni std",\r\n type: "video",\r\n modelName: "kling-v3-omni-std",\r\n durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["540p", "720p", "1080p"] }],\r\n mode: ["singleImage", "startEndRequired", "text"],\r\n audio: false,\r\n },\r\n {\r\n name: "kling-v3-omni pro",\r\n type: "video",\r\n modelName: "kling-v3-omni-pro",\r\n durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["540p", "720p", "1080p"] }],\r\n mode: ["singleImage", "startEndRequired", "text"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v1 std 5s",\r\n type: "video",\r\n modelName: "kling-v1-std-5s",\r\n durationResolutionMap: [{ duration: [5], resolution: ["720p"] }],\r\n mode: ["singleImage", "text", "startEndRequired"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v1 std 10s",\r\n type: "video",\r\n modelName: "kling-v1-std-10s",\r\n durationResolutionMap: [{ duration: [10], resolution: ["720p"] }],\r\n mode: ["singleImage", "text"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v1 pro 5s",\r\n type: "video",\r\n modelName: "kling-v1-pro-5s",\r\n durationResolutionMap: [{ duration: [5], resolution: ["720p"] }],\r\n mode: ["singleImage", "text", "startEndRequired"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v1 pro 10s",\r\n type: "video",\r\n modelName: "kling-v1-pro-10s",\r\n durationResolutionMap: [{ duration: [10], resolution: ["720p"] }],\r\n mode: ["singleImage", "text"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v1-5 std 5s",\r\n type: "video",\r\n modelName: "kling-v1-5-std-5s",\r\n durationResolutionMap: [{ duration: [5], resolution: ["720p"] }],\r\n mode: ["singleImage"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v1-5 std 10s",\r\n type: "video",\r\n modelName: "kling-v1-5-std-10s",\r\n durationResolutionMap: [{ duration: [10], resolution: ["720p"] }],\r\n mode: ["singleImage"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v1-5 pro 5s",\r\n type: "video",\r\n modelName: "kling-v1-5-pro-5s",\r\n durationResolutionMap: [{ duration: [5], resolution: ["1080p"] }],\r\n mode: ["singleImage", "startEndRequired", "startFrameOptional"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v1-5 pro 10s",\r\n type: "video",\r\n modelName: "kling-v1-5-pro-10s",\r\n durationResolutionMap: [{ duration: [10], resolution: ["1080p"] }],\r\n mode: ["singleImage", "startEndRequired", "startFrameOptional"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v1-6 std 5s",\r\n type: "video",\r\n modelName: "kling-v1-5-std-5s",\r\n durationResolutionMap: [{ duration: [5], resolution: ["720p"] }],\r\n mode: ["singleImage", "text"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v1-6 std 10s",\r\n type: "video",\r\n modelName: "kling-v1-5-std-10s",\r\n durationResolutionMap: [{ duration: [10], resolution: ["720p"] }],\r\n mode: ["singleImage", "text"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v1-6 pro 5s",\r\n type: "video",\r\n modelName: "kling-v1-5-pro-5s",\r\n durationResolutionMap: [{ duration: [5], resolution: ["1080p"] }],\r\n mode: ["singleImage", "text", "startEndRequired", "startFrameOptional"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v1-6 pro 10s",\r\n type: "video",\r\n modelName: "kling-v1-5-pro-10s",\r\n durationResolutionMap: [{ duration: [10], resolution: ["1080p"] }],\r\n mode: ["singleImage", "text", "startEndRequired", "startFrameOptional"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v2-master 5s",\r\n type: "video",\r\n modelName: "kling-v2-master-5s",\r\n durationResolutionMap: [{ duration: [5], resolution: ["720p"] }],\r\n mode: ["singleImage", "text"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v2-master 10s",\r\n type: "video",\r\n modelName: "kling-v1-5-std-10s",\r\n durationResolutionMap: [{ duration: [10], resolution: ["720p"] }],\r\n mode: ["singleImage", "text"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v2-1 std 5s",\r\n type: "video",\r\n modelName: "kling-v1-5-std-5s",\r\n durationResolutionMap: [{ duration: [5], resolution: ["720p"] }],\r\n mode: ["singleImage"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v2-1 std 10s",\r\n type: "video",\r\n modelName: "kling-v1-5-std-10s",\r\n durationResolutionMap: [{ duration: [10], resolution: ["720p"] }],\r\n mode: ["singleImage"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v2-1 pro 5s",\r\n type: "video",\r\n modelName: "kling-v2-1-pro-5s",\r\n durationResolutionMap: [{ duration: [5], resolution: ["1080p"] }],\r\n mode: ["singleImage", "startEndRequired"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v2-1 pro 10s",\r\n type: "video",\r\n modelName: "kling-v2-1-pro-10s",\r\n durationResolutionMap: [{ duration: [10], resolution: ["1080p"] }],\r\n mode: ["singleImage", "startEndRequired"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v2-1-master 5s",\r\n type: "video",\r\n modelName: "kling-v2-1-master-5s",\r\n durationResolutionMap: [{ duration: [5], resolution: ["1080p"] }],\r\n mode: ["singleImage", "text"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v2-1-master 10s",\r\n type: "video",\r\n modelName: "kling-v2-1-master-10s",\r\n durationResolutionMap: [{ duration: [10], resolution: ["1080p"] }],\r\n mode: ["singleImage", "text"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v2-5-turbo std 5s",\r\n type: "video",\r\n modelName: "kling-v2-5-turbo-std-5s",\r\n durationResolutionMap: [{ duration: [5], resolution: ["1080p"] }],\r\n mode: ["text", "singleImage"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v2-5-turbo std 10s",\r\n type: "video",\r\n modelName: "kling-v2-5-turbo-std-10s",\r\n durationResolutionMap: [{ duration: [10], resolution: ["1080p"] }],\r\n mode: ["text", "singleImage"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v2-5-turbo pro 5s",\r\n type: "video",\r\n modelName: "kling-v2-5-turbo-pro-5s",\r\n durationResolutionMap: [{ duration: [5], resolution: ["1080p"] }],\r\n mode: ["text", "singleImage", "startEndRequired"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v2-5-turbo pro 10s",\r\n type: "video",\r\n modelName: "kling-v2-5-turbo-pro-10s",\r\n durationResolutionMap: [{ duration: [10], resolution: ["1080p"] }],\r\n mode: ["text", "singleImage", "startEndRequired"],\r\n audio: true,\r\n },\r\n {\r\n name: "kling-v2-6 std 5s",\r\n type: "video",\r\n modelName: "kling-v2-6-std-5s",\r\n durationResolutionMap: [{ duration: [5], resolution: ["1080p"] }],\r\n mode: ["text", "singleImage"],\r\n audio: false,\r\n },\r\n {\r\n name: "kling-v2-6 std 10s",\r\n type: "video",\r\n modelName: "kling-v2-6-std-10s",\r\n durationResolutionMap: [{ duration: [10], resolution: ["1080p"] }],\r\n mode: ["text", "singleImage"],\r\n audio: false,\r\n },\r\n {\r\n name: "kling-v2-6 pro 5s",\r\n type: "video",\r\n modelName: "kling-v2-6-pro-5s",\r\n durationResolutionMap: [{ duration: [5], resolution: ["1080p"] }],\r\n mode: ["text", "singleImage", "startEndRequired"],\r\n audio: false,\r\n },\r\n {\r\n name: "kling-v2-6 pro 10s",\r\n type: "video",\r\n modelName: "kling-v2-6-pro-10s",\r\n durationResolutionMap: [{ duration: [10], resolution: ["1080p"] }],\r\n mode: ["text", "singleImage", "startEndRequired"],\r\n audio: false,\r\n },\r\n {\r\n name: "kling-v3-omni std",\r\n type: "video",\r\n modelName: "kling-v3-omni-std",\r\n durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["540p", "720p", "1080p"] }],\r\n mode: ["singleImage", "startEndRequired", "text"],\r\n audio: false,\r\n },\r\n {\r\n name: "kling-v3-omni pro",\r\n type: "video",\r\n modelName: "kling-v3-omni-pro",\r\n durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["540p", "720p", "1080p"] }],\r\n mode: ["singleImage", "startEndRequired", "text"],\r\n audio: false,\r\n },\r\n ],\r\n};\r\nexports.vendor = vendor;\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\n\r\ndeclare const JWT: any;\r\n// ==================== 适配器函数 ====================\r\n\r\nfunction getToken() {\r\n const headers = {\r\n alg: "HS256",\r\n typ: "JWT",\r\n };\r\n const payload = {\r\n iss: vendor.inputValues.ak,\r\n exp: Date.now() + 1800000,\r\n nbf: Date.now() - 5000,\r\n };\r\n let token = jwt.sign(payload, vendor.inputValues.sk, headers);\r\n return token;\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 token = getToken();\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_name: 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 = KLINGAI_API_URL + "/v1/images/omni-image";\r\n const response = await fetch(createImageUrl, {\r\n method: "POST",\r\n headers: { Authorization: `Bearer ${token}`, "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 checkUrl = KLINGAI_API_URL + "/v1/images/omni-image/{id}";\r\n const res = await checkKlingTaskResult(data.data.task_id, checkUrl);\r\n const resData = JSON.parse(JSON.stringify(res.data));\r\n return resData?.task_result.images[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// 检查生成物结果\r\nconst checkKlingTaskResult = async (taskId: string, checkUrl: string) => {\r\n const token = getToken();\r\n const res = await pollTask(async () => {\r\n const queryResponse = await fetch(checkUrl.replace("{id}", taskId), {\r\n method: "GET",\r\n headers: { Authorization: `Bearer ${token}`, "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.data };\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 token = getToken();\r\n\r\n //公共请求参数\r\n const publicBody = {\r\n model_name: 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 };\r\n\r\n const requestUrl = KLINGAI_API_URL + "/v1/videos/omni-video";\r\n const response = await fetch(requestUrl, {\r\n method: "POST",\r\n headers: { Authorization: `Bearer ${token}`, "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\r\n const checkUrl = KLINGAI_API_URL + "/v1/videos/omni-video/{id}";\r\n const result = await checkKlingTaskResult(data.data.task_id, checkUrl);\r\n const resData = JSON.parse(JSON.stringify(result.data));\r\n return resData?.task_result.videos;\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("可灵 暂不支持语音合成(TTS)");\r\n};\r\n', + inputValues: "", + models: [], enable: 0, - createTime: 1775155145953, }, { id: "vidu", - author: "搬砖的Coder", - description: - "Vidu 是由生数科技联合清华大学正式发布的中国首个长时长、高一致性、高动态性视频大模型。Vidu 在语义理解、推理速度、动态幅度等方面具备领先优势,并上线了全球首个“多主体参考”功能,突破视频模型一致性生成难题,开启了视觉上下文时代", - name: "Vidu 开放平台", - icon: "", - inputs: - '[{"key":"apiKey","label":"API密钥","type":"password","required":true,"placeholder":"请到Vidu官方申请"},{"key":"baseUrl","label":"接口路径","type":"url","required":false,"placeholder":"https://api.vidu.cn/ent/v2"}]', - inputValues: '{"apiKey":"","baseUrl":"https://api.vidu.cn/ent/v2"}', - models: - '[{"name":"ViduQ3 turbo","type":"video","modelName":"ViduQ3-turbo","durationResolutionMap":[{"duration":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16],"resolution":["540p","720p","1080p"]}],"mode":["singleImage","startEndRequired","text"],"audio":true},{"name":"ViduQ3 pro","type":"video","modelName":"ViduQ3-pro","durationResolutionMap":[{"duration":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16],"resolution":["540p","720p","1080p"]}],"mode":["singleImage","startEndRequired","text"],"audio":true},{"name":"ViduQ2 pro fast","type":"video","modelName":"ViduQ2-pro-fast","durationResolutionMap":[{"duration":[1,2,3,4,5,6,7,8,9,10],"resolution":["720p","1080p"]}],"mode":["singleImage","startEndRequired"],"audio":true},{"name":"viduQ2 turbo","type":"video","modelName":"ViduQ2-turbo","durationResolutionMap":[{"duration":[1,2,3,4,5,6,7,8,9,10],"resolution":["540p","720p","1080p"]}],"mode":["singleImage","startEndRequired"],"audio":true},{"name":"ViduQ2 pro","type":"video","modelName":"ViduQ2-pro","durationResolutionMap":[{"duration":[1,2,3,4,5,6,7,8,9,10],"resolution":["540p","720p","1080p"]}],"mode":["singleImage","startEndRequired"],"audio":true},{"name":"ViduQ2","type":"video","modelName":"ViduQ2","durationResolutionMap":[{"duration":[5],"resolution":["1080p"]}],"mode":["text"],"audio":true},{"name":"ViduQ1","type":"video","modelName":"ViduQ1","durationResolutionMap":[{"duration":[5],"resolution":["1080p"]}],"mode":["singleImage","startEndRequired","text"],"audio":true},{"name":"ViduQ1 classic","type":"video","modelName":"viduQ1-classic","durationResolutionMap":[{"duration":[5],"resolution":["1080p"]}],"mode":["singleImage","startEndRequired"],"audio":true},{"name":"Vidu2.0","type":"video","modelName":"vidu2.0","durationResolutionMap":[{"duration":[4,8],"resolution":["360p","720p","1080p"]}],"mode":["singleImage","startEndRequired"],"audio":true},{"name":"viduq1 for image","type":"image","modelName":"viduq1","mode":["text"]},{"name":"viduq2 for image","type":"image","modelName":"viduq2","mode":["text","singleImage","multiReference"]}]', - code: '//如需遥测AI请使用在toonflow安装目录运行npx @ai-sdk/devtools (要求在其他设置中打开遥测功能,且toonflow有权限在安装目录创建.devtools文件夹)\r\n// ==================== 类型定义 ====================\r\n// 文本模型\r\ninterface TextModel {\r\n name: string; // 显示名称\r\n modelName: string;\r\n type: "text";\r\n think: boolean; // 前端显示用\r\n}\r\n\r\n// 图像模型\r\ninterface ImageModel {\r\n name: string; // 显示名称\r\n modelName: string;\r\n type: "image";\r\n mode: ("text" | "singleImage" | "multiReference")[];\r\n associationSkills?: string; // 关联技能,多个技能用逗号分隔\r\n}\r\n// 视频模型\r\ninterface VideoModel {\r\n name: string; // 显示名称\r\n modelName: string; //全局唯一\r\n type: "video";\r\n mode: (\r\n | "singleImage" // 单图\r\n | "startEndRequired" // 首尾帧(两张都得有)\r\n | "endFrameOptional" // 首尾帧(尾帧可选)\r\n | "startFrameOptional" // 首尾帧(首帧可选)\r\n | "text" // 文本生视频\r\n | ("videoReference" | "imageReference" | "audioReference" | "textReference")[] // 混合参考\r\n )[];\r\n associationSkills?: string; // 关联技能,多个技能用逗号分隔\r\n audio: "optional" | false | true; // 音频配置\r\n durationResolutionMap: { duration: number[]; resolution: string[] }[];\r\n}\r\n\r\ninterface TTSModel {\r\n name: string; // 显示名称\r\n modelName: string;\r\n type: "tts";\r\n voices: {\r\n title: string; //显示名称\r\n voice: string; //说话人\r\n }[];\r\n}\r\n// 供应商配置\r\ninterface VendorConfig {\r\n id: string; //供应商唯一标识,必须全局唯一\r\n author: string;\r\n description?: string; //md5格式\r\n name: string;\r\n icon?: string; //仅支持base64格式\r\n inputs: {\r\n key: string;\r\n label: string;\r\n type: "text" | "password" | "url";\r\n required: boolean;\r\n placeholder?: string;\r\n }[];\r\n inputValues: Record;\r\n models: (TextModel | ImageModel | VideoModel)[];\r\n}\r\n// ==================== 全局工具函数 ====================\r\n//Axios实例\r\n//压缩图片大小(1MB = 1 * 1024 * 1024)\r\ndeclare const zipImage: (completeBase64: string, size: number) => Promise;\r\n//压缩图片分辨率\r\ndeclare const zipImageResolution: (completeBase64: string, width: number, height: number) => Promise;\r\n//多图拼接乘单图 maxSize 最大输出大小,默认为 10mb\r\ndeclare const mergeImages: (completeBase64: string[], maxSize?: string) => Promise;\r\n//Url转Base64\r\ndeclare const urlToBase64: (url: string) => Promise;\r\n//轮询函数\r\ndeclare const pollTask: (\r\n fn: () => Promise<{ completed: boolean; data?: string; error?: string }>,\r\n interval?: number,\r\n timeout?: number,\r\n) => Promise<{ completed: boolean; data?: string; error?: string }>;\r\ndeclare const axios: any;\r\ndeclare const createOpenAI: any;\r\ndeclare const createDeepSeek: any;\r\ndeclare const createZhipu: any;\r\ndeclare const createQwen: any;\r\ndeclare const createAnthropic: any;\r\ndeclare const createOpenAICompatible: any;\r\ndeclare const createXai: any;\r\ndeclare const createMinimax: any;\r\ndeclare const createGoogleGenerativeAI: any;\r\ndeclare const logger: (logstring: string) => void;\r\ndeclare const jsonwebtoken: any;\r\n// ==================== 供应商数据 ====================\r\nconst vendor: VendorConfig = {\r\n id: "vidu",\r\n author: "搬砖的Coder",\r\n description:\r\n "Vidu 是由生数科技联合清华大学正式发布的中国首个长时长、高一致性、高动态性视频大模型。Vidu 在语义理解、推理速度、动态幅度等方面具备领先优势,并上线了全球首个“多主体参考”功能,突破视频模型一致性生成难题,开启了视觉上下文时代",\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: false, 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', + inputValues: "", + models: [], enable: 0, - createTime: 1775155162784, }, ]); }, diff --git a/src/router.ts b/src/router.ts index 59dd134..731c1e7 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1,4 +1,4 @@ -// @routes-hash 04a067f990aac8e584acabe110983618 +// @routes-hash 62534cff632db5d31442f1bca1932925 import { Express } from "express"; import route1 from "./routes/agents/clearMemory"; @@ -63,86 +63,87 @@ import route59 from "./routes/production/editImage/saveImageFlow"; import route60 from "./routes/production/editImage/updateImageFlow"; import route61 from "./routes/production/editImage/uploadImage"; import route62 from "./routes/production/getFlowData"; -import route63 from "./routes/production/getProductionData"; -import route64 from "./routes/production/getStoryboardData"; -import route65 from "./routes/production/saveFlowData"; -import route66 from "./routes/production/storyboard/addStoryboard"; -import route67 from "./routes/production/storyboard/batchAddStoryboardInfo"; -import route68 from "./routes/production/storyboard/batchGenerateImage"; -import route69 from "./routes/production/storyboard/downPreviewImage"; -import route70 from "./routes/production/storyboard/editStoryboardInfo"; -import route71 from "./routes/production/storyboard/getStoryboardData"; -import route72 from "./routes/production/storyboard/pollingImage"; -import route73 from "./routes/production/storyboard/previewImage"; -import route74 from "./routes/production/storyboard/removeFrame"; -import route75 from "./routes/production/storyboard/updateStoryboardUrl"; -import route76 from "./routes/production/workbench/addTrack"; -import route77 from "./routes/production/workbench/deleteTrack"; -import route78 from "./routes/production/workbench/delVideo"; -import route79 from "./routes/production/workbench/generateVideo"; -import route80 from "./routes/production/workbench/generateVideoPrompt"; -import route81 from "./routes/production/workbench/getGenerateData"; -import route82 from "./routes/production/workbench/getVideoList"; -import route83 from "./routes/production/workbench/getVideoModelDetail"; -import route84 from "./routes/production/workbench/selectVideo"; -import route85 from "./routes/production/workbench/updateVideoPrompt"; -import route86 from "./routes/project/addDirectorManual"; -import route87 from "./routes/project/addProject"; -import route88 from "./routes/project/addVisualManual"; -import route89 from "./routes/project/deleteDirectorManual"; -import route90 from "./routes/project/deleteVisualManual"; -import route91 from "./routes/project/delProject"; -import route92 from "./routes/project/editDirectorlManual"; -import route93 from "./routes/project/editProject"; -import route94 from "./routes/project/editVisualManual"; -import route95 from "./routes/project/getProject"; -import route96 from "./routes/project/getVisualManual"; -import route97 from "./routes/project/queryDirectorManual"; -import route98 from "./routes/project/visualManual"; -import route99 from "./routes/script/addScript"; -import route100 from "./routes/script/batchAddScript"; -import route101 from "./routes/script/delScript"; -import route102 from "./routes/script/exportScript"; -import route103 from "./routes/script/extractAssets"; -import route104 from "./routes/script/getScrptApi"; -import route105 from "./routes/script/pollScriptAssets"; -import route106 from "./routes/script/updateScript"; -import route107 from "./routes/scriptAgent/getPlanData"; -import route108 from "./routes/scriptAgent/setPlanData"; -import route109 from "./routes/scriptAgent/updateData"; -import route110 from "./routes/setting/about/checkUpdate"; -import route111 from "./routes/setting/about/downloadApp"; -import route112 from "./routes/setting/agentDeploy/agentSetKey"; -import route113 from "./routes/setting/agentDeploy/deployAgentModel"; -import route114 from "./routes/setting/agentDeploy/getAgentDeploy"; -import route115 from "./routes/setting/dbConfig/clearData"; -import route116 from "./routes/setting/dev/getSwitchAiDevTool"; -import route117 from "./routes/setting/dev/updateSwitchAiDevTool"; -import route118 from "./routes/setting/fileManagement/openFolder"; -import route119 from "./routes/setting/getTextModel"; -import route120 from "./routes/setting/loginConfig/getUser"; -import route121 from "./routes/setting/loginConfig/updateUserPwd"; -import route122 from "./routes/setting/memoryConfig/delAllMemory"; -import route123 from "./routes/setting/memoryConfig/getMemory"; -import route124 from "./routes/setting/memoryConfig/sureMemory"; -import route125 from "./routes/setting/promptManage/getPrompt"; -import route126 from "./routes/setting/promptManage/updatePrompt"; -import route127 from "./routes/setting/skillManagement/getSkillContent"; -import route128 from "./routes/setting/skillManagement/getSkillList"; -import route129 from "./routes/setting/skillManagement/saveSkillContent"; -import route130 from "./routes/setting/vendorConfig/addVendor"; -import route131 from "./routes/setting/vendorConfig/deleteVendor"; +import route63 from "./routes/production/getStoryboardData"; +import route64 from "./routes/production/saveFlowData"; +import route65 from "./routes/production/storyboard/addStoryboard"; +import route66 from "./routes/production/storyboard/batchAddStoryboardInfo"; +import route67 from "./routes/production/storyboard/batchGenerateImage"; +import route68 from "./routes/production/storyboard/downPreviewImage"; +import route69 from "./routes/production/storyboard/editStoryboardInfo"; +import route70 from "./routes/production/storyboard/getStoryboardData"; +import route71 from "./routes/production/storyboard/pollingImage"; +import route72 from "./routes/production/storyboard/previewImage"; +import route73 from "./routes/production/storyboard/removeFrame"; +import route74 from "./routes/production/storyboard/updateStoryboardUrl"; +import route75 from "./routes/production/workbench/addTrack"; +import route76 from "./routes/production/workbench/deleteTrack"; +import route77 from "./routes/production/workbench/delVideo"; +import route78 from "./routes/production/workbench/generateVideo"; +import route79 from "./routes/production/workbench/generateVideoPrompt"; +import route80 from "./routes/production/workbench/getGenerateData"; +import route81 from "./routes/production/workbench/getVideoList"; +import route82 from "./routes/production/workbench/selectVideo"; +import route83 from "./routes/production/workbench/updateVideoPrompt"; +import route84 from "./routes/project/addDirectorManual"; +import route85 from "./routes/project/addProject"; +import route86 from "./routes/project/addVisualManual"; +import route87 from "./routes/project/deleteDirectorManual"; +import route88 from "./routes/project/deleteVisualManual"; +import route89 from "./routes/project/delProject"; +import route90 from "./routes/project/editDirectorlManual"; +import route91 from "./routes/project/editProject"; +import route92 from "./routes/project/editVisualManual"; +import route93 from "./routes/project/getProject"; +import route94 from "./routes/project/getVisualManual"; +import route95 from "./routes/project/queryDirectorManual"; +import route96 from "./routes/project/visualManual"; +import route97 from "./routes/script/addScript"; +import route98 from "./routes/script/batchAddScript"; +import route99 from "./routes/script/delScript"; +import route100 from "./routes/script/exportScript"; +import route101 from "./routes/script/extractAssets"; +import route102 from "./routes/script/getScrptApi"; +import route103 from "./routes/script/pollScriptAssets"; +import route104 from "./routes/script/updateScript"; +import route105 from "./routes/scriptAgent/getPlanData"; +import route106 from "./routes/scriptAgent/setPlanData"; +import route107 from "./routes/scriptAgent/updateData"; +import route108 from "./routes/setting/about/checkUpdate"; +import route109 from "./routes/setting/about/downloadApp"; +import route110 from "./routes/setting/agentDeploy/agentSetKey"; +import route111 from "./routes/setting/agentDeploy/deployAgentModel"; +import route112 from "./routes/setting/agentDeploy/getAgentDeploy"; +import route113 from "./routes/setting/dbConfig/clearData"; +import route114 from "./routes/setting/dev/getSwitchAiDevTool"; +import route115 from "./routes/setting/dev/updateSwitchAiDevTool"; +import route116 from "./routes/setting/fileManagement/openFolder"; +import route117 from "./routes/setting/getTextModel"; +import route118 from "./routes/setting/loginConfig/getUser"; +import route119 from "./routes/setting/loginConfig/updateUserPwd"; +import route120 from "./routes/setting/memoryConfig/delAllMemory"; +import route121 from "./routes/setting/memoryConfig/getMemory"; +import route122 from "./routes/setting/memoryConfig/sureMemory"; +import route123 from "./routes/setting/promptManage/getPrompt"; +import route124 from "./routes/setting/promptManage/updatePrompt"; +import route125 from "./routes/setting/skillManagement/getSkillContent"; +import route126 from "./routes/setting/skillManagement/getSkillList"; +import route127 from "./routes/setting/skillManagement/saveSkillContent"; +import route128 from "./routes/setting/vendorConfig/addVendor"; +import route129 from "./routes/setting/vendorConfig/addVendorModel"; +import route130 from "./routes/setting/vendorConfig/deleteVendor"; +import route131 from "./routes/setting/vendorConfig/delVendorModel"; import route132 from "./routes/setting/vendorConfig/enableVendor"; import route133 from "./routes/setting/vendorConfig/getCodeByLink"; import route134 from "./routes/setting/vendorConfig/getVendorList"; import route135 from "./routes/setting/vendorConfig/modelTest"; import route136 from "./routes/setting/vendorConfig/updateCode"; -import route137 from "./routes/setting/vendorConfig/updateVendor"; -import route138 from "./routes/task/getProject"; -import route139 from "./routes/task/getTaskApi"; -import route140 from "./routes/task/getTaskCategories"; -import route141 from "./routes/task/taskDetails"; -import route142 from "./routes/test/test"; +import route137 from "./routes/setting/vendorConfig/updateVendorInputs"; +import route138 from "./routes/setting/vendorConfig/upVendorModel"; +import route139 from "./routes/task/getProject"; +import route140 from "./routes/task/getTaskApi"; +import route141 from "./routes/task/getTaskCategories"; +import route142 from "./routes/task/taskDetails"; +import route143 from "./routes/test/test"; export default async (app: Express) => { app.use("/api/agents/clearMemory", route1); @@ -207,84 +208,85 @@ export default async (app: Express) => { app.use("/api/production/editImage/updateImageFlow", route60); app.use("/api/production/editImage/uploadImage", route61); app.use("/api/production/getFlowData", route62); - app.use("/api/production/getProductionData", route63); - app.use("/api/production/getStoryboardData", route64); - app.use("/api/production/saveFlowData", route65); - app.use("/api/production/storyboard/addStoryboard", route66); - app.use("/api/production/storyboard/batchAddStoryboardInfo", route67); - app.use("/api/production/storyboard/batchGenerateImage", route68); - app.use("/api/production/storyboard/downPreviewImage", route69); - app.use("/api/production/storyboard/editStoryboardInfo", route70); - app.use("/api/production/storyboard/getStoryboardData", route71); - app.use("/api/production/storyboard/pollingImage", route72); - app.use("/api/production/storyboard/previewImage", route73); - app.use("/api/production/storyboard/removeFrame", route74); - app.use("/api/production/storyboard/updateStoryboardUrl", route75); - app.use("/api/production/workbench/addTrack", route76); - app.use("/api/production/workbench/deleteTrack", route77); - app.use("/api/production/workbench/delVideo", route78); - app.use("/api/production/workbench/generateVideo", route79); - app.use("/api/production/workbench/generateVideoPrompt", route80); - app.use("/api/production/workbench/getGenerateData", route81); - app.use("/api/production/workbench/getVideoList", route82); - app.use("/api/production/workbench/getVideoModelDetail", route83); - app.use("/api/production/workbench/selectVideo", route84); - app.use("/api/production/workbench/updateVideoPrompt", route85); - app.use("/api/project/addDirectorManual", route86); - app.use("/api/project/addProject", route87); - app.use("/api/project/addVisualManual", route88); - app.use("/api/project/deleteDirectorManual", route89); - app.use("/api/project/deleteVisualManual", route90); - app.use("/api/project/delProject", route91); - app.use("/api/project/editDirectorlManual", route92); - app.use("/api/project/editProject", route93); - app.use("/api/project/editVisualManual", route94); - app.use("/api/project/getProject", route95); - app.use("/api/project/getVisualManual", route96); - app.use("/api/project/queryDirectorManual", route97); - app.use("/api/project/visualManual", route98); - app.use("/api/script/addScript", route99); - app.use("/api/script/batchAddScript", route100); - app.use("/api/script/delScript", route101); - app.use("/api/script/exportScript", route102); - app.use("/api/script/extractAssets", route103); - app.use("/api/script/getScrptApi", route104); - app.use("/api/script/pollScriptAssets", route105); - app.use("/api/script/updateScript", route106); - app.use("/api/scriptAgent/getPlanData", route107); - app.use("/api/scriptAgent/setPlanData", route108); - app.use("/api/scriptAgent/updateData", route109); - app.use("/api/setting/about/checkUpdate", route110); - app.use("/api/setting/about/downloadApp", route111); - app.use("/api/setting/agentDeploy/agentSetKey", route112); - app.use("/api/setting/agentDeploy/deployAgentModel", route113); - app.use("/api/setting/agentDeploy/getAgentDeploy", route114); - app.use("/api/setting/dbConfig/clearData", route115); - app.use("/api/setting/dev/getSwitchAiDevTool", route116); - app.use("/api/setting/dev/updateSwitchAiDevTool", route117); - app.use("/api/setting/fileManagement/openFolder", route118); - app.use("/api/setting/getTextModel", route119); - app.use("/api/setting/loginConfig/getUser", route120); - app.use("/api/setting/loginConfig/updateUserPwd", route121); - app.use("/api/setting/memoryConfig/delAllMemory", route122); - app.use("/api/setting/memoryConfig/getMemory", route123); - app.use("/api/setting/memoryConfig/sureMemory", route124); - app.use("/api/setting/promptManage/getPrompt", route125); - app.use("/api/setting/promptManage/updatePrompt", route126); - app.use("/api/setting/skillManagement/getSkillContent", route127); - app.use("/api/setting/skillManagement/getSkillList", route128); - app.use("/api/setting/skillManagement/saveSkillContent", route129); - app.use("/api/setting/vendorConfig/addVendor", route130); - app.use("/api/setting/vendorConfig/deleteVendor", route131); + app.use("/api/production/getStoryboardData", route63); + app.use("/api/production/saveFlowData", route64); + app.use("/api/production/storyboard/addStoryboard", route65); + app.use("/api/production/storyboard/batchAddStoryboardInfo", route66); + app.use("/api/production/storyboard/batchGenerateImage", route67); + app.use("/api/production/storyboard/downPreviewImage", route68); + app.use("/api/production/storyboard/editStoryboardInfo", route69); + app.use("/api/production/storyboard/getStoryboardData", route70); + app.use("/api/production/storyboard/pollingImage", route71); + app.use("/api/production/storyboard/previewImage", route72); + app.use("/api/production/storyboard/removeFrame", route73); + app.use("/api/production/storyboard/updateStoryboardUrl", route74); + app.use("/api/production/workbench/addTrack", route75); + app.use("/api/production/workbench/deleteTrack", route76); + app.use("/api/production/workbench/delVideo", route77); + app.use("/api/production/workbench/generateVideo", route78); + app.use("/api/production/workbench/generateVideoPrompt", route79); + app.use("/api/production/workbench/getGenerateData", route80); + app.use("/api/production/workbench/getVideoList", route81); + app.use("/api/production/workbench/selectVideo", route82); + app.use("/api/production/workbench/updateVideoPrompt", route83); + app.use("/api/project/addDirectorManual", route84); + app.use("/api/project/addProject", route85); + app.use("/api/project/addVisualManual", route86); + app.use("/api/project/deleteDirectorManual", route87); + app.use("/api/project/deleteVisualManual", route88); + app.use("/api/project/delProject", route89); + app.use("/api/project/editDirectorlManual", route90); + app.use("/api/project/editProject", route91); + app.use("/api/project/editVisualManual", route92); + app.use("/api/project/getProject", route93); + app.use("/api/project/getVisualManual", route94); + app.use("/api/project/queryDirectorManual", route95); + app.use("/api/project/visualManual", route96); + app.use("/api/script/addScript", route97); + app.use("/api/script/batchAddScript", route98); + app.use("/api/script/delScript", route99); + app.use("/api/script/exportScript", route100); + app.use("/api/script/extractAssets", route101); + app.use("/api/script/getScrptApi", route102); + app.use("/api/script/pollScriptAssets", route103); + app.use("/api/script/updateScript", route104); + app.use("/api/scriptAgent/getPlanData", route105); + app.use("/api/scriptAgent/setPlanData", route106); + app.use("/api/scriptAgent/updateData", route107); + app.use("/api/setting/about/checkUpdate", route108); + app.use("/api/setting/about/downloadApp", route109); + app.use("/api/setting/agentDeploy/agentSetKey", route110); + app.use("/api/setting/agentDeploy/deployAgentModel", route111); + app.use("/api/setting/agentDeploy/getAgentDeploy", route112); + app.use("/api/setting/dbConfig/clearData", route113); + app.use("/api/setting/dev/getSwitchAiDevTool", route114); + app.use("/api/setting/dev/updateSwitchAiDevTool", route115); + app.use("/api/setting/fileManagement/openFolder", route116); + app.use("/api/setting/getTextModel", route117); + app.use("/api/setting/loginConfig/getUser", route118); + app.use("/api/setting/loginConfig/updateUserPwd", route119); + app.use("/api/setting/memoryConfig/delAllMemory", route120); + app.use("/api/setting/memoryConfig/getMemory", route121); + app.use("/api/setting/memoryConfig/sureMemory", route122); + app.use("/api/setting/promptManage/getPrompt", route123); + app.use("/api/setting/promptManage/updatePrompt", route124); + app.use("/api/setting/skillManagement/getSkillContent", route125); + app.use("/api/setting/skillManagement/getSkillList", route126); + app.use("/api/setting/skillManagement/saveSkillContent", route127); + app.use("/api/setting/vendorConfig/addVendor", route128); + app.use("/api/setting/vendorConfig/addVendorModel", route129); + app.use("/api/setting/vendorConfig/deleteVendor", route130); + app.use("/api/setting/vendorConfig/delVendorModel", route131); app.use("/api/setting/vendorConfig/enableVendor", route132); app.use("/api/setting/vendorConfig/getCodeByLink", route133); app.use("/api/setting/vendorConfig/getVendorList", route134); app.use("/api/setting/vendorConfig/modelTest", route135); app.use("/api/setting/vendorConfig/updateCode", route136); - app.use("/api/setting/vendorConfig/updateVendor", route137); - app.use("/api/task/getProject", route138); - app.use("/api/task/getTaskApi", route139); - app.use("/api/task/getTaskCategories", route140); - app.use("/api/task/taskDetails", route141); - app.use("/api/test/test", route142); + app.use("/api/setting/vendorConfig/updateVendorInputs", route137); + app.use("/api/setting/vendorConfig/upVendorModel", route138); + app.use("/api/task/getProject", route139); + app.use("/api/task/getTaskApi", route140); + app.use("/api/task/getTaskCategories", route141); + app.use("/api/task/taskDetails", route142); + app.use("/api/test/test", route143); } diff --git a/src/routes/assetsGenerate/batchGenerateImageAssets.ts b/src/routes/assetsGenerate/batchGenerateImageAssets.ts index e1aba45..3a0bc67 100644 --- a/src/routes/assetsGenerate/batchGenerateImageAssets.ts +++ b/src/routes/assetsGenerate/batchGenerateImageAssets.ts @@ -116,7 +116,7 @@ export default router.post("/", validateFields(requestSchema), async (req, res) await aiImage.run( { prompt: userPrompt, - imageBase64: item.base64 ? [item.base64] : [], + referenceList: item.base64 ? [{ base64: item.base64, type: "image" }] : [], size: resolution, aspectRatio: "16:9", }, diff --git a/src/routes/assetsGenerate/generateAssets.ts b/src/routes/assetsGenerate/generateAssets.ts index da71f4f..53f5834 100644 --- a/src/routes/assetsGenerate/generateAssets.ts +++ b/src/routes/assetsGenerate/generateAssets.ts @@ -100,7 +100,7 @@ export default router.post("/", validateFields(requestSchema), async (req, res) await aiImage.run( { prompt: userPrompt, - imageBase64: base64 ? [base64] : [], + referenceList: base64 ? [{ type: "image", base64 }] : [], size: resolution, aspectRatio: "16:9", }, diff --git a/src/routes/modelSelect/getModelDetail.ts b/src/routes/modelSelect/getModelDetail.ts index 15fe2e4..4e3d73b 100644 --- a/src/routes/modelSelect/getModelDetail.ts +++ b/src/routes/modelSelect/getModelDetail.ts @@ -13,11 +13,7 @@ export default router.post( async (req, res) => { const { modelId } = req.body; const [id, name] = modelId.split(":"); - const data = await u.db("o_vendorConfig").where("id", id).andWhere("enable", 1).select("models").first(); - if (!data) { - return res.status(404).send({ error: "模型未找到" }); - } - const models = JSON.parse(data.models!); + 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/modelSelect/getModelList.ts b/src/routes/modelSelect/getModelList.ts index 78cb516..ebf2d51 100644 --- a/src/routes/modelSelect/getModelList.ts +++ b/src/routes/modelSelect/getModelList.ts @@ -12,24 +12,28 @@ export default router.post( }), async (req, res) => { const { type } = req.body; - const dataList = await u.db("o_vendorConfig").select("id", "models", "name").where("enable", 1); + const dataList = await u.db("o_vendorConfig").select("id").where("enable", 1); if (!dataList || dataList.length === 0) { return res.status(404).send({ error: "模型未找到" }); } - const result = dataList.flatMap((data) => { - const models = JSON.parse(data.models!); - const filtered = - type === "all" - ? models.filter((item: { type: string }) => item.type !== "video") - : models.filter((item: { type: string }) => item.type === type); - return filtered.map((item: { name: string; modelName: string; type: string }) => ({ - id: data.id, - label: item.name, - value: item.modelName, - type: item.type, - name: data.name, - })); - }); - res.status(200).send(success(result)); + const modelList = await Promise.all(dataList.map((i) => u.vendor.getModelList(i.id!))); + const result = await Promise.all( + dataList.map(async (data, index) => { + const vendorData = await u.vendor.getVendor(data.id!); + const models = modelList[index]; + const filtered = + type === "all" + ? models.filter((item: { type: string }) => item.type !== "video") + : models.filter((item: { type: string }) => item.type === type); + return filtered.map((item: { name: string; modelName: string; type: string }) => ({ + id: data.id, + label: item.name, + value: item.modelName, + type: item.type, + name: vendorData.name, + })); + }), + ); + res.status(200).send(success(result.flat())); }, ); diff --git a/src/routes/production/assets/batchGenerateAssetsImage.ts b/src/routes/production/assets/batchGenerateAssetsImage.ts index d7ec5d8..ef1a32e 100644 --- a/src/routes/production/assets/batchGenerateAssetsImage.ts +++ b/src/routes/production/assets/batchGenerateAssetsImage.ts @@ -101,7 +101,7 @@ export default router.post( }; const imageCls = await u.Ai.Image(projectSettingData?.imageModel as `${string}:${string}`).run( { - imageBase64: imageBase64 ? [imageBase64] : [], + referenceList: imageBase64 ? [{ type: "image", base64: imageBase64 }] : [], ...repeloadObj, }, { diff --git a/src/routes/production/editImage/generateFlowImage.ts b/src/routes/production/editImage/generateFlowImage.ts index 3c4a1b5..4fa90c8 100644 --- a/src/routes/production/editImage/generateFlowImage.ts +++ b/src/routes/production/editImage/generateFlowImage.ts @@ -28,7 +28,13 @@ export default router.post( const imageClass = await u.Ai.Image(model).run( { prompt: prompt, - imageBase64: references && references.length ? await Promise.all(references.map((url: string) => urlToBase64(url))) : [], + referenceList: await (async () => { + const list: { type: "image"; base64: string }[] = []; + for (const url of references) { + list.push({ type: "image" as const, base64: await urlToBase64(url) }); + } + return list; + })(), size: quality, aspectRatio: ratio, }, diff --git a/src/routes/production/getFlowData.ts b/src/routes/production/getFlowData.ts index 6917d08..15a1155 100644 --- a/src/routes/production/getFlowData.ts +++ b/src/routes/production/getFlowData.ts @@ -73,6 +73,7 @@ export default router.post( storyboardTable: "", storyboard: [], //todo:矫正workbench数据 + //@ts-ignore workbench: { videoList: [], }, diff --git a/src/routes/production/getProductionData.ts b/src/routes/production/getProductionData.ts deleted file mode 100644 index 90b725e..0000000 --- a/src/routes/production/getProductionData.ts +++ /dev/null @@ -1,74 +0,0 @@ -import express from "express"; -import u from "@/utils"; -import { z } from "zod"; -import { success } from "@/lib/responseFormat"; -import { validateFields } from "@/middleware/middleware"; -const router = express.Router(); -export default router.post( - "/", - validateFields({ - ids: z.array(z.number()), - }), - async (req, res) => { - const { ids } = req.body; - - //查询分镜配置 - const storyboardConfigs = await u.db("o_videoConfig").whereIn("storyboardId", ids).select("*"); - - //查询视频数据 - const videos = await u.db("o_video").whereIn("storyboardId", ids).select("*"); - - //组装数据 - const data = await Promise.all( - ids.map(async (storyboardId: number) => { - // 处理配置 - const configRow = storyboardConfigs.find((item) => item.storyboardId === storyboardId) || null; - let config = null; - if (configRow?.data) { - const parsedData = JSON.parse(configRow.data); - const dataWithFilePath = await Promise.all( - parsedData.map(async (d: { type: string; id: number }) => { - if (d.type === "assets" && d.id) { - const row = await u - .db("o_assets") - .where("o_assets.id", d.id) - .leftJoin("o_image", "o_assets.imageId", "o_image.id") - .select("o_image.filePath as imageFilePath") - .first(); - if (row?.imageFilePath) { - return { id: d.id, type: "assets", url: await u.oss.getFileUrl(row.imageFilePath) }; - } - return null; - } - if (d.type === "storyboard" && d.id) { - const row = await u.db("o_storyboard").where("id", d.id).select("filePath").first(); - if (row?.filePath) { - return { id: d.id, type: "storyboard", url: await u.oss.getFileUrl(row.filePath) }; - } - return null; - } - return null; - }), - ); - config = { ...configRow, data: dataWithFilePath }; - } - - // 处理视频 - const storyboardVideos = videos.filter((v) => v.storyboardId === storyboardId); - const videosList = await Promise.all( - storyboardVideos.map(async (item) => ({ - ...item, - filePath: item.filePath ? await u.oss.getFileUrl(item.filePath) : null, - })), - ); - - return { - id: storyboardId, - config, - videos: videosList, - }; - }), - ); - return res.status(200).send(success(data)); - }, -); diff --git a/src/routes/production/storyboard/batchGenerateImage.ts b/src/routes/production/storyboard/batchGenerateImage.ts index d66463b..743e1a3 100644 --- a/src/routes/production/storyboard/batchGenerateImage.ts +++ b/src/routes/production/storyboard/batchGenerateImage.ts @@ -87,7 +87,7 @@ export default router.post( await u.Ai.Image(projectSettingData?.imageModel as `${string}:${string}`) .run( { - imageBase64: await getAssetsImageBase64(assetRecord[item.id!] || []), + referenceList: await getAssetsImageBase64(assetRecord[item.id!] || []), ...repeloadObj, }, { @@ -151,5 +151,5 @@ async function getAssetsImageBase64(imageIds: number[]) { }), ); // 保留顺序,并且过滤掉无效项 - return imageUrls.filter(Boolean) as string[]; + return (imageUrls.filter(Boolean) as string[]).map((url) => ({ type: "image" as const, base64: url })); } diff --git a/src/routes/production/workbench/generateVideo.ts b/src/routes/production/workbench/generateVideo.ts index 9587fd2..b741a3d 100644 --- a/src/routes/production/workbench/generateVideo.ts +++ b/src/routes/production/workbench/generateVideo.ts @@ -95,10 +95,10 @@ export default router.post( await aiVideo.run( { prompt, - imageBase64: base64.filter((item) => item !== null) as string[], + referenceList: base64.filter((item) => item !== null).map((item) => ({ type: "image" as const, base64: item! })), mode: modeData.length > 0 ? modeData : mode, duration, - aspectRatio: (ratio?.videoRatio as `${number}:${number}`) || "16:9", + aspectRatio: (ratio?.videoRatio as "16:9" | "9:16") || "16:9", resolution, audio, }, diff --git a/src/routes/production/workbench/generateVideoPrompt.ts b/src/routes/production/workbench/generateVideoPrompt.ts index 9d0895c..474444a 100644 --- a/src/routes/production/workbench/generateVideoPrompt.ts +++ b/src/routes/production/workbench/generateVideoPrompt.ts @@ -42,7 +42,7 @@ export default router.post( } if (item.sources === "assets") { // 查询素材 - const assetsData = await u.db("o_assets").where("o_assets.id", item.id).select("id", "type", "name").first(); + const assetsData = await u.db("o_assets").leftJoin("o_image","o_image.id","o_assets.imageId").where("o_assets.id", item.id).select("o_assets.id", "o_assets.type", "o_assets.name","o_image.filePath").first(); return { ...assetsData, _type: "assets", // 标记类型 @@ -61,6 +61,7 @@ export default router.post( id: item.id, type: item.type, name: item.name, + filePath:item.filePath }); if (item._type === "storyboard") storyboard.push({ @@ -85,18 +86,16 @@ export default router.post( const visualManual = u.getArtPrompt(artStyle, "art_skills", "art_storyboard_video"); const content = ` **模型名称**:${modelData}, - **资产信息**(角色、场景、道具):${assets.map((i) => `[${i.id},${i.type},${i.name}]`).join(",")}, + **资产信息**(角色、场景、道具):${assets.filter(i => i.filePath).map((i) => `[${i.id},${i.type},${i.name}]`).join(",")}, **分镜信息**:${storyboard.map( (i) => ``, )}, `; + console.log("%c Line:87 🌮 content", "background:#2eafb0", content); + try { const { text } = await u.Ai.Text("universalAi").invoke({ system: videoPromptGeneration, diff --git a/src/routes/production/workbench/getGenerateData.ts b/src/routes/production/workbench/getGenerateData.ts index 5d43291..abce73f 100644 --- a/src/routes/production/workbench/getGenerateData.ts +++ b/src/routes/production/workbench/getGenerateData.ts @@ -42,8 +42,7 @@ export default router.post( return res.status(400).json(success("项目未配置视频模型")); } const [videoId, videoModelName] = projectData.videoModel.split(":"); - const vendorData = await u.db("o_vendorConfig").where("id", videoId).select("models").first(); - const models = JSON.parse(vendorData!.models!); + const models = await u.vendor.getModelList(videoId); const findData = models.find((i: any) => i.modelName == videoModelName); const isRef = findData.mode.every((i: any) => Array.isArray(i)); diff --git a/src/routes/production/workbench/getVideoModelDetail.ts b/src/routes/production/workbench/getVideoModelDetail.ts deleted file mode 100644 index 00a721c..0000000 --- a/src/routes/production/workbench/getVideoModelDetail.ts +++ /dev/null @@ -1,17 +0,0 @@ -import express from "express"; -import u from "@/utils"; -const router = express.Router(); - -export default router.post("/", async (req, res) => { - const { type } = req.body; - const vendorData = await u.db("o_vendorConfig").select("id", "models", "name"); - if (!vendorData) { - return res.status(404).send({ error: "模型未找到" }); - } - for (const item of vendorData) { - const modelsData = JSON.parse(item.models! ?? "[]"); - const filterData = modelsData.filter((item: { type: string }) => item.type === type); - if (filterData.length > 0) { - } - } -}); diff --git a/src/routes/setting/agentDeploy/getAgentDeploy.ts b/src/routes/setting/agentDeploy/getAgentDeploy.ts index f1fdfd7..5bf76f0 100644 --- a/src/routes/setting/agentDeploy/getAgentDeploy.ts +++ b/src/routes/setting/agentDeploy/getAgentDeploy.ts @@ -4,6 +4,6 @@ import u from "@/utils"; const router = express.Router(); export default router.post("/", async (req, res) => { - const data = await u.db("o_agentDeploy").leftJoin("o_vendorConfig", "o_vendorConfig.id", "o_agentDeploy.vendorId").select("o_agentDeploy.*", "o_vendorConfig.icon"); + const data = await u.db("o_agentDeploy").leftJoin("o_vendorConfig", "o_vendorConfig.id", "o_agentDeploy.vendorId").select("o_agentDeploy.*"); res.status(200).send(success(data)); }); diff --git a/src/routes/setting/vendorConfig/addVendor.ts b/src/routes/setting/vendorConfig/addVendor.ts index f293131..2453557 100644 --- a/src/routes/setting/vendorConfig/addVendor.ts +++ b/src/routes/setting/vendorConfig/addVendor.ts @@ -43,7 +43,7 @@ const vendorConfigSchema = z.object({ mode: z.array( z.union([ z.enum(["singleImage", "startEndRequired", "endFrameOptional", "startFrameOptional", "text", "audioReference", "videoReference"]), - z.array(z.enum(["videoReference", "imageReference", "audioReference", "textReference"])), + z.array(z.string().regex(/^(videoReference|imageReference|audioReference):\d+$/)), ]), ), audio: z.union([z.literal("optional"), z.boolean()]), @@ -75,26 +75,39 @@ export default router.post( const vendor = exports.vendor; const result = vendorConfigSchema.safeParse(vendor); if (!result.success) { - const errorMsg = result.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join("; "); - return res.status(400).send(error(`vendor配置校验失败: ${errorMsg}`)); + const issueLines = result.error.issues.map((issue, index) => { + const path = issue.path.length ? issue.path.join(".") : "root"; + let detail = issue.message; + + if (issue.code === "invalid_union") { + const unionDetails = [ + ...new Set( + issue.errors + .flat() + .map((e) => e.message) + .filter(Boolean), + ), + ]; + if (unionDetails.length > 0) { + detail = `${issue.message}(${unionDetails.join(";")})`; + } + } + return `${index + 1}. ${path}: ${detail}`; + }); + + return res.status(400).send(error(`vendor配置校验失败,共 ${issueLines.length} 处:\n${issueLines.join("\n")}`)); } if ((vendor.id as string).includes(":")) return res.status(400).send(error("id不能包含英文冒号")); const data = await u.db("o_vendorConfig").where("id", vendor.id).first(); if (data) return res.status(500).send(error("供应商id已存在")); - await u.db("o_vendorConfig").insert({ + const [id] = await u.db("o_vendorConfig").insert({ id: vendor.id, - author: vendor.author, - description: vendor.description || "", - name: vendor.name, - icon: vendor.icon || "", - inputs: JSON.stringify(vendor.inputs ?? []), inputValues: JSON.stringify(vendor.inputValues ?? {}), - models: JSON.stringify(vendor.models ?? []), - code: tsCode, - createTime: Date.now(), + models: JSON.stringify([]), enable: vendor.id == "toonflow" ? 1 : 0, }); + u.vendor.writeCode(vendor.id, tsCode); res.status(200).send(success(result.data)); }, ); diff --git a/src/routes/setting/vendorConfig/addVendorModel.ts b/src/routes/setting/vendorConfig/addVendorModel.ts new file mode 100644 index 0000000..78ce3a5 --- /dev/null +++ b/src/routes/setting/vendorConfig/addVendorModel.ts @@ -0,0 +1,61 @@ +import express from "express"; +import { success, error } from "@/lib/responseFormat"; +import { validateFields } from "@/middleware/middleware"; +import u from "@/utils"; +import { z } from "zod"; +const router = express.Router(); + +export default router.post( + "/", + validateFields({ + id: z.string(), + model: z.discriminatedUnion("type", [ + z.object({ + name: z.string(), + modelName: z.string(), + type: z.literal("text"), + think: z.boolean(), + }), + z.object({ + name: z.string(), + modelName: z.string(), + type: z.literal("image"), + mode: z.array(z.enum(["text", "singleImage", "multiReference"])), + }), + z.object({ + name: z.string(), + modelName: z.string(), + type: z.literal("video"), + mode: z.array( + z.union([ + z.enum(["singleImage", "startEndRequired", "endFrameOptional", "startFrameOptional", "text", "audioReference", "videoReference"]), + z.array(z.string().regex(/^(videoReference|imageReference|audioReference):\d+$/)), + ]), + ), + audio: z.union([z.literal("optional"), z.boolean()]), + durationResolutionMap: z.array( + z.object({ + duration: z.array(z.number()), + resolution: z.array(z.string()), + }), + ), + }), + ]), + }), + async (req, res) => { + const { id, model } = req.body; + + const models = await u.db("o_vendorConfig").where("id", id).first("models"); + if (models?.models) { + const existingModels = JSON.parse(models.models); + existingModels.push(model); + await u + .db("o_vendorConfig") + .where("id", id) + .update({ + models: JSON.stringify(existingModels), + }); + } + res.status(200).send(success("更新成功")); + }, +); diff --git a/src/routes/setting/vendorConfig/delVendorModel.ts b/src/routes/setting/vendorConfig/delVendorModel.ts new file mode 100644 index 0000000..2a812f0 --- /dev/null +++ b/src/routes/setting/vendorConfig/delVendorModel.ts @@ -0,0 +1,33 @@ +import express from "express"; +import { success, error } from "@/lib/responseFormat"; +import { validateFields } from "@/middleware/middleware"; +import u from "@/utils"; +import { z } from "zod"; +const router = express.Router(); + +export default router.post( + "/", + validateFields({ + id: z.string(), + modelName: z.string(), + }), + async (req, res) => { + const { id, modelName } = req.body; + + const models = await u.db("o_vendorConfig").where("id", id).first("models"); + if (models?.models) { + const existingModels = JSON.parse(models.models); + if (!existingModels.some((model: any) => model.modelName === modelName)) { + return res.status(400).send(error("基本模型不允许删除")); + } + const updatedModels = existingModels.filter((model: any) => model.modelName !== modelName); + await u + .db("o_vendorConfig") + .where("id", id) + .update({ + models: JSON.stringify(updatedModels), + }); + } + res.status(200).send(success("更新成功")); + }, +); diff --git a/src/routes/setting/vendorConfig/deleteVendor.ts b/src/routes/setting/vendorConfig/deleteVendor.ts index a9bd942..ac8b744 100644 --- a/src/routes/setting/vendorConfig/deleteVendor.ts +++ b/src/routes/setting/vendorConfig/deleteVendor.ts @@ -1,6 +1,8 @@ import express from "express"; import { success, error } from "@/lib/responseFormat"; import { validateFields } from "@/middleware/middleware"; +import path from "path"; +import fs from "fs"; import u from "@/utils"; import { z } from "zod"; const router = express.Router(); @@ -16,6 +18,7 @@ export default router.post( model: null, vendorId: null, }); + fs.rmSync(path.join(u.getPath("vendor"), `${id}.ts`), { recursive: true, force: true }); res.status(200).send(success("删除成功")); }, ); diff --git a/src/routes/setting/vendorConfig/enableVendor.ts b/src/routes/setting/vendorConfig/enableVendor.ts index 8536504..6a10166 100644 --- a/src/routes/setting/vendorConfig/enableVendor.ts +++ b/src/routes/setting/vendorConfig/enableVendor.ts @@ -12,9 +12,7 @@ export default router.post( }), async (req, res) => { const { id, enable } = req.body; - await u.db("o_vendorConfig").where("id", id).update({ - enable, - }); + await u.db("o_vendorConfig").where("id", id).update({ enable }); res.status(200).send(success("更新成功")); }, ); diff --git a/src/routes/setting/vendorConfig/getVendorList.ts b/src/routes/setting/vendorConfig/getVendorList.ts index 15622a3..59589ed 100644 --- a/src/routes/setting/vendorConfig/getVendorList.ts +++ b/src/routes/setting/vendorConfig/getVendorList.ts @@ -6,11 +6,23 @@ const router = express.Router(); export default router.post("/", async (req, res) => { const data = await u.db("o_vendorConfig").select("*"); - const list = data.map((item) => ({ - ...item, - inputs: JSON.parse(item.inputs ?? "{}"), - inputValues: JSON.parse(item.inputValues ?? "{}"), - models: JSON.parse(item.models ?? "[]"), - })); + const list = await Promise.all( + data.map(async (item) => { + const vendor = u.vendor.getVendor(item.id!); + return { + ...item, + inputValues: JSON.parse(item.inputValues ?? "{}"), + models: await u.vendor.getModelList(item.id!), + code: u.vendor.getCode(item.id!), + description: vendor.description, + inputs: vendor.inputs, + author: vendor.author, + name: vendor.name, + version: vendor.version ?? "1.0", + }; + }), + ); + + list.sort((a, b) => (a.id === "toonflow" ? -1 : b.id === "toonflow" ? 1 : 0)); res.status(200).send(success(list)); }); diff --git a/src/routes/setting/vendorConfig/modelTest.ts b/src/routes/setting/vendorConfig/modelTest.ts index 34ab8d8..a789459 100644 --- a/src/routes/setting/vendorConfig/modelTest.ts +++ b/src/routes/setting/vendorConfig/modelTest.ts @@ -25,7 +25,7 @@ export default router.post( modelData: { prompt: "一张16:9比例的图片,完美等分为2x2四宫格布局,各区域无缝衔接:\n左上宫格:一只可爱的猫,毛发蓬松,眼睛明亮,姿态俏皮\n右上宫格:一只友善的狗,金毛犬,表情愉悦,摇着尾巴\n左下宫格:一头健壮的牛,田园背景,目光温和,皮毛光泽\n右下宫格:一匹骏马,姿态优雅,鬃毛飘逸,肌肉健美\n风格要求:四个宫格风格统一,色彩鲜艳饱和,高清画质,细节清晰锐利,专业插画风格,线条干净,统一的左上方光源,柔和阴影,和谐配色,卡通/半写实风格,宫格间用白色或浅灰细线分隔", //图片提示词 - imageBase64: [], //输入的图片提示词 + referenceList: [], //输入的图片提示词 size: "1K", // 图片尺寸 aspectRatio: "16:9", }, @@ -37,7 +37,7 @@ export default router.post( if (!vendorConfigData) return res.status(500).send(error("未找到该供应商配置")); if (!vendorConfigData.models) return res.status(500).send(error("未找到模型列表")); - const modelList = JSON.parse(vendorConfigData.models); + const modelList = await u.vendor.getModelList(vendorConfigData.id!); const selectedModel = modelList.find((i: any) => i.modelName == modelName); if (type == "video") { @@ -46,8 +46,9 @@ export default router.post( duration: selectedModel.durationResolutionMap[0].duration[0], resolution: selectedModel.durationResolutionMap[0].resolution[0], aspectRatio: "16:9", - prompt: "生成一个卖火柴的小女孩,保持镜头稳定,从远景到近景", - imageBase64: [], + prompt: + "A shirtless middle-aged man with a horse head is standing in a supermarket, carefully comparing two identical bottles of shampoo for 3 seconds, then suddenly bursts into tears, drops to his knees dramatically, a flock of pigeons explodes out of nowhere from behind him, the supermarket lights flicker, an old grandma nearby continues shopping completely unbothered, the horse head man instantly stops crying, puts both shampoo bottles back, and moonwalks away disappearing into the vegetable section. Security camera footage style, slightly grainy, 5 seconds.", + referenceList: [], audio: false, mode: "text", }; diff --git a/src/routes/setting/vendorConfig/upVendorModel.ts b/src/routes/setting/vendorConfig/upVendorModel.ts new file mode 100644 index 0000000..7b4058a --- /dev/null +++ b/src/routes/setting/vendorConfig/upVendorModel.ts @@ -0,0 +1,66 @@ +import express from "express"; +import { success, error } from "@/lib/responseFormat"; +import { validateFields } from "@/middleware/middleware"; +import u from "@/utils"; +import { z } from "zod"; +const router = express.Router(); + +export default router.post( + "/", + validateFields({ + id: z.string(), + modelName: z.string(), + model: z.discriminatedUnion("type", [ + z.object({ + name: z.string(), + modelName: z.string(), + type: z.literal("text"), + think: z.boolean(), + }), + z.object({ + name: z.string(), + modelName: z.string(), + type: z.literal("image"), + mode: z.array(z.enum(["text", "singleImage", "multiReference"])), + }), + z.object({ + name: z.string(), + modelName: z.string(), + type: z.literal("video"), + mode: z.array( + z.union([ + z.enum(["singleImage", "startEndRequired", "endFrameOptional", "startFrameOptional", "text", "audioReference", "videoReference"]), + z.array(z.string().regex(/^(videoReference|imageReference|audioReference):\d+$/)), + ]), + ), + audio: z.union([z.literal("optional"), z.boolean()]), + durationResolutionMap: z.array( + z.object({ + duration: z.array(z.number()), + resolution: z.array(z.string()), + }), + ), + }), + ]), + }), + async (req, res) => { + const { id, modelName, model } = req.body; + + const models = await u.db("o_vendorConfig").where("id", id).first("models"); + if (models?.models) { + const existingModels = JSON.parse(models.models); + const modelIndex = existingModels.findIndex((m: any) => m.modelName !== modelName); + if (modelIndex === -1) { + existingModels.push(model); + } + existingModels[modelIndex] = model; + await u + .db("o_vendorConfig") + .where("id", id) + .update({ + models: JSON.stringify(existingModels), + }); + } + res.status(200).send(success("更新成功")); + }, +); diff --git a/src/routes/setting/vendorConfig/updateCode.ts b/src/routes/setting/vendorConfig/updateCode.ts index 08721bb..3c88614 100644 --- a/src/routes/setting/vendorConfig/updateCode.ts +++ b/src/routes/setting/vendorConfig/updateCode.ts @@ -44,7 +44,7 @@ const vendorConfigSchema = z.object({ mode: z.array( z.union([ z.enum(["singleImage", "startEndRequired", "endFrameOptional", "startFrameOptional", "text", "audioReference", "videoReference"]), - z.array(z.enum(["audioReference", "videoReference", "textReference", "imageReference"])), + z.array(z.string().regex(/^(videoReference|imageReference|audioReference):\d+$/)), ]), ), audio: z.union([z.literal("optional"), z.boolean()]), @@ -85,16 +85,11 @@ export default router.post( .db("o_vendorConfig") .where("id", id) .update({ - author: vendor.author, - description: vendor.description || "", - name: vendor.name, - icon: vendor.icon || "", - inputs: JSON.stringify(vendor.inputs ?? []), inputValues: JSON.stringify(vendor.inputValues ?? {}), models: JSON.stringify(vendor.models ?? []), - code: tsCode, - createTime: Date.now(), }); + u.vendor.writeCode(id, tsCode); + res.status(200).send(success(result.data)); } catch (err) { console.log(err); diff --git a/src/routes/setting/vendorConfig/updateVendor.ts b/src/routes/setting/vendorConfig/updateVendor.ts deleted file mode 100644 index 1f9338f..0000000 --- a/src/routes/setting/vendorConfig/updateVendor.ts +++ /dev/null @@ -1,71 +0,0 @@ -import express from "express"; -import { success, error } from "@/lib/responseFormat"; -import { validateFields } from "@/middleware/middleware"; -import u from "@/utils"; -import { z } from "zod"; -import { transform } from "sucrase"; -const router = express.Router(); - -export default router.post( - "/", - validateFields({ - id: z.string(), - inputValues: z.record(z.string(), z.string()), - inputs: z.array( - z.object({ - key: z.string(), - label: z.string(), - type: z.enum(["text", "password", "url"]), - required: z.boolean(), - placeholder: z.string().optional(), - }), - ), - models: z.array( - z.discriminatedUnion("type", [ - z.object({ - name: z.string(), - modelName: z.string(), - type: z.literal("text"), - think: z.boolean(), - }), - z.object({ - name: z.string(), - modelName: z.string(), - type: z.literal("image"), - mode: z.array(z.enum(["text", "singleImage", "multiReference"])), - }), - z.object({ - name: z.string(), - modelName: z.string(), - type: z.literal("video"), - mode: z.array( - z.union([ - z.enum(["singleImage", "startEndRequired", "endFrameOptional", "startFrameOptional", "text"]), - z.array(z.enum(["audioReference", "videoReference", "textReference", "imageReference"])), - ]), - ), - audio: z.union([z.literal("optional"), z.boolean()]), - durationResolutionMap: z.array( - z.object({ - duration: z.array(z.number()), - resolution: z.array(z.string()), - }), - ), - }), - ]), - ), - }), - async (req, res) => { - const { id, models, inputs, inputValues } = req.body; - - await u - .db("o_vendorConfig") - .where("id", id) - .update({ - inputs: JSON.stringify(inputs), - inputValues: JSON.stringify(inputValues), - models: JSON.stringify(models), - }); - res.status(200).send(success("更新成功")); - }, -); diff --git a/src/routes/setting/vendorConfig/updateVendorInputs.ts b/src/routes/setting/vendorConfig/updateVendorInputs.ts new file mode 100644 index 0000000..acad026 --- /dev/null +++ b/src/routes/setting/vendorConfig/updateVendorInputs.ts @@ -0,0 +1,26 @@ +import express from "express"; +import { success, error } from "@/lib/responseFormat"; +import { validateFields } from "@/middleware/middleware"; +import u from "@/utils"; +import { z } from "zod"; +import { transform } from "sucrase"; +const router = express.Router(); + +export default router.post( + "/", + validateFields({ + id: z.string(), + inputValues: z.record(z.string(), z.string()), + }), + async (req, res) => { + const { id, inputValues } = req.body; + + await u + .db("o_vendorConfig") + .where("id", id) + .update({ + inputValues: JSON.stringify(inputValues), + }); + res.status(200).send(success("更新成功")); + }, +); diff --git a/src/types/database.d.ts b/src/types/database.d.ts index a787e02..551db81 100644 --- a/src/types/database.d.ts +++ b/src/types/database.d.ts @@ -1,4 +1,4 @@ -// @db-hash 71e339c0a728c10bedb294a93976dcd8 +// @db-hash 9248d7bcfe0a1bc57e5b9bc33d8c7d83 //该文件由脚本自动生成,请勿手动修改 export interface memories { @@ -201,17 +201,10 @@ export interface o_user { 'password'?: string | null; } export interface o_vendorConfig { - 'author'?: string | null; - 'code'?: string | null; - 'createTime'?: number | null; - 'description'?: string | null; 'enable'?: number | null; - 'icon'?: string | null; 'id'?: string; - 'inputs'?: string | null; 'inputValues'?: string | null; 'models'?: string | null; - 'name'?: string | null; } export interface o_video { 'errorReason'?: string | null; diff --git a/src/utils.ts b/src/utils.ts index d3a6c7d..4e87d29 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -12,6 +12,7 @@ import { getPrompts } from "@/utils/getPrompts"; import { getArtPrompt } from "@/utils/getArtPrompt"; import replaceUrl from "@/utils/replaceUrl"; import writeVersion from "@/utils/writeVersion"; +import * as vendor from "@/utils/vendor"; export default { db, @@ -28,4 +29,5 @@ export default { getArtPrompt, replaceUrl, writeVersion, + vendor, }; diff --git a/src/utils/ai.ts b/src/utils/ai.ts index 4e1defa..5539cba 100644 --- a/src/utils/ai.ts +++ b/src/utils/ai.ts @@ -1,4 +1,4 @@ -import { generateText, streamText, wrapLanguageModel, stepCountIs } from "ai"; +import { generateText, streamText, wrapLanguageModel, stepCountIs, extractReasoningMiddleware } from "ai"; import { devToolsMiddleware } from "@ai-sdk/devtools"; import axios from "axios"; import { transform } from "sucrase"; @@ -17,14 +17,20 @@ async function resolveModelName(value: AiType | `${string}:${string}`): Promise< return value as `${number}:${string}`; } -async function getVendorTemplateFn(fnName: FnName, modelName: `${string}:${string}`) { +async function getVendorTemplateFn( + fnName: "textRequest", + modelName: `${string}:${string}`, +): 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 vendorConfigData = await u.db("o_vendorConfig").where("id", id).first(); if (!vendorConfigData) throw new Error(`未找到供应商配置 id=${id}`); - const modelList = JSON.parse(vendorConfigData.models ?? "[]"); + const modelList = await u.vendor.getModelList(id); const selectedModel = modelList.find((i: any) => i.modelName == name); if (!selectedModel) throw new Error(`未找到模型 ${name} id=${id}`); - const jsCode = transform(vendorConfigData.code!, { transforms: ["typescript"] }).code; + const code = u.vendor.getCode(id); + const jsCode = transform(code, { transforms: ["typescript"] }).code; const running = u.vm(jsCode); if (running.vendor) { Object.assign(running.vendor.inputValues, JSON.parse(vendorConfigData.inputValues ?? "{}")); @@ -32,7 +38,11 @@ async function getVendorTemplateFn(fnName: FnName, modelName: `${string}:${strin } const fn = running[fnName]; if (!fn) throw new Error(`未找到供应商配置中的函数 ${fnName} id=${id}`); - if (fnName == "textRequest") return fn(selectedModel); + if (fnName == "textRequest") + return (think?: boolean, thinkLevel: 0 | 1 | 2 | 3 = 0) => { + const effectiveThink = think ?? !!selectedModel.think; + return fn(selectedModel, effectiveThink, thinkLevel); + }; else return (input: T) => fn(input, selectedModel); } @@ -42,13 +52,13 @@ async function withTaskRecord( describe: string, relatedObjects: string, projectId: number, - fn: (modelName: `${string}:${string}`) => Promise, + fn: (modelName: `${string}:${string}`, think: Boolean, thinkLevel: 0 | 1 | 2 | 3) => Promise, ): Promise { const modelName = await resolveModelName(modelKey); const [id, model] = modelName.split(":"); const taskRecord = await u.task(projectId, taskClass, model, { describe: describe, content: relatedObjects }); try { - const result = await fn(modelName); + const result = await fn(modelName, false, 0); taskRecord(1); return result; } catch (e) { @@ -72,46 +82,73 @@ async function urlToBase64(url: string, retries = 3, delay = 1000): Promise[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 generateText({ ...(input.tools && { stopWhen: stepCountIs(Object.keys(input.tools).length * 50) }), ...input, model: switchAiDevTool?.value === "1" ? wrapLanguageModel({ - model: await getVendorTemplateFn("textRequest", modelName), + model: await sdkFn(this.think, this.thinkLevel), middleware: devToolsMiddleware(), }) - : await getVendorTemplateFn("textRequest", modelName), + : await sdkFn(this.think, this.thinkLevel), } 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: await getVendorTemplateFn("textRequest", modelName), - middleware: devToolsMiddleware(), + model: sdkFn(this.think, this.thinkLevel), + middleware: [ + devToolsMiddleware(), + extractReasoningMiddleware({ + tagName: "reasoning_content", + }), + ], }) - : await getVendorTemplateFn("textRequest", modelName), + : wrapLanguageModel({ + model: sdkFn(this.think, this.thinkLevel), + middleware: extractReasoningMiddleware({ + tagName: "reasoning_content", + }), + }), } as Parameters[0]); } } +function referenceList2imageBase642(id: string, input: any) { + const version = u.vendor.getVendor(id).version; + if (!version || isNaN(parseFloat(version)) || parseFloat(version) < 2.0) { + input.imageBase64 = input.referenceList.map((item: any) => item.base64); + return input; + } + return input; +} + +type ReferenceList = { type: "image"; base64: string } | { type: "audio"; base64: string } | { type: "video"; base64: string }; + interface ImageConfig { - prompt: string; //图片提示词 - imageBase64: string[]; //输入的图片提示词 - size: "1K" | "2K" | "4K"; // 图片尺寸 - aspectRatio: `${number}:${number}`; // 长宽比 + prompt: string; + referenceList?: Extract[]; + size: "1K" | "2K" | "4K"; + aspectRatio: `${number}:${number}`; } interface TaskRecord { @@ -131,6 +168,7 @@ class AiImage { const modelName = await resolveModelName(this.key); const exec = async (mn: `${string}:${string}`) => { const fn = await getVendorTemplateFn("imageRequest", mn); + await referenceList2imageBase642(mn.split(":")[0], input); this.result = await fn(input); if (this.result.startsWith("http")) this.result = await urlToBase64(this.result); return this; @@ -145,14 +183,23 @@ class AiImage { return this; } } + +type VideoMode = + | "singleImage" //单图参考 + | "startEndRequired" //首尾帧(两张都得有) + | "endFrameOptional" //首尾帧(尾帧可选) + | "startFrameOptional" //首尾帧(首帧可选) + | "text" //文本 + | (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[]; //多参考(数字代表限制数量) + interface VideoConfig { - prompt: string; //视频提示词 - imageBase64: string[]; //输入的图片提示词 - aspectRatio: `${number}:${number}`; // 长宽比 - mode: string; //模式 - duration: number; // 视频时长,单位秒 - resolution: string; // 视频分辨率 - audio: boolean; // 是否需要配音 + duration: number; + resolution: string; + aspectRatio: "16:9" | "9:16"; + prompt: string; + referenceList?: ReferenceList[]; + audio?: boolean; + mode: VideoMode[]; } class AiVideo { @@ -165,6 +212,7 @@ class AiVideo { const modelName = await resolveModelName(this.key); const exec = async (mn: `${string}:${string}`) => { const fn = await getVendorTemplateFn("videoRequest", mn); + await referenceList2imageBase642(mn.split(":")[0], input); this.result = await fn(input); if (this.result.startsWith("http")) this.result = await urlToBase64(this.result); return this; @@ -189,6 +237,7 @@ class AiAudio { const modelName = await resolveModelName(this.key); const exec = async (mn: `${string}:${string}`) => { const fn = await getVendorTemplateFn("ttsRequest", mn); + await referenceList2imageBase642(mn.split(":")[0], input); this.result = await fn(input); if (this.result.startsWith("http")) this.result = await urlToBase64(this.result); return this; @@ -205,7 +254,7 @@ class AiAudio { } export default { - Text: (AiType: AiType | `${string}:${string}`) => new AiText(AiType), + Text: (AiType: AiType | `${string}:${string}`, think?: boolean, thinkLevel?: 0 | 1 | 2 | 3) => new AiText(AiType, think, thinkLevel), Image: (key: `${string}:${string}`) => new AiImage(key), Video: (key: `${string}:${string}`) => new AiVideo(key), Audio: (key: `${string}:${string}`) => new AiAudio(key), diff --git a/src/utils/ai/video/owned/volcengine.ts b/src/utils/ai/video/owned/volcengine.ts deleted file mode 100644 index c58a478..0000000 --- a/src/utils/ai/video/owned/volcengine.ts +++ /dev/null @@ -1,88 +0,0 @@ -import "../type"; -import axios from "axios"; -import { pollTask, validateVideoConfig } from "@/utils/ai/utils"; - -export default async (input: VideoConfig, config: AIConfig) => { - if (!config.apiKey) throw new Error("缺少API Key"); - - // const { owned, images, hasStartEndType } = validateVideoConfig(input, config); - const hasStartEndType = input.mode === "startEnd"; - const authorization = "Bearer " + config.apiKey.replace(/^Bearer\s*/i, "").trim(); - const baseUrl = config.baseURL ?? "https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks"; - const images = input.imageBase64 || []; - - // 构建图片内容 - const imageContent = images.map((base64, index) => { - const item: Record = { - type: "image_url", - image_url: { url: base64 }, - }; - if (hasStartEndType) { - item.role = index === 0 ? "first_frame" : "last_frame"; - } else { - item.role = "reference_image"; - } - return item; - }); - - // 构建请求体 - const requestBody: Record = { - model: config.model, - content: [{ type: "text", text: input.prompt }, ...imageContent], - duration: input.duration, - resolution: input.resolution, - watermark: false, - }; - - // 仅当模型支持音频时才添加 generate_audio 字段 - if (typeof input?.audio == "boolean") { - requestBody.generate_audio = input.audio ?? false; - } - - // 创建视频生成任务 - const createResponse = await axios.post(baseUrl, requestBody, { - headers: { - "Content-Type": "application/json", - Authorization: authorization, - }, - }); - console.log("%c Line:44 🍡 createResponse", "background:#2eafb0", createResponse.data); - - const taskId = createResponse.data.id; - - if (!taskId) throw new Error("视频任务创建失败"); - - // 轮询任务状态 - return await pollTask(async () => { - const data = await axios.get(`${baseUrl}/${taskId}`, { - headers: { Authorization: authorization }, - }); - console.log("%c Line:62 🥕 data.data", "background:#e41a6a", data.data); - - const { status, content, error } = data.data; - - switch (status) { - case "succeeded": - case "completed": - return { completed: true, url: content?.video_url }; - case "failed": - case "cancelled": - case "expired": - let errorMsg = ""; - try { - errorMsg = typeof error === "string" ? error : JSON.stringify(error); - } catch (e) { - errorMsg = error || ""; - } - return { completed: false, error: `任务${status}: ${errorMsg}` }; - case "queued": - case "running": - case "unknown": - case "submit": - case "in_progress": - return { completed: false }; - default: - return { completed: false, error: `未知状态: ${status}` }; - } - }); -}; diff --git a/src/utils/vendor.ts b/src/utils/vendor.ts new file mode 100644 index 0000000..c574222 --- /dev/null +++ b/src/utils/vendor.ts @@ -0,0 +1,41 @@ +import { transform } from "sucrase"; +import fs from "fs"; +import path from "path"; +import u from "@/utils"; + +export function writeCode(id: string | number, tsCode: string) { + const rootDir = u.getPath("vendor") + fs.mkdirSync(rootDir, { recursive: true }) + if (fs.existsSync(path.join(rootDir, `${id}.ts`))) { + fs.writeFileSync(path.join(rootDir, `${id}.ts`), tsCode); + } + fs.writeFileSync(path.join(rootDir, `${id}.ts`), tsCode); +} + +export function getCode(id: string): string { + const rootDir = u.getPath("vendor"); + const targetFile = path.join(rootDir, `${id}.ts`); + if (!fs.existsSync(targetFile)) return ""; + return fs.readFileSync(targetFile, "utf-8"); +} + +export async function getModelList(id: string): Promise> { + const models = await u.db("o_vendorConfig").where("id", id).select("models").first(); + if (!models || !models.models) return []; + const code = getCode(id); + const jsCode = transform(code, { transforms: ["typescript"] }).code; + const vendorData = u.vm(jsCode); + const combined = [...JSON.parse(JSON.stringify(vendorData.vendor.models)), ...JSON.parse(models?.models ?? "[]")]; + const map = new Map(); + for (const m of combined) { + map.set(m.modelName, m); + } + return [...map.values()]; +} + +export function getVendor(id: string) { + const code = getCode(id); + const jsCode = transform(code, { transforms: ["typescript"] }).code; + const vendorData = u.vm(jsCode); + return vendorData.vendor; +} diff --git a/src/utils/vm.ts b/src/utils/vm.ts index c1ad891..f71706b 100644 --- a/src/utils/vm.ts +++ b/src/utils/vm.ts @@ -14,6 +14,7 @@ import FormData from "form-data"; import jsonwebtoken from "jsonwebtoken"; import u from "@/utils"; export default function runCode(code: string, vendor?: Record) { + code = code.replace(/export\s*\{\s*\};?/g, ""); // 去掉 export {} 以免沙盒环境报错 // 创建一个沙盒 const exports = {}; const sandbox: Record = { @@ -31,7 +32,7 @@ export default function runCode(code: string, vendor?: Record) { urlToBase64, mergeImages, pollTask, - fetch, + fetch: fetch, exports, axios, FormData, diff --git a/tsconfig.json b/tsconfig.json index 3237094..03159da 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "ignoreDeprecations": "5.0", "target": "ESNext", "module": "CommonJS", "moduleResolution": "Node", @@ -12,10 +13,21 @@ "outDir": "build", "baseUrl": ".", "paths": { - "@/*": ["src/*"] + "@/*": [ + "src/*" + ] }, "incremental": true, - "typeRoots": ["./node_modules/@types", "./src/types"], + "typeRoots": [ + "./node_modules/@types", + "./src/types" + ], "resolveJsonModule": true - } -} + }, + "exclude": [ + "node_modules", + "data/**/*.ts", + "dist", + "build" + ] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 537a199..0a2ab4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1869,11 +1869,6 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.6: shebang-command "^2.0.0" which "^2.0.1" -custom-electron-titlebar@^4.2.8: - version "4.4.1" - resolved "https://registry.npmmirror.com/custom-electron-titlebar/-/custom-electron-titlebar-4.4.1.tgz#aea64f009697c9771cb2a67d2eb5ac8059696906" - integrity sha512-I+sOGBdslrGpuCWlhda8P0vtRAZK+W2NzjHLsxTiE2bNmhAIs9YLDe6iRBExwU1xVZt+J1hSXzUT67BlAuMWLA== - debug@2.6.9: version "2.6.9" resolved "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"