新增厂商2.0,染上深度思考模块,修正厂商模块
This commit is contained in:
parent
b891abbe93
commit
02883472e7
@ -23,12 +23,12 @@ description: >-
|
||||
| 操作 | 调用 |
|
||||
|------|------|
|
||||
| 读取资产列表 | `get_flowData("assets")` |
|
||||
| 生成资产图片 | `generate_assets_images({ ids: [资产id列表] })` |
|
||||
| 生成资产图片 | `generate_deriveAsset({ ids: [资产id列表] })` |
|
||||
|
||||
### 执行流程
|
||||
|
||||
1. 获取 `assets`,收集所有需要生成图片的资产 id
|
||||
2. 调用 `generate_assets_images({ ids: [资产id列表] })` 生成图片(异步,发起即返回)
|
||||
2. 调用 `generate_deriveAsset({ ids: [资产id列表] })` 生成图片(异步,发起即返回)
|
||||
|
||||
### 约束
|
||||
|
||||
|
||||
318
data/vendor/grsai.ts
vendored
Normal file
318
data/vendor/grsai.ts
vendored
Normal file
@ -0,0 +1,318 @@
|
||||
/**
|
||||
* Toonflow AI供应商模板
|
||||
* @version 2.0
|
||||
*/
|
||||
|
||||
// ============================================================
|
||||
// 类型定义
|
||||
// ============================================================
|
||||
|
||||
type VideoMode =
|
||||
| "singleImage" //单图参考
|
||||
| "startEndRequired" //首尾帧(两张都得有)
|
||||
| "endFrameOptional" //首尾帧(尾帧可选)
|
||||
| "startFrameOptional" //首尾帧(首帧可选)
|
||||
| "text" //文本
|
||||
| (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[]; //多参考(数字代表限制数量)
|
||||
|
||||
interface TextModel {
|
||||
name: string;
|
||||
modelName: string;
|
||||
type: "text";
|
||||
think: boolean;
|
||||
}
|
||||
|
||||
interface ImageModel {
|
||||
name: string;
|
||||
modelName: string;
|
||||
type: "image";
|
||||
mode: ("text" | "singleImage" | "multiReference")[];
|
||||
associationSkills?: string;
|
||||
}
|
||||
|
||||
interface VideoModel {
|
||||
name: string;
|
||||
modelName: string;
|
||||
type: "video";
|
||||
mode: VideoMode[];
|
||||
associationSkills?: string;
|
||||
audio: "optional" | false | true;
|
||||
durationResolutionMap: { duration: number[]; resolution: string[] }[];
|
||||
}
|
||||
|
||||
interface TTSModel {
|
||||
name: string;
|
||||
modelName: string;
|
||||
type: "tts";
|
||||
voices: { title: string; voice: string }[];
|
||||
}
|
||||
|
||||
interface VendorConfig {
|
||||
id: string; //唯一ID,作为文件名存储用户磁盘上,禁止符号
|
||||
version: string; //版本号,格式为x.y,需遵守语义化版本控制
|
||||
name: string; //供应商名称
|
||||
author: string; //作者
|
||||
description?: string; //描述,支持Markdown格式
|
||||
icon?: string; //图标,仅支持Base64格式,建议尺寸为128x128像素
|
||||
inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[];
|
||||
inputValues: Record<string, string>;
|
||||
models: (TextModel | ImageModel | VideoModel | TTSModel)[];
|
||||
}
|
||||
|
||||
type ReferenceList =
|
||||
| { type: "image"; sourceType: "base64"; base64: string }
|
||||
| { type: "audio"; sourceType: "base64"; base64: string }
|
||||
| { type: "video"; sourceType: "base64"; base64: string };
|
||||
|
||||
interface ImageConfig {
|
||||
prompt: string;
|
||||
referenceList?: Extract<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; //文本模型
|
||||
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: "grsai",
|
||||
version: "1.0",
|
||||
author: "Toonflow",
|
||||
name: "Grsai",
|
||||
description: "Grsai AI平台适配,支持文生图、图生图、文生视频、Gemini兼容文本模型 \n [前往中转平台](https://tf.grsai.ai/zh)",
|
||||
inputs: [
|
||||
{ key: "apiKey", label: "API密钥", type: "password", required: true },
|
||||
{ key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "示例:https://grsai.dakka.com.cn" },
|
||||
],
|
||||
inputValues: { apiKey: "", baseUrl: "https://grsai.dakka.com.cn" },
|
||||
models: [
|
||||
{ name: "Nano Banana Fast", modelName: "nano-banana-fast", type: "image", mode: ["text", "singleImage", "multiReference"] },
|
||||
{ name: "Nano Banana 2", modelName: "nano-banana-2", type: "image", mode: ["text", "singleImage", "multiReference"] },
|
||||
{ name: "Nano Banana Pro", modelName: "nano-banana-pro", type: "image", mode: ["text", "singleImage", "multiReference"] },
|
||||
],
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 辅助工具
|
||||
// ============================================================
|
||||
|
||||
const getHeaders = () => {
|
||||
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
};
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 适配器函数
|
||||
// ============================================================
|
||||
|
||||
const textRequest = (model: TextModel) => {
|
||||
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
|
||||
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
|
||||
return createGoogleGenerativeAI({
|
||||
baseURL: `${vendor.inputValues.baseUrl}/v1beta`,
|
||||
apiKey,
|
||||
}).chat(model.modelName);
|
||||
};
|
||||
|
||||
const imageRequest = async (config: ImageConfig, model: ImageModel): Promise<string> => {
|
||||
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
|
||||
const baseUrl = vendor.inputValues.baseUrl;
|
||||
const headers = getHeaders();
|
||||
|
||||
// 构造请求参数
|
||||
const requestBody: any = {
|
||||
model: model.modelName,
|
||||
prompt: config.prompt,
|
||||
aspectRatio: config.aspectRatio,
|
||||
webHook: "-1",
|
||||
shutProgress: true,
|
||||
};
|
||||
|
||||
// 补充模型专属参数
|
||||
if (model.modelName.startsWith("nano-banana")) {
|
||||
requestBody.imageSize = config.size;
|
||||
} else {
|
||||
requestBody.size = config.aspectRatio;
|
||||
requestBody.variants = 1;
|
||||
}
|
||||
|
||||
// 处理参考图
|
||||
if (config.referenceList && config.referenceList.length > 0) {
|
||||
requestBody.urls = config.referenceList.map((img) => img.base64);
|
||||
}
|
||||
|
||||
// 选择接口路径
|
||||
const apiPath = model.modelName.startsWith("nano-banana") ? "/v1/draw/nano-banana" : "/v1/draw/completions";
|
||||
|
||||
logger(`开始提交图片生成任务,模型:${model.modelName}`);
|
||||
const submitResp = await axios.post(`${baseUrl}${apiPath}`, requestBody, { headers });
|
||||
if (submitResp.data.code !== 0) throw new Error(`任务提交失败:${submitResp.data.msg}`);
|
||||
|
||||
const taskId = submitResp.data.data.id;
|
||||
logger(`图片任务提交成功,任务ID:${taskId}`);
|
||||
|
||||
// 轮询结果
|
||||
const pollResult = await pollTask(
|
||||
async () => {
|
||||
const resp = await axios.post(`${baseUrl}/v1/draw/result`, { id: taskId }, { headers });
|
||||
if (resp.data.code !== 0) return { completed: true, error: resp.data.msg };
|
||||
|
||||
const taskData = resp.data.data;
|
||||
if (taskData.status === "failed") return { completed: true, error: taskData.failure_reason || taskData.error };
|
||||
if (taskData.status === "succeeded") {
|
||||
const imgUrl = taskData.results?.[0]?.url || taskData.url;
|
||||
return { completed: true, data: imgUrl };
|
||||
}
|
||||
logger(`图片任务生成中,进度:${taskData.progress}%`);
|
||||
return { completed: false };
|
||||
},
|
||||
3000,
|
||||
600000,
|
||||
);
|
||||
|
||||
if (pollResult.error) throw new Error(pollResult.error);
|
||||
logger(`图片生成完成,开始转换Base64`);
|
||||
return await urlToBase64(pollResult.data!);
|
||||
};
|
||||
|
||||
const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {
|
||||
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
|
||||
const baseUrl = vendor.inputValues.baseUrl;
|
||||
const headers = getHeaders();
|
||||
|
||||
// 构造请求参数
|
||||
const requestBody: any = {
|
||||
model: model.modelName,
|
||||
prompt: config.prompt,
|
||||
aspectRatio: config.aspectRatio,
|
||||
webHook: "-1",
|
||||
shutProgress: true,
|
||||
};
|
||||
|
||||
// 处理参考资源
|
||||
if (config.referenceList && config.referenceList.length > 0) {
|
||||
const imageRefs = config.referenceList.filter((item) => item.type === "image") as Extract<ReferenceList, { type: "image" }>[];
|
||||
if (config.mode.includes("endFrameOptional") && imageRefs.length >= 1) {
|
||||
requestBody.firstFrameUrl = imageRefs[0].base64;
|
||||
if (imageRefs.length >= 2) requestBody.lastFrameUrl = imageRefs[1].base64;
|
||||
} else if (config.mode.some((m) => Array.isArray(m) && m.includes("imageReference:3"))) {
|
||||
requestBody.urls = imageRefs.map((img) => img.base64);
|
||||
}
|
||||
}
|
||||
|
||||
logger(`开始提交视频生成任务,模型:${model.modelName}`);
|
||||
const submitResp = await axios.post(`${baseUrl}/v1/video/veo`, requestBody, { headers });
|
||||
if (submitResp.data.code !== 0) throw new Error(`任务提交失败:${submitResp.data.msg}`);
|
||||
|
||||
const taskId = submitResp.data.data.id;
|
||||
logger(`视频任务提交成功,任务ID:${taskId}`);
|
||||
|
||||
// 轮询结果
|
||||
const pollResult = await pollTask(
|
||||
async () => {
|
||||
const resp = await axios.post(`${baseUrl}/v1/draw/result`, { id: taskId }, { headers });
|
||||
if (resp.data.code !== 0) return { completed: true, error: resp.data.msg };
|
||||
|
||||
const taskData = resp.data.data;
|
||||
if (taskData.status === "failed") return { completed: true, error: taskData.failure_reason || taskData.error };
|
||||
if (taskData.status === "succeeded") {
|
||||
return { completed: true, data: taskData.url };
|
||||
}
|
||||
logger(`视频任务生成中,进度:${taskData.progress}%`);
|
||||
return { completed: false };
|
||||
},
|
||||
5000,
|
||||
1800000,
|
||||
);
|
||||
|
||||
if (pollResult.error) throw new Error(pollResult.error);
|
||||
logger(`视频生成完成,开始转换Base64`);
|
||||
return await urlToBase64(pollResult.data!);
|
||||
};
|
||||
|
||||
const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<string> => {
|
||||
return "";
|
||||
};
|
||||
|
||||
const checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => {
|
||||
return { hasUpdate: false, latestVersion: "1.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 {};
|
||||
27
data/vendor/klingai.ts
vendored
27
data/vendor/klingai.ts
vendored
@ -60,9 +60,9 @@ interface VendorConfig {
|
||||
}
|
||||
|
||||
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 }));
|
||||
| { type: "image"; sourceType: "base64"; base64: string }
|
||||
| { type: "audio"; sourceType: "base64"; base64: string }
|
||||
| { type: "video"; sourceType: "base64"; base64: string };
|
||||
|
||||
interface ImageConfig {
|
||||
prompt: string;
|
||||
@ -120,7 +120,6 @@ 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>;
|
||||
@ -138,11 +137,11 @@ const vendor: VendorConfig = {
|
||||
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。",
|
||||
"可灵AI视频生成\n\n支持可灵全系列视频模型,包括 kling-video-o1、kling-v3-omni、kling-v3、kling-v2-6、kling-v2-5-turbo、kling-v2-1、kling-v2-master、kling-v1-6、kling-v1-5、kling-v1 等。\n\n需要在[可灵AI开放平台](https://klingai.com)\n\n获取 Access Key 和 Secret Key。",
|
||||
inputs: [
|
||||
{ key: "accessKey", label: "Access Key", type: "password", required: true, placeholder: "请输入可灵AI的Access Key" },
|
||||
{ key: "secretKey", label: "Secret Key", type: "password", required: true, placeholder: "请输入可灵AI的Secret Key" },
|
||||
{ key: "baseUrl", label: "请求地址", type: "url", required: false, placeholder: "默认:https://api-beijing.klingai.com" },
|
||||
{ key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "默认:https://api-beijing.klingai.com" },
|
||||
],
|
||||
inputValues: { accessKey: "", secretKey: "", baseUrl: "https://api-beijing.klingai.com" },
|
||||
models: [
|
||||
@ -352,9 +351,6 @@ const getBaseUrl = (): string => {
|
||||
* 对于 url 类型返回 url,对于 base64 类型返回纯 base64(去掉 data: 前缀)
|
||||
*/
|
||||
const extractRawBase64 = (ref: ReferenceList): string => {
|
||||
if (ref.sourceType === "url") {
|
||||
return ref.url;
|
||||
}
|
||||
return ref.base64.replace(/^data:[^;]+;base64,/, "");
|
||||
};
|
||||
|
||||
@ -363,9 +359,6 @@ const extractRawBase64 = (ref: ReferenceList): string => {
|
||||
* 用于 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}`;
|
||||
};
|
||||
|
||||
@ -447,15 +440,6 @@ 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不支持图片模型");
|
||||
};
|
||||
@ -646,7 +630,6 @@ const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<string> =
|
||||
|
||||
exports.vendor = vendor;
|
||||
exports.textRequest = textRequest;
|
||||
exports.uploadReference = uploadReference;
|
||||
exports.imageRequest = imageRequest;
|
||||
exports.videoRequest = videoRequest;
|
||||
exports.ttsRequest = ttsRequest;
|
||||
|
||||
11
data/vendor/minimax.ts
vendored
11
data/vendor/minimax.ts
vendored
@ -60,9 +60,9 @@ interface VendorConfig {
|
||||
}
|
||||
|
||||
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 }));
|
||||
| { type: "image"; sourceType: "base64"; base64: string }
|
||||
| { type: "audio"; sourceType: "base64"; base64: string }
|
||||
| { type: "video"; sourceType: "base64"; base64: string };
|
||||
|
||||
interface ImageConfig {
|
||||
prompt: string;
|
||||
@ -137,7 +137,7 @@ const vendor: VendorConfig = {
|
||||
version: "2.0",
|
||||
author: "Toonflow",
|
||||
name: "MiniMax(海螺AI)",
|
||||
description: "## MiniMax官方接口适配,支持M系列推理文本模型、文生图/图生图、视频生成(文生视频、图生视频、首尾帧生成)能力",
|
||||
description: "MiniMax官方接口适配,支持M系列推理文本模型、文生图/图生图、视频生成(文生视频、图生视频、首尾帧生成)能力 \n [前往平台](https://minimaxi.com/)",
|
||||
inputs: [
|
||||
{ key: "apiKey", label: "API密钥", type: "password", required: true },
|
||||
{ key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "示例:https://api.minimaxi.com" },
|
||||
@ -218,9 +218,6 @@ const getBaseUrl = (): string => {
|
||||
* 从 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}`;
|
||||
};
|
||||
|
||||
|
||||
39
data/vendor/null.ts
vendored
39
data/vendor/null.ts
vendored
@ -60,9 +60,9 @@ interface VendorConfig {
|
||||
}
|
||||
|
||||
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 }));
|
||||
| { type: "image"; sourceType: "base64"; base64: string }
|
||||
| { type: "audio"; sourceType: "base64"; base64: string }
|
||||
| { type: "video"; sourceType: "base64"; base64: string };
|
||||
|
||||
interface ImageConfig {
|
||||
prompt: string;
|
||||
@ -120,7 +120,6 @@ 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字符串
|
||||
@ -133,10 +132,10 @@ declare const exports: {
|
||||
// ============================================================
|
||||
|
||||
const vendor: VendorConfig = {
|
||||
id: "openai",
|
||||
id: "bull",
|
||||
version: "2.0",
|
||||
author: "Toonflow",
|
||||
name: "OpenAI标准接口",
|
||||
name: "空模板",
|
||||
description: "## OpenAI标准格式接口,可修改请求地址并手动添加模型。",
|
||||
inputs: [
|
||||
{ key: "apiKey", label: "API密钥", type: "password", required: true },
|
||||
@ -156,10 +155,6 @@ const textRequest = (model: TextModel) => {
|
||||
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 "";
|
||||
};
|
||||
@ -186,7 +181,6 @@ const updateVendor = async (): Promise<string> => {
|
||||
|
||||
exports.vendor = vendor;
|
||||
exports.textRequest = textRequest;
|
||||
exports.uploadReference = uploadReference;
|
||||
exports.imageRequest = imageRequest;
|
||||
exports.videoRequest = videoRequest;
|
||||
exports.ttsRequest = ttsRequest;
|
||||
@ -228,7 +222,7 @@ export {};
|
||||
* 如果是纯逻辑内部使用的临时变量,应内联在对应的 exports.* 函数体内部,使用小驼峰命名。
|
||||
*
|
||||
* 3. 逻辑尽量聚合在 exports.* 对应的函数内部
|
||||
* 每个适配函数(textRequest / uploadReference / imageRequest / videoRequest / ttsRequest)
|
||||
* 每个适配函数(textRequest / imageRequest / videoRequest / ttsRequest)
|
||||
* 应自包含,将请求构造、发送、轮询、结果解析等逻辑写在函数体内,避免拆分出大量外部辅助函数。
|
||||
* 如果多个函数确实存在公共逻辑(如签名计算、Token 生成、请求头构造),
|
||||
* 可提取为文件内的小驼峰命名函数,放在「适配器函数」区块之前的「辅助工具」区块中,
|
||||
@ -244,17 +238,12 @@ export {};
|
||||
*
|
||||
* 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)。
|
||||
* 每个引用条目均为 base64 形式(sourceType 固定为 "base64")。
|
||||
* - videoRequest(config, model):返回有头 base64 字符串(如 "data:video/mp4;base64,...")。
|
||||
* config.referenceList 为 ReferenceList[] 类型,可包含 image / video / audio 三种引用。
|
||||
* config.referenceList 为 ReferenceList[] 类型,可包含 image / video / audio 三种引用,
|
||||
* 每个引用条目均为 base64 形式(sourceType 固定为 "base64")。
|
||||
* config.mode 为当前激活的视频模式数组,需根据 mode 决定如何使用 referenceList。
|
||||
* - ttsRequest(config, model):返回有头 base64 字符串(如 "data:audio/mp3;base64,...")。
|
||||
* config.referenceList 为 Extract<ReferenceList, { type: "audio" }>[] 类型(音频参考)。
|
||||
@ -263,8 +252,8 @@ export {};
|
||||
* 7. ReferenceList 与 VideoMode 说明
|
||||
* ReferenceList 是统一的多媒体引用类型,每个条目包含:
|
||||
* - type: "image" | "audio" | "video"(媒体类型)
|
||||
* - sourceType: "url" | "base64"(数据来源)
|
||||
* - url 或 base64(对应的数据)
|
||||
* - sourceType: "base64"(当前模板固定为 base64)
|
||||
* - base64(对应的数据)
|
||||
*
|
||||
* VideoMode 定义了视频模型支持的输入模式:
|
||||
* - "text":纯文本生成视频
|
||||
@ -326,7 +315,6 @@ export {};
|
||||
* 必须导出以下字段(通过 exports.xxx = xxx 赋值):
|
||||
* - exports.vendor(必须)
|
||||
* - exports.textRequest(必须)
|
||||
* - exports.uploadReference(必须)
|
||||
* - exports.imageRequest(必须)
|
||||
* - exports.videoRequest(必须)
|
||||
* - exports.ttsRequest(必须)
|
||||
@ -340,8 +328,7 @@ export {};
|
||||
* 1. 确认用户已提供 curl 示例或 API 文档。
|
||||
* 2. 分析 API 的认证方式、端点地址、请求/响应结构。
|
||||
* 3. 基于本模板结构,填充 vendor 配置和对应的适配器函数。
|
||||
* 4. 实现 uploadReference:如果 API 需要 URL 引用,则上传 base64 到供应商文件服务并返回 URL;
|
||||
* 如果 API 直接接受 base64,则原样返回。
|
||||
* 4. 根据当前模板的 ReferenceList 定义,按 base64 形式构造和消费 referenceList。
|
||||
* 5. 仅实现用户需要的模型类型,未用到的函数保留空实现(return "")。
|
||||
* 6. 生成完整可用的代码,确保无语法错误、无遗漏导出。
|
||||
*/
|
||||
*/
|
||||
|
||||
2
data/vendor/openai.ts
vendored
2
data/vendor/openai.ts
vendored
@ -115,7 +115,7 @@ const vendor: VendorConfig = {
|
||||
version: "2.0",
|
||||
author: "Toonflow",
|
||||
name: "OpenAI标准接口",
|
||||
description: "## OpenAI标准格式接口,可修改请求地址并手动添加模型。",
|
||||
description: "OpenAI标准格式接口,可修改请求地址并手动添加模型。",
|
||||
icon: "",
|
||||
inputs: [
|
||||
{ key: "apiKey", label: "API密钥", type: "password", required: true },
|
||||
|
||||
789
data/vendor/toonflow.ts
vendored
789
data/vendor/toonflow.ts
vendored
@ -1,82 +1,113 @@
|
||||
//如需遥测AI请使用在toonflow安装目录运行npx @ai-sdk/devtools (要求在其他设置中打开遥测功能,且toonflow有权限在安装目录创建.devtools文件夹)
|
||||
// ==================== 类型定义 ====================
|
||||
// 文本模型
|
||||
/**
|
||||
* Toonflow官方中转平台 供应商适配
|
||||
* @version 2.0
|
||||
*/
|
||||
|
||||
// ============================================================
|
||||
// 类型定义
|
||||
// ============================================================
|
||||
|
||||
type VideoMode =
|
||||
| "singleImage"
|
||||
| "startEndRequired"
|
||||
| "endFrameOptional"
|
||||
| "startFrameOptional"
|
||||
| "text"
|
||||
| (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[];
|
||||
|
||||
interface TextModel {
|
||||
name: string; // 显示名称
|
||||
name: string;
|
||||
modelName: string;
|
||||
type: "text";
|
||||
think: boolean; // 前端显示用
|
||||
think: boolean;
|
||||
}
|
||||
|
||||
// 图像模型
|
||||
interface ImageModel {
|
||||
name: string; // 显示名称
|
||||
name: string;
|
||||
modelName: string;
|
||||
type: "image";
|
||||
mode: ("text" | "singleImage" | "multiReference")[];
|
||||
associationSkills?: string; // 关联技能,多个技能用逗号分隔
|
||||
associationSkills?: string;
|
||||
}
|
||||
// 视频模型
|
||||
|
||||
interface VideoModel {
|
||||
name: string; // 显示名称
|
||||
modelName: string; //全局唯一
|
||||
name: string;
|
||||
modelName: string;
|
||||
type: "video";
|
||||
mode: (
|
||||
| "singleImage" // 单图
|
||||
| "startEndRequired" // 首尾帧(两张都得有)
|
||||
| "endFrameOptional" // 首尾帧(尾帧可选)
|
||||
| "startFrameOptional" // 首尾帧(首帧可选)
|
||||
| "text" // 文本生视频
|
||||
| ("videoReference" | "imageReference" | "audioReference" | "textReference")[]
|
||||
)[]; // 混合参考
|
||||
associationSkills?: string; // 关联技能,多个技能用逗号分隔
|
||||
audio: "optional" | false | true; // 音频配置
|
||||
mode: VideoMode[];
|
||||
associationSkills?: string;
|
||||
audio: "optional" | false | true;
|
||||
durationResolutionMap: { duration: number[]; resolution: string[] }[];
|
||||
}
|
||||
|
||||
interface TTSModel {
|
||||
name: string; // 显示名称
|
||||
name: string;
|
||||
modelName: string;
|
||||
type: "tts";
|
||||
voices: {
|
||||
title: string; //显示名称
|
||||
voice: string; //说话人
|
||||
}[];
|
||||
voices: { title: string; voice: string }[];
|
||||
}
|
||||
// 供应商配置
|
||||
|
||||
interface VendorConfig {
|
||||
id: string; //供应商唯一标识,必须全局唯一
|
||||
author: string;
|
||||
description?: string; //md5格式
|
||||
id: string;
|
||||
version: string;
|
||||
name: string;
|
||||
icon?: string; //仅支持base64格式
|
||||
inputs: {
|
||||
key: string;
|
||||
label: string;
|
||||
type: "text" | "password" | "url";
|
||||
required: boolean;
|
||||
placeholder?: 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)[];
|
||||
models: (TextModel | ImageModel | VideoModel | TTSModel)[];
|
||||
}
|
||||
// ==================== 全局工具函数 ====================
|
||||
//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 }>;
|
||||
|
||||
type ReferenceList =
|
||||
| { type: "image"; sourceType: "base64"; base64: string }
|
||||
| { type: "audio"; sourceType: "base64"; base64: string }
|
||||
| { type: "video"; sourceType: "base64"; base64: string };
|
||||
|
||||
interface ImageConfig {
|
||||
prompt: string;
|
||||
referenceList?: Extract<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;
|
||||
@ -86,16 +117,27 @@ 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;
|
||||
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: "toonflow",
|
||||
version: "2.0",
|
||||
author: "Toonflow",
|
||||
name: "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: {
|
||||
@ -103,60 +145,15 @@ const vendor: VendorConfig = {
|
||||
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: "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",
|
||||
@ -177,35 +174,34 @@ const vendor: VendorConfig = {
|
||||
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"],
|
||||
durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }],
|
||||
audio: true,
|
||||
},
|
||||
{
|
||||
name: "vidu2 turbo",
|
||||
type: "video",
|
||||
modelName: "ViduQ2-turbo",
|
||||
durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["540p", "720p", "1080p"] }],
|
||||
mode: ["singleImage", "startEndRequired"],
|
||||
durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["540p", "720p", "1080p"] }],
|
||||
audio: false,
|
||||
},
|
||||
{
|
||||
name: "ViduQ3 pro",
|
||||
type: "video",
|
||||
modelName: "ViduQ3-pro",
|
||||
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"],
|
||||
durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], resolution: ["540p", "720p", "1080p"] }],
|
||||
audio: false,
|
||||
},
|
||||
{
|
||||
name: "ViduQ2 pro",
|
||||
type: "video",
|
||||
modelName: "ViduQ2-pro",
|
||||
durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["540p", "720p", "1080p"] }],
|
||||
mode: ["singleImage", "startEndRequired"],
|
||||
durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["540p", "720p", "1080p"] }],
|
||||
audio: false,
|
||||
},
|
||||
|
||||
{
|
||||
name: "Doubao Seedream 5.0 Lite",
|
||||
type: "image",
|
||||
@ -220,350 +216,307 @@ const vendor: VendorConfig = {
|
||||
},
|
||||
],
|
||||
};
|
||||
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) {
|
||||
// 从 markdown 内容中提取第一张图片
|
||||
function extractFirstImageFromMd(content: string) {
|
||||
const regex = /!\[([^\]]*)\]\((data:image\/[^;]+;base64,[A-Za-z0-9+/=]+|https?:\/\/[^\s)]+|\/\/[^\s)]+|[^\s)]+)\)/;
|
||||
const match = content.match(regex);
|
||||
if (!match) return null;
|
||||
const raw = match[2].trim();
|
||||
const url = raw.startsWith("data:") ? raw : raw.split(/\s+/)[0];
|
||||
return {
|
||||
alt: match[1],
|
||||
url,
|
||||
type: url.startsWith("data:image") ? "base64" : "url",
|
||||
};
|
||||
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) => {
|
||||
|
||||
// ============================================================
|
||||
// 适配器函数
|
||||
// ============================================================
|
||||
|
||||
const textRequest = (model: TextModel) => {
|
||||
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);
|
||||
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
|
||||
return createOpenAI({ baseURL: vendor.inputValues.baseUrl, apiKey }).chat(model.modelName);
|
||||
};
|
||||
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");
|
||||
}
|
||||
const imageRequest = async (config: ImageConfig, model: ImageModel): Promise<string> => {
|
||||
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
|
||||
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
|
||||
const baseUrl = vendor.inputValues.baseUrl;
|
||||
const lowerName = model.modelName.toLowerCase();
|
||||
const imageBase64List = (config.referenceList ?? []).map((r) => r.base64);
|
||||
|
||||
// Gemini / nano 系模型:走 chat/completions 接口,从返回的 markdown 中提取图片
|
||||
if (lowerName.includes("gemini") || lowerName.includes("nano")) {
|
||||
const imageConfigGoogle: Record<string, string> = {
|
||||
aspect_ratio: config.aspectRatio,
|
||||
image_size: config.size,
|
||||
};
|
||||
const messages: any[] = [];
|
||||
if (imageBase64List.length) {
|
||||
messages.push({
|
||||
role: "user",
|
||||
content: imageBase64List.map((b) => ({ type: "image_url", image_url: { url: b } })),
|
||||
});
|
||||
}
|
||||
messages.push({ role: "user", content: config.prompt + "请直接输出图片" });
|
||||
const body = {
|
||||
model: model.modelName,
|
||||
messages,
|
||||
extra_body: { google: { image_config: imageConfigGoogle } },
|
||||
};
|
||||
logger(`[imageRequest] 使用 gemini 适配器,模型: ${model.modelName}`);
|
||||
const response = await fetch(`${baseUrl}/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const imageResult = extractFirstImageFromMd(data.choices[0].message.content);
|
||||
if (!imageResult) throw new Error("未能从响应中提取图片");
|
||||
if (imageResult.type === "base64") return imageResult.url;
|
||||
return await urlToBase64(imageResult.url);
|
||||
}
|
||||
|
||||
// 豆包 / seedream 系模型:走 images/generations 接口
|
||||
if (lowerName.includes("doubao") || lowerName.includes("seedream")) {
|
||||
const effectiveSize = config.size === "1K" ? "2K" : config.size;
|
||||
const sizeMap: Record<string, Record<string, string>> = {
|
||||
"16:9": { "2K": "2848x1600", "4K": "4096x2304" },
|
||||
"9:16": { "2K": "1600x2848", "4K": "2304x4096" },
|
||||
};
|
||||
const resolvedSize = sizeMap[config.aspectRatio]?.[effectiveSize];
|
||||
const body: Record<string, any> = {
|
||||
model: model.modelName,
|
||||
prompt: config.prompt,
|
||||
size: resolvedSize,
|
||||
response_format: "url",
|
||||
sequential_image_generation: "disabled",
|
||||
stream: false,
|
||||
watermark: false,
|
||||
...(imageBase64List.length && { image: imageBase64List }),
|
||||
};
|
||||
logger(`[imageRequest] 使用 doubao 适配器,模型: ${model.modelName}`);
|
||||
const response = await fetch(`${baseUrl}/images/generations`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const resultUrl = data.data[0].url;
|
||||
return await urlToBase64(resultUrl);
|
||||
}
|
||||
|
||||
throw new Error(`不支持的图像模型: ${model.modelName}`);
|
||||
};
|
||||
|
||||
const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {
|
||||
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
|
||||
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
|
||||
const baseUrl = vendor.inputValues.baseUrl;
|
||||
const lowerName = model.modelName.toLowerCase();
|
||||
|
||||
// 当前激活的单一 VideoMode(取第一个非数组模式,或数组模式)
|
||||
const activeMode = config.mode[0];
|
||||
const imageRefs = (config.referenceList ?? []).filter((r) => r.type === "image").map((r) => r.base64);
|
||||
const videoRefs = (config.referenceList ?? []).filter((r) => r.type === "video").map((r) => r.base64);
|
||||
const audioRefs = (config.referenceList ?? []).filter((r) => r.type === "audio").map((r) => r.base64);
|
||||
|
||||
// 构建模型专属 metadata
|
||||
let metadata: Record<string, any> = {};
|
||||
|
||||
if (lowerName.includes("wan")) {
|
||||
// 万象系列
|
||||
if (
|
||||
(activeMode === "startEndRequired" || activeMode === "endFrameOptional" || activeMode === "startFrameOptional") &&
|
||||
imageRefs.length >= 2
|
||||
) {
|
||||
if (imageRefs[0]) metadata.first_frame_url = imageRefs[0];
|
||||
if (imageRefs[1]) metadata.last_frame_url = imageRefs[1];
|
||||
} else if (imageRefs.length) {
|
||||
metadata.img_url = imageRefs[0];
|
||||
}
|
||||
if (typeof config.audio === "boolean") metadata.audio = config.audio;
|
||||
|
||||
// 万象需要额外传 size 字段
|
||||
const wanSizeMap: Record<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 wanSize = wanSizeMap[config.resolution]?.[config.aspectRatio];
|
||||
const body: Record<string, any> = {
|
||||
model: model.modelName,
|
||||
prompt: config.prompt,
|
||||
duration: config.duration,
|
||||
size: wanSize,
|
||||
metadata,
|
||||
};
|
||||
logger(`[videoRequest] 提交万象视频任务,模型: ${model.modelName}`);
|
||||
const response = await fetch(`${baseUrl}/video/generations`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const taskId = data.id;
|
||||
logger(`[videoRequest] 万象任务ID: ${taskId}`);
|
||||
const res = await pollTask(async () => {
|
||||
const queryResponse = await fetch(`${baseUrl}/video/generations/${taskId}`, {
|
||||
method: "GET",
|
||||
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
||||
});
|
||||
if (!queryResponse.ok) {
|
||||
const errorText = await queryResponse.text();
|
||||
throw new Error(`轮询失败,状态码: ${queryResponse.status}, 错误信息: ${errorText}`);
|
||||
}
|
||||
const queryData = await queryResponse.json();
|
||||
const status = queryData?.status ?? queryData?.data?.status;
|
||||
switch (status) {
|
||||
case "completed":
|
||||
case "SUCCESS":
|
||||
case "success":
|
||||
return { completed: true, data: queryData.data.result_url };
|
||||
case "FAILURE":
|
||||
case "failed":
|
||||
return { completed: true, error: queryData?.data?.fail_reason ?? "视频生成失败" };
|
||||
default:
|
||||
return { completed: false };
|
||||
}
|
||||
});
|
||||
if (res.error) throw new Error(res.error);
|
||||
return await urlToBase64(res.data!);
|
||||
}
|
||||
|
||||
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",
|
||||
},
|
||||
if (lowerName.includes("doubao") || lowerName.includes("seedance")) {
|
||||
// 豆包/Seedance 系列
|
||||
metadata = {
|
||||
...(typeof config.audio === "boolean" && { generate_audio: config.audio }),
|
||||
ratio: config.aspectRatio,
|
||||
image_roles: [] as string[],
|
||||
references: [] as string[],
|
||||
};
|
||||
const size = sizeMap[videoConfig.resolution]?.[videoConfig.aspectRatio];
|
||||
publicBody.size = size;
|
||||
if (Array.isArray(activeMode)) {
|
||||
// 多参考模式
|
||||
imageRefs.forEach((b) => metadata.references.push(b));
|
||||
videoRefs.forEach((b) => metadata.references.push(b));
|
||||
audioRefs.forEach((b) => metadata.references.push(b));
|
||||
} else if (activeMode === "startEndRequired" || activeMode === "endFrameOptional" || activeMode === "startFrameOptional") {
|
||||
imageRefs.forEach((_, i) => (metadata.image_roles as string[]).push(i === 0 ? "first_frame" : "last_frame"));
|
||||
} else if (activeMode === "singleImage") {
|
||||
imageRefs.forEach(() => (metadata.image_roles as string[]).push("reference_image"));
|
||||
}
|
||||
} else if (lowerName.includes("vidu")) {
|
||||
// Vidu 系列
|
||||
metadata = {
|
||||
aspect_ratio: config.aspectRatio,
|
||||
audio: config.audio ?? false,
|
||||
off_peak: false,
|
||||
};
|
||||
} else if (lowerName.includes("kling")) {
|
||||
// 可灵系列
|
||||
metadata = { aspect_ratio: config.aspectRatio };
|
||||
if (Array.isArray(activeMode)) {
|
||||
metadata.reference = [...imageRefs, ...videoRefs, ...audioRefs];
|
||||
} else if (activeMode === "endFrameOptional" && imageRefs.length) {
|
||||
metadata.image_tail = imageRefs[0];
|
||||
} else if (activeMode === "startEndRequired" && imageRefs.length >= 2) {
|
||||
metadata.image_list = [
|
||||
{ image_url: imageRefs[0], type: "first_frame" },
|
||||
{ image_url: imageRefs[1], type: "last_frame" },
|
||||
];
|
||||
} else if (activeMode === "singleImage" && imageRefs.length) {
|
||||
metadata.image = imageRefs[0];
|
||||
}
|
||||
}
|
||||
const requestUrl = vendor.inputValues.baseUrl + "/video/generations";
|
||||
const queryUrl = vendor.inputValues.baseUrl + "/video/generations/{id}";
|
||||
const response = await fetch(requestUrl, {
|
||||
|
||||
// 公共请求体(非万象通用路径)
|
||||
const publicBody: Record<string, any> = {
|
||||
model: model.modelName,
|
||||
...(!Array.isArray(activeMode) && imageRefs.length ? { images: imageRefs } : {}),
|
||||
prompt: config.prompt,
|
||||
duration: config.duration,
|
||||
metadata,
|
||||
};
|
||||
|
||||
logger(`[videoRequest] 提交视频任务,模型: ${model.modelName}`);
|
||||
const response = await fetch(`${baseUrl}/video/generations`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(publicBody),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text(); // 获取错误信息
|
||||
console.error("请求失败,状态码:", response.status, ", 错误信息:", errorText);
|
||||
const errorText = await response.text();
|
||||
throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const taskId = data.id;
|
||||
logger(`[videoRequest] 任务ID: ${taskId}`);
|
||||
|
||||
const res = await pollTask(async () => {
|
||||
const queryResponse = await fetch(queryUrl.replace("{id}", taskId), {
|
||||
const queryResponse = await fetch(`${baseUrl}/video/generations/${taskId}`, {
|
||||
method: "GET",
|
||||
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
||||
});
|
||||
if (!queryResponse.ok) {
|
||||
const errorText = await queryResponse.text(); // 获取错误信息
|
||||
console.error("请求失败,状态码:", queryResponse.status, ", 错误信息:", errorText);
|
||||
throw new Error(`请求失败,状态码: ${queryResponse.status}, 错误信息: ${errorText}`);
|
||||
const errorText = await queryResponse.text();
|
||||
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 || "视频生成失败" };
|
||||
case "failed":
|
||||
return { completed: true, error: queryData?.data?.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;
|
||||
if (res.error) throw new Error(res.error);
|
||||
return await urlToBase64(res.data!);
|
||||
};
|
||||
|
||||
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 {};
|
||||
368
data/vendor/vidu.ts
vendored
Normal file
368
data/vendor/vidu.ts
vendored
Normal file
@ -0,0 +1,368 @@
|
||||
//如需遥测AI请使用在toonflow安装目录运行npx @ai-sdk/devtools (要求在其他设置中打开遥测功能,且toonflow有权限在安装目录创建.devtools文件夹)
|
||||
// ==================== 类型定义 ====================
|
||||
// 文本模型
|
||||
interface TextModel {
|
||||
name: string; // 显示名称
|
||||
modelName: string;
|
||||
type: "text";
|
||||
think: boolean; // 前端显示用
|
||||
}
|
||||
|
||||
// 图像模型
|
||||
interface ImageModel {
|
||||
name: string; // 显示名称
|
||||
modelName: string;
|
||||
type: "image";
|
||||
mode: ("text" | "singleImage" | "multiReference")[];
|
||||
associationSkills?: string; // 关联技能,多个技能用逗号分隔
|
||||
}
|
||||
// 视频模型
|
||||
interface VideoModel {
|
||||
name: string; // 显示名称
|
||||
modelName: string; //全局唯一
|
||||
type: "video";
|
||||
mode: (
|
||||
| "singleImage" // 单图
|
||||
| "startEndRequired" // 首尾帧(两张都得有)
|
||||
| "endFrameOptional" // 首尾帧(尾帧可选)
|
||||
| "startFrameOptional" // 首尾帧(首帧可选)
|
||||
| "text" // 文本生视频
|
||||
| ("videoReference" | "imageReference" | "audioReference" | "textReference")[] // 混合参考
|
||||
)[];
|
||||
associationSkills?: string; // 关联技能,多个技能用逗号分隔
|
||||
audio: "optional" | false | true; // 音频配置
|
||||
durationResolutionMap: { duration: number[]; resolution: string[] }[];
|
||||
}
|
||||
|
||||
interface TTSModel {
|
||||
name: string; // 显示名称
|
||||
modelName: string;
|
||||
type: "tts";
|
||||
voices: {
|
||||
title: string; //显示名称
|
||||
voice: string; //说话人
|
||||
}[];
|
||||
}
|
||||
// 供应商配置
|
||||
interface VendorConfig {
|
||||
id: string; //供应商唯一标识,必须全局唯一
|
||||
author: string;
|
||||
description?: string; //md5格式
|
||||
name: string;
|
||||
icon?: string; //仅支持base64格式
|
||||
inputs: {
|
||||
key: string;
|
||||
label: string;
|
||||
type: "text" | "password" | "url";
|
||||
required: boolean;
|
||||
placeholder?: string;
|
||||
}[];
|
||||
inputValues: Record<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: "vidu",
|
||||
author: "搬砖的Coder",
|
||||
description:
|
||||
"Vidu 官方视频生成平台。 [前往平台](https://platform.vidu.cn/login/)",
|
||||
name: "Vidu 开放平台",
|
||||
inputs: [
|
||||
{ key: "apiKey", label: "API密钥", type: "password", required: true, placeholder: "请到Vidu官方申请" },
|
||||
{ key: "baseUrl", label: "接口路径", type: "url", required: true, placeholder: "https://api.vidu.cn/ent/v2" },
|
||||
],
|
||||
inputValues: {
|
||||
apiKey: "",
|
||||
baseUrl: "https://api.vidu.cn/ent/v2",
|
||||
},
|
||||
models: [
|
||||
{
|
||||
name: "ViduQ3 turbo",
|
||||
type: "video",
|
||||
modelName: "ViduQ3-turbo",
|
||||
durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], resolution: ["540p", "720p", "1080p"] }],
|
||||
mode: ["singleImage", "startEndRequired", "text"],
|
||||
audio: true,
|
||||
},
|
||||
{
|
||||
name: "ViduQ3 pro",
|
||||
type: "video",
|
||||
modelName: "ViduQ3-pro",
|
||||
durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], resolution: ["540p", "720p", "1080p"] }],
|
||||
mode: ["singleImage", "startEndRequired", "text"],
|
||||
audio: true,
|
||||
},
|
||||
{
|
||||
name: "ViduQ2 pro fast",
|
||||
type: "video",
|
||||
modelName: "ViduQ2-pro-fast",
|
||||
durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["720p", "1080p"] }],
|
||||
mode: ["singleImage", "startEndRequired"],
|
||||
audio: true,
|
||||
},
|
||||
{
|
||||
name: "viduQ2 turbo",
|
||||
type: "video",
|
||||
modelName: "ViduQ2-turbo",
|
||||
durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["540p", "720p", "1080p"] }],
|
||||
mode: ["singleImage", "startEndRequired"],
|
||||
audio: true,
|
||||
},
|
||||
{
|
||||
name: "ViduQ2 pro",
|
||||
type: "video",
|
||||
modelName: "ViduQ2-pro",
|
||||
durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["540p", "720p", "1080p"] }],
|
||||
mode: ["singleImage", "startEndRequired"], //参考生视频无有效设置值
|
||||
audio: true,
|
||||
},
|
||||
{
|
||||
name: "ViduQ2",
|
||||
type: "video",
|
||||
modelName: "ViduQ2",
|
||||
durationResolutionMap: [{ duration: [5], resolution: ["1080p"] }],
|
||||
mode: ["text"],
|
||||
audio: true,
|
||||
},
|
||||
{
|
||||
name: "ViduQ1",
|
||||
type: "video",
|
||||
modelName: "ViduQ1",
|
||||
durationResolutionMap: [{ duration: [5], resolution: ["1080p"] }],
|
||||
mode: ["singleImage", "startEndRequired", "text"],
|
||||
audio: true,
|
||||
},
|
||||
{
|
||||
name: "ViduQ1 classic",
|
||||
type: "video",
|
||||
modelName: "viduQ1-classic",
|
||||
durationResolutionMap: [{ duration: [5], resolution: ["1080p"] }],
|
||||
mode: ["singleImage", "startEndRequired"],
|
||||
audio: true,
|
||||
},
|
||||
{
|
||||
name: "Vidu2.0",
|
||||
type: "video",
|
||||
modelName: "vidu2.0",
|
||||
durationResolutionMap: [{ duration: [4, 8], resolution: ["360p", "720p", "1080p"] }],
|
||||
mode: ["singleImage", "startEndRequired"],
|
||||
audio: true,
|
||||
},
|
||||
{
|
||||
name: "viduq1 for image",
|
||||
type: "image",
|
||||
modelName: "viduq1",
|
||||
mode: ["text"],
|
||||
},
|
||||
{
|
||||
name: "viduq2 for image",
|
||||
type: "image",
|
||||
modelName: "viduq2",
|
||||
mode: ["text", "singleImage", "multiReference"],
|
||||
},
|
||||
],
|
||||
};
|
||||
exports.vendor = vendor;
|
||||
|
||||
// ==================== 适配器函数 ====================
|
||||
|
||||
// 文本请求函数
|
||||
const textRequest: (textModel: TextModel) => { url: string; model: string } = (textModel) => {
|
||||
throw new Error("当前供应商仅支持视频大模型,谢谢!");
|
||||
};
|
||||
exports.textRequest = textRequest;
|
||||
|
||||
//图片请求函数
|
||||
interface ImageConfig {
|
||||
prompt: string; //图片提示词
|
||||
imageBase64: string[]; //输入的图片提示词
|
||||
size: "1K" | "2K" | "4K"; // 图片尺寸
|
||||
aspectRatio: `${number}:${number}`; // 长宽比
|
||||
}
|
||||
const imageRequest = async (imageConfig: ImageConfig, imageModel: ImageModel) => {
|
||||
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
|
||||
const apiKey = vendor.inputValues.apiKey.replace("Token ", "");
|
||||
|
||||
const size = imageConfig.size === "1K" ? "2K" : imageConfig.size;
|
||||
const sizeMap: Record<string, Record<string, string>> = {
|
||||
"16:9": {
|
||||
"1k": "1920x1080",
|
||||
"2K": "2848x1600",
|
||||
"4K": "4096x2304",
|
||||
},
|
||||
"9:16": {
|
||||
"1k": "1920x1080",
|
||||
"2K": "1600x2848",
|
||||
"4K": "2304x4096",
|
||||
},
|
||||
};
|
||||
|
||||
const body: Record<string, any> = {
|
||||
model: imageModel.modelName,
|
||||
prompt: imageConfig.prompt,
|
||||
aspect_ratio: sizeMap[imageConfig.aspectRatio][size],
|
||||
seed: 0,
|
||||
resolution: size,
|
||||
...(imageConfig.imageBase64 && { image: imageConfig.imageBase64 }),
|
||||
};
|
||||
|
||||
const createImageUrl = vendor.inputValues.baseUrl + "/reference2image";
|
||||
const response = await fetch(createImageUrl, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Token ${apiKey}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text(); // 获取错误信息
|
||||
console.error("请求失败,状态码:", response.status, ", 错误信息:", errorText);
|
||||
throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const res = await checkTaskResult(data.task_id);
|
||||
if (!res.data) {
|
||||
throw new Error("图片未能生成");
|
||||
}
|
||||
const list = JSON.parse(JSON.stringify(res.data));
|
||||
return list[0].url;
|
||||
};
|
||||
exports.imageRequest = imageRequest;
|
||||
|
||||
interface VideoConfig {
|
||||
duration: number;
|
||||
resolution: string;
|
||||
aspectRatio: "16:9" | "9:16";
|
||||
prompt: string;
|
||||
imageBase64?: string[];
|
||||
audio?: boolean;
|
||||
mode:
|
||||
| "singleImage" // 单图
|
||||
| "multiImage" // 多图模式
|
||||
| "gridImage" // 网格单图(传入一张图片,但该图片是网格图)
|
||||
| "startEndRequired" // 首尾帧(两张都得有)
|
||||
| "endFrameOptional" // 首尾帧(尾帧可选)
|
||||
| "startFrameOptional" // 首尾帧(首帧可选)
|
||||
| "text" // 文本生视频
|
||||
| ("video" | "image" | "audio" | "text")[]; // 混合参考
|
||||
}
|
||||
|
||||
// 构建 各个平台的metadata参数
|
||||
|
||||
const buildViduMetadata = (videoConfig: VideoConfig) => ({
|
||||
aspect_ratio: videoConfig.aspectRatio,
|
||||
audio: videoConfig.audio ?? false,
|
||||
off_peak: false,
|
||||
});
|
||||
|
||||
type MetadataBuilder = (config: VideoConfig) => Record<string, any>;
|
||||
const METADATA_BUILDERS: Array<[string, MetadataBuilder]> = [["vidu", buildViduMetadata]];
|
||||
const buildModelMetadata = (modelName: string, videoConfig: VideoConfig) => {
|
||||
const lowerName = modelName.toLowerCase();
|
||||
const match = METADATA_BUILDERS.find(([key]) => lowerName.includes(key));
|
||||
return match ? match[1](videoConfig) : {};
|
||||
};
|
||||
// 检查生成物结果
|
||||
const checkTaskResult = async (taskId: string) => {
|
||||
const queryUrl = vendor.inputValues.baseUrl + "/tasks/{id}/creations";
|
||||
const apiKey = vendor.inputValues.apiKey;
|
||||
const res = await pollTask(async () => {
|
||||
const queryResponse = await fetch(queryUrl.replace("{id}", taskId), {
|
||||
method: "GET",
|
||||
headers: { Authorization: `Token ${apiKey}`, "Content-Type": "application/json" },
|
||||
});
|
||||
if (!queryResponse.ok) {
|
||||
const errorText = await queryResponse.text(); // 获取错误信息
|
||||
console.error("请求失败,状态码:", queryResponse.status, ", 错误信息:", errorText);
|
||||
throw new Error(`请求失败,状态码: ${queryResponse.status}, 错误信息: ${errorText}`);
|
||||
}
|
||||
const queryData = await queryResponse.json();
|
||||
const status = queryData?.state ?? queryData?.data?.state;
|
||||
const fail_reason = queryData?.data?.err_code ?? queryData?.data;
|
||||
switch (status) {
|
||||
case "completed":
|
||||
case "SUCCESS":
|
||||
case "success":
|
||||
return { completed: true, data: queryData.creations };
|
||||
case "FAILURE":
|
||||
case "failed":
|
||||
return { completed: false, error: fail_reason || "生成失败" };
|
||||
default:
|
||||
return { completed: false };
|
||||
}
|
||||
});
|
||||
if (res.error) throw new Error(res.error);
|
||||
return res;
|
||||
};
|
||||
|
||||
const videoRequest = async (videoConfig: VideoConfig, videoModel: VideoModel) => {
|
||||
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
|
||||
const apiKey = vendor.inputValues.apiKey.replace("Token ", "");
|
||||
|
||||
// 构建每个模型对应的附加参数
|
||||
const metadata = buildModelMetadata(videoModel.modelName, videoConfig);
|
||||
|
||||
//公共请求参数
|
||||
const publicBody = {
|
||||
model: videoModel.modelName,
|
||||
...(videoConfig.imageBase64 && videoConfig.imageBase64.length ? { images: videoConfig.imageBase64 } : {}),
|
||||
prompt: videoConfig.prompt,
|
||||
size: videoConfig.resolution,
|
||||
duration: videoConfig.duration,
|
||||
metadata: metadata,
|
||||
};
|
||||
|
||||
const requestUrl = vendor.inputValues.baseUrl + "/start-end2video";
|
||||
const response = await fetch(requestUrl, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Token ${apiKey}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(publicBody),
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text(); // 获取错误信息
|
||||
console.error("请求失败,状态码:", response.status, ", 错误信息:", errorText);
|
||||
throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const taskId = data.id;
|
||||
const result = await checkTaskResult(taskId);
|
||||
return result.data;
|
||||
};
|
||||
exports.videoRequest = videoRequest;
|
||||
|
||||
interface TTSConfig {
|
||||
text: string;
|
||||
voice: string;
|
||||
speechRate: number;
|
||||
pitchRate: number;
|
||||
volume: number;
|
||||
}
|
||||
const ttsRequest = async (ttsConfig: TTSConfig, ttsModel: TTSModel) => {
|
||||
throw new Error("Vidu 暂不支持语音合成(TTS)");
|
||||
};
|
||||
305
data/vendor/volcengine.ts
vendored
305
data/vendor/volcengine.ts
vendored
@ -2,9 +2,11 @@
|
||||
* Toonflow AI供应商模板 - 火山引擎(豆包)
|
||||
* @version 2.0
|
||||
*/
|
||||
|
||||
// ============================================================
|
||||
// 类型定义
|
||||
// ============================================================
|
||||
|
||||
type VideoMode =
|
||||
| "singleImage"
|
||||
| "startEndRequired"
|
||||
@ -12,12 +14,14 @@ type VideoMode =
|
||||
| "startFrameOptional"
|
||||
| "text"
|
||||
| (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[];
|
||||
|
||||
interface TextModel {
|
||||
name: string;
|
||||
modelName: string;
|
||||
type: "text";
|
||||
think: boolean;
|
||||
}
|
||||
|
||||
interface ImageModel {
|
||||
name: string;
|
||||
modelName: string;
|
||||
@ -25,6 +29,7 @@ interface ImageModel {
|
||||
mode: ("text" | "singleImage" | "multiReference")[];
|
||||
associationSkills?: string;
|
||||
}
|
||||
|
||||
interface VideoModel {
|
||||
name: string;
|
||||
modelName: string;
|
||||
@ -34,12 +39,14 @@ interface VideoModel {
|
||||
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;
|
||||
@ -51,36 +58,48 @@ interface VendorConfig {
|
||||
inputValues: Record<string, string>;
|
||||
models: (TextModel | ImageModel | VideoModel | TTSModel)[];
|
||||
}
|
||||
|
||||
type ReferenceList =
|
||||
| { type: "image"; sourceType: "base64"; base64: string }
|
||||
| { type: "audio"; sourceType: "base64"; base64: string }
|
||||
| { type: "video"; sourceType: "base64"; base64: string };
|
||||
|
||||
interface ImageConfig {
|
||||
prompt: string;
|
||||
imageBase64: 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?: 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;
|
||||
@ -107,15 +126,18 @@ declare const exports: {
|
||||
checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>;
|
||||
updateVendor?: () => Promise<string>;
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 供应商配置
|
||||
// ============================================================
|
||||
|
||||
const vendor: VendorConfig = {
|
||||
id: "volcengine-doubao",
|
||||
id: "volcengine",
|
||||
version: "2.0",
|
||||
author: "Toonflow",
|
||||
author: "leeqi",
|
||||
name: "火山引擎(豆包)",
|
||||
description: "## 火山引擎豆包大模型,支持文本、图片生成、视频生成等能力。\n\n需要在[火山引擎控制台](https://console.volcengine.com/ark)获取API密钥。",
|
||||
description:
|
||||
"火山引擎豆包大模型,支持文本、图片生成、视频生成等能力。\n\n需要在[火山引擎控制台](https://console.volcengine.com/ark)获取API密钥。",
|
||||
icon: "",
|
||||
inputs: [
|
||||
{ key: "apiKey", label: "API密钥", type: "password", required: true, placeholder: "火山引擎API Key" },
|
||||
@ -191,95 +213,69 @@ const vendor: VendorConfig = {
|
||||
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"],
|
||||
],
|
||||
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"] },
|
||||
],
|
||||
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"],
|
||||
],
|
||||
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"] },
|
||||
],
|
||||
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"] },
|
||||
],
|
||||
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"] },
|
||||
],
|
||||
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"] },
|
||||
],
|
||||
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"] },
|
||||
],
|
||||
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"] },
|
||||
],
|
||||
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 {
|
||||
@ -294,40 +290,58 @@ const getBaseUrl = () => vendor.inputValues.baseUrl.replace(/\/+$/, "");
|
||||
// 适配器函数
|
||||
// ============================================================
|
||||
|
||||
/** 文本请求 - 直接使用 createOpenAI */
|
||||
const textRequest = (model: TextModel) => {
|
||||
const textRequest = (model: TextModel, think: boolean, thinkLevel: 0 | 1 | 2 | 3) => {
|
||||
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
|
||||
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
|
||||
return createOpenAI({ baseURL: getBaseUrl(), apiKey }).chat(model.modelName);
|
||||
|
||||
const effortMap: Record<number, string> = {
|
||||
0: "minimal",
|
||||
1: "low",
|
||||
2: "medium",
|
||||
3: "high",
|
||||
};
|
||||
|
||||
return createOpenAI({
|
||||
baseURL: getBaseUrl(),
|
||||
apiKey,
|
||||
compatibility: "compatible",
|
||||
fetch: async (url: string, options?: RequestInit) => {
|
||||
const rawBody = JSON.parse((options?.body as string) ?? "{}");
|
||||
const modifiedBody = {
|
||||
...rawBody,
|
||||
thinking: {
|
||||
type: "enabled",
|
||||
},
|
||||
reasoning_effort: effortMap[thinkLevel],
|
||||
};
|
||||
return await fetch(url, {
|
||||
...options,
|
||||
body: JSON.stringify(modifiedBody),
|
||||
});
|
||||
},
|
||||
}).chat(model.modelName);
|
||||
};
|
||||
|
||||
/** 图片生成请求 */
|
||||
const imageRequest = async (config: ImageConfig, model: ImageModel): Promise<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) {
|
||||
if (config.referenceList && config.referenceList.length > 0) {
|
||||
for (const ref of config.referenceList) {
|
||||
content.push({
|
||||
type: "image_url",
|
||||
image_url: { url: `data:image/png;base64,${base64}` },
|
||||
image_url: { url: ref.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)) },
|
||||
@ -354,101 +368,136 @@ const imageRequest = async (config: ImageConfig, model: ImageModel): Promise<str
|
||||
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) {
|
||||
case "singleImage": {
|
||||
const firstImage = config.referenceList?.find((r) => r.type === "image");
|
||||
if (firstImage) {
|
||||
content.push({
|
||||
type: "image_url",
|
||||
image_url: { url: `data:image/png;base64,${config.imageBase64[0]}` },
|
||||
image_url: { url: firstImage.base64 },
|
||||
role: "first_frame",
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "startFrameOptional":
|
||||
// 首帧 + 可选尾帧模式
|
||||
if (config.imageBase64 && config.imageBase64.length > 0) {
|
||||
}
|
||||
case "startFrameOptional": {
|
||||
const images = config.referenceList?.filter((r) => r.type === "image") ?? [];
|
||||
if (images.length > 0) {
|
||||
content.push({
|
||||
type: "image_url",
|
||||
image_url: { url: `data:image/png;base64,${config.imageBase64[0]}` },
|
||||
image_url: { url: images[0].base64 },
|
||||
role: "first_frame",
|
||||
});
|
||||
if (config.imageBase64.length > 1) {
|
||||
if (images.length > 1) {
|
||||
content.push({
|
||||
type: "image_url",
|
||||
image_url: { url: `data:image/png;base64,${config.imageBase64[1]}` },
|
||||
image_url: { url: images[1].base64 },
|
||||
role: "last_frame",
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "startEndRequired": {
|
||||
const images = config.referenceList?.filter((r) => r.type === "image") ?? [];
|
||||
if (images.length >= 2) {
|
||||
content.push({
|
||||
type: "image_url",
|
||||
image_url: { url: images[0].base64 },
|
||||
role: "first_frame",
|
||||
});
|
||||
content.push({
|
||||
type: "image_url",
|
||||
image_url: { url: images[1].base64 },
|
||||
role: "last_frame",
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "endFrameOptional": {
|
||||
const images = config.referenceList?.filter((r) => r.type === "image") ?? [];
|
||||
if (images.length > 0) {
|
||||
content.push({
|
||||
type: "image_url",
|
||||
image_url: { url: images[0].base64 },
|
||||
role: "first_frame",
|
||||
});
|
||||
if (images.length > 1) {
|
||||
content.push({
|
||||
type: "image_url",
|
||||
image_url: { url: images[1].base64 },
|
||||
role: "last_frame",
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "text":
|
||||
// 纯文生视频,无需额外处理
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (Array.isArray(activeMode)) {
|
||||
// 多模态参考模式
|
||||
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;
|
||||
// 多模态参考模式:按类型分别提取并添加
|
||||
const imageRefs = config.referenceList?.filter((r) => r.type === "image") ?? [];
|
||||
const videoRefs = config.referenceList?.filter((r) => r.type === "video") ?? [];
|
||||
const audioRefs = config.referenceList?.filter((r) => r.type === "audio") ?? [];
|
||||
|
||||
for (const refDef of activeMode) {
|
||||
if (typeof refDef === "string") {
|
||||
if (refDef.startsWith("imageReference:")) {
|
||||
const maxCount = parseInt(refDef.split(":")[1], 10);
|
||||
for (const ref of imageRefs.slice(0, maxCount)) {
|
||||
content.push({
|
||||
type: "image_url",
|
||||
image_url: { url: ref.base64 },
|
||||
role: "reference_image",
|
||||
});
|
||||
}
|
||||
} else if (refDef.startsWith("videoReference:")) {
|
||||
const maxCount = parseInt(refDef.split(":")[1], 10);
|
||||
for (const ref of videoRefs.slice(0, maxCount)) {
|
||||
content.push({
|
||||
type: "video_url",
|
||||
video_url: { url: ref.base64 },
|
||||
role: "reference_video",
|
||||
});
|
||||
}
|
||||
} else if (refDef.startsWith("audioReference:")) {
|
||||
const maxCount = parseInt(refDef.split(":")[1], 10);
|
||||
for (const ref of audioRefs.slice(0, maxCount)) {
|
||||
content.push({
|
||||
type: "audio_url",
|
||||
audio_url: { url: ref.base64 },
|
||||
role: "reference_audio",
|
||||
});
|
||||
}
|
||||
}
|
||||
// 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,
|
||||
ratio: config.aspectRatio,
|
||||
duration: config.duration,
|
||||
resolution: config.resolution || "720p",
|
||||
watermark: false,
|
||||
};
|
||||
|
||||
// 音频控制
|
||||
if (model.audio === "optional") {
|
||||
body.generate_audio = config.audio !== false;
|
||||
} else if (model.audio === true) {
|
||||
@ -459,7 +508,6 @@ const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<str
|
||||
|
||||
logger(`[视频生成] 提交任务, 模型: ${model.modelName}, 时长: ${config.duration}s, 分辨率: ${config.resolution}`);
|
||||
|
||||
// 提交创建任务
|
||||
const createResponse = await axios.post(`${baseUrl}/contents/generations/tasks`, body, { headers });
|
||||
const taskId = createResponse.data?.id;
|
||||
|
||||
@ -469,39 +517,40 @@ const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<str
|
||||
|
||||
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;
|
||||
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}`);
|
||||
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分钟
|
||||
switch (task.status) {
|
||||
case "succeeded":
|
||||
if (task.content?.video_url) {
|
||||
return { completed: true, data: task.content.video_url };
|
||||
}
|
||||
return { completed: true, error: "任务成功但未返回视频URL" };
|
||||
case "failed":
|
||||
return { completed: true, error: task.error?.message || "视频生成失败" };
|
||||
case "expired":
|
||||
return { completed: true, error: "视频生成任务超时" };
|
||||
case "cancelled":
|
||||
return { completed: true, error: "视频生成任务已取消" };
|
||||
default:
|
||||
return { completed: false };
|
||||
}
|
||||
},
|
||||
10000,
|
||||
600000,
|
||||
);
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return result.data || "";
|
||||
return await urlToBase64(result.data!);
|
||||
};
|
||||
|
||||
/** TTS请求(火山引擎暂无TTS模型配置,预留接口) */
|
||||
const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<string> => {
|
||||
return "";
|
||||
};
|
||||
@ -517,6 +566,7 @@ const updateVendor = async (): Promise<string> => {
|
||||
// ============================================================
|
||||
// 导出
|
||||
// ============================================================
|
||||
|
||||
exports.vendor = vendor;
|
||||
exports.textRequest = textRequest;
|
||||
exports.imageRequest = imageRequest;
|
||||
@ -524,4 +574,5 @@ exports.videoRequest = videoRequest;
|
||||
exports.ttsRequest = ttsRequest;
|
||||
exports.checkForUpdates = checkForUpdates;
|
||||
exports.updateVendor = updateVendor;
|
||||
export {};
|
||||
|
||||
export {};
|
||||
|
||||
14
nodemon.json
Normal file
14
nodemon.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
"data/*",
|
||||
"build/*",
|
||||
"dist/*",
|
||||
"router.ts",
|
||||
"database.d.ts"
|
||||
],
|
||||
"events": {
|
||||
"restart": ""
|
||||
},
|
||||
"delay": 0
|
||||
}
|
||||
21
src/app.ts
21
src/app.ts
@ -12,19 +12,6 @@ import fs from "fs";
|
||||
import u from "@/utils";
|
||||
import jwt from "jsonwebtoken";
|
||||
import socketInit from "@/socket/index";
|
||||
import path from "path";
|
||||
|
||||
declare const __APP_VERSION__: string;
|
||||
|
||||
const APP_VERSION: string = (() => {
|
||||
if (typeof __APP_VERSION__ !== "undefined") {
|
||||
return __APP_VERSION__;
|
||||
}
|
||||
// 开发环境回退:从 package.json 读取
|
||||
const pkgPath = path.resolve(process.cwd(), "package.json");
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
||||
return pkg.version;
|
||||
})();
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
@ -49,7 +36,7 @@ export default async function startServe(randomPort: Boolean = false) {
|
||||
fs.mkdirSync(ossDir, { recursive: true });
|
||||
}
|
||||
console.log("文件目录:", ossDir);
|
||||
app.use("/oss", express.static(ossDir));
|
||||
app.use("/oss", express.static(ossDir, { acceptRanges: false }));
|
||||
// skills 静态资源
|
||||
const skillsDir = u.getPath("skills");
|
||||
if (!fs.existsSync(skillsDir)) {
|
||||
@ -62,7 +49,7 @@ export default async function startServe(randomPort: Boolean = false) {
|
||||
(req, res, next) => {
|
||||
/\.(jpe?g|png|gif|webp|svg|ico|bmp)$/i.test(req.path) ? next() : res.status(403).end();
|
||||
},
|
||||
express.static(skillsDir),
|
||||
express.static(skillsDir, { acceptRanges: false }),
|
||||
);
|
||||
|
||||
// assets 静态资源
|
||||
@ -71,13 +58,13 @@ export default async function startServe(randomPort: Boolean = false) {
|
||||
fs.mkdirSync(assetsDir, { recursive: true });
|
||||
}
|
||||
console.log("文件目录:", assetsDir);
|
||||
app.use("/assets", express.static(assetsDir));
|
||||
app.use("/assets", express.static(assetsDir, { acceptRanges: false }));
|
||||
|
||||
// data/web 静态网站
|
||||
const webDir = u.getPath("web");
|
||||
if (fs.existsSync(webDir)) {
|
||||
console.log("静态网站目录:", webDir);
|
||||
app.use(express.static(webDir));
|
||||
app.use(express.static(webDir, { acceptRanges: false }));
|
||||
} else {
|
||||
console.warn("静态网站目录不存在:", webDir);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -16,8 +16,9 @@ export default router.post(
|
||||
if (!dataList || dataList.length === 0) {
|
||||
return res.status(404).send({ error: "模型未找到" });
|
||||
}
|
||||
const result = dataList.flatMap((data) => {
|
||||
const models = JSON.parse(data.models!);
|
||||
const modelList = await Promise.all(dataList.map(i=> u.vendor.getModelList(i.id!)));
|
||||
const result = dataList.flatMap((data, index) => {
|
||||
const models = modelList[index];
|
||||
const filtered =
|
||||
type === "all"
|
||||
? models.filter((item: { type: string }) => item.type !== "video")
|
||||
|
||||
@ -43,7 +43,7 @@ const vendorConfigSchema = z.object({
|
||||
mode: z.array(
|
||||
z.union([
|
||||
z.enum(["singleImage", "startEndRequired", "endFrameOptional", "startFrameOptional", "text", "audioReference", "videoReference"]),
|
||||
z.array(z.enum(["videoReference", "imageReference", "audioReference", "textReference"])),
|
||||
z.array(z.string().regex(/^(videoReference|imageReference|audioReference):\d+$/)),
|
||||
]),
|
||||
),
|
||||
audio: z.union([z.literal("optional"), z.boolean()]),
|
||||
@ -75,8 +75,20 @@ export default router.post(
|
||||
const vendor = exports.vendor;
|
||||
const result = vendorConfigSchema.safeParse(vendor);
|
||||
if (!result.success) {
|
||||
const errorMsg = result.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
|
||||
return res.status(400).send(error(`vendor配置校验失败: ${errorMsg}`));
|
||||
const issueLines = result.error.issues.map((issue, index) => {
|
||||
const path = issue.path.length ? issue.path.join(".") : "root";
|
||||
let detail = issue.message;
|
||||
|
||||
if (issue.code === "invalid_union") {
|
||||
const unionDetails = [...new Set(issue.errors.flat().map((e) => e.message).filter(Boolean))];
|
||||
if (unionDetails.length > 0) {
|
||||
detail = `${issue.message}(${unionDetails.join(";")})`;
|
||||
}
|
||||
}
|
||||
return `${index + 1}. ${path}: ${detail}`;
|
||||
});
|
||||
|
||||
return res.status(400).send(error(`vendor配置校验失败,共 ${issueLines.length} 处:\n${issueLines.join("\n")}`));
|
||||
}
|
||||
|
||||
if ((vendor.id as string).includes(":")) return res.status(400).send(error("id不能包含英文冒号"));
|
||||
|
||||
@ -6,11 +6,18 @@ const router = express.Router();
|
||||
export default router.post("/", async (req, res) => {
|
||||
const data = await u.db("o_vendorConfig").select("*");
|
||||
|
||||
const list = data.map((item) => ({
|
||||
...item,
|
||||
inputs: JSON.parse(item.inputs ?? "{}"),
|
||||
inputValues: JSON.parse(item.inputValues ?? "{}"),
|
||||
models: JSON.parse(item.models ?? "[]"),
|
||||
}));
|
||||
const list = await Promise.all(
|
||||
data.map(async (item) => ({
|
||||
...item,
|
||||
inputValues: JSON.parse(item.inputValues ?? "{}"),
|
||||
models: await u.vendor.getModelList(item.id!),
|
||||
code: u.vendor.getCode(item.id!),
|
||||
description: u.vendor.getVendor(item.id!).description,
|
||||
inputs: u.vendor.getVendor(item.id!).inputs,
|
||||
author: u.vendor.getVendor(item.id!).author,
|
||||
name: u.vendor.getVendor(item.id!).name,
|
||||
})),
|
||||
);
|
||||
|
||||
res.status(200).send(success(list));
|
||||
});
|
||||
|
||||
@ -37,7 +37,7 @@ export default router.post(
|
||||
if (!vendorConfigData) return res.status(500).send(error("未找到该供应商配置"));
|
||||
if (!vendorConfigData.models) return res.status(500).send(error("未找到模型列表"));
|
||||
|
||||
const modelList = JSON.parse(vendorConfigData.models);
|
||||
const modelList = await u.vendor.getModelList(vendorConfigData.id!);
|
||||
|
||||
const selectedModel = modelList.find((i: any) => i.modelName == modelName);
|
||||
if (type == "video") {
|
||||
@ -46,7 +46,8 @@ export default router.post(
|
||||
duration: selectedModel.durationResolutionMap[0].duration[0],
|
||||
resolution: selectedModel.durationResolutionMap[0].resolution[0],
|
||||
aspectRatio: "16:9",
|
||||
prompt: "生成一个卖火柴的小女孩,保持镜头稳定,从远景到近景",
|
||||
prompt:
|
||||
"A shirtless middle-aged man with a horse head is standing in a supermarket, carefully comparing two identical bottles of shampoo for 3 seconds, then suddenly bursts into tears, drops to his knees dramatically, a flock of pigeons explodes out of nowhere from behind him, the supermarket lights flicker, an old grandma nearby continues shopping completely unbothered, the horse head man instantly stops crying, puts both shampoo bottles back, and moonwalks away disappearing into the vegetable section. Security camera footage style, slightly grainy, 5 seconds.",
|
||||
imageBase64: [],
|
||||
audio: false,
|
||||
mode: "text",
|
||||
|
||||
@ -44,7 +44,7 @@ const vendorConfigSchema = z.object({
|
||||
mode: z.array(
|
||||
z.union([
|
||||
z.enum(["singleImage", "startEndRequired", "endFrameOptional", "startFrameOptional", "text", "audioReference", "videoReference"]),
|
||||
z.array(z.enum(["audioReference", "videoReference", "textReference", "imageReference"])),
|
||||
z.array(z.string().regex(/^(videoReference|imageReference|audioReference):\d+$/)),
|
||||
]),
|
||||
),
|
||||
audio: z.union([z.literal("optional"), z.boolean()]),
|
||||
@ -92,9 +92,10 @@ export default router.post(
|
||||
inputs: JSON.stringify(vendor.inputs ?? []),
|
||||
inputValues: JSON.stringify(vendor.inputValues ?? {}),
|
||||
models: JSON.stringify(vendor.models ?? []),
|
||||
code: tsCode,
|
||||
createTime: Date.now(),
|
||||
});
|
||||
u.vendor.upCode(id, tsCode);
|
||||
|
||||
res.status(200).send(success(result.data));
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
|
||||
@ -41,7 +41,7 @@ export default router.post(
|
||||
mode: z.array(
|
||||
z.union([
|
||||
z.enum(["singleImage", "startEndRequired", "endFrameOptional", "startFrameOptional", "text"]),
|
||||
z.array(z.enum(["audioReference", "videoReference", "textReference", "imageReference"])),
|
||||
z.array(z.string().regex(/^(videoReference|imageReference|audioReference):\d+$/)),
|
||||
]),
|
||||
),
|
||||
audio: z.union([z.literal("optional"), z.boolean()]),
|
||||
|
||||
37
src/types/database.d.ts
vendored
37
src/types/database.d.ts
vendored
@ -1,37 +1,6 @@
|
||||
// @db-hash 6cd709d9bdfe00c4dc87961a8ebba149
|
||||
// @db-hash 32fc2b4cbb0daffa7f8df1dabb511518
|
||||
//该文件由脚本自动生成,请勿手动修改
|
||||
|
||||
export interface _o_project_old_20260404 {
|
||||
'artStyle'?: string | null;
|
||||
'createTime'?: number | null;
|
||||
'directorManual'?: string | null;
|
||||
'id'?: number | null;
|
||||
'imageModel'?: string | null;
|
||||
'imageQuality'?: string | null;
|
||||
'intro'?: string | null;
|
||||
'mode'?: string | null;
|
||||
'name'?: string | null;
|
||||
'projectType'?: string | null;
|
||||
'type'?: string | null;
|
||||
'userId'?: number | null;
|
||||
'videoModel'?: string | null;
|
||||
'videoRatio'?: string | null;
|
||||
}
|
||||
export interface _o_prompt_old_20260406 {
|
||||
'data'?: string | null;
|
||||
'id'?: number;
|
||||
'name'?: string | null;
|
||||
'type'?: string | null;
|
||||
'useData'?: string | null;
|
||||
}
|
||||
export interface _o_prompt_old_20260406_1 {
|
||||
'data'?: string | null;
|
||||
'id'?: number;
|
||||
'name'?: string | null;
|
||||
'TEXT'?: any | null;
|
||||
'type'?: string | null;
|
||||
'useData'?: string | null;
|
||||
}
|
||||
export interface memories {
|
||||
'content': string;
|
||||
'createTime': number;
|
||||
@ -229,7 +198,6 @@ export interface o_user {
|
||||
}
|
||||
export interface o_vendorConfig {
|
||||
'author'?: string | null;
|
||||
'code'?: string | null;
|
||||
'createTime'?: number | null;
|
||||
'description'?: string | null;
|
||||
'enable'?: number | null;
|
||||
@ -263,9 +231,6 @@ export interface o_videoTrack {
|
||||
}
|
||||
|
||||
export interface DB {
|
||||
"_o_project_old_20260404": _o_project_old_20260404;
|
||||
"_o_prompt_old_20260406": _o_prompt_old_20260406;
|
||||
"_o_prompt_old_20260406_1": _o_prompt_old_20260406_1;
|
||||
"memories": memories;
|
||||
"o_agentDeploy": o_agentDeploy;
|
||||
"o_agentWorkData": o_agentWorkData;
|
||||
|
||||
@ -12,6 +12,7 @@ import { getPrompts } from "@/utils/getPrompts";
|
||||
import { getArtPrompt } from "@/utils/getArtPrompt";
|
||||
import replaceUrl from "@/utils/replaceUrl";
|
||||
import writeVersion from "@/utils/writeVersion";
|
||||
import * as vendor from "@/utils/vendor";
|
||||
|
||||
export default {
|
||||
db,
|
||||
@ -28,4 +29,5 @@ export default {
|
||||
getArtPrompt,
|
||||
replaceUrl,
|
||||
writeVersion,
|
||||
vendor,
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { generateText, streamText, wrapLanguageModel, stepCountIs } from "ai";
|
||||
import { generateText, streamText, wrapLanguageModel, stepCountIs, extractReasoningMiddleware } from "ai";
|
||||
import { devToolsMiddleware } from "@ai-sdk/devtools";
|
||||
import axios from "axios";
|
||||
import { transform } from "sucrase";
|
||||
@ -17,14 +17,20 @@ async function resolveModelName(value: AiType | `${string}:${string}`): Promise<
|
||||
return value as `${number}:${string}`;
|
||||
}
|
||||
|
||||
async function getVendorTemplateFn(fnName: FnName, modelName: `${string}:${string}`) {
|
||||
async function getVendorTemplateFn(
|
||||
fnName: "textRequest",
|
||||
modelName: `${string}:${string}`,
|
||||
): Promise<(think?: boolean, thinkLevel?: 0 | 1 | 2 | 3) => any>;
|
||||
async function getVendorTemplateFn(fnName: Exclude<FnName, "textRequest">, modelName: `${string}:${string}`): Promise<(input: any) => any>;
|
||||
async function getVendorTemplateFn(fnName: FnName, modelName: `${string}:${string}`): Promise<any> {
|
||||
const [id, name] = modelName.split(":");
|
||||
const vendorConfigData = await u.db("o_vendorConfig").where("id", id).first();
|
||||
if (!vendorConfigData) throw new Error(`未找到供应商配置 id=${id}`);
|
||||
const modelList = JSON.parse(vendorConfigData.models ?? "[]");
|
||||
const modelList = await u.vendor.getModelList(id);
|
||||
const selectedModel = modelList.find((i: any) => i.modelName == name);
|
||||
if (!selectedModel) throw new Error(`未找到模型 ${name} id=${id}`);
|
||||
const jsCode = transform(vendorConfigData.code!, { transforms: ["typescript"] }).code;
|
||||
const code = u.vendor.getCode(id);
|
||||
const jsCode = transform(code, { transforms: ["typescript"] }).code;
|
||||
const running = u.vm(jsCode);
|
||||
if (running.vendor) {
|
||||
Object.assign(running.vendor.inputValues, JSON.parse(vendorConfigData.inputValues ?? "{}"));
|
||||
@ -32,7 +38,11 @@ async function getVendorTemplateFn(fnName: FnName, modelName: `${string}:${strin
|
||||
}
|
||||
const fn = running[fnName];
|
||||
if (!fn) throw new Error(`未找到供应商配置中的函数 ${fnName} id=${id}`);
|
||||
if (fnName == "textRequest") return fn(selectedModel);
|
||||
if (fnName == "textRequest")
|
||||
return (think?: boolean, thinkLevel: 0 | 1 | 2 | 3 = 0) => {
|
||||
const effectiveThink = think ?? !!selectedModel.think;
|
||||
return fn(selectedModel, effectiveThink, thinkLevel);
|
||||
};
|
||||
else return <T>(input: T) => fn(input, selectedModel);
|
||||
}
|
||||
|
||||
@ -42,13 +52,13 @@ async function withTaskRecord<T>(
|
||||
describe: string,
|
||||
relatedObjects: string,
|
||||
projectId: number,
|
||||
fn: (modelName: `${string}:${string}`) => Promise<T>,
|
||||
fn: (modelName: `${string}:${string}`, think: Boolean, thinkLevel: 0 | 1 | 2 | 3) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const modelName = await resolveModelName(modelKey);
|
||||
const [id, model] = modelName.split(":");
|
||||
const taskRecord = await u.task(projectId, taskClass, model, { describe: describe, content: relatedObjects });
|
||||
try {
|
||||
const result = await fn(modelName);
|
||||
const result = await fn(modelName, false, 0);
|
||||
taskRecord(1);
|
||||
return result;
|
||||
} catch (e) {
|
||||
@ -72,46 +82,67 @@ async function urlToBase64(url: string, retries = 3, delay = 1000): Promise<stri
|
||||
}
|
||||
class AiText {
|
||||
private AiType: AiType | `${string}:${string}`;
|
||||
constructor(AiType: AiType | `${string}:${string}`) {
|
||||
private think?: boolean;
|
||||
private thinkLevel: 0 | 1 | 2 | 3;
|
||||
constructor(AiType: AiType | `${string}:${string}`, think?: boolean, thinkLevel: 0 | 1 | 2 | 3 = 0) {
|
||||
this.AiType = AiType;
|
||||
this.think = think;
|
||||
this.thinkLevel = thinkLevel;
|
||||
}
|
||||
async invoke(input: Omit<Parameters<typeof generateText>[0], "model">) {
|
||||
const switchAiDevTool = await u.db("o_setting").where("key", "switchAiDevTool").first();
|
||||
const modelName = await resolveModelName(this.AiType);
|
||||
const sdkFn = await getVendorTemplateFn("textRequest", modelName);
|
||||
return generateText({
|
||||
...(input.tools && { stopWhen: stepCountIs(Object.keys(input.tools).length * 50) }),
|
||||
...input,
|
||||
model:
|
||||
switchAiDevTool?.value === "1"
|
||||
? wrapLanguageModel({
|
||||
model: await getVendorTemplateFn("textRequest", modelName),
|
||||
model: await sdkFn(this.think, this.thinkLevel),
|
||||
middleware: devToolsMiddleware(),
|
||||
})
|
||||
: await getVendorTemplateFn("textRequest", modelName),
|
||||
: await sdkFn(this.think, this.thinkLevel),
|
||||
} as Parameters<typeof generateText>[0]);
|
||||
}
|
||||
async stream(input: Omit<Parameters<typeof streamText>[0], "model">) {
|
||||
const switchAiDevTool = await u.db("o_setting").where("key", "switchAiDevTool").first();
|
||||
const modelName = await resolveModelName(this.AiType);
|
||||
const sdkFn = await getVendorTemplateFn("textRequest", modelName);
|
||||
return streamText({
|
||||
...(input.tools && { stopWhen: stepCountIs(Object.keys(input.tools).length * 50) }),
|
||||
...input,
|
||||
model:
|
||||
switchAiDevTool?.value == "1"
|
||||
? wrapLanguageModel({
|
||||
model: await getVendorTemplateFn("textRequest", modelName),
|
||||
middleware: devToolsMiddleware(),
|
||||
model: sdkFn(this.think, this.thinkLevel),
|
||||
middleware: [
|
||||
devToolsMiddleware(),
|
||||
extractReasoningMiddleware({
|
||||
tagName: "reasoning_content",
|
||||
}),
|
||||
],
|
||||
})
|
||||
: await getVendorTemplateFn("textRequest", modelName),
|
||||
: wrapLanguageModel({
|
||||
model: sdkFn(this.think, this.thinkLevel),
|
||||
middleware: extractReasoningMiddleware({
|
||||
tagName: "reasoning_content",
|
||||
}),
|
||||
}),
|
||||
} as Parameters<typeof streamText>[0]);
|
||||
}
|
||||
}
|
||||
|
||||
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; //图片提示词
|
||||
imageBase64: string[]; //输入的图片提示词
|
||||
size: "1K" | "2K" | "4K"; // 图片尺寸
|
||||
aspectRatio: `${number}:${number}`; // 长宽比
|
||||
prompt: string;
|
||||
referenceList?: Extract<ReferenceList, { type: "image" }>[];
|
||||
size: "1K" | "2K" | "4K";
|
||||
aspectRatio: `${number}:${number}`;
|
||||
}
|
||||
|
||||
interface TaskRecord {
|
||||
@ -145,14 +176,23 @@ class AiImage {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
type VideoMode =
|
||||
| "singleImage" //单图参考
|
||||
| "startEndRequired" //首尾帧(两张都得有)
|
||||
| "endFrameOptional" //首尾帧(尾帧可选)
|
||||
| "startFrameOptional" //首尾帧(首帧可选)
|
||||
| "text" //文本
|
||||
| (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[]; //多参考(数字代表限制数量)
|
||||
|
||||
interface VideoConfig {
|
||||
prompt: string; //视频提示词
|
||||
imageBase64: string[]; //输入的图片提示词
|
||||
aspectRatio: `${number}:${number}`; // 长宽比
|
||||
mode: string; //模式
|
||||
duration: number; // 视频时长,单位秒
|
||||
resolution: string; // 视频分辨率
|
||||
audio: boolean; // 是否需要配音
|
||||
duration: number;
|
||||
resolution: string;
|
||||
aspectRatio: "16:9" | "9:16";
|
||||
prompt: string;
|
||||
referenceList?: ReferenceList[];
|
||||
audio?: boolean;
|
||||
mode: VideoMode[];
|
||||
}
|
||||
|
||||
class AiVideo {
|
||||
@ -205,7 +245,7 @@ class AiAudio {
|
||||
}
|
||||
|
||||
export default {
|
||||
Text: (AiType: AiType | `${string}:${string}`) => new AiText(AiType),
|
||||
Text: (AiType: AiType | `${string}:${string}`, think?: boolean, thinkLevel?: 0 | 1 | 2 | 3) => new AiText(AiType, think, thinkLevel),
|
||||
Image: (key: `${string}:${string}`) => new AiImage(key),
|
||||
Video: (key: `${string}:${string}`) => new AiVideo(key),
|
||||
Audio: (key: `${string}:${string}`) => new AiAudio(key),
|
||||
|
||||
42
src/utils/vendor.ts
Normal file
42
src/utils/vendor.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { transform } from "sucrase";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import u from "@/utils";
|
||||
|
||||
export function upCode(id: string, tsCode: string) {
|
||||
const rootDir = u.getPath();
|
||||
const vendor = u.vendor.getVendor(id);
|
||||
if (!vendor) throw new Error("供应商不存在");
|
||||
if (fs.existsSync(path.join(rootDir, "vendor", `${id}.ts`))) {
|
||||
fs.writeFileSync(path.join(rootDir, "vendor", `${id}.ts`), tsCode);
|
||||
}
|
||||
fs.writeFileSync(path.join(rootDir, "vendor", `${id}.ts`), tsCode);
|
||||
}
|
||||
|
||||
export function getCode(id: string): string {
|
||||
const rootDir = u.getPath();
|
||||
const targetFile = path.join(rootDir, "vendor", `${id}.ts`);
|
||||
if (!fs.existsSync(targetFile)) return "";
|
||||
return fs.readFileSync(targetFile, "utf-8");
|
||||
}
|
||||
|
||||
export async function getModelList(id: string): Promise<Array<any>> {
|
||||
const models = await u.db("o_vendorConfig").where("id", id).select("models").first();
|
||||
if (!models || !models.models) return [];
|
||||
const code = getCode(id);
|
||||
const jsCode = transform(code, { transforms: ["typescript"] }).code;
|
||||
const vendorData = u.vm(jsCode);
|
||||
const combined = [...vendorData.vendor.models, ...JSON.parse(models?.models ?? "[]")];
|
||||
const map = new Map<string, any>();
|
||||
for (const m of combined) {
|
||||
map.set(m.modelName, m);
|
||||
}
|
||||
return [...map.values()];
|
||||
}
|
||||
|
||||
export function getVendor(id: string) {
|
||||
const code = getCode(id);
|
||||
const jsCode = transform(code, { transforms: ["typescript"] }).code;
|
||||
const vendorData = u.vm(jsCode);
|
||||
return vendorData.vendor;
|
||||
}
|
||||
@ -32,7 +32,7 @@ export default function runCode(code: string, vendor?: Record<string, any>) {
|
||||
urlToBase64,
|
||||
mergeImages,
|
||||
pollTask,
|
||||
fetch,
|
||||
fetch: fetch,
|
||||
exports,
|
||||
axios,
|
||||
FormData,
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"ignoreDeprecations": "6.0",
|
||||
"ignoreDeprecations": "5.0",
|
||||
"target": "ESNext",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user