新增厂商2.0,染上深度思考模块,修正厂商模块

This commit is contained in:
ACT丶流星雨 2026-04-10 21:11:50 +08:00
parent b891abbe93
commit 02883472e7
25 changed files with 1452 additions and 711 deletions

View File

@ -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
View 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 {};

View File

@ -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;

View File

@ -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}`;
};

37
data/vendor/null.ts vendored
View File

@ -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.
*/

View File

@ -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 },

View File

@ -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({
// ============================================================
// 适配器函数
// ============================================================
const textRequest = (model: TextModel) => {
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
return createOpenAI({ baseURL: vendor.inputValues.baseUrl, apiKey }).chat(model.modelName);
};
const imageRequest = async (config: ImageConfig, model: ImageModel): Promise<string> => {
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: imageConfig.imageBase64.map((i) => ({
type: "image_url",
image_url: {
url: i,
},
})),
content: imageBase64List.map((b) => ({ type: "image_url", image_url: { url: b } })),
});
}
const imageConfigGoogle = {
aspect_ratio: imageConfig.aspectRatio,
};
// if(imageModel.ModelName == 'gemini-3-pro-image-preview-vt'){
imageConfigGoogle.image_size = imageConfig.size;
// }
messages.push({ role: "user", content: config.prompt + "请直接输出图片" });
const body = {
model: imageModel.modelName,
messages: [{ role: "user", content: imageConfig.prompt + `请直接输出图片` }, ...images],
extra_body: {
google: {
image_config: {
...imageConfigGoogle,
},
},
},
model: model.modelName,
messages,
extra_body: { google: { image_config: imageConfigGoogle } },
};
return {
body,
url: `${vendor.inputValues.baseUrl}/chat/completions`,
processFn: (data: any) => {
return extractFirstImageFromMd(data.choices[0].message.content).url;
},
};
}
function commonAdaptor(imageConfig: ImageConfig, imageModel: ImageModel) {
const defaultImageFn = [
["doubao", doubaoAdaptor],
["nano", geminiImageAdaptor],
["gemini", geminiImageAdaptor],
["seedream", doubaoAdaptor],
];
const modelName = imageModel.modelName;
const lowerName = modelName.toLowerCase();
const match = defaultImageFn.find(([key]) => lowerName.includes(key));
return match ? match[1](imageConfig, imageModel) : {};
}
const imageRequest = async (imageConfig: ImageConfig, imageModel: ImageModel) => {
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
const apiKey = vendor.inputValues.apiKey.replace("Bearer ", "");
const adaptor = commonAdaptor(imageConfig, imageModel);
const requestUrl = adaptor?.url ? `${vendor.inputValues.baseUrl}/chat/completions` : vendor.inputValues.baseUrl + "/images/generations";
const response = await fetch(requestUrl, {
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(adaptor.body),
body: JSON.stringify(body),
});
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();
return adaptor.processFn(data);
};
exports.imageRequest = imageRequest;
interface VideoConfig {
duration: number; //视频时长,单位秒
resolution: string; //视频分辨率,如"720p"、"1080p"
aspectRatio: "16:9" | "9:16"; //视频长宽比
prompt: string; //视频提示词
fileBase64?: string[]; // 文件base64 包含图片base64、视频base64、音频base64
audio?: boolean;
mode:
| "singleImage" // 单图
| "multiImage" // 多图模式
| "gridImage" // 网格单图(传入一张图片,但该图片是网格图)
| "startEndRequired" // 首尾帧(两张都得有)
| "endFrameOptional" // 首尾帧(尾帧可选)
| "startFrameOptional" // 首尾帧(首帧可选)
| "text" // 文本生视频
| ("videoReference" | "imageReference" | "audioReference" | "textReference")[]; // 混合参考
}
// 豆包视频
const buildDoubaoMetadata = (videoConfig: VideoConfig) => {
const metaData = {
...(typeof videoConfig.audio == "boolean" && { generate_audio: videoConfig.audio ?? false }),
ratio: videoConfig.aspectRatio,
image_roles: [],
references: [],
};
if (videoConfig.imageBase64 && videoConfig.imageBase64.length) {
videoConfig.imageBase64.forEach((i, index) => {
if (Array.isArray(videoConfig.mode)) {
metaData.references.push(i);
} else {
if (videoConfig.mode == "startEndRequired" || videoConfig.mode == "endFrameOptional" || videoConfig.mode == "startFrameOptional") {
(metaData.image_roles as string[]).push(index == 0 ? "first_frame" : "last_frame");
}
if (videoConfig.mode == "singleImage") {
(metaData.image_roles as string[]).push("reference_image");
}
}
});
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);
}
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")) {
// 豆包 / 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>> = {
"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",
},
"16:9": { "2K": "2848x1600", "4K": "4096x2304" },
"9:16": { "2K": "1600x2848", "4K": "2304x4096" },
};
const size = sizeMap[videoConfig.resolution]?.[videoConfig.aspectRatio];
publicBody.size = size;
}
const requestUrl = vendor.inputValues.baseUrl + "/video/generations";
const queryUrl = vendor.inputValues.baseUrl + "/video/generations/{id}";
const response = await fetch(requestUrl, {
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(publicBody),
body: JSON.stringify(body),
});
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 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(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;
return await urlToBase64(res.data!);
}
const ttsRequest = async (ttsConfig: TTSConfig, ttsModel: TTSModel) => {
return null;
if (lowerName.includes("doubao") || lowerName.includes("seedance")) {
// 豆包/Seedance 系列
metadata = {
...(typeof config.audio === "boolean" && { generate_audio: config.audio }),
ratio: config.aspectRatio,
image_roles: [] as string[],
references: [] as string[],
};
if (Array.isArray(activeMode)) {
// 多参考模式
imageRefs.forEach((b) => metadata.references.push(b));
videoRefs.forEach((b) => metadata.references.push(b));
audioRefs.forEach((b) => metadata.references.push(b));
} else if (activeMode === "startEndRequired" || activeMode === "endFrameOptional" || activeMode === "startFrameOptional") {
imageRefs.forEach((_, i) => (metadata.image_roles as string[]).push(i === 0 ? "first_frame" : "last_frame"));
} else if (activeMode === "singleImage") {
imageRefs.forEach(() => (metadata.image_roles as string[]).push("reference_image"));
}
} else if (lowerName.includes("vidu")) {
// Vidu 系列
metadata = {
aspect_ratio: config.aspectRatio,
audio: config.audio ?? false,
off_peak: false,
};
} else if (lowerName.includes("kling")) {
// 可灵系列
metadata = { aspect_ratio: config.aspectRatio };
if (Array.isArray(activeMode)) {
metadata.reference = [...imageRefs, ...videoRefs, ...audioRefs];
} else if (activeMode === "endFrameOptional" && imageRefs.length) {
metadata.image_tail = imageRefs[0];
} else if (activeMode === "startEndRequired" && imageRefs.length >= 2) {
metadata.image_list = [
{ image_url: imageRefs[0], type: "first_frame" },
{ image_url: imageRefs[1], type: "last_frame" },
];
} else if (activeMode === "singleImage" && imageRefs.length) {
metadata.image = imageRefs[0];
}
}
// 公共请求体(非万象通用路径)
const publicBody: Record<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();
throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`);
}
const data = await response.json();
const taskId = data.id;
logger(`[videoRequest] 任务ID: ${taskId}`);
const res = await pollTask(async () => {
const queryResponse = await fetch(`${baseUrl}/video/generations/${taskId}`, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
});
if (!queryResponse.ok) {
const errorText = await queryResponse.text();
throw new Error(`轮询失败,状态码: ${queryResponse.status}, 错误信息: ${errorText}`);
}
const queryData = await queryResponse.json();
const status = queryData?.status ?? queryData?.data?.status;
switch (status) {
case "completed":
case "SUCCESS":
case "success":
return { completed: true, data: queryData.data.result_url };
case "FAILURE":
case "failed":
return { completed: true, error: queryData?.data?.fail_reason ?? "视频生成失败" };
default:
return { completed: false };
}
});
if (res.error) throw new Error(res.error);
return await urlToBase64(res.data!);
};
const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<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
View 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");
};

View File

@ -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) {
// 多模态参考模式:按类型分别提取并添加
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: `data:image/png;base64,${base64}` },
image_url: { url: ref.base64 },
role: "reference_image",
});
}
imageIndex += images.length;
} 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,8 +517,8 @@ const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<str
logger(`[视频生成] 任务已创建, ID: ${taskId}`);
// 轮询查询任务状态
const result = await pollTask(async (): Promise<PollResult> => {
const result = await pollTask(
async (): Promise<PollResult> => {
const queryResponse = await axios.get(`${baseUrl}/contents/generations/tasks/${taskId}`, { headers });
const task = queryResponse.data;
@ -489,19 +537,20 @@ const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<str
case "cancelled":
return { completed: true, error: "视频生成任务已取消" };
default:
// queued / running
return { completed: false };
}
}, 10000, 600000); // 每10秒查询一次最长等待10分钟
},
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 {};

14
nodemon.json Normal file
View File

@ -0,0 +1,14 @@
{
"ignore": [
"node_modules",
"data/*",
"build/*",
"dist/*",
"router.ts",
"database.d.ts"
],
"events": {
"restart": ""
},
"delay": 0
}

View File

@ -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

View File

@ -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")

View File

@ -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不能包含英文冒号"));

View File

@ -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) => ({
const list = await Promise.all(
data.map(async (item) => ({
...item,
inputs: JSON.parse(item.inputs ?? "{}"),
inputValues: JSON.parse(item.inputValues ?? "{}"),
models: JSON.parse(item.models ?? "[]"),
}));
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));
});

View File

@ -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",

View File

@ -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);

View File

@ -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()]),

View File

@ -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;

View File

@ -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,
};

View File

@ -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
View 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;
}

View File

@ -32,7 +32,7 @@ export default function runCode(code: string, vendor?: Record<string, any>) {
urlToBase64,
mergeImages,
pollTask,
fetch,
fetch: fetch,
exports,
axios,
FormData,

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"ignoreDeprecations": "6.0",
"ignoreDeprecations": "5.0",
"target": "ESNext",
"module": "CommonJS",
"moduleResolution": "Node",