diff --git a/data/vendor/klingai.ts b/data/vendor/klingai.ts new file mode 100644 index 0000000..2e72cae --- /dev/null +++ b/data/vendor/klingai.ts @@ -0,0 +1,655 @@ +/** + * 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: "url"; url: string } | { sourceType: "base64"; base64: string })) + | ({ type: "audio" } & ({ sourceType: "url"; url: string } | { sourceType: "base64"; base64: string })) + | ({ type: "video" } & ({ sourceType: "url"; url: string } | { 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) => 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: "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)获取 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: false, 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 => { + if (ref.sourceType === "url") { + return ref.url; + } + return ref.base64.replace(/^data:[^;]+;base64,/, ""); +}; + +/** + * 从 ReferenceList 条目中提取带头的 base64 或 url + * 用于 omni-video 接口,该接口的 image_url 支持带前缀的 base64 和 url + */ +const extractImageUrl = (ref: ReferenceList): string => { + if (ref.sourceType === "url") { + return ref.url; + } + 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) => { + throw new Error("可灵AI不支持文本模型"); +}; + +const uploadReference = async (base64: string, fileType: "image" | "audio" | "video"): Promise => { + // 可灵AI的接口直接接受 base64,压缩图片后原样返回 + if (fileType === "image") { + const compressed = await zipImage(base64, 10240); + return { type: "image", sourceType: "base64", base64: compressed }; + } + return { type: fileType, sourceType: "base64", base64 } as ReferenceList; +}; + +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.uploadReference = uploadReference; +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..fe8a26d --- /dev/null +++ b/data/vendor/minimax.ts @@ -0,0 +1,402 @@ +/** + * 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: "url"; url: string } | { sourceType: "base64"; base64: string })) + | ({ type: "audio" } & ({ sourceType: "url"; url: string } | { sourceType: "base64"; base64: string })) + | ({ type: "video" } & ({ sourceType: "url"; url: string } | { 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) => 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系列推理文本模型、文生图/图生图、视频生成(文生视频、图生视频、首尾帧生成)能力", + 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 => { + if (ref.sourceType === "url") { + return ref.url; + } + return ref.base64.startsWith("data:") ? ref.base64 : `data:image/png;base64,${ref.base64}`; +}; + +// ============================================================ +// 适配器函数 +// ============================================================ + +const textRequest = (model: TextModel) => { + 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..c21ec13 --- /dev/null +++ b/data/vendor/null.ts @@ -0,0 +1,347 @@ +/** + * 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: "url"; url: string } | { sourceType: "base64"; base64: string })) + | ({ type: "audio" } & ({ sourceType: "url"; url: string } | { sourceType: "base64"; base64: string })) + | ({ type: "video" } & ({ sourceType: "url"; url: string } | { 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) => any; //文本模型 + uploadReference: (base64: string, fileType: "image" | "audio" | "video") => Promise; // reference前置处理器,专门用于处理referenceList中的条目,将有头base64上传并返回URL,然后reference才会传入videoRequest/imageRequest/ttsRequest中 + 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: "openai", + version: "2.0", + author: "Toonflow", + name: "OpenAI标准接口", + description: "## OpenAI标准格式接口,可修改请求地址并手动添加模型。", + 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) => { + 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 uploadReference = async (base64: string, fileType: "image" | "audio" | "video"): Promise => { + return { type: fileType, sourceType: "base64", base64 }; +}; + +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.uploadReference = uploadReference; +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 / uploadReference / 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 等工厂函数创建)。 + * - uploadReference(base64, fileType):reference 前置处理器,用于将 referenceList 中的 + * 有头 base64 条目上传到供应商的文件服务并返回 ReferenceList 对象(通常转为 URL 形式)。 + * 如果供应商 API 直接接受 base64,可以原样返回 { type: fileType, sourceType: "base64", base64 }。 + * 上传后应返回 { type: fileType, sourceType: "url", url: "..." }。 + * 该函数在 imageRequest / videoRequest / ttsRequest 被调用前执行, + * 处理后的 referenceList 才会传入后续函数。 + * - imageRequest(config, model):返回有头 base64 字符串(如 "data:image/png;base64,...")。 + * config.referenceList 为 Extract[] 类型, + * 包含经过 uploadReference 处理后的图片引用(可能是 URL 或 base64)。 + * - videoRequest(config, model):返回有头 base64 字符串(如 "data:video/mp4;base64,...")。 + * config.referenceList 为 ReferenceList[] 类型,可包含 image / video / audio 三种引用。 + * 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: "url" | "base64"(数据来源) + * - url 或 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.uploadReference(必须) + * - exports.imageRequest(必须) + * - exports.videoRequest(必须) + * - exports.ttsRequest(必须) + * - exports.checkForUpdates(可选) + * - exports.updateVendor(可选) + * 未实现的适配器函数保留空实现(return ""),不可省略导出。 + * 文件末尾必须包含 export {}; 以确保文件被识别为模块。 + * + * 【生成流程】 + * 当用户请求生成新的供应商适配时: + * 1. 确认用户已提供 curl 示例或 API 文档。 + * 2. 分析 API 的认证方式、端点地址、请求/响应结构。 + * 3. 基于本模板结构,填充 vendor 配置和对应的适配器函数。 + * 4. 实现 uploadReference:如果 API 需要 URL 引用,则上传 base64 到供应商文件服务并返回 URL; + * 如果 API 直接接受 base64,则原样返回。 + * 5. 仅实现用户需要的模型类型,未用到的函数保留空实现(return "")。 + * 6. 生成完整可用的代码,确保无语法错误、无遗漏导出。 + */ \ No newline at end of file diff --git a/data/vendor/openai.ts b/data/vendor/openai.ts new file mode 100644 index 0000000..c990993 --- /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) => 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) => { + 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..7622675 --- /dev/null +++ b/data/vendor/toonflow.ts @@ -0,0 +1,569 @@ +//如需遥测AI请使用在toonflow安装目录运行npx @ai-sdk/devtools (要求在其他设置中打开遥测功能,且toonflow有权限在安装目录创建.devtools文件夹) +// ==================== 类型定义 ==================== +// 文本模型 +interface TextModel { + name: string; // 显示名称 + modelName: string; + type: "text"; + think: boolean; // 前端显示用 +} + +// 图像模型 +interface ImageModel { + name: string; // 显示名称 + modelName: string; + type: "image"; + mode: ("text" | "singleImage" | "multiReference")[]; + associationSkills?: string; // 关联技能,多个技能用逗号分隔 +} +// 视频模型 +interface VideoModel { + name: string; // 显示名称 + modelName: string; //全局唯一 + type: "video"; + mode: ( + | "singleImage" // 单图 + | "startEndRequired" // 首尾帧(两张都得有) + | "endFrameOptional" // 首尾帧(尾帧可选) + | "startFrameOptional" // 首尾帧(首帧可选) + | "text" // 文本生视频 + | ("videoReference" | "imageReference" | "audioReference" | "textReference")[] + )[]; // 混合参考 + associationSkills?: string; // 关联技能,多个技能用逗号分隔 + audio: "optional" | false | true; // 音频配置 + durationResolutionMap: { duration: number[]; resolution: string[] }[]; +} + +interface TTSModel { + name: string; // 显示名称 + modelName: string; + type: "tts"; + voices: { + title: string; //显示名称 + voice: string; //说话人 + }[]; +} +// 供应商配置 +interface VendorConfig { + id: string; //供应商唯一标识,必须全局唯一 + author: string; + description?: string; //md5格式 + name: string; + icon?: string; //仅支持base64格式 + inputs: { + key: string; + label: string; + type: "text" | "password" | "url"; + required: boolean; + placeholder?: string; + }[]; + inputValues: Record; + models: (TextModel | ImageModel | VideoModel)[]; +} +// ==================== 全局工具函数 ==================== +//Axios实例 +//压缩图片大小(1MB = 1 * 1024 * 1024) +declare const zipImage: (completeBase64: string, size: number) => Promise; +//压缩图片分辨率 +declare const zipImageResolution: (completeBase64: string, width: number, height: number) => Promise; +//多图拼接乘单图 maxSize 最大输出大小,默认为 10mb +declare const mergeImages: (completeBase64: string[], maxSize?: string) => Promise; +//Url转Base64 +declare const urlToBase64: (url: string) => Promise; +//轮询函数 +declare const pollTask: ( + fn: () => Promise<{ completed: boolean; data?: string; error?: string }>, + interval?: number, + timeout?: number, +) => Promise<{ completed: boolean; data?: string; error?: string }>; +declare const axios: any; +declare const createOpenAI: any; +declare const createDeepSeek: any; +declare const createZhipu: any; +declare const createQwen: any; +declare const createAnthropic: any; +declare const createOpenAICompatible: any; +declare const createXai: any; +declare const createMinimax: any; +declare const createGoogleGenerativeAI: any; +declare const logger: (logstring: string) => void; +declare const jsonwebtoken: any; + +// ==================== 供应商数据 ==================== +const vendor: VendorConfig = { + id: "toonflow", + author: "Toonflow", + description: + "## Toonflow官方中转平台\n\nToonflow官方中转平台,提供**文本、图像、视频、音频**等多模态生成能力的中转服务,支持接入多个大模型供应商,方便用户统一管理和调用不同供应商的生成能力。\n\n🔗 [前往中转平台](https://api.toonflow.net/)\n\n如果这个项目对你有帮助,可以考虑支持一下我们的开发工作 ☕", + name: "Toonflow官方中转平台", + icon: "", + inputs: [{ key: "apiKey", label: "API密钥", type: "password", required: true }], + inputValues: { + apiKey: "", + baseUrl: "https://api.toonflow.net/v1", + }, + models: [ + { + name: "claude-sonnet-4-6", + type: "text", + modelName: "claude-sonnet-4-6", + think: false, + }, + { + name: "claude-opus-4-6", + type: "text", + modelName: "claude-opus-4-6", + think: false, + }, + { + name: "claude-sonnet-4-5-20250929", + type: "text", + modelName: "claude-sonnet-4-5-20250929", + think: false, + }, + { + name: "claude-opus-4-5-20251101", + type: "text", + modelName: "claude-opus-4-5-20251101", + think: false, + }, + { + name: "claude-haiku-4-5-20251001", + type: "text", + modelName: "claude-haiku-4-5-20251001", + think: false, + }, + { + name: "gpt-5.4", + type: "text", + modelName: "gpt-5.4", + think: false, + }, + { + name: "gpt-5.2", + type: "text", + modelName: "gpt-5.2", + think: false, + }, + { + name: "MiniMax-M2.7", + type: "text", + modelName: "MiniMax-M2.7", + think: true, + }, + { + name: "MiniMax-M2.5", + type: "text", + modelName: "MiniMax-M2.5", + think: true, + }, + { + name: "Wan2.6 I2V 1080P (支持真人)", + type: "video", + modelName: "Wan2.6-I2V-1080P", + mode: ["text", "startEndRequired"], + durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["1080p"] }], + audio: true, + }, + { + name: "Wan2.6 I2V 720P (支持真人)", + type: "video", + modelName: "Wan2.6-I2V-720P", + mode: ["text", "startEndRequired"], + durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["720p"] }], + audio: true, + }, + { + name: "Seedance 1.5 Pro", + type: "video", + modelName: "doubao-seedance-1-5-pro-251215", + durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }], + mode: ["text", "endFrameOptional"], + audio: true, + }, + { + name: "vidu2 turbo", + type: "video", + modelName: "ViduQ2-turbo", + durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["540p", "720p", "1080p"] }], + mode: ["singleImage", "startEndRequired"], + audio: false, + }, + { + name: "ViduQ3 pro", + type: "video", + modelName: "ViduQ3-pro", + durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], resolution: ["540p", "720p", "1080p"] }], + mode: ["singleImage", "startEndRequired"], + audio: false, + }, + { + name: "ViduQ2 pro", + type: "video", + modelName: "ViduQ2-pro", + durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["540p", "720p", "1080p"] }], + mode: ["singleImage", "startEndRequired"], + audio: false, + }, + + { + name: "Doubao Seedream 5.0 Lite", + type: "image", + modelName: "Doubao-Seedream-5.0-Lite", + mode: ["text", "singleImage", "multiReference"], + }, + { + name: "Doubao Seedream 4.5", + type: "image", + modelName: "doubao-seedream-4-5-251128", + mode: ["text", "singleImage", "multiReference"], + }, + ], +}; +exports.vendor = vendor; + +// ==================== 适配器函数 ==================== + +// 文本请求函数 +const textRequest: (textModel: TextModel) => { url: string; model: string } = (textModel) => { + if (!vendor.inputValues.apiKey) throw new Error("缺少API Key"); + const apiKey = vendor.inputValues.apiKey.replace("Bearer ", ""); + + return createOpenAI({ + baseURL: vendor.inputValues.baseUrl, + apiKey: apiKey, + }).chat(textModel.modelName); +}; +exports.textRequest = textRequest; + +//图片请求函数 +interface ImageConfig { + prompt: string; //图片提示词 + imageBase64: string[]; //输入的图片提示词 + size: "1K" | "2K" | "4K"; // 图片尺寸 + aspectRatio: `${number}:${number}`; // 长宽比 +} +//豆包格式适配 +function doubaoAdaptor(imageConfig: ImageConfig, imageModel: ImageModel) { + const size = imageConfig.size === "1K" ? "2K" : imageConfig.size; + const sizeMap: Record> = { + "16:9": { + "2k": "2848x1600", + "2K": "2848x1600", + "4K": "4096x2304", + "4k": "4096x2304", + }, + "9:16": { + "4k": "2304x4096", + "2k": "1600x2848", + "2K": "1600x2848", + "4K": "2304x4096", + }, + }; + const body = { + model: imageModel.modelName, + prompt: imageConfig.prompt, + size: sizeMap[imageConfig.aspectRatio][size], + response_format: "url", + sequential_image_generation: "disabled", + stream: false, + watermark: false, + ...(imageConfig.imageBase64 && { image: imageConfig.imageBase64 }), + }; + return { + body, + processFn: (data) => { + return data.data[0].url; + }, + }; +} + +// 提取图片内容 +function extractFirstImageFromMd(content) { + const regex = /!\[([^\]]*)\]\((data:image\/[^;]+;base64,[A-Za-z0-9+/=]+|https?:\/\/[^\s)]+|\/\/[^\s)]+|[^\s)]+)\)/; + const match = content.match(regex); + if (!match) return null; + const raw = match[2].trim(); + const url = raw.startsWith("data:") ? raw : raw.split(/\s+/)[0]; + return { + alt: match[1], + url, + type: url.startsWith("data:image") ? "base64" : "url", + }; +} +// gemini 图片请求适配 +function geminiImageAdaptor(imageConfig: ImageConfig, imageModel: ImageModel) { + const images = []; + if (imageConfig.imageBase64 && imageConfig.imageBase64.length) { + images.push({ + role: "user", + content: imageConfig.imageBase64.map((i) => ({ + type: "image_url", + image_url: { + url: i, + }, + })), + }); + } + const imageConfigGoogle = { + aspect_ratio: imageConfig.aspectRatio, + }; + // if(imageModel.ModelName == 'gemini-3-pro-image-preview-vt'){ + imageConfigGoogle.image_size = imageConfig.size; + // } + const body = { + model: imageModel.modelName, + messages: [{ role: "user", content: imageConfig.prompt + `请直接输出图片` }, ...images], + extra_body: { + google: { + image_config: { + ...imageConfigGoogle, + }, + }, + }, + }; + return { + body, + url: `${vendor.inputValues.baseUrl}/chat/completions`, + processFn: (data: any) => { + return extractFirstImageFromMd(data.choices[0].message.content).url; + }, + }; +} +function commonAdaptor(imageConfig: ImageConfig, imageModel: ImageModel) { + const defaultImageFn = [ + ["doubao", doubaoAdaptor], + ["nano", geminiImageAdaptor], + ["gemini", geminiImageAdaptor], + ["seedream", doubaoAdaptor], + ]; + const modelName = imageModel.modelName; + const lowerName = modelName.toLowerCase(); + const match = defaultImageFn.find(([key]) => lowerName.includes(key)); + return match ? match[1](imageConfig, imageModel) : {}; +} +const imageRequest = async (imageConfig: ImageConfig, imageModel: ImageModel) => { + if (!vendor.inputValues.apiKey) throw new Error("缺少API Key"); + const apiKey = vendor.inputValues.apiKey.replace("Bearer ", ""); + const adaptor = commonAdaptor(imageConfig, imageModel); + + const requestUrl = adaptor?.url ? `${vendor.inputValues.baseUrl}/chat/completions` : vendor.inputValues.baseUrl + "/images/generations"; + const response = await fetch(requestUrl, { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify(adaptor.body), + }); + if (!response.ok) { + const errorText = await response.text(); // 获取错误信息 + console.error("请求失败,状态码:", response.status, ", 错误信息:", errorText); + throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`); + } + const data = await response.json(); + return adaptor.processFn(data); +}; +exports.imageRequest = imageRequest; + +interface VideoConfig { + duration: number; //视频时长,单位秒 + resolution: string; //视频分辨率,如"720p"、"1080p" + aspectRatio: "16:9" | "9:16"; //视频长宽比 + prompt: string; //视频提示词 + fileBase64?: string[]; // 文件base64 包含图片base64、视频base64、音频base64 + audio?: boolean; + mode: + | "singleImage" // 单图 + | "multiImage" // 多图模式 + | "gridImage" // 网格单图(传入一张图片,但该图片是网格图) + | "startEndRequired" // 首尾帧(两张都得有) + | "endFrameOptional" // 首尾帧(尾帧可选) + | "startFrameOptional" // 首尾帧(首帧可选) + | "text" // 文本生视频 + | ("videoReference" | "imageReference" | "audioReference" | "textReference")[]; // 混合参考 +} +// 豆包视频 +const buildDoubaoMetadata = (videoConfig: VideoConfig) => { + const metaData = { + ...(typeof videoConfig.audio == "boolean" && { generate_audio: videoConfig.audio ?? false }), + ratio: videoConfig.aspectRatio, + image_roles: [], + references: [], + }; + if (videoConfig.imageBase64 && videoConfig.imageBase64.length) { + videoConfig.imageBase64.forEach((i, index) => { + if (Array.isArray(videoConfig.mode)) { + metaData.references.push(i); + } else { + if (videoConfig.mode == "startEndRequired" || videoConfig.mode == "endFrameOptional" || videoConfig.mode == "startFrameOptional") { + (metaData.image_roles as string[]).push(index == 0 ? "first_frame" : "last_frame"); + } + if (videoConfig.mode == "singleImage") { + (metaData.image_roles as string[]).push("reference_image"); + } + } + }); + } + + return metaData; +}; + +// 万象 +const buildWanMetadata = (videoConfig: VideoConfig) => { + const images = videoConfig.imageBase64 ?? []; + const metaData: Record = {}; + if ( + (videoConfig.mode === "startEndRequired" || videoConfig.mode == "endFrameOptional" || videoConfig.mode == "startFrameOptional") && + images.length == 2 + ) { + if (images[0]) metaData.first_frame_url = images[0]; + if (images[1]) metaData.last_frame_url = images[1]; + } else if (images.length) { + metaData.img_url = images[0]!; + } + if (typeof videoConfig.audio == "boolean") { + metaData.audio = videoConfig.audio; + } + return metaData; +}; +// 千问视频 +const buildViduMetadata = (videoConfig: VideoConfig) => ({ + aspect_ratio: videoConfig.aspectRatio, + audio: videoConfig.audio ?? false, + off_peak: false, +}); +// 可灵 +const buildKlingAdaptor = (videoConfig: VideoConfig) => { + const metaData: any = { + aspect_ratio: videoConfig.aspectRatio, + }; + + if (videoConfig.imageBase64 && videoConfig.imageBase64.length) { + if (Array.isArray(videoConfig.mode)) { + metaData.reference = videoConfig.imageBase64; + } + if (videoConfig.mode == "endFrameOptional") { + metaData.image_tail = videoConfig.imageBase64[0]; + } + if (videoConfig.mode == "startEndRequired") { + metaData.image_list = [ + { + image_url: videoConfig.imageBase64[0], + type: "first_frame", + }, + { + image_url: videoConfig.imageBase64[1], + type: "last_frame", + }, + ]; + } + if (videoConfig.mode == "singleImage") { + metaData.image = videoConfig.imageBase64[0]; + } + } + + return metaData; +}; +type MetadataBuilder = (config: VideoConfig) => Record; +const METADATA_BUILDERS: Array<[string, MetadataBuilder]> = [ + ["doubao", buildDoubaoMetadata], + ["wan", buildWanMetadata], + ["vidu", buildViduMetadata], + ["seedance", buildDoubaoMetadata], + ["kling", buildKlingAdaptor], +]; +const buildModelMetadata = (modelName: string, videoConfig: VideoConfig) => { + const lowerName = modelName.toLowerCase(); + const match = METADATA_BUILDERS.find(([key]) => lowerName.includes(key)); + return match ? match[1](videoConfig) : {}; +}; +const videoRequest = async (videoConfig: VideoConfig, videoModel: VideoModel) => { + if (!vendor.inputValues.apiKey) throw new Error("缺少API Key"); + const apiKey = vendor.inputValues.apiKey.replace("Bearer ", ""); + try { + videoConfig.mode = JSON.parse(videoConfig.mode); + } catch (e) { + videoConfig.mode = videoConfig.mode as any; + } + // 构建每个模型对应的附加参数 + const metadata = buildModelMetadata(videoModel.modelName, videoConfig); + + //公共请求参数 + const publicBody = { + model: videoModel.modelName, + ...(videoConfig.imageBase64 && videoConfig.imageBase64.length && !Array.isArray(videoConfig.mode) ? { images: videoConfig.imageBase64 } : {}), + prompt: videoConfig.prompt, + duration: videoConfig.duration, + metadata: metadata, + }; + + if (videoModel.modelName.toLocaleLowerCase().includes("wan")) { + const sizeMap: Record> = { + "480p": { + "16:9": "832*480", + "9:16": "480*832", + }, + "720p": { + "16:9": "1280*720", + "9:16": "720*1280", + }, + "1080p": { + "16:9": "1920*1080", + "9:16": "1080*1920", + }, + }; + const size = sizeMap[videoConfig.resolution]?.[videoConfig.aspectRatio]; + publicBody.size = size; + } + const requestUrl = vendor.inputValues.baseUrl + "/video/generations"; + const queryUrl = vendor.inputValues.baseUrl + "/video/generations/{id}"; + const response = await fetch(requestUrl, { + method: "POST", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + body: JSON.stringify(publicBody), + }); + if (!response.ok) { + const errorText = await response.text(); // 获取错误信息 + console.error("请求失败,状态码:", response.status, ", 错误信息:", errorText); + throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`); + } + const data = await response.json(); + const taskId = data.id; + const res = await pollTask(async () => { + const queryResponse = await fetch(queryUrl.replace("{id}", taskId), { + method: "GET", + headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" }, + }); + if (!queryResponse.ok) { + const errorText = await queryResponse.text(); // 获取错误信息 + console.error("请求失败,状态码:", queryResponse.status, ", 错误信息:", errorText); + throw new Error(`请求失败,状态码: ${queryResponse.status}, 错误信息: ${errorText}`); + } + const queryData = await queryResponse.json(); + const status = queryData?.status ?? queryData?.data?.status; + const fail_reason = queryData?.data?.fail_reason ?? queryData?.data; + switch (status) { + case "completed": + case "SUCCESS": + case "success": + return { completed: true, data: queryData.data.result_url }; + case "FAILURE": + return { completed: false, error: fail_reason || "视频生成失败" }; + default: + return { completed: false }; + } + }); + if (res.error) throw new Error(res.error); + return res.data; +}; +exports.videoRequest = videoRequest; + +interface TTSConfig { + text: string; + voice: string; + speechRate: number; + pitchRate: number; + volume: number; +} +const ttsRequest = async (ttsConfig: TTSConfig, ttsModel: TTSModel) => { + return null; +}; +exports.ttsRequest = ttsRequest; diff --git a/data/vendor/volcengine.ts b/data/vendor/volcengine.ts new file mode 100644 index 0000000..2d1f308 --- /dev/null +++ b/data/vendor/volcengine.ts @@ -0,0 +1,527 @@ +/** + * 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; + referenceList?: 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) => 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-doubao", + version: "2.0", + author: "Toonflow", + 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"], + }, + // ===================== 视频生成模型 ===================== + // Seedance 2.0: 多模态参考(图0~9+视频0~3+音频0~3) + 首尾帧 + 首帧 + 文生视频 + { + 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"] }, + ], + }, + // Seedance 1.5 pro: 首尾帧 + 首帧 + 文生视频 + { + 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"] }, + ], + }, + // Seedance 1.0 pro: 首尾帧 + 首帧 + 文生视频 + { + 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"] }, + ], + }, + // Seedance 1.0 pro fast: 首帧 + 文生视频(不支持首尾帧) + { + 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"] }, + ], + }, + // Seedance 1.0 lite t2v: 仅文生视频 + { + 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"] }, + ], + }, + // Seedance 1.0 lite i2v: 参考图(1~4) + 首尾帧 + 首帧 + { + 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(/\/+$/, ""); + +// ============================================================ +// 适配器函数 +// ============================================================ + +/** 文本请求 - 直接使用 createOpenAI */ +const textRequest = (model: TextModel) => { + if (!vendor.inputValues.apiKey) throw new Error("缺少API Key"); + const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, ""); + return createOpenAI({ baseURL: getBaseUrl(), apiKey }).chat(model.modelName); +}; + +/** 图片生成请求 */ +const imageRequest = async (config: ImageConfig, model: ImageModel): Promise => { + const baseUrl = getBaseUrl(); + const headers = getHeaders(); + + // 构建 content + const content: any[] = []; + + // 文本提示词 + if (config.prompt) { + content.push({ type: "text", text: config.prompt }); + } + + // 图片输入 + if (config.imageBase64 && config.imageBase64.length > 0) { + for (const base64 of config.imageBase64) { + content.push({ + type: "image_url", + image_url: { url: `data:image/png;base64,${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(); + + // 构建 content + const content: any[] = []; + + // 文本提示词 + if (config.prompt) { + content.push({ type: "text", text: config.prompt }); + } + + // 判断当前使用的 mode + const activeMode = config.mode && config.mode.length > 0 ? config.mode[0] : "text"; + + if (typeof activeMode === "string") { + switch (activeMode) { + case "singleImage": + // 首帧模式:单张图片,role 为 first_frame + if (config.imageBase64 && config.imageBase64.length > 0) { + content.push({ + type: "image_url", + image_url: { url: `data:image/png;base64,${config.imageBase64[0]}` }, + role: "first_frame", + }); + } + break; + case "startFrameOptional": + // 首帧 + 可选尾帧模式 + if (config.imageBase64 && config.imageBase64.length > 0) { + content.push({ + type: "image_url", + image_url: { url: `data:image/png;base64,${config.imageBase64[0]}` }, + role: "first_frame", + }); + if (config.imageBase64.length > 1) { + content.push({ + type: "image_url", + image_url: { url: `data:image/png;base64,${config.imageBase64[1]}` }, + role: "last_frame", + }); + } + } + break; + case "text": + // 纯文生视频,无需额外处理 + break; + } + } else if (Array.isArray(activeMode)) { + // 多模态参考模式 + let imageIndex = 0; + for (const ref of activeMode) { + if (typeof ref === "string") { + if (ref.startsWith("imageReference:")) { + // 参考图片 + const maxCount = parseInt(ref.split(":")[1], 10); + if (config.imageBase64) { + const images = config.imageBase64.slice(imageIndex, imageIndex + maxCount); + for (const base64 of images) { + content.push({ + type: "image_url", + image_url: { url: `data:image/png;base64,${base64}` }, + role: "reference_image", + }); + } + imageIndex += images.length; + } + } + // videoReference 和 audioReference 需要 URL,当前框架暂不支持直接传入 + } + } + } + + // 映射宽高比 + const ratioMap: Record = { + "16:9": "16:9", + "9:16": "9:16", + "4:3": "4:3", + "3:4": "3:4", + "1:1": "1:1", + "21:9": "21:9", + }; + const ratio = ratioMap[config.aspectRatio] || "16:9"; + + const body: any = { + model: model.modelName, + content, + ratio, + 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: + // queued / running + return { completed: false }; + } + }, 10000, 600000); // 每10秒查询一次,最长等待10分钟 + + if (result.error) { + throw new Error(result.error); + } + + return result.data || ""; +}; + +/** TTS请求(火山引擎暂无TTS模型配置,预留接口) */ +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/src/agents/productionAgent/tools.ts b/src/agents/productionAgent/tools.ts index bdebaf2..60790c1 100644 --- a/src/agents/productionAgent/tools.ts +++ b/src/agents/productionAgent/tools.ts @@ -140,7 +140,7 @@ export default (toolCpnfig: ToolConfig) => { }, }), generate_deriveAsset: tool({ - description: "生成衍生资产", + description: "生成衍生资产图片", inputSchema: z.object({ ids: z.array(z.number()).describe("需要生成的 衍生资产ID"), }), 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/vm.ts b/src/utils/vm.ts index c1ad891..91a5b93 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 = { diff --git a/tsconfig.json b/tsconfig.json index 3237094..8095e32 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "ignoreDeprecations": "6.0", "target": "ESNext", "module": "CommonJS", "moduleResolution": "Node", @@ -12,10 +13,19 @@ "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" + ] +} \ No newline at end of file