更新供应商函数
This commit is contained in:
parent
cacadd94c4
commit
a58e5a3ab8
655
data/vendor/klingai.ts
vendored
Normal file
655
data/vendor/klingai.ts
vendored
Normal file
@ -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<string, string>;
|
||||
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<ReferenceList, { type: "image" }>[];
|
||||
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<ReferenceList, { type: "audio" }>[];
|
||||
}
|
||||
|
||||
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<string>;
|
||||
declare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>;
|
||||
declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>;
|
||||
declare const urlToBase64: (url: string) => Promise<string>;
|
||||
declare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>;
|
||||
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<ReferenceList>;
|
||||
imageRequest: (c: ImageConfig, m: ImageModel) => Promise<string>;
|
||||
videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>;
|
||||
ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>;
|
||||
checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>;
|
||||
updateVendor?: () => Promise<string>;
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 供应商配置
|
||||
// ============================================================
|
||||
|
||||
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<string> => {
|
||||
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<ReferenceList> => {
|
||||
// 可灵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<string> => {
|
||||
throw new Error("可灵AI不支持图片模型");
|
||||
};
|
||||
|
||||
const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {
|
||||
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) => `<<<image_${idx + 1}>>>`).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<string> => {
|
||||
return "";
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 导出
|
||||
// ============================================================
|
||||
|
||||
exports.vendor = vendor;
|
||||
exports.textRequest = textRequest;
|
||||
exports.uploadReference = uploadReference;
|
||||
exports.imageRequest = imageRequest;
|
||||
exports.videoRequest = videoRequest;
|
||||
exports.ttsRequest = ttsRequest;
|
||||
|
||||
// 这行代码用于确保当前文件被识别为模块,避免全局变量冲突
|
||||
export {};
|
||||
402
data/vendor/minimax.ts
vendored
Normal file
402
data/vendor/minimax.ts
vendored
Normal file
@ -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<string, string>;
|
||||
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<ReferenceList, { type: "image" }>[];
|
||||
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<ReferenceList, { type: "audio" }>[];
|
||||
}
|
||||
|
||||
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<string>;
|
||||
declare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>;
|
||||
declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>;
|
||||
declare const urlToBase64: (url: string) => Promise<string>;
|
||||
declare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>;
|
||||
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<ReferenceList>;
|
||||
imageRequest: (c: ImageConfig, m: ImageModel) => Promise<string>;
|
||||
videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>;
|
||||
ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>;
|
||||
checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>;
|
||||
updateVendor?: () => Promise<string>;
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 供应商配置
|
||||
// ============================================================
|
||||
|
||||
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<string, string> => {
|
||||
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<ReferenceList> => {
|
||||
// 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<string> => {
|
||||
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<string> => {
|
||||
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<string> => {
|
||||
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<string> => {
|
||||
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 {};
|
||||
347
data/vendor/null.ts
vendored
Normal file
347
data/vendor/null.ts
vendored
Normal file
@ -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<string, string>;
|
||||
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<ReferenceList, { type: "image" }>[];
|
||||
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<ReferenceList, { type: "audio" }>[];
|
||||
}
|
||||
|
||||
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<string>; // 图片压缩函数,返回有头base64字符串
|
||||
declare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>; // 图片分辨率调整函数,返回有头base64字符串
|
||||
declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>; // 图片合成函数,返回有头base64字符串
|
||||
declare const urlToBase64: (url: string) => Promise<string>; // URL转Base64函数,返回有头base64字符串
|
||||
declare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>; // 轮询函数,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<ReferenceList>; // reference前置处理器,专门用于处理referenceList中的条目,将有头base64上传并返回URL,然后reference才会传入videoRequest/imageRequest/ttsRequest中
|
||||
imageRequest: (c: ImageConfig, m: ImageModel) => Promise<string>; //图片模型,返回有头base64字符串
|
||||
videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>; //视频模型,返回有头base64字符串
|
||||
ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>; //(暂未开放)语音模型,返回有头base64字符串
|
||||
checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>; //检查更新函数,返回是否有更新和最新版本号和更公告(支持Markdown格式)
|
||||
updateVendor?: () => Promise<string>; //更新函数,返回最新的代码文本
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 供应商配置
|
||||
// ============================================================
|
||||
|
||||
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<ReferenceList> => {
|
||||
return { type: fileType, sourceType: "base64", base64 };
|
||||
};
|
||||
|
||||
const imageRequest = async (config: ImageConfig, model: ImageModel): Promise<string> => {
|
||||
return "";
|
||||
};
|
||||
|
||||
const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {
|
||||
return "";
|
||||
};
|
||||
|
||||
const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<string> => {
|
||||
return "";
|
||||
};
|
||||
|
||||
const checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => {
|
||||
return { hasUpdate: false, latestVersion: "2.0", notice: "## 新版本更新公告" };
|
||||
};
|
||||
|
||||
const updateVendor = async (): Promise<string> => {
|
||||
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<ReferenceList, { type: "image" }>[] 类型,
|
||||
* 包含经过 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<ReferenceList, { type: "audio" }>[] 类型(音频参考)。
|
||||
* 当 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. 生成完整可用的代码,确保无语法错误、无遗漏导出。
|
||||
*/
|
||||
169
data/vendor/openai.ts
vendored
Normal file
169
data/vendor/openai.ts
vendored
Normal file
@ -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<string, string>;
|
||||
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<string>;
|
||||
declare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>;
|
||||
declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>;
|
||||
declare const urlToBase64: (url: string) => Promise<string>;
|
||||
declare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>;
|
||||
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<string>;
|
||||
videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>;
|
||||
ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>;
|
||||
checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>;
|
||||
updateVendor?: () => Promise<string>;
|
||||
};
|
||||
// ============================================================
|
||||
// 供应商配置
|
||||
// ============================================================
|
||||
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<string> => {
|
||||
return "";
|
||||
};
|
||||
const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {
|
||||
return "";
|
||||
};
|
||||
const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<string> => {
|
||||
return "";
|
||||
};
|
||||
const checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => {
|
||||
return { hasUpdate: false, latestVersion: "2.0", notice: "" };
|
||||
};
|
||||
const updateVendor = async (): Promise<string> => {
|
||||
return "";
|
||||
};
|
||||
// ============================================================
|
||||
// 导出
|
||||
// ============================================================
|
||||
exports.vendor = vendor;
|
||||
exports.textRequest = textRequest;
|
||||
exports.imageRequest = imageRequest;
|
||||
exports.videoRequest = videoRequest;
|
||||
exports.ttsRequest = ttsRequest;
|
||||
exports.checkForUpdates = checkForUpdates;
|
||||
exports.updateVendor = updateVendor;
|
||||
export {};
|
||||
569
data/vendor/toonflow.ts
vendored
Normal file
569
data/vendor/toonflow.ts
vendored
Normal file
@ -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<string, string>;
|
||||
models: (TextModel | ImageModel | VideoModel)[];
|
||||
}
|
||||
// ==================== 全局工具函数 ====================
|
||||
//Axios实例
|
||||
//压缩图片大小(1MB = 1 * 1024 * 1024)
|
||||
declare const zipImage: (completeBase64: string, size: number) => Promise<string>;
|
||||
//压缩图片分辨率
|
||||
declare const zipImageResolution: (completeBase64: string, width: number, height: number) => Promise<string>;
|
||||
//多图拼接乘单图 maxSize 最大输出大小,默认为 10mb
|
||||
declare const mergeImages: (completeBase64: string[], maxSize?: string) => Promise<string>;
|
||||
//Url转Base64
|
||||
declare const urlToBase64: (url: string) => Promise<string>;
|
||||
//轮询函数
|
||||
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<string, Record<string, string>> = {
|
||||
"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<string, string | boolean> = {};
|
||||
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<string, any>;
|
||||
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<string, Record<string, string>> = {
|
||||
"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;
|
||||
527
data/vendor/volcengine.ts
vendored
Normal file
527
data/vendor/volcengine.ts
vendored
Normal file
@ -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<string, string>;
|
||||
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<string>;
|
||||
declare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>;
|
||||
declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>;
|
||||
declare const urlToBase64: (url: string) => Promise<string>;
|
||||
declare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>;
|
||||
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<string>;
|
||||
videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>;
|
||||
ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>;
|
||||
checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>;
|
||||
updateVendor?: () => Promise<string>;
|
||||
};
|
||||
// ============================================================
|
||||
// 供应商配置
|
||||
// ============================================================
|
||||
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<string> => {
|
||||
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<string, { width: number; height: number }> = {
|
||||
"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<string> => {
|
||||
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<string, string> = {
|
||||
"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<PollResult> => {
|
||||
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<string> => {
|
||||
return "";
|
||||
};
|
||||
|
||||
const checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => {
|
||||
return { hasUpdate: false, latestVersion: "2.0", notice: "" };
|
||||
};
|
||||
|
||||
const updateVendor = async (): Promise<string> => {
|
||||
return "";
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 导出
|
||||
// ============================================================
|
||||
exports.vendor = vendor;
|
||||
exports.textRequest = textRequest;
|
||||
exports.imageRequest = imageRequest;
|
||||
exports.videoRequest = videoRequest;
|
||||
exports.ttsRequest = ttsRequest;
|
||||
exports.checkForUpdates = checkForUpdates;
|
||||
exports.updateVendor = updateVendor;
|
||||
export {};
|
||||
@ -140,7 +140,7 @@ export default (toolCpnfig: ToolConfig) => {
|
||||
},
|
||||
}),
|
||||
generate_deriveAsset: tool({
|
||||
description: "生成衍生资产",
|
||||
description: "生成衍生资产图片",
|
||||
inputSchema: z.object({
|
||||
ids: z.array(z.number()).describe("需要生成的 衍生资产ID"),
|
||||
}),
|
||||
|
||||
@ -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<string, any> = {
|
||||
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<string, any> = {
|
||||
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}` };
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -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<string, any>) {
|
||||
code = code.replace(/export\s*\{\s*\};?/g, ""); // 去掉 export {} 以免沙盒环境报错
|
||||
// 创建一个沙盒
|
||||
const exports = {};
|
||||
const sandbox: Record<string, any> = {
|
||||
|
||||
@ -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"
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user