Merge branch 'develop' into develop

This commit is contained in:
ACT丶流星雨 2026-04-12 17:41:07 +08:00 committed by GitHub
commit 9de2b94c29
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
62 changed files with 6034 additions and 2080 deletions

View File

@ -70,6 +70,12 @@ Repository: https://github.com/DefinitelyTyped/DefinitelyTyped
-----------------------------
Name: @types/graphlib
License: MIT
Repository: https://github.com/DefinitelyTyped/DefinitelyTyped
-----------------------------
Name: @types/jsonwebtoken
License: MIT
Repository: https://github.com/DefinitelyTyped/DefinitelyTyped
@ -142,18 +148,18 @@ Repository: https://github.com/motdotla/dotenv
-----------------------------
Name: dotenv
License: BSD-2-Clause
Repository: https://github.com/motdotla/dotenv
-----------------------------
Name: electron-builder
License: MIT
Repository: https://github.com/electron-userland/electron-builder
-----------------------------
Name: electron-rebuild
License: MIT
Repository: https://github.com/electron/electron-rebuild
-----------------------------
Name: electron
License: MIT
Repository: https://github.com/electron/electron
@ -244,15 +250,15 @@ Repository: https://github.com/remy/nodemon
-----------------------------
Name: qwen-ai-provider-v5
License: Apache-2.0
Repository: https://github.com/bolechen/qwen-ai-provider-v5
Name: p-limit
License: MIT
Repository: https://github.com/sindresorhus/p-limit
-----------------------------
Name: serialize-error
License: MIT
Repository: https://github.com/sindresorhus/serialize-error
Name: qwen-ai-provider-v5
License: Apache-2.0
Repository: https://github.com/bolechen/qwen-ai-provider-v5
-----------------------------
@ -304,6 +310,12 @@ Repository: https://github.com/uuidjs/uuid
-----------------------------
Name: vercel-minimax-ai-provider
License: Apache-2.0
Repository: https://github.com/MiniMax-AI/vercel-minimax-ai-provider
-----------------------------
Name: vm2
License: MIT
Repository: https://github.com/patriksimek/vm2

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, t: boolean, tl: 0 | 1 | 2 | 3) => 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: "2.0",
author: "Toonflow",
name: "Grsai",
description: "Grsai AI平台适配支持文生图、图生图、文生视频、Gemini兼容文本模型 \n [前往中转平台](https://tf.grsai.ai/zh)",
inputs: [
{ key: "apiKey", label: "API密钥", type: "password", required: true },
{ key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "示例https://grsai.dakka.com.cn" },
],
inputValues: { apiKey: "", baseUrl: "https://grsai.dakka.com.cn" },
models: [
{ name: "Nano Banana Fast", modelName: "nano-banana-fast", type: "image", mode: ["text", "singleImage", "multiReference"] },
{ name: "Nano Banana 2", modelName: "nano-banana-2", type: "image", mode: ["text", "singleImage", "multiReference"] },
{ name: "Nano Banana Pro", modelName: "nano-banana-pro", type: "image", mode: ["text", "singleImage", "multiReference"] },
],
};
// ============================================================
// 辅助工具
// ============================================================
const getHeaders = () => {
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
return {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
};
};
// ============================================================
// 适配器函数
// ============================================================
const textRequest = (model: TextModel, think: boolean, thinkLevel: 0 | 1 | 2 | 3) => {
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
return createGoogleGenerativeAI({
baseURL: `${vendor.inputValues.baseUrl}/v1beta`,
apiKey,
}).chat(model.modelName);
};
const imageRequest = async (config: ImageConfig, model: ImageModel): Promise<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 {};

638
data/vendor/klingai.ts vendored Normal file
View File

@ -0,0 +1,638 @@
/**
* Toonflow AI供应商模板 - AI
* @version 2.0
*/
// ============================================================
// 类型定义
// ============================================================
type VideoMode =
| "singleImage"
| "startEndRequired"
| "endFrameOptional"
| "startFrameOptional"
| "text"
| (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[];
interface TextModel {
name: string;
modelName: string;
type: "text";
think: boolean;
}
interface ImageModel {
name: string;
modelName: string;
type: "image";
mode: ("text" | "singleImage" | "multiReference")[];
associationSkills?: string;
}
interface VideoModel {
name: string;
modelName: string;
type: "video";
mode: VideoMode[];
associationSkills?: string;
audio: "optional" | false | true;
durationResolutionMap: { duration: number[]; resolution: string[] }[];
}
interface TTSModel {
name: string;
modelName: string;
type: "tts";
voices: { title: string; voice: string }[];
}
interface VendorConfig {
id: string;
version: string;
name: string;
author: string;
description?: string;
icon?: string;
inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[];
inputValues: Record<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;
declare const logger: (msg: string) => void;
declare const jsonwebtoken: any;
declare const zipImage: (base64: string, size: number) => Promise<string>;
declare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>;
declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>;
declare const urlToBase64: (url: string) => Promise<string>;
declare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>;
declare const createOpenAI: any;
declare const createDeepSeek: any;
declare const createZhipu: any;
declare const createQwen: any;
declare const createAnthropic: any;
declare const createOpenAICompatible: any;
declare const createXai: any;
declare const createMinimax: any;
declare const createGoogleGenerativeAI: any;
declare const exports: {
vendor: VendorConfig;
textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => 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: "klingai",
version: "2.0",
author: "Toonflow",
name: "可灵AI",
description:
"可灵AI视频生成\n\n支持可灵全系列视频模型包括 kling-video-o1、kling-v3-omni、kling-v3、kling-v2-6、kling-v2-5-turbo、kling-v2-1、kling-v2-master、kling-v1-6、kling-v1-5、kling-v1 等。\n\n需要在[可灵AI开放平台](https://klingai.com)\n\n获取 Access Key 和 Secret Key。",
inputs: [
{ key: "accessKey", label: "Access Key", type: "password", required: true, placeholder: "请输入可灵AI的Access Key" },
{ key: "secretKey", label: "Secret Key", type: "password", required: true, placeholder: "请输入可灵AI的Secret Key" },
{ key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "默认https://api-beijing.klingai.com" },
],
inputValues: { accessKey: "", secretKey: "", baseUrl: "https://api-beijing.klingai.com" },
models: [
// kling-video-o1 (Omni)
{
name: "kling-video-o1 标准",
modelName: "kling-video-o1:std",
type: "video",
mode: ["text", "singleImage", "startEndRequired", ["imageReference:7", "videoReference:1"]],
audio: false,
durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }],
},
{
name: "kling-video-o1 专家",
modelName: "kling-video-o1:pro",
type: "video",
mode: ["text", "singleImage", "startEndRequired", ["imageReference:7", "videoReference:1"]],
audio: false,
durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }],
},
// kling-v3-omni (Omni)
{
name: "kling-v3-omni 标准",
modelName: "kling-v3-omni:std",
type: "video",
mode: ["text", "singleImage", "startEndRequired", ["imageReference:7", "videoReference:1"]],
audio: false,
durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["720p"] }],
},
{
name: "kling-v3-omni 专家",
modelName: "kling-v3-omni:pro",
type: "video",
mode: ["text", "singleImage", "startEndRequired", ["imageReference:7", "videoReference:1"]],
audio: false,
durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["720p"] }],
},
// kling-v3
{
name: "kling-v3 标准",
modelName: "kling-v3:std",
type: "video",
mode: ["text", "singleImage", "startEndRequired"],
audio: false,
durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["720p"] }],
},
{
name: "kling-v3 专家",
modelName: "kling-v3:pro",
type: "video",
mode: ["text", "singleImage", "startEndRequired"],
audio: false,
durationResolutionMap: [{ duration: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["720p"] }],
},
// kling-v2-6
{
name: "kling-v2-6 标准",
modelName: "kling-v2-6:std",
type: "video",
mode: ["text", "singleImage"],
audio: false,
durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }],
},
{
name: "kling-v2-6 专家",
modelName: "kling-v2-6:pro",
type: "video",
mode: ["text", "singleImage", "startEndRequired"],
audio: "optional",
durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }],
},
// kling-v2-5-turbo
{
name: "kling-v2-5-turbo 标准",
modelName: "kling-v2-5-turbo:std",
type: "video",
mode: ["text", "singleImage"],
audio: false,
durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }],
},
{
name: "kling-v2-5-turbo 专家",
modelName: "kling-v2-5-turbo:pro",
type: "video",
mode: ["text", "singleImage", "startEndRequired"],
audio: false,
durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }],
},
// kling-v2-1
{
name: "kling-v2-1 标准",
modelName: "kling-v2-1:std",
type: "video",
mode: ["singleImage"],
audio: false,
durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }],
},
{
name: "kling-v2-1 专家",
modelName: "kling-v2-1:pro",
type: "video",
mode: ["singleImage", "startEndRequired"],
audio: false,
durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }],
},
// kling-v2-1-master
{
name: "kling-v2-1 Master",
modelName: "kling-v2-1-master:pro",
type: "video",
mode: ["text", "singleImage"],
audio: false,
durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }],
},
// kling-v2-master
{
name: "kling-v2 Master",
modelName: "kling-v2-master:pro",
type: "video",
mode: ["text", "singleImage"],
audio: false,
durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }],
},
// kling-v1-6
{
name: "kling-v1-6 标准",
modelName: "kling-v1-6:std",
type: "video",
mode: ["text", "singleImage", ["imageReference:4"]],
audio: false,
durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }],
},
{
name: "kling-v1-6 专家",
modelName: "kling-v1-6:pro",
type: "video",
mode: ["text", "singleImage", "endFrameOptional", ["imageReference:4"]],
audio: false,
durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }],
},
// kling-v1-5
{
name: "kling-v1-5 标准",
modelName: "kling-v1-5:std",
type: "video",
mode: ["singleImage"],
audio: false,
durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }],
},
{
name: "kling-v1-5 专家",
modelName: "kling-v1-5:pro",
type: "video",
mode: ["singleImage", "endFrameOptional"],
audio: false,
durationResolutionMap: [{ duration: [5, 10], resolution: ["1080p"] }],
},
// kling-v1
{
name: "kling-v1 标准",
modelName: "kling-v1:std",
type: "video",
mode: ["text", "singleImage", "startEndRequired"],
audio: false,
durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }],
},
{
name: "kling-v1 专家",
modelName: "kling-v1:pro",
type: "video",
mode: ["text", "singleImage", "startEndRequired"],
audio: false,
durationResolutionMap: [{ duration: [5, 10], resolution: ["720p"] }],
},
],
};
// ============================================================
// 辅助工具
// ============================================================
/**
* AI的JWT鉴权Token
*/
const generateAuthToken = (): string => {
const now = Math.floor(Date.now() / 1000);
const payload = {
iss: vendor.inputValues.accessKey,
exp: now + 1800,
nbf: now - 5,
};
return jsonwebtoken.sign(payload, vendor.inputValues.secretKey, {
algorithm: "HS256",
header: { alg: "HS256", typ: "JWT" },
});
};
/**
*
*/
const getBaseUrl = (): string => {
return vendor.inputValues.baseUrl || "https://api-beijing.klingai.com";
};
/**
* ReferenceList
* url url base64 base64 data: 前缀
*/
const extractRawBase64 = (ref: ReferenceList): string => {
return ref.base64.replace(/^data:[^;]+;base64,/, "");
};
/**
* ReferenceList base64 url
* omni-video image_url base64 url
*/
const extractImageUrl = (ref: ReferenceList): string => {
return ref.base64.startsWith("data:") ? ref.base64 : `data:image/jpeg;base64,${ref.base64}`;
};
/**
*
*/
const submitAndPoll = async (submitUrl: string, queryUrlBase: string, requestBody: any): Promise<string> => {
const token = generateAuthToken();
logger(`开始提交可灵AI视频生成任务: ${submitUrl}`);
logger(
`请求参数: ${JSON.stringify({
...requestBody,
image: requestBody.image ? "[BASE64]" : undefined,
image_tail: requestBody.image_tail ? "[BASE64]" : undefined,
image_list: requestBody.image_list ? "[IMAGES]" : undefined,
})}`,
);
const submitResp = await axios.post(submitUrl, requestBody, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
if (submitResp.data.code !== 0) {
throw new Error(`提交任务失败: ${submitResp.data.message || JSON.stringify(submitResp.data)}`);
}
const taskId = submitResp.data.data.task_id;
logger(`任务已提交任务ID: ${taskId}`);
const result = await pollTask(
async () => {
const freshToken = generateAuthToken();
const queryResp = await axios.get(`${queryUrlBase}/${taskId}`, {
headers: {
Authorization: `Bearer ${freshToken}`,
},
});
if (queryResp.data.code !== 0) {
return { completed: true, error: `查询任务失败: ${queryResp.data.message}` };
}
const taskData = queryResp.data.data;
const status = taskData.task_status;
logger(`轮询中... 任务状态: ${status}`);
if (status === "succeed") {
const videoUrl = taskData.task_result?.videos?.[0]?.url;
if (!videoUrl) {
return { completed: true, error: "任务完成但未获取到视频URL" };
}
return { completed: true, data: videoUrl };
}
if (status === "failed") {
return { completed: true, error: `视频生成失败: ${taskData.task_status_msg || "未知错误"}` };
}
return { completed: false };
},
5000,
600000,
);
if (result.error) throw new Error(result.error);
logger(`视频生成完成正在转换为Base64...`);
return await urlToBase64(result.data!);
};
// ============================================================
// 适配器函数
// ============================================================
const textRequest = (model: TextModel, think: boolean, thinkLevel: 0 | 1 | 2 | 3) => {
throw new Error("可灵AI不支持文本模型");
};
const imageRequest = async (config: ImageConfig, model: ImageModel): Promise<string> => {
throw new Error("可灵AI不支持图片模型");
};
const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {
if (!vendor.inputValues.accessKey) throw new Error("缺少Access Key");
if (!vendor.inputValues.secretKey) throw new Error("缺少Secret Key");
const baseUrl = getBaseUrl();
// 解析 modelName格式kling-video-o1:pro => modelName=kling-video-o1, mode=pro
const colonIdx = model.modelName.indexOf(":");
const modelName = colonIdx > -1 ? model.modelName.substring(0, colonIdx) : model.modelName;
const mode = colonIdx > -1 ? model.modelName.substring(colonIdx + 1) : "pro";
// 判断是否为 Omni 模型
const isOmniModel = modelName === "kling-video-o1" || modelName === "kling-v3-omni";
// 判断当前选中的视频生成模式
const currentMode = config.mode;
const isText = currentMode.includes("text");
const isSingleImage = currentMode.includes("singleImage");
const isStartEndRequired = currentMode.includes("startEndRequired");
const isEndFrameOptional = currentMode.includes("endFrameOptional");
const isStartFrameOptional = currentMode.includes("startFrameOptional");
const hasMultiRef = currentMode.some((m) => Array.isArray(m));
// 提取不同类型的引用
const imageRefs = (config.referenceList || []).filter((r) => r.type === "image");
const videoRefs = (config.referenceList || []).filter((r) => r.type === "video");
// =====================================================
// Omni 模型 —— 使用 /v1/videos/omni-video 接口
// =====================================================
if (isOmniModel) {
const requestBody: any = {
model_name: modelName,
mode: mode,
duration: String(config.duration),
sound: config.audio === true ? "on" : "off",
};
if (config.prompt) {
requestBody.prompt = config.prompt;
}
if (isSingleImage && imageRefs.length > 0) {
const imageUrl = extractImageUrl(imageRefs[0]);
requestBody.image_list = [{ image_url: imageUrl, type: "first_frame" }];
if (!requestBody.prompt) requestBody.prompt = "根据图片生成视频";
} else if (isStartEndRequired && imageRefs.length >= 2) {
const firstUrl = extractImageUrl(imageRefs[0]);
const endUrl = extractImageUrl(imageRefs[1]);
requestBody.image_list = [
{ image_url: firstUrl, type: "first_frame" },
{ image_url: endUrl, type: "end_frame" },
];
if (!requestBody.prompt) requestBody.prompt = "根据首尾帧图片生成过渡视频";
} else if (isEndFrameOptional && imageRefs.length >= 1) {
const firstUrl = extractImageUrl(imageRefs[0]);
requestBody.image_list = [{ image_url: firstUrl, type: "first_frame" }];
if (imageRefs.length >= 2) {
const endUrl = extractImageUrl(imageRefs[1]);
requestBody.image_list.push({ image_url: endUrl, type: "end_frame" });
}
if (!requestBody.prompt) requestBody.prompt = "根据图片生成视频";
} else if (isStartFrameOptional && imageRefs.length >= 1) {
if (imageRefs.length >= 2) {
const firstUrl = extractImageUrl(imageRefs[0]);
const endUrl = extractImageUrl(imageRefs[1]);
requestBody.image_list = [
{ image_url: firstUrl, type: "first_frame" },
{ image_url: endUrl, type: "end_frame" },
];
} else {
const endUrl = extractImageUrl(imageRefs[0]);
requestBody.image_list = [{ image_url: endUrl, type: "end_frame" }];
}
if (!requestBody.prompt) requestBody.prompt = "根据图片生成视频";
} else if (hasMultiRef && (imageRefs.length > 0 || videoRefs.length > 0)) {
requestBody.image_list = [];
for (let i = 0; i < imageRefs.length; i++) {
const imageUrl = extractImageUrl(imageRefs[i]);
requestBody.image_list.push({ image_url: imageUrl });
}
if (!requestBody.prompt) {
const refs = imageRefs.map((_, idx) => `<<<image_${idx + 1}>>>`).join("、");
requestBody.prompt = `参考${refs}生成视频`;
}
}
// 文生视频或无图片输入时需要设置宽高比
const hasImageInput = requestBody.image_list && requestBody.image_list.length > 0;
if (!hasImageInput) {
requestBody.aspect_ratio = config.aspectRatio || "16:9";
if (!requestBody.prompt) throw new Error("文生视频模式需要提供提示词");
}
const apiPath = "/v1/videos/omni-video";
return await submitAndPoll(`${baseUrl}${apiPath}`, `${baseUrl}${apiPath}`, requestBody);
}
// =====================================================
// 非 Omni 模型 —— 根据模式选择不同接口
// =====================================================
// 多图参考模式 —— 使用 /v1/videos/multi-image2video 接口(仅 kling-v1-6 支持)
if (hasMultiRef && imageRefs.length > 0) {
const imageList = [];
for (let i = 0; i < imageRefs.length; i++) {
const rawBase64 = extractRawBase64(imageRefs[i]);
imageList.push({ image: rawBase64 });
}
const requestBody: any = {
model_name: modelName,
image_list: imageList,
prompt: config.prompt || "根据参考图片生成视频",
mode: mode,
duration: String(config.duration),
aspect_ratio: config.aspectRatio || "16:9",
};
const apiPath = "/v1/videos/multi-image2video";
return await submitAndPoll(`${baseUrl}${apiPath}`, `${baseUrl}${apiPath}`, requestBody);
}
// 文生视频模式 —— 使用 /v1/videos/text2video 接口
if (isText) {
if (!config.prompt) throw new Error("文生视频模式需要提供提示词");
const requestBody: any = {
model_name: modelName,
prompt: config.prompt,
mode: mode,
duration: String(config.duration),
aspect_ratio: config.aspectRatio || "16:9",
sound: config.audio === true ? "on" : "off",
};
const apiPath = "/v1/videos/text2video";
return await submitAndPoll(`${baseUrl}${apiPath}`, `${baseUrl}${apiPath}`, requestBody);
}
// 图生视频模式(单图 / 首尾帧 / 尾帧可选等)—— 使用 /v1/videos/image2video 接口
if ((isSingleImage || isStartEndRequired || isEndFrameOptional || isStartFrameOptional) && imageRefs.length > 0) {
const requestBody: any = {
model_name: modelName,
prompt: config.prompt || "根据图片生成视频",
mode: mode,
duration: String(config.duration),
sound: config.audio === true ? "on" : "off",
};
if (isSingleImage) {
requestBody.image = extractRawBase64(imageRefs[0]);
} else if (isStartEndRequired && imageRefs.length >= 2) {
requestBody.image = extractRawBase64(imageRefs[0]);
requestBody.image_tail = extractRawBase64(imageRefs[1]);
} else if (isEndFrameOptional) {
requestBody.image = extractRawBase64(imageRefs[0]);
if (imageRefs.length >= 2) {
requestBody.image_tail = extractRawBase64(imageRefs[1]);
}
} else if (isStartFrameOptional) {
if (imageRefs.length >= 2) {
requestBody.image = extractRawBase64(imageRefs[0]);
requestBody.image_tail = extractRawBase64(imageRefs[1]);
} else {
requestBody.image = extractRawBase64(imageRefs[0]);
}
}
const apiPath = "/v1/videos/image2video";
return await submitAndPoll(`${baseUrl}${apiPath}`, `${baseUrl}${apiPath}`, requestBody);
}
throw new Error("不支持的视频生成模式或缺少必要的输入参数");
};
const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<string> => {
return "";
};
// ============================================================
// 导出
// ============================================================
exports.vendor = vendor;
exports.textRequest = textRequest;
exports.imageRequest = imageRequest;
exports.videoRequest = videoRequest;
exports.ttsRequest = ttsRequest;
// 这行代码用于确保当前文件被识别为模块,避免全局变量冲突
export {};

399
data/vendor/minimax.ts vendored Normal file
View File

@ -0,0 +1,399 @@
/**
* Toonflow AI供应商模板 - MiniMax(AI)
* @version 2.0
*/
// ============================================================
// 类型定义
// ============================================================
type VideoMode =
| "singleImage"
| "startEndRequired"
| "endFrameOptional"
| "startFrameOptional"
| "text"
| (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[];
interface TextModel {
name: string;
modelName: string;
type: "text";
think: boolean;
}
interface ImageModel {
name: string;
modelName: string;
type: "image";
mode: ("text" | "singleImage" | "multiReference")[];
associationSkills?: string;
}
interface VideoModel {
name: string;
modelName: string;
type: "video";
mode: VideoMode[];
associationSkills?: string;
audio: "optional" | false | true;
durationResolutionMap: { duration: number[]; resolution: string[] }[];
}
interface TTSModel {
name: string;
modelName: string;
type: "tts";
voices: { title: string; voice: string }[];
}
interface VendorConfig {
id: string;
version: string;
name: string;
author: string;
description?: string;
icon?: string;
inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[];
inputValues: Record<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;
declare const logger: (msg: string) => void;
declare const jsonwebtoken: any;
declare const zipImage: (base64: string, size: number) => Promise<string>;
declare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>;
declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>;
declare const urlToBase64: (url: string) => Promise<string>;
declare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>;
declare const createOpenAI: any;
declare const createDeepSeek: any;
declare const createZhipu: any;
declare const createQwen: any;
declare const createAnthropic: any;
declare const createOpenAICompatible: any;
declare const createXai: any;
declare const createMinimax: any;
declare const createGoogleGenerativeAI: any;
declare const exports: {
vendor: VendorConfig;
textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => any;
uploadReference: (base64: string, fileType: "image" | "audio" | "video") => Promise<ReferenceList>;
imageRequest: (c: ImageConfig, m: ImageModel) => Promise<string>;
videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>;
ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>;
checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>;
updateVendor?: () => Promise<string>;
};
// ============================================================
// 供应商配置
// ============================================================
const vendor: VendorConfig = {
id: "minimax",
version: "2.0",
author: "Toonflow",
name: "MiniMax(海螺AI)",
description: "MiniMax官方接口适配支持M系列推理文本模型、文生图/图生图、视频生成(文生视频、图生视频、首尾帧生成)能力 \n [前往平台](https://minimaxi.com/)",
inputs: [
{ key: "apiKey", label: "API密钥", type: "password", required: true },
{ key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "示例https://api.minimaxi.com" },
],
inputValues: { apiKey: "", baseUrl: "https://api.minimaxi.com" },
models: [
// 文本模型
{ name: "MiniMax-M2.7 (推理版)", modelName: "MiniMax-M2.7", type: "text", think: true },
{ name: "MiniMax-M2.7 极速版 (推理版)", modelName: "MiniMax-M2.7-highspeed", type: "text", think: true },
{ name: "MiniMax-M2.5 (推理版)", modelName: "MiniMax-M2.5", type: "text", think: true },
{ name: "MiniMax-M2.5 极速版 (推理版)", modelName: "MiniMax-M2.5-highspeed", type: "text", think: true },
{ name: "MiniMax-M2.1 (编程版)", modelName: "MiniMax-M2.1", type: "text", think: true },
{ name: "MiniMax-M2.1 极速版 (编程版)", modelName: "MiniMax-M2.1-highspeed", type: "text", think: true },
{ name: "MiniMax-M2 (Agent版)", modelName: "MiniMax-M2", type: "text", think: false },
// 图片模型
{ name: "海螺图像V1", modelName: "image-01", type: "image", mode: ["text", "singleImage"] },
{ name: "海螺图像V1 Live版", modelName: "image-01-live", type: "image", mode: ["text", "singleImage"], associationSkills: "支持自定义画风" },
// 视频模型
{
name: "海螺2.3",
modelName: "MiniMax-Hailuo-2.3",
type: "video",
mode: ["text", "singleImage"],
audio: false,
durationResolutionMap: [
{ duration: [6], resolution: ["768P", "1080P"] },
{ duration: [10], resolution: ["768P"] },
],
},
{
name: "海螺2.3极速版",
modelName: "MiniMax-Hailuo-2.3-Fast",
type: "video",
mode: ["text", "singleImage"],
audio: false,
durationResolutionMap: [
{ duration: [6], resolution: ["768P", "1080P"] },
{ duration: [10], resolution: ["768P"] },
],
},
{
name: "海螺02",
modelName: "MiniMax-Hailuo-02",
type: "video",
mode: ["text", "singleImage", "startEndRequired"],
audio: false,
durationResolutionMap: [
{ duration: [6], resolution: ["512P", "768P", "1080P"] },
{ duration: [10], resolution: ["512P", "768P"] },
],
},
],
};
// ============================================================
// 辅助工具
// ============================================================
/**
*
*/
const getHeaders = (): Record<string, string> => {
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
return {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
};
};
/**
*
*/
const getBaseUrl = (): string => {
return vendor.inputValues.baseUrl.replace(/\/$/, "");
};
/**
* ReferenceList base64
*/
const extractBase64WithHead = (ref: ReferenceList): string => {
return ref.base64.startsWith("data:") ? ref.base64 : `data:image/png;base64,${ref.base64}`;
};
// ============================================================
// 适配器函数
// ============================================================
const textRequest = (model: TextModel, think: boolean, thinkLevel: 0 | 1 | 2 | 3) => {
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
const baseUrl = getBaseUrl();
const openaiBaseUrl = `${baseUrl}/v1`;
const extraBody = model.think ? { reasoning_split: true } : {};
return createOpenAI({ baseURL: openaiBaseUrl, apiKey, extraBody }).chat(model.modelName);
};
const uploadReference = async (base64: string, fileType: "image" | "audio" | "video"): Promise<ReferenceList> => {
// MiniMax的图片接口直接接受 base64压缩后原样返回
if (fileType === "image") {
const compressed = await zipImage(base64, 10 * 1024);
return { type: "image", sourceType: "base64", base64: compressed };
}
// 视频接口的图片参数也是 base64压缩到20MB
return { type: fileType, sourceType: "base64", base64 } as ReferenceList;
};
const imageRequest = async (config: ImageConfig, model: ImageModel): Promise<string> => {
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
const baseUrl = getBaseUrl();
const headers = getHeaders();
const reqBody: any = {
model: model.modelName,
prompt: config.prompt,
aspect_ratio: config.aspectRatio,
response_format: "base64",
n: 1,
prompt_optimizer: true,
aigc_watermark: false,
};
// 处理图生图参考
const imageRefs = config.referenceList || [];
if (imageRefs.length > 0) {
const refBase64 = extractBase64WithHead(imageRefs[0]);
reqBody.subject_reference = [{ type: "character", image_file: refBase64 }];
}
logger("开始提交MiniMax图像生成任务");
const resp = await axios.post(`${baseUrl}/v1/image_generation`, reqBody, { headers });
if (resp.data.base_resp.status_code !== 0) {
throw new Error(`图像生成失败:${resp.data.base_resp.status_msg}`);
}
if (resp.data.metadata.success_count === 0) {
throw new Error("图像生成被安全策略拦截请调整prompt或参考图");
}
const imgBase64 = resp.data.data.image_base64[0];
return imgBase64.startsWith("data:") ? imgBase64 : `data:image/png;base64,${imgBase64}`;
};
const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
const baseUrl = getBaseUrl();
const headers = getHeaders();
const reqBody: any = {
model: model.modelName,
prompt: config.prompt,
duration: config.duration,
resolution: config.resolution,
aigc_watermark: false,
prompt_optimizer: true,
};
// 提取图片类型的引用
const imageRefs = (config.referenceList || []).filter((r) => r.type === "image");
if (imageRefs.length > 0) {
// 压缩图片到20MB以内
const compressedImages: string[] = [];
for (const ref of imageRefs) {
const base64 = extractBase64WithHead(ref);
const compressed = await zipImage(base64, 20 * 1024);
compressedImages.push(compressed);
}
if (config.mode.includes("startEndRequired")) {
if (compressedImages.length < 2) throw new Error("首尾帧模式需要上传两张图片");
reqBody.first_frame_image = compressedImages[0];
reqBody.last_frame_image = compressedImages[1];
} else if (config.mode.includes("singleImage")) {
reqBody.first_frame_image = compressedImages[0];
}
}
logger("开始提交MiniMax视频生成任务");
const submitResp = await axios.post(`${baseUrl}/v1/video_generation`, reqBody, { headers });
if (submitResp.data.base_resp.status_code !== 0) {
throw new Error(`任务提交失败:${submitResp.data.base_resp.status_msg}`);
}
const taskId = submitResp.data.task_id;
logger(`视频任务提交成功任务ID: ${taskId}`);
// 轮询任务状态
const pollResult = await pollTask(
async () => {
const queryResp = await axios.get(`${baseUrl}/v1/query/video_generation`, {
headers: getHeaders(),
params: { task_id: taskId },
});
if (queryResp.data.base_resp.status_code !== 0) {
return { completed: true, error: queryResp.data.base_resp.status_msg };
}
const status = queryResp.data.status;
if (status === "Success") {
return { completed: true, data: queryResp.data.file_id };
}
if (status === "Fail") {
return { completed: true, error: "视频生成失败" };
}
logger(`视频任务生成中,当前状态:${status}`);
return { completed: false };
},
5000,
600000,
);
if (pollResult.error) throw new Error(pollResult.error);
const fileId = pollResult.data!;
logger(`视频任务生成成功文件ID: ${fileId}`);
// 获取下载地址
const fileResp = await axios.get(`${baseUrl}/v1/files/retrieve`, {
headers: getHeaders(),
params: { file_id: fileId },
});
if (fileResp.data.base_resp.status_code !== 0) {
throw new Error(`获取文件地址失败:${fileResp.data.base_resp.status_msg}`);
}
const downloadUrl = fileResp.data.file.download_url;
logger(`视频下载地址获取成功开始转Base64`);
return await urlToBase64(downloadUrl);
};
const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<string> => {
return "";
};
const checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => {
return {
hasUpdate: false,
latestVersion: "2.0",
notice:
"## 新版本更新公告\n1. 适配新版模板架构,支持 ReferenceList 统一引用类型\n2. 新增 uploadReference 前置处理器\n3. 优化图片压缩和引用提取逻辑",
};
};
const updateVendor = async (): Promise<string> => {
return "";
};
// ============================================================
// 导出
// ============================================================
exports.vendor = vendor;
exports.textRequest = textRequest;
exports.uploadReference = uploadReference;
exports.imageRequest = imageRequest;
exports.videoRequest = videoRequest;
exports.ttsRequest = ttsRequest;
exports.checkForUpdates = checkForUpdates;
exports.updateVendor = updateVendor;
// 这行代码用于确保当前文件被识别为模块,避免全局变量冲突
export {};

334
data/vendor/null.ts vendored Normal file
View File

@ -0,0 +1,334 @@
/**
* Toonflow AI供应商模板
* @version 2.0
*/
// ============================================================
// 类型定义
// ============================================================
type VideoMode =
| "singleImage" //单图参考
| "startEndRequired" //首尾帧(两张都得有)
| "endFrameOptional" //首尾帧(尾帧可选)
| "startFrameOptional" //首尾帧(首帧可选)
| "text" //文本
| (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[]; //多参考(数字代表限制数量)
interface TextModel {
name: string;
modelName: string;
type: "text";
think: boolean;
}
interface ImageModel {
name: string;
modelName: string;
type: "image";
mode: ("text" | "singleImage" | "multiReference")[];
associationSkills?: string;
}
interface VideoModel {
name: string;
modelName: string;
type: "video";
mode: VideoMode[];
associationSkills?: string;
audio: "optional" | false | true;
durationResolutionMap: { duration: number[]; resolution: string[] }[];
}
interface TTSModel {
name: string;
modelName: string;
type: "tts";
voices: { title: string; voice: string }[];
}
interface VendorConfig {
id: string; //唯一ID作为文件名存储用户磁盘上禁止符号
version: string; //版本号格式为x.y需遵守语义化版本控制
name: string; //供应商名称
author: string; //作者
description?: string; //描述支持Markdown格式
icon?: string; //图标仅支持Base64格式建议尺寸为128x128像素
inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[];
inputValues: Record<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, t: boolean, tl: 0 | 1 | 2 | 3) => 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: "null",
version: "2.0",
author: "Toonflow",
name: "空模板",
description: "## 开发模板您可以使用此模板进行Vibe Coding",
inputs: [
{ key: "apiKey", label: "API密钥", type: "password", required: true },
{ key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "示例https://api.openai.com/v1" },
],
inputValues: { apiKey: "", baseUrl: "https://api.openai.com/v1" },
models: [{ name: "GPT-4o", modelName: "gpt-4o", type: "text", think: false }],
};
// ============================================================
// 适配器函数
// ============================================================
const textRequest = (model: TextModel, think: boolean, thinkLevel: 0 | 1 | 2 | 3) => {
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
return createOpenAI({ baseURL: vendor.inputValues.baseUrl, apiKey }).chat(model.modelName);
};
const imageRequest = async (config: ImageConfig, model: ImageModel): Promise<string> => {
return "";
};
const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {
return "";
};
const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<string> => {
return "";
};
const checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => {
return { hasUpdate: false, latestVersion: "2.0", notice: "## 新版本更新公告" };
};
const updateVendor = async (): Promise<string> => {
return "";
};
// ============================================================
// 导出
// ============================================================
exports.vendor = vendor;
exports.textRequest = textRequest;
exports.imageRequest = imageRequest;
exports.videoRequest = videoRequest;
exports.ttsRequest = ttsRequest;
exports.checkForUpdates = checkForUpdates;
exports.updateVendor = updateVendor;
// 这行代码用于确保当前文件被识别为模块,避免全局变量冲突
export {};
/**
* ============================================================
* AI
* ============================================================
*
*
* Toonflow AI AI
* curl API
*
*
*
* 1. API curl HeadersBody
* 2. API /
* 3. text / image / video / tts
* API
*
*
*
* 1.
* 使 import / require使
* axiosloggerjsonwebtokenzipImagezipImageResolutionmergeImages
* urlToBase64pollTask createOpenAIcreateDeepSeekcreateZhipucreateQwen
* createAnthropiccreateOpenAICompatiblecreateXaicreateMinimax
* createGoogleGenerativeAI AI SDK
*
* 2. exports.*
* const API_URL = "https://..."; const MAX_RETRY = 3;
* vendor.inputValues
* vendor.inputValues.xxx 访
* 使 exports.* 使
*
* 3. exports.*
* textRequest / imageRequest / videoRequest / ttsRequest
*
* Token
*
* 使
*
* 4.
* 使camelCase使 UPPER_SNAKE_CASE
*
* 5.
* VendorConfigImageConfigVideoConfig
* TTSConfigTextModelImageModelVideoModelTTSModelReferenceListPollResult
* AI 使
*
* 6.
* - textRequest(model) AI SDK chat model createOpenAI
* - imageRequest(config, model) base64 "data:image/png;base64,..."
* config.referenceList Extract<ReferenceList, { type: "image" }>[]
* base64 sourceType "base64"
* - videoRequest(config, model) base64 "data:video/mp4;base64,..."
* config.referenceList ReferenceList[] image / video / audio
* base64 sourceType "base64"
* config.mode mode 使 referenceList
* - ttsRequest(config, model) base64 "data:audio/mp3;base64,..."
* config.referenceList Extract<ReferenceList, { type: "audio" }>[]
* API URL 使 urlToBase64(url)
*
* 7. ReferenceList VideoMode
* ReferenceList
* - type: "image" | "audio" | "video"
* - sourceType: "base64" base64
* - base64
*
* VideoMode
* - "text"
* - "singleImage"
* - "startEndRequired"
* - "endFrameOptional"
* - "startFrameOptional"
* - ["imageReference:9", "videoReference:3", "audioReference:3"]
*
*
* videoRequest config.mode
* - config.referenceList
* - API //
*
* 8.
* 使 pollTask
* const result = await pollTask(async () => {
* const resp = await axios.get(...);
* if (resp.data.status === "SUCCESS") return { completed: true, data: resp.data.url };
* if (resp.data.status === "FAILED") return { completed: true, error: resp.data.message };
* return { completed: false };
* }, 5000, 600000); // 每5秒轮询10分钟超时
* if (result.error) throw new Error(result.error);
* return await urlToBase64(result.data!);
*
* 9.
* API Key使 throw new Error("...")
* API
*
* 10.
* 使 logger("...") "开始提交任务""任务ID: xxx""轮询中..."
* 便
*
* 11. vendor
* - id使
* - version "x.y"
* - inputs API API KeySecret
* - models type
* - VideoModel mode API 7 VideoMode
* - VideoModel audio truefalse"optional"
* - VideoModel durationResolutionMap
* - VideoModel associationSkills
* - ImageModel mode API "text" "singleImage" "multiReference"
* - TTSModel voices
*
* 12.
* - 使 zipImage(base64, maxSizeKB)
* - 使 zipImageResolution(base64, width, height)
* - 使 mergeImages(base64Arr, maxSize)
* - base64
*
* 13.
*
* []
* 线
* getHeadersgetBaseUrl
*
* 14.
* exports.xxx = xxx
* - exports.vendor
* - exports.textRequest
* - exports.imageRequest
* - exports.videoRequest
* - exports.ttsRequest
* - exports.checkForUpdates
* - exports.updateVendor
* return ""
* export {};
*
*
*
* 1. curl API
* 2. API /
* 3. vendor
* 4. ReferenceList base64 referenceList
* 5. return ""
* 6.
*/

169
data/vendor/openai.ts vendored Normal file
View File

@ -0,0 +1,169 @@
/**
* Toonflow AI供应商模板
* @version 2.0
*/
// ============================================================
// 类型定义
// ============================================================
type VideoMode =
| "singleImage"
| "startEndRequired"
| "endFrameOptional"
| "startFrameOptional"
| "text"
| (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[];
interface TextModel {
name: string;
modelName: string;
type: "text";
think: boolean;
}
interface ImageModel {
name: string;
modelName: string;
type: "image";
mode: ("text" | "singleImage" | "multiReference")[];
associationSkills?: string;
}
interface VideoModel {
name: string;
modelName: string;
type: "video";
mode: VideoMode[];
associationSkills?: string;
audio: "optional" | false | true;
durationResolutionMap: { duration: number[]; resolution: string[] }[];
}
interface TTSModel {
name: string;
modelName: string;
type: "tts";
voices: { title: string; voice: string }[];
}
interface VendorConfig {
id: string;
version: string;
name: string;
author: string;
description?: string;
icon?: string;
inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[];
inputValues: Record<string, string>;
models: (TextModel | ImageModel | VideoModel | TTSModel)[];
}
interface ImageConfig {
prompt: string;
imageBase64: string[];
size: "1K" | "2K" | "4K";
aspectRatio: `${number}:${number}`;
}
interface VideoConfig {
duration: number;
resolution: string;
aspectRatio: "16:9" | "9:16";
prompt: string;
imageBase64?: string[];
audio?: boolean;
mode: VideoMode[];
}
interface TTSConfig {
text: string;
voice: string;
speechRate: number;
pitchRate: number;
volume: number;
}
interface PollResult {
completed: boolean;
data?: string;
error?: string;
}
// ============================================================
// 全局声明
// ============================================================
declare const axios: any;
declare const logger: (msg: string) => void;
declare const jsonwebtoken: any;
declare const zipImage: (base64: string, size: number) => Promise<string>;
declare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>;
declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>;
declare const urlToBase64: (url: string) => Promise<string>;
declare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>;
declare const createOpenAI: any;
declare const createDeepSeek: any;
declare const createZhipu: any;
declare const createQwen: any;
declare const createAnthropic: any;
declare const createOpenAICompatible: any;
declare const createXai: any;
declare const createMinimax: any;
declare const createGoogleGenerativeAI: any;
declare const exports: {
vendor: VendorConfig;
textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => any;
imageRequest: (c: ImageConfig, m: ImageModel) => Promise<string>;
videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>;
ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>;
checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>;
updateVendor?: () => Promise<string>;
};
// ============================================================
// 供应商配置
// ============================================================
const vendor: VendorConfig = {
id: "openai",
version: "2.0",
author: "Toonflow",
name: "OpenAI标准接口",
description: "OpenAI标准格式接口可修改请求地址并手动添加模型。",
icon: "",
inputs: [
{ key: "apiKey", label: "API密钥", type: "password", required: true },
{ key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "以v1结束示例https://api.openai.com/v1" },
],
inputValues: {
apiKey: "",
baseUrl: "https://api.openai.com/v1",
},
models: [
{ name: "GPT-4o", modelName: "gpt-4o", type: "text", think: false },
{ name: "GPT-4.1", modelName: "gpt-4.1", type: "text", think: false },
{ name: "GPT-5.1", modelName: "gpt-5.1", type: "text", think: false },
{ name: "GPT-5.2", modelName: "gpt-5.2", type: "text", think: false },
{ name: "GPT-5.4", modelName: "gpt-5.4", type: "text", think: false },
],
};
// ============================================================
// 适配器函数
// ============================================================
const textRequest = (model: TextModel, think: boolean, thinkLevel: 0 | 1 | 2 | 3) => {
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
return createOpenAI({ baseURL: vendor.inputValues.baseUrl, apiKey }).chat(model.modelName);
};
const imageRequest = async (config: ImageConfig, model: ImageModel): Promise<string> => {
return "";
};
const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {
return "";
};
const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<string> => {
return "";
};
const checkForUpdates = async (): Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }> => {
return { hasUpdate: false, latestVersion: "2.0", notice: "" };
};
const updateVendor = async (): Promise<string> => {
return "";
};
// ============================================================
// 导出
// ============================================================
exports.vendor = vendor;
exports.textRequest = textRequest;
exports.imageRequest = imageRequest;
exports.videoRequest = videoRequest;
exports.ttsRequest = ttsRequest;
exports.checkForUpdates = checkForUpdates;
exports.updateVendor = updateVendor;
export {};

522
data/vendor/toonflow.ts vendored Normal file
View File

@ -0,0 +1,522 @@
/**
* Toonflow官方中转平台
* @version 2.0
*/
// ============================================================
// 类型定义
// ============================================================
type VideoMode =
| "singleImage"
| "startEndRequired"
| "endFrameOptional"
| "startFrameOptional"
| "text"
| (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[];
interface TextModel {
name: string;
modelName: string;
type: "text";
think: boolean;
}
interface ImageModel {
name: string;
modelName: string;
type: "image";
mode: ("text" | "singleImage" | "multiReference")[];
associationSkills?: string;
}
interface VideoModel {
name: string;
modelName: string;
type: "video";
mode: VideoMode[];
associationSkills?: string;
audio: "optional" | false | true;
durationResolutionMap: { duration: number[]; resolution: string[] }[];
}
interface TTSModel {
name: string;
modelName: string;
type: "tts";
voices: { title: string; voice: string }[];
}
interface VendorConfig {
id: string;
version: string;
name: string;
author: string;
description?: string;
icon?: string;
inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[];
inputValues: Record<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;
declare const logger: (msg: string) => void;
declare const jsonwebtoken: any;
declare const zipImage: (base64: string, size: number) => Promise<string>;
declare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>;
declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>;
declare const urlToBase64: (url: string) => Promise<string>;
declare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>;
declare const createOpenAI: any;
declare const createDeepSeek: any;
declare const createZhipu: any;
declare const createQwen: any;
declare const createAnthropic: any;
declare const createOpenAICompatible: any;
declare const createXai: any;
declare const createMinimax: any;
declare const createGoogleGenerativeAI: any;
declare const exports: {
vendor: VendorConfig;
textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => 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如果这个项目对你有帮助可以考虑支持一下我们的开发工作 ☕",
icon: "",
inputs: [{ key: "apiKey", label: "API密钥", type: "password", required: true }],
inputValues: {
apiKey: "",
baseUrl: "https://api.toonflow.net/v1",
},
models: [
{ name: "claude-sonnet-4-6", type: "text", modelName: "claude-sonnet-4-6", think: false },
{ name: "claude-opus-4-6", type: "text", modelName: "claude-opus-4-6", think: false },
{ name: "claude-sonnet-4-5-20250929", type: "text", modelName: "claude-sonnet-4-5-20250929", think: false },
{ name: "claude-opus-4-5-20251101", type: "text", modelName: "claude-opus-4-5-20251101", think: false },
{ name: "claude-haiku-4-5-20251001", type: "text", modelName: "claude-haiku-4-5-20251001", think: false },
{ name: "gpt-5.4", type: "text", modelName: "gpt-5.4", think: false },
{ name: "gpt-5.2", type: "text", modelName: "gpt-5.2", think: false },
{ name: "MiniMax-M2.7", type: "text", modelName: "MiniMax-M2.7", think: true },
{ name: "MiniMax-M2.5", type: "text", modelName: "MiniMax-M2.5", think: true },
{
name: "Wan2.6 I2V 1080P (支持真人)",
type: "video",
modelName: "Wan2.6-I2V-1080P",
mode: ["text", "startEndRequired"],
durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["1080p"] }],
audio: true,
},
{
name: "Wan2.6 I2V 720P (支持真人)",
type: "video",
modelName: "Wan2.6-I2V-720P",
mode: ["text", "startEndRequired"],
durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["720p"] }],
audio: true,
},
{
name: "Seedance 1.5 Pro",
type: "video",
modelName: "doubao-seedance-1-5-pro-251215",
mode: ["text", "endFrameOptional"],
durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }],
audio: true,
},
{
name: "vidu2 turbo",
type: "video",
modelName: "ViduQ2-turbo",
mode: ["singleImage", "startEndRequired"],
durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["540p", "720p", "1080p"] }],
audio: false,
},
{
name: "ViduQ3 pro",
type: "video",
modelName: "ViduQ3-pro",
mode: ["singleImage", "startEndRequired"],
durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16], resolution: ["540p", "720p", "1080p"] }],
audio: false,
},
{
name: "ViduQ2 pro",
type: "video",
modelName: "ViduQ2-pro",
mode: ["singleImage", "startEndRequired"],
durationResolutionMap: [{ duration: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], resolution: ["540p", "720p", "1080p"] }],
audio: false,
},
{
name: "Doubao Seedream 5.0 Lite",
type: "image",
modelName: "Doubao-Seedream-5.0-Lite",
mode: ["text", "singleImage", "multiReference"],
},
{
name: "Doubao Seedream 4.5",
type: "image",
modelName: "doubao-seedream-4-5-251128",
mode: ["text", "singleImage", "multiReference"],
},
],
};
// ============================================================
// 辅助工具
// ============================================================
// 从 markdown 内容中提取第一张图片
function extractFirstImageFromMd(content: string) {
const regex = /!\[([^\]]*)\]\((data:image\/[^;]+;base64,[A-Za-z0-9+/=]+|https?:\/\/[^\s)]+|\/\/[^\s)]+|[^\s)]+)\)/;
const match = content.match(regex);
if (!match) return null;
const raw = match[2].trim();
const url = raw.startsWith("data:") ? raw : raw.split(/\s+/)[0];
return { alt: match[1], url, type: url.startsWith("data:image") ? "base64" : "url" };
}
// ============================================================
// 适配器函数
// ============================================================
const textRequest = (model: TextModel, think: boolean, thinkLevel: 0 | 1 | 2 | 3) => {
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
return createOpenAI({ baseURL: vendor.inputValues.baseUrl, apiKey }).chat(model.modelName);
};
const imageRequest = async (config: ImageConfig, model: ImageModel): Promise<string> => {
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
const baseUrl = vendor.inputValues.baseUrl;
const lowerName = model.modelName.toLowerCase();
const imageBase64List = (config.referenceList ?? []).map((r) => r.base64);
// Gemini / nano 系模型:走 chat/completions 接口,从返回的 markdown 中提取图片
if (lowerName.includes("gemini") || lowerName.includes("nano")) {
const imageConfigGoogle: Record<string, string> = {
aspect_ratio: config.aspectRatio,
image_size: config.size,
};
const messages: any[] = [];
if (imageBase64List.length) {
messages.push({
role: "user",
content: imageBase64List.map((b) => ({ type: "image_url", image_url: { url: b } })),
});
}
messages.push({ role: "user", content: config.prompt + "请直接输出图片" });
const body = {
model: model.modelName,
messages,
extra_body: { google: { image_config: imageConfigGoogle } },
};
logger(`[imageRequest] 使用 gemini 适配器,模型: ${model.modelName}`);
const response = await fetch(`${baseUrl}/chat/completions`, {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`);
}
const data = await response.json();
const imageResult = extractFirstImageFromMd(data.choices[0].message.content);
if (!imageResult) throw new Error("未能从响应中提取图片");
if (imageResult.type === "base64") return imageResult.url;
return await urlToBase64(imageResult.url);
}
// 豆包 / seedream 系模型:走 images/generations 接口
if (lowerName.includes("doubao") || lowerName.includes("seedream")) {
const effectiveSize = config.size === "1K" ? "2K" : config.size;
const sizeMap: Record<string, Record<string, string>> = {
"16:9": { "2K": "2848x1600", "4K": "4096x2304" },
"9:16": { "2K": "1600x2848", "4K": "2304x4096" },
};
const resolvedSize = sizeMap[config.aspectRatio]?.[effectiveSize];
const body: Record<string, any> = {
model: model.modelName,
prompt: config.prompt,
size: resolvedSize,
response_format: "url",
sequential_image_generation: "disabled",
stream: false,
watermark: false,
...(imageBase64List.length && { image: imageBase64List }),
};
logger(`[imageRequest] 使用 doubao 适配器,模型: ${model.modelName}`);
const response = await fetch(`${baseUrl}/images/generations`, {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`);
}
const data = await response.json();
const resultUrl = data.data[0].url;
return await urlToBase64(resultUrl);
}
throw new Error(`不支持的图像模型: ${model.modelName}`);
};
const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
const baseUrl = vendor.inputValues.baseUrl;
const lowerName = model.modelName.toLowerCase();
// 当前激活的单一 VideoMode取第一个非数组模式或数组模式
const activeMode = config.mode[0];
const imageRefs = (config.referenceList ?? []).filter((r) => r.type === "image").map((r) => r.base64);
const videoRefs = (config.referenceList ?? []).filter((r) => r.type === "video").map((r) => r.base64);
const audioRefs = (config.referenceList ?? []).filter((r) => r.type === "audio").map((r) => r.base64);
// 构建模型专属 metadata
let metadata: Record<string, any> = {};
if (lowerName.includes("wan")) {
// 万象系列
if (
(activeMode === "startEndRequired" || activeMode === "endFrameOptional" || activeMode === "startFrameOptional") &&
imageRefs.length >= 2
) {
if (imageRefs[0]) metadata.first_frame_url = imageRefs[0];
if (imageRefs[1]) metadata.last_frame_url = imageRefs[1];
} else if (imageRefs.length) {
metadata.img_url = imageRefs[0];
}
if (typeof config.audio === "boolean") metadata.audio = config.audio;
// 万象需要额外传 size 字段
const wanSizeMap: Record<string, Record<string, string>> = {
"480p": { "16:9": "832*480", "9:16": "480*832" },
"720p": { "16:9": "1280*720", "9:16": "720*1280" },
"1080p": { "16:9": "1920*1080", "9:16": "1080*1920" },
};
const wanSize = wanSizeMap[config.resolution]?.[config.aspectRatio];
const body: Record<string, any> = {
model: model.modelName,
prompt: config.prompt,
duration: config.duration,
size: wanSize,
metadata,
};
logger(`[videoRequest] 提交万象视频任务,模型: ${model.modelName}`);
const response = await fetch(`${baseUrl}/video/generations`, {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`请求失败,状态码: ${response.status}, 错误信息: ${errorText}`);
}
const data = await response.json();
const taskId = data.id;
logger(`[videoRequest] 万象任务ID: ${taskId}`);
const res = await pollTask(async () => {
const queryResponse = await fetch(`${baseUrl}/video/generations/${taskId}`, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
});
if (!queryResponse.ok) {
const errorText = await queryResponse.text();
throw new Error(`轮询失败,状态码: ${queryResponse.status}, 错误信息: ${errorText}`);
}
const queryData = await queryResponse.json();
const status = queryData?.status ?? queryData?.data?.status;
switch (status) {
case "completed":
case "SUCCESS":
case "success":
return { completed: true, data: queryData.data.result_url };
case "FAILURE":
case "failed":
return { completed: true, error: queryData?.data?.fail_reason ?? "视频生成失败" };
default:
return { completed: false };
}
});
if (res.error) throw new Error(res.error);
return await urlToBase64(res.data!);
}
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");
};

578
data/vendor/volcengine.ts vendored Normal file
View File

@ -0,0 +1,578 @@
/**
* Toonflow AI供应商模板 - ()
* @version 2.0
*/
// ============================================================
// 类型定义
// ============================================================
type VideoMode =
| "singleImage"
| "startEndRequired"
| "endFrameOptional"
| "startFrameOptional"
| "text"
| (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[];
interface TextModel {
name: string;
modelName: string;
type: "text";
think: boolean;
}
interface ImageModel {
name: string;
modelName: string;
type: "image";
mode: ("text" | "singleImage" | "multiReference")[];
associationSkills?: string;
}
interface VideoModel {
name: string;
modelName: string;
type: "video";
mode: VideoMode[];
associationSkills?: string;
audio: "optional" | false | true;
durationResolutionMap: { duration: number[]; resolution: string[] }[];
}
interface TTSModel {
name: string;
modelName: string;
type: "tts";
voices: { title: string; voice: string }[];
}
interface VendorConfig {
id: string;
version: string;
name: string;
author: string;
description?: string;
icon?: string;
inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[];
inputValues: Record<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;
declare const logger: (msg: string) => void;
declare const jsonwebtoken: any;
declare const zipImage: (base64: string, size: number) => Promise<string>;
declare const zipImageResolution: (base64: string, w: number, h: number) => Promise<string>;
declare const mergeImages: (base64Arr: string[], maxSize?: string) => Promise<string>;
declare const urlToBase64: (url: string) => Promise<string>;
declare const pollTask: (fn: () => Promise<PollResult>, interval?: number, timeout?: number) => Promise<PollResult>;
declare const createOpenAI: any;
declare const createDeepSeek: any;
declare const createZhipu: any;
declare const createQwen: any;
declare const createAnthropic: any;
declare const createOpenAICompatible: any;
declare const createXai: any;
declare const createMinimax: any;
declare const createGoogleGenerativeAI: any;
declare const exports: {
vendor: VendorConfig;
textRequest: (m: TextModel, t: boolean, tl: 0 | 1 | 2 | 3) => any;
imageRequest: (c: ImageConfig, m: ImageModel) => Promise<string>;
videoRequest: (c: VideoConfig, m: VideoModel) => Promise<string>;
ttsRequest: (c: TTSConfig, m: TTSModel) => Promise<string>;
checkForUpdates?: () => Promise<{ hasUpdate: boolean; latestVersion: string; notice: string }>;
updateVendor?: () => Promise<string>;
};
// ============================================================
// 供应商配置
// ============================================================
const vendor: VendorConfig = {
id: "volcengine",
version: "2.0",
author: "leeqi",
name: "火山引擎(豆包)",
description:
"火山引擎豆包大模型,支持文本、图片生成、视频生成等能力。\n\n需要在[火山引擎控制台](https://console.volcengine.com/ark)获取API密钥。",
icon: "",
inputs: [
{ key: "apiKey", label: "API密钥", type: "password", required: true, placeholder: "火山引擎API Key" },
{ key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "以v3结束示例https://ark.cn-beijing.volces.com/api/v3" },
],
inputValues: {
apiKey: "",
baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
},
models: [
// ===================== 文本模型 - 推荐 =====================
{ name: "Doubao-Seed-2.0-Pro", modelName: "doubao-seed-2-0-pro-260215", type: "text", think: true },
{ name: "Doubao-Seed-2.0-Lite", modelName: "doubao-seed-2-0-lite-260215", type: "text", think: true },
{ name: "Doubao-Seed-2.0-Mini", modelName: "doubao-seed-2-0-mini-260215", type: "text", think: true },
{ name: "Doubao-Seed-2.0-Code-Preview", modelName: "doubao-seed-2-0-code-preview-260215", type: "text", think: true },
{ name: "Doubao-Seed-Character", modelName: "doubao-seed-character-251128", type: "text", think: false },
// ===================== 文本模型 - 往期 =====================
{ name: "Doubao-Seed-1.8", modelName: "doubao-seed-1-8-251228", type: "text", think: true },
{ name: "Doubao-Seed-Code-Preview", modelName: "doubao-seed-code-preview-251028", type: "text", think: true },
{ name: "Doubao-Seed-1.6-Lite", modelName: "doubao-seed-1-6-lite-251015", type: "text", think: true },
{ name: "Doubao-Seed-1.6-Flash(0828)", modelName: "doubao-seed-1-6-flash-250828", type: "text", think: true },
{ name: "Doubao-Seed-1.6-Vision", modelName: "doubao-seed-1-6-vision-250815", type: "text", think: true },
{ name: "Doubao-Seed-1.6(1015)", modelName: "doubao-seed-1-6-251015", type: "text", think: true },
{ name: "Doubao-Seed-1.6(0615)", modelName: "doubao-seed-1-6-250615", type: "text", think: true },
{ name: "Doubao-Seed-1.6-Flash(0615)", modelName: "doubao-seed-1-6-flash-250615", type: "text", think: true },
{ name: "Doubao-Seed-Translation", modelName: "doubao-seed-translation-250915", type: "text", think: false },
{ name: "Doubao-1.5-Pro-32K", modelName: "doubao-1-5-pro-32k-250115", type: "text", think: false },
{ name: "Doubao-1.5-Pro-32K-Character(0715)", modelName: "doubao-1-5-pro-32k-character-250715", type: "text", think: false },
{ name: "Doubao-1.5-Pro-32K-Character(0228)", modelName: "doubao-1-5-pro-32k-character-250228", type: "text", think: false },
{ name: "Doubao-1.5-Lite-32K", modelName: "doubao-1-5-lite-32k-250115", type: "text", think: false },
{ name: "Doubao-1.5-Vision-Pro-32K", modelName: "doubao-1-5-vision-pro-32k-250115", type: "text", think: false },
// ===================== 文本模型 - 第三方(火山引擎托管) =====================
{ name: "GLM-4-7", modelName: "glm-4-7-251222", type: "text", think: true },
{ name: "DeepSeek-V3-2", modelName: "deepseek-v3-2-251201", type: "text", think: true },
{ name: "DeepSeek-V3-1-Terminus", modelName: "deepseek-v3-1-terminus", type: "text", think: true },
{ name: "DeepSeek-V3(0324)", modelName: "deepseek-v3-250324", type: "text", think: false },
{ name: "DeepSeek-R1(0528)", modelName: "deepseek-r1-250528", type: "text", think: true },
{ name: "Qwen3-32B", modelName: "qwen3-32b-20250429", type: "text", think: false },
{ name: "Qwen3-14B", modelName: "qwen3-14b-20250429", type: "text", think: false },
{ name: "Qwen3-8B", modelName: "qwen3-8b-20250429", type: "text", think: false },
{ name: "Qwen3-0.6B", modelName: "qwen3-0-6b-20250429", type: "text", think: false },
{ name: "Qwen2.5-72B", modelName: "qwen2-5-72b-20240919", type: "text", think: false },
{ name: "GLM-4.5-Air", modelName: "glm-4-5-air", type: "text", think: false },
// ===================== 图片生成模型 =====================
{
name: "Seedream-5.0",
modelName: "doubao-seedream-5-0-260128",
type: "image",
mode: ["text", "singleImage", "multiReference"],
},
{
name: "Seedream-5.0-Lite",
modelName: "doubao-seedream-5-0-lite-260128",
type: "image",
mode: ["text", "singleImage", "multiReference"],
},
{
name: "Seedream-4.5",
modelName: "doubao-seedream-4-5-251128",
type: "image",
mode: ["text", "singleImage", "multiReference"],
},
{
name: "Seedream-4.0",
modelName: "doubao-seedream-4-0-250828",
type: "image",
mode: ["text", "singleImage", "multiReference"],
},
{
name: "Seedream-3.0-T2I",
modelName: "doubao-seedream-3-0-t2i-250415",
type: "image",
mode: ["text"],
},
// ===================== 视频生成模型 =====================
{
name: "Seedance-2.0(音画同生)",
modelName: "doubao-seedance-2-0-260128",
type: "video",
mode: ["text", "startFrameOptional", ["imageReference:9", "videoReference:3", "audioReference:3"]],
audio: "optional",
durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["480p", "720p"] }],
},
{
name: "Seedance-2.0-Fast(音画同生)",
modelName: "doubao-seedance-2-0-fast-260128",
type: "video",
mode: ["text", "startFrameOptional", ["imageReference:9", "videoReference:3", "audioReference:3"]],
audio: "optional",
durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], resolution: ["480p", "720p"] }],
},
{
name: "Seedance-1.5-Pro(音画同生)",
modelName: "doubao-seedance-1-5-pro-251215",
type: "video",
mode: ["text", "startFrameOptional"],
audio: "optional",
durationResolutionMap: [{ duration: [4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }],
},
{
name: "Seedance-1.0-Pro",
modelName: "doubao-seedance-1-0-pro-250528",
type: "video",
mode: ["text", "startFrameOptional"],
audio: false,
durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }],
},
{
name: "Seedance-1.0-Pro-Fast",
modelName: "doubao-seedance-1-0-pro-fast-251015",
type: "video",
mode: ["text", "singleImage"],
audio: false,
durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }],
},
{
name: "Seedance-1.0-Lite-T2V",
modelName: "doubao-seedance-1-0-lite-t2v-250428",
type: "video",
mode: ["text"],
audio: false,
durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }],
},
{
name: "Seedance-1.0-Lite-I2V",
modelName: "doubao-seedance-1-0-lite-i2v-250428",
type: "video",
mode: ["startFrameOptional", ["imageReference:4"]],
audio: false,
durationResolutionMap: [{ duration: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], resolution: ["480p", "720p", "1080p"] }],
},
],
};
// ============================================================
// 辅助工具
// ============================================================
const getHeaders = () => {
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
return {
"Content-Type": "application/json",
Authorization: `Bearer ${vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "")}`,
};
};
const getBaseUrl = () => vendor.inputValues.baseUrl.replace(/\/+$/, "");
// ============================================================
// 适配器函数
// ============================================================
const textRequest = (model: TextModel, think: boolean, thinkLevel: 0 | 1 | 2 | 3) => {
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
const effortMap: Record<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();
const content: any[] = [];
if (config.prompt) {
content.push({ type: "text", text: config.prompt });
}
if (config.referenceList && config.referenceList.length > 0) {
for (const ref of config.referenceList) {
content.push({
type: "image_url",
image_url: { url: ref.base64 },
});
}
}
const [w, h] = config.aspectRatio.split(":").map(Number);
const sizeMap: Record<string, { width: number; height: number }> = {
"1K": { width: 1024, height: Math.round(1024 * (h / w)) },
"2K": { width: 2048, height: Math.round(2048 * (h / w)) },
"4K": { width: 4096, height: Math.round(4096 * (h / w)) },
};
const size = sizeMap[config.size] || sizeMap["1K"];
const body = {
model: model.modelName,
content,
size: `${size.width}x${size.height}`,
response_format: "url",
};
logger(`[图片生成] 请求模型: ${model.modelName}`);
const response = await axios.post(`${baseUrl}/images/generations`, body, { headers });
const data = response.data;
if (data?.data?.[0]?.url) {
return await urlToBase64(data.data[0].url);
}
throw new Error("图片生成失败:未返回有效结果");
};
const videoRequest = async (config: VideoConfig, model: VideoModel): Promise<string> => {
const baseUrl = getBaseUrl();
const headers = getHeaders();
const content: any[] = [];
if (config.prompt) {
content.push({ type: "text", text: config.prompt });
}
const activeMode = config.mode && config.mode.length > 0 ? config.mode[0] : "text";
if (typeof activeMode === "string") {
switch (activeMode) {
case "singleImage": {
const firstImage = config.referenceList?.find((r) => r.type === "image");
if (firstImage) {
content.push({
type: "image_url",
image_url: { url: firstImage.base64 },
role: "first_frame",
});
}
break;
}
case "startFrameOptional": {
const images = config.referenceList?.filter((r) => r.type === "image") ?? [];
if (images.length > 0) {
content.push({
type: "image_url",
image_url: { url: images[0].base64 },
role: "first_frame",
});
if (images.length > 1) {
content.push({
type: "image_url",
image_url: { url: images[1].base64 },
role: "last_frame",
});
}
}
break;
}
case "startEndRequired": {
const images = config.referenceList?.filter((r) => r.type === "image") ?? [];
if (images.length >= 2) {
content.push({
type: "image_url",
image_url: { url: images[0].base64 },
role: "first_frame",
});
content.push({
type: "image_url",
image_url: { url: images[1].base64 },
role: "last_frame",
});
}
break;
}
case "endFrameOptional": {
const images = config.referenceList?.filter((r) => r.type === "image") ?? [];
if (images.length > 0) {
content.push({
type: "image_url",
image_url: { url: images[0].base64 },
role: "first_frame",
});
if (images.length > 1) {
content.push({
type: "image_url",
image_url: { url: images[1].base64 },
role: "last_frame",
});
}
}
break;
}
case "text":
default:
break;
}
} else if (Array.isArray(activeMode)) {
// 多模态参考模式:按类型分别提取并添加
const imageRefs = config.referenceList?.filter((r) => r.type === "image") ?? [];
const videoRefs = config.referenceList?.filter((r) => r.type === "video") ?? [];
const audioRefs = config.referenceList?.filter((r) => r.type === "audio") ?? [];
for (const refDef of activeMode) {
if (typeof refDef === "string") {
if (refDef.startsWith("imageReference:")) {
const maxCount = parseInt(refDef.split(":")[1], 10);
for (const ref of imageRefs.slice(0, maxCount)) {
content.push({
type: "image_url",
image_url: { url: ref.base64 },
role: "reference_image",
});
}
} else if (refDef.startsWith("videoReference:")) {
const maxCount = parseInt(refDef.split(":")[1], 10);
for (const ref of videoRefs.slice(0, maxCount)) {
content.push({
type: "video_url",
video_url: { url: ref.base64 },
role: "reference_video",
});
}
} else if (refDef.startsWith("audioReference:")) {
const maxCount = parseInt(refDef.split(":")[1], 10);
for (const ref of audioRefs.slice(0, maxCount)) {
content.push({
type: "audio_url",
audio_url: { url: ref.base64 },
role: "reference_audio",
});
}
}
}
}
}
const body: any = {
model: model.modelName,
content,
ratio: config.aspectRatio,
duration: config.duration,
resolution: config.resolution || "720p",
watermark: false,
};
if (model.audio === "optional") {
body.generate_audio = config.audio !== false;
} else if (model.audio === true) {
body.generate_audio = true;
} else {
body.generate_audio = false;
}
logger(`[视频生成] 提交任务, 模型: ${model.modelName}, 时长: ${config.duration}s, 分辨率: ${config.resolution}`);
const createResponse = await axios.post(`${baseUrl}/contents/generations/tasks`, body, { headers });
const taskId = createResponse.data?.id;
if (!taskId) {
throw new Error("视频生成任务创建失败未返回任务ID");
}
logger(`[视频生成] 任务已创建, ID: ${taskId}`);
const result = await pollTask(
async (): Promise<PollResult> => {
const queryResponse = await axios.get(`${baseUrl}/contents/generations/tasks/${taskId}`, { headers });
const task = queryResponse.data;
logger(`[视频生成] 任务状态: ${task.status}`);
switch (task.status) {
case "succeeded":
if (task.content?.video_url) {
return { completed: true, data: task.content.video_url };
}
return { completed: true, error: "任务成功但未返回视频URL" };
case "failed":
return { completed: true, error: task.error?.message || "视频生成失败" };
case "expired":
return { completed: true, error: "视频生成任务超时" };
case "cancelled":
return { completed: true, error: "视频生成任务已取消" };
default:
return { completed: false };
}
},
10000,
600000,
);
if (result.error) {
throw new Error(result.error);
}
return await urlToBase64(result.data!);
};
const ttsRequest = async (config: TTSConfig, model: TTSModel): Promise<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 {};

View File

@ -1 +1 @@
1.1.2
1.1.3

File diff suppressed because one or more lines are too long

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

@ -1,6 +1,6 @@
{
"name": "toonflow",
"version": "1.1.2",
"version": "1.1.3",
"description": "Toonflow 是一款 AI 短剧漫剧工具,能够利用 AI 技术将小说自动转化为剧本,并结合 AI 生成的图片和视频,实现高效的短剧创作。",
"author": "HBAI-Ltd <ltlctools@outlook.com>",
"license": "Apache-2.0",
@ -49,7 +49,6 @@
"better-sqlite3": "^12.8.0",
"compressing": "^2.1.0",
"cors": "^2.8.5",
"custom-electron-titlebar": "^4.2.8",
"dotenv": "^17.2.3",
"express": "^5.2.1",
"express-ws": "^5.0.2",

View File

@ -61,7 +61,34 @@ checker.init({ start: process.cwd() }, (err: Error, packages: Record<string, any
// 排除名单过滤
const filteredDeclare = needDeclare.filter((pkg) => pkg.name && !excludeNames.some((exName) => pkg.name.startsWith(exName)));
const content = filteredDeclare
// 去重:同一个 name@version 只保留一条,并合并 licenses
const dedupedDeclare = Array.from(
filteredDeclare
.reduce((acc, pkg) => {
const key = `${pkg.name}@${pkg.version}`;
const licenseList = Array.isArray(pkg.licenses) ? pkg.licenses : [pkg.licenses];
const existing = acc.get(key);
if (!existing) {
acc.set(key, {
...pkg,
licenses: [...new Set(licenseList.filter(Boolean))],
});
return acc;
}
const existingLicenses = Array.isArray(existing.licenses) ? existing.licenses : [existing.licenses];
existing.licenses = [...new Set([...existingLicenses, ...licenseList].filter(Boolean))];
if (!existing.repository && pkg.repository) {
existing.repository = pkg.repository;
}
return acc;
}, new Map<string, PackageInfo>())
.values()
);
const content = dedupedDeclare
.map(
(pkg) =>
`Name: ${pkg.name}\nLicense: ${Array.isArray(pkg.licenses) ? pkg.licenses.join(", ") : pkg.licenses}\nRepository: ${pkg.repository ?? "N/A"}`

View File

@ -7,7 +7,7 @@ import Module from "module";
app.commandLine.appendSwitch("disable-gpu-shader-disk-cache");
app.commandLine.appendSwitch("disable-features", "CalculateNativeWinOcclusion");
const TARGET_ENTRIES = new Set(["assets", "models", "serve", "skills", "web"]);
const TARGET_ENTRIES = new Set(["assets", "models", "serve", "skills", "web", "vendor"]);
function copyDir(src: string, dest: string): void {
if (!fs.existsSync(src)) return;

View File

@ -86,7 +86,12 @@ export default (toolCpnfig: ToolConfig) => {
description: "新增或更新衍生资产",
inputSchema: z.object({
assetsId: z.number().describe("关联的资产ID"),
id: z.number().nullable().describe("衍生资产ID,如果新增则为空"),
id: z.preprocess(
(val) => {
if (val === "null" || val === "" || val === undefined) return null;
return val;
},
z.number().nullable().describe("衍生资产ID,如果新增则为空")),
name: z.string().describe("衍生资产名称"),
desc: z.string().describe("衍生资产描述"),
}),
@ -140,7 +145,7 @@ export default (toolCpnfig: ToolConfig) => {
},
}),
generate_deriveAsset: tool({
description: "生成衍生资产",
description: "生成衍生资产图片",
inputSchema: z.object({
ids: z.array(z.number()).describe("需要生成的 衍生资产ID"),
}),

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

@ -1,4 +1,4 @@
// @routes-hash 5f94013d5d29facaed754713858ed40b
// @routes-hash 62534cff632db5d31442f1bca1932925
import { Express } from "express";
import route1 from "./routes/agents/clearMemory";
@ -8,137 +8,142 @@ import route4 from "./routes/artStyle/editArtStyle";
import route5 from "./routes/artStyle/extractStylePrompt";
import route6 from "./routes/artStyle/getArtStyle";
import route7 from "./routes/assets/addAssets";
import route8 from "./routes/assets/batchDelete";
import route9 from "./routes/assets/batchGenerationData";
import route10 from "./routes/assets/delAssets";
import route11 from "./routes/assets/delImage";
import route12 from "./routes/assets/getAssetsApi";
import route13 from "./routes/assets/getImage";
import route14 from "./routes/assets/getMaterialData";
import route15 from "./routes/assets/pollingImageAssets";
import route16 from "./routes/assets/pollingPromptAssets";
import route17 from "./routes/assets/saveAssets";
import route18 from "./routes/assets/updateAssets";
import route19 from "./routes/assets/uploadClip";
import route20 from "./routes/assetsGenerate/batchGenerateImageAssets";
import route21 from "./routes/assetsGenerate/batchPolishAssetsPrompt";
import route22 from "./routes/assetsGenerate/cancelGenerate";
import route23 from "./routes/assetsGenerate/generateAssets";
import route24 from "./routes/assetsGenerate/polishAssetsPrompt";
import route25 from "./routes/cornerScape/getAllAssets";
import route26 from "./routes/general/generalStatistics";
import route27 from "./routes/general/getSingleProject";
import route28 from "./routes/general/updateProject";
import route29 from "./routes/login/login";
import route30 from "./routes/migrate/migrateData";
import route31 from "./routes/modelSelect/getModelDetail";
import route32 from "./routes/modelSelect/getModelList";
import route33 from "./routes/novel/addNovel";
import route34 from "./routes/novel/batchDeleteNovel";
import route35 from "./routes/novel/delNovel";
import route36 from "./routes/novel/event/batchDeleteEvent";
import route37 from "./routes/novel/event/deletEvent";
import route38 from "./routes/novel/event/generateEvents";
import route39 from "./routes/novel/event/getEvent";
import route40 from "./routes/novel/getNovel";
import route41 from "./routes/novel/getNovelData";
import route42 from "./routes/novel/getNovelEventState";
import route43 from "./routes/novel/getNovelIndex";
import route44 from "./routes/novel/updateNovel";
import route45 from "./routes/other/deleteAllData";
import route46 from "./routes/other/getVersion";
import route47 from "./routes/production/assets/batchGenerateAssetsImage";
import route48 from "./routes/production/assets/deleteAssetsDireve";
import route49 from "./routes/production/assets/getAssetsData";
import route50 from "./routes/production/assets/pollingImage";
import route51 from "./routes/production/assets/updateAssetsUrl";
import route52 from "./routes/production/editImage/generateFlowImage";
import route53 from "./routes/production/editImage/getImageDefaultModle";
import route54 from "./routes/production/editImage/getImageFlow";
import route55 from "./routes/production/editImage/saveImageFlow";
import route56 from "./routes/production/editImage/updateImageFlow";
import route57 from "./routes/production/editImage/uploadImage";
import route58 from "./routes/production/getFlowData";
import route59 from "./routes/production/getProductionData";
import route60 from "./routes/production/getStoryboardData";
import route61 from "./routes/production/saveFlowData";
import route62 from "./routes/production/storyboard/addStoryboard";
import route63 from "./routes/production/storyboard/batchAddStoryboardInfo";
import route64 from "./routes/production/storyboard/batchGenerateImage";
import route65 from "./routes/production/storyboard/downPreviewImage";
import route66 from "./routes/production/storyboard/editStoryboardInfo";
import route67 from "./routes/production/storyboard/getStoryboardData";
import route68 from "./routes/production/storyboard/pollingImage";
import route69 from "./routes/production/storyboard/previewImage";
import route70 from "./routes/production/storyboard/removeFrame";
import route71 from "./routes/production/storyboard/updateStoryboardUrl";
import route72 from "./routes/production/workbench/addTrack";
import route73 from "./routes/production/workbench/deleteTrack";
import route74 from "./routes/production/workbench/delVideo";
import route75 from "./routes/production/workbench/generateVideo";
import route76 from "./routes/production/workbench/generateVideoPrompt";
import route77 from "./routes/production/workbench/getGenerateData";
import route78 from "./routes/production/workbench/getVideoList";
import route79 from "./routes/production/workbench/getVideoModelDetail";
import route80 from "./routes/production/workbench/selectVideo";
import route81 from "./routes/production/workbench/updateVideoPrompt";
import route82 from "./routes/project/addDirectorManual";
import route83 from "./routes/project/addProject";
import route84 from "./routes/project/addVisualManual";
import route85 from "./routes/project/deleteDirectorManual";
import route86 from "./routes/project/deleteVisualManual";
import route87 from "./routes/project/delProject";
import route88 from "./routes/project/editDirectorlManual";
import route89 from "./routes/project/editProject";
import route90 from "./routes/project/editVisualManual";
import route91 from "./routes/project/getProject";
import route92 from "./routes/project/getVisualManual";
import route93 from "./routes/project/queryDirectorManual";
import route94 from "./routes/project/visualManual";
import route95 from "./routes/script/addScript";
import route96 from "./routes/script/batchAddScript";
import route97 from "./routes/script/delScript";
import route98 from "./routes/script/exportScript";
import route99 from "./routes/script/extractAssets";
import route100 from "./routes/script/getScrptApi";
import route101 from "./routes/script/pollScriptAssets";
import route102 from "./routes/script/updateScript";
import route103 from "./routes/scriptAgent/getPlanData";
import route104 from "./routes/scriptAgent/setPlanData";
import route105 from "./routes/scriptAgent/updateData";
import route106 from "./routes/setting/about/checkUpdate";
import route107 from "./routes/setting/about/downloadApp";
import route108 from "./routes/setting/agentDeploy/agentSetKey";
import route109 from "./routes/setting/agentDeploy/deployAgentModel";
import route110 from "./routes/setting/agentDeploy/getAgentDeploy";
import route111 from "./routes/setting/dbConfig/clearData";
import route112 from "./routes/setting/dev/getSwitchAiDevTool";
import route113 from "./routes/setting/dev/updateSwitchAiDevTool";
import route114 from "./routes/setting/fileManagement/openFolder";
import route115 from "./routes/setting/getTextModel";
import route116 from "./routes/setting/loginConfig/getUser";
import route117 from "./routes/setting/loginConfig/updateUserPwd";
import route118 from "./routes/setting/memoryConfig/delAllMemory";
import route119 from "./routes/setting/memoryConfig/getMemory";
import route120 from "./routes/setting/memoryConfig/sureMemory";
import route121 from "./routes/setting/promptManage/getPrompt";
import route122 from "./routes/setting/promptManage/updatePrompt";
import route123 from "./routes/setting/skillManagement/getSkillContent";
import route124 from "./routes/setting/skillManagement/getSkillList";
import route125 from "./routes/setting/skillManagement/saveSkillContent";
import route126 from "./routes/setting/vendorConfig/addVendor";
import route127 from "./routes/setting/vendorConfig/deleteVendor";
import route128 from "./routes/setting/vendorConfig/enableVendor";
import route129 from "./routes/setting/vendorConfig/getCodeByLink";
import route130 from "./routes/setting/vendorConfig/getVendorList";
import route131 from "./routes/setting/vendorConfig/modelTest";
import route132 from "./routes/setting/vendorConfig/updateCode";
import route133 from "./routes/setting/vendorConfig/updateVendor";
import route134 from "./routes/task/getProject";
import route135 from "./routes/task/getTaskApi";
import route136 from "./routes/task/getTaskCategories";
import route137 from "./routes/task/taskDetails";
import route138 from "./routes/test/test";
import route8 from "./routes/assets/addAudioAssets";
import route9 from "./routes/assets/batchDelete";
import route10 from "./routes/assets/batchGenerationData";
import route11 from "./routes/assets/delAssets";
import route12 from "./routes/assets/delImage";
import route13 from "./routes/assets/getAssetsApi";
import route14 from "./routes/assets/getImage";
import route15 from "./routes/assets/getMaterialData";
import route16 from "./routes/assets/pollingImageAssets";
import route17 from "./routes/assets/pollingPromptAssets";
import route18 from "./routes/assets/saveAssets";
import route19 from "./routes/assets/updateAssets";
import route20 from "./routes/assets/updateAudioAssets";
import route21 from "./routes/assets/uploadClip";
import route22 from "./routes/assetsGenerate/batchGenerateImageAssets";
import route23 from "./routes/assetsGenerate/batchPolishAssetsPrompt";
import route24 from "./routes/assetsGenerate/cancelGenerate";
import route25 from "./routes/assetsGenerate/generateAssets";
import route26 from "./routes/assetsGenerate/polishAssetsPrompt";
import route27 from "./routes/cornerScape/batchBindAudio";
import route28 from "./routes/cornerScape/getAllAssets";
import route29 from "./routes/cornerScape/updateAssetsAudio";
import route30 from "./routes/general/generalStatistics";
import route31 from "./routes/general/getSingleProject";
import route32 from "./routes/general/updateProject";
import route33 from "./routes/login/login";
import route34 from "./routes/migrate/migrateData";
import route35 from "./routes/modelSelect/getModelDetail";
import route36 from "./routes/modelSelect/getModelList";
import route37 from "./routes/novel/addNovel";
import route38 from "./routes/novel/batchDeleteNovel";
import route39 from "./routes/novel/delNovel";
import route40 from "./routes/novel/event/batchDeleteEvent";
import route41 from "./routes/novel/event/deletEvent";
import route42 from "./routes/novel/event/generateEvents";
import route43 from "./routes/novel/event/getEvent";
import route44 from "./routes/novel/getNovel";
import route45 from "./routes/novel/getNovelData";
import route46 from "./routes/novel/getNovelEventState";
import route47 from "./routes/novel/getNovelIndex";
import route48 from "./routes/novel/updateNovel";
import route49 from "./routes/other/deleteAllData";
import route50 from "./routes/other/getVersion";
import route51 from "./routes/production/assets/batchGenerateAssetsImage";
import route52 from "./routes/production/assets/deleteAssetsDireve";
import route53 from "./routes/production/assets/getAssetsData";
import route54 from "./routes/production/assets/pollingImage";
import route55 from "./routes/production/assets/updateAssetsUrl";
import route56 from "./routes/production/editImage/generateFlowImage";
import route57 from "./routes/production/editImage/getImageDefaultModle";
import route58 from "./routes/production/editImage/getImageFlow";
import route59 from "./routes/production/editImage/saveImageFlow";
import route60 from "./routes/production/editImage/updateImageFlow";
import route61 from "./routes/production/editImage/uploadImage";
import route62 from "./routes/production/getFlowData";
import route63 from "./routes/production/getStoryboardData";
import route64 from "./routes/production/saveFlowData";
import route65 from "./routes/production/storyboard/addStoryboard";
import route66 from "./routes/production/storyboard/batchAddStoryboardInfo";
import route67 from "./routes/production/storyboard/batchGenerateImage";
import route68 from "./routes/production/storyboard/downPreviewImage";
import route69 from "./routes/production/storyboard/editStoryboardInfo";
import route70 from "./routes/production/storyboard/getStoryboardData";
import route71 from "./routes/production/storyboard/pollingImage";
import route72 from "./routes/production/storyboard/previewImage";
import route73 from "./routes/production/storyboard/removeFrame";
import route74 from "./routes/production/storyboard/updateStoryboardUrl";
import route75 from "./routes/production/workbench/addTrack";
import route76 from "./routes/production/workbench/deleteTrack";
import route77 from "./routes/production/workbench/delVideo";
import route78 from "./routes/production/workbench/generateVideo";
import route79 from "./routes/production/workbench/generateVideoPrompt";
import route80 from "./routes/production/workbench/getGenerateData";
import route81 from "./routes/production/workbench/getVideoList";
import route82 from "./routes/production/workbench/selectVideo";
import route83 from "./routes/production/workbench/updateVideoPrompt";
import route84 from "./routes/project/addDirectorManual";
import route85 from "./routes/project/addProject";
import route86 from "./routes/project/addVisualManual";
import route87 from "./routes/project/deleteDirectorManual";
import route88 from "./routes/project/deleteVisualManual";
import route89 from "./routes/project/delProject";
import route90 from "./routes/project/editDirectorlManual";
import route91 from "./routes/project/editProject";
import route92 from "./routes/project/editVisualManual";
import route93 from "./routes/project/getProject";
import route94 from "./routes/project/getVisualManual";
import route95 from "./routes/project/queryDirectorManual";
import route96 from "./routes/project/visualManual";
import route97 from "./routes/script/addScript";
import route98 from "./routes/script/batchAddScript";
import route99 from "./routes/script/delScript";
import route100 from "./routes/script/exportScript";
import route101 from "./routes/script/extractAssets";
import route102 from "./routes/script/getScrptApi";
import route103 from "./routes/script/pollScriptAssets";
import route104 from "./routes/script/updateScript";
import route105 from "./routes/scriptAgent/getPlanData";
import route106 from "./routes/scriptAgent/setPlanData";
import route107 from "./routes/scriptAgent/updateData";
import route108 from "./routes/setting/about/checkUpdate";
import route109 from "./routes/setting/about/downloadApp";
import route110 from "./routes/setting/agentDeploy/agentSetKey";
import route111 from "./routes/setting/agentDeploy/deployAgentModel";
import route112 from "./routes/setting/agentDeploy/getAgentDeploy";
import route113 from "./routes/setting/dbConfig/clearData";
import route114 from "./routes/setting/dev/getSwitchAiDevTool";
import route115 from "./routes/setting/dev/updateSwitchAiDevTool";
import route116 from "./routes/setting/fileManagement/openFolder";
import route117 from "./routes/setting/getTextModel";
import route118 from "./routes/setting/loginConfig/getUser";
import route119 from "./routes/setting/loginConfig/updateUserPwd";
import route120 from "./routes/setting/memoryConfig/delAllMemory";
import route121 from "./routes/setting/memoryConfig/getMemory";
import route122 from "./routes/setting/memoryConfig/sureMemory";
import route123 from "./routes/setting/promptManage/getPrompt";
import route124 from "./routes/setting/promptManage/updatePrompt";
import route125 from "./routes/setting/skillManagement/getSkillContent";
import route126 from "./routes/setting/skillManagement/getSkillList";
import route127 from "./routes/setting/skillManagement/saveSkillContent";
import route128 from "./routes/setting/vendorConfig/addVendor";
import route129 from "./routes/setting/vendorConfig/addVendorModel";
import route130 from "./routes/setting/vendorConfig/deleteVendor";
import route131 from "./routes/setting/vendorConfig/delVendorModel";
import route132 from "./routes/setting/vendorConfig/enableVendor";
import route133 from "./routes/setting/vendorConfig/getCodeByLink";
import route134 from "./routes/setting/vendorConfig/getVendorList";
import route135 from "./routes/setting/vendorConfig/modelTest";
import route136 from "./routes/setting/vendorConfig/updateCode";
import route137 from "./routes/setting/vendorConfig/updateVendorInputs";
import route138 from "./routes/setting/vendorConfig/upVendorModel";
import route139 from "./routes/task/getProject";
import route140 from "./routes/task/getTaskApi";
import route141 from "./routes/task/getTaskCategories";
import route142 from "./routes/task/taskDetails";
import route143 from "./routes/test/test";
export default async (app: Express) => {
app.use("/api/agents/clearMemory", route1);
@ -148,135 +153,140 @@ export default async (app: Express) => {
app.use("/api/artStyle/extractStylePrompt", route5);
app.use("/api/artStyle/getArtStyle", route6);
app.use("/api/assets/addAssets", route7);
app.use("/api/assets/batchDelete", route8);
app.use("/api/assets/batchGenerationData", route9);
app.use("/api/assets/delAssets", route10);
app.use("/api/assets/delImage", route11);
app.use("/api/assets/getAssetsApi", route12);
app.use("/api/assets/getImage", route13);
app.use("/api/assets/getMaterialData", route14);
app.use("/api/assets/pollingImageAssets", route15);
app.use("/api/assets/pollingPromptAssets", route16);
app.use("/api/assets/saveAssets", route17);
app.use("/api/assets/updateAssets", route18);
app.use("/api/assets/uploadClip", route19);
app.use("/api/assetsGenerate/batchGenerateImageAssets", route20);
app.use("/api/assetsGenerate/batchPolishAssetsPrompt", route21);
app.use("/api/assetsGenerate/cancelGenerate", route22);
app.use("/api/assetsGenerate/generateAssets", route23);
app.use("/api/assetsGenerate/polishAssetsPrompt", route24);
app.use("/api/cornerScape/getAllAssets", route25);
app.use("/api/general/generalStatistics", route26);
app.use("/api/general/getSingleProject", route27);
app.use("/api/general/updateProject", route28);
app.use("/api/login/login", route29);
app.use("/api/migrate/migrateData", route30);
app.use("/api/modelSelect/getModelDetail", route31);
app.use("/api/modelSelect/getModelList", route32);
app.use("/api/novel/addNovel", route33);
app.use("/api/novel/batchDeleteNovel", route34);
app.use("/api/novel/delNovel", route35);
app.use("/api/novel/event/batchDeleteEvent", route36);
app.use("/api/novel/event/deletEvent", route37);
app.use("/api/novel/event/generateEvents", route38);
app.use("/api/novel/event/getEvent", route39);
app.use("/api/novel/getNovel", route40);
app.use("/api/novel/getNovelData", route41);
app.use("/api/novel/getNovelEventState", route42);
app.use("/api/novel/getNovelIndex", route43);
app.use("/api/novel/updateNovel", route44);
app.use("/api/other/deleteAllData", route45);
app.use("/api/other/getVersion", route46);
app.use("/api/production/assets/batchGenerateAssetsImage", route47);
app.use("/api/production/assets/deleteAssetsDireve", route48);
app.use("/api/production/assets/getAssetsData", route49);
app.use("/api/production/assets/pollingImage", route50);
app.use("/api/production/assets/updateAssetsUrl", route51);
app.use("/api/production/editImage/generateFlowImage", route52);
app.use("/api/production/editImage/getImageDefaultModle", route53);
app.use("/api/production/editImage/getImageFlow", route54);
app.use("/api/production/editImage/saveImageFlow", route55);
app.use("/api/production/editImage/updateImageFlow", route56);
app.use("/api/production/editImage/uploadImage", route57);
app.use("/api/production/getFlowData", route58);
app.use("/api/production/getProductionData", route59);
app.use("/api/production/getStoryboardData", route60);
app.use("/api/production/saveFlowData", route61);
app.use("/api/production/storyboard/addStoryboard", route62);
app.use("/api/production/storyboard/batchAddStoryboardInfo", route63);
app.use("/api/production/storyboard/batchGenerateImage", route64);
app.use("/api/production/storyboard/downPreviewImage", route65);
app.use("/api/production/storyboard/editStoryboardInfo", route66);
app.use("/api/production/storyboard/getStoryboardData", route67);
app.use("/api/production/storyboard/pollingImage", route68);
app.use("/api/production/storyboard/previewImage", route69);
app.use("/api/production/storyboard/removeFrame", route70);
app.use("/api/production/storyboard/updateStoryboardUrl", route71);
app.use("/api/production/workbench/addTrack", route72);
app.use("/api/production/workbench/deleteTrack", route73);
app.use("/api/production/workbench/delVideo", route74);
app.use("/api/production/workbench/generateVideo", route75);
app.use("/api/production/workbench/generateVideoPrompt", route76);
app.use("/api/production/workbench/getGenerateData", route77);
app.use("/api/production/workbench/getVideoList", route78);
app.use("/api/production/workbench/getVideoModelDetail", route79);
app.use("/api/production/workbench/selectVideo", route80);
app.use("/api/production/workbench/updateVideoPrompt", route81);
app.use("/api/project/addDirectorManual", route82);
app.use("/api/project/addProject", route83);
app.use("/api/project/addVisualManual", route84);
app.use("/api/project/deleteDirectorManual", route85);
app.use("/api/project/deleteVisualManual", route86);
app.use("/api/project/delProject", route87);
app.use("/api/project/editDirectorlManual", route88);
app.use("/api/project/editProject", route89);
app.use("/api/project/editVisualManual", route90);
app.use("/api/project/getProject", route91);
app.use("/api/project/getVisualManual", route92);
app.use("/api/project/queryDirectorManual", route93);
app.use("/api/project/visualManual", route94);
app.use("/api/script/addScript", route95);
app.use("/api/script/batchAddScript", route96);
app.use("/api/script/delScript", route97);
app.use("/api/script/exportScript", route98);
app.use("/api/script/extractAssets", route99);
app.use("/api/script/getScrptApi", route100);
app.use("/api/script/pollScriptAssets", route101);
app.use("/api/script/updateScript", route102);
app.use("/api/scriptAgent/getPlanData", route103);
app.use("/api/scriptAgent/setPlanData", route104);
app.use("/api/scriptAgent/updateData", route105);
app.use("/api/setting/about/checkUpdate", route106);
app.use("/api/setting/about/downloadApp", route107);
app.use("/api/setting/agentDeploy/agentSetKey", route108);
app.use("/api/setting/agentDeploy/deployAgentModel", route109);
app.use("/api/setting/agentDeploy/getAgentDeploy", route110);
app.use("/api/setting/dbConfig/clearData", route111);
app.use("/api/setting/dev/getSwitchAiDevTool", route112);
app.use("/api/setting/dev/updateSwitchAiDevTool", route113);
app.use("/api/setting/fileManagement/openFolder", route114);
app.use("/api/setting/getTextModel", route115);
app.use("/api/setting/loginConfig/getUser", route116);
app.use("/api/setting/loginConfig/updateUserPwd", route117);
app.use("/api/setting/memoryConfig/delAllMemory", route118);
app.use("/api/setting/memoryConfig/getMemory", route119);
app.use("/api/setting/memoryConfig/sureMemory", route120);
app.use("/api/setting/promptManage/getPrompt", route121);
app.use("/api/setting/promptManage/updatePrompt", route122);
app.use("/api/setting/skillManagement/getSkillContent", route123);
app.use("/api/setting/skillManagement/getSkillList", route124);
app.use("/api/setting/skillManagement/saveSkillContent", route125);
app.use("/api/setting/vendorConfig/addVendor", route126);
app.use("/api/setting/vendorConfig/deleteVendor", route127);
app.use("/api/setting/vendorConfig/enableVendor", route128);
app.use("/api/setting/vendorConfig/getCodeByLink", route129);
app.use("/api/setting/vendorConfig/getVendorList", route130);
app.use("/api/setting/vendorConfig/modelTest", route131);
app.use("/api/setting/vendorConfig/updateCode", route132);
app.use("/api/setting/vendorConfig/updateVendor", route133);
app.use("/api/task/getProject", route134);
app.use("/api/task/getTaskApi", route135);
app.use("/api/task/getTaskCategories", route136);
app.use("/api/task/taskDetails", route137);
app.use("/api/test/test", route138);
app.use("/api/assets/addAudioAssets", route8);
app.use("/api/assets/batchDelete", route9);
app.use("/api/assets/batchGenerationData", route10);
app.use("/api/assets/delAssets", route11);
app.use("/api/assets/delImage", route12);
app.use("/api/assets/getAssetsApi", route13);
app.use("/api/assets/getImage", route14);
app.use("/api/assets/getMaterialData", route15);
app.use("/api/assets/pollingImageAssets", route16);
app.use("/api/assets/pollingPromptAssets", route17);
app.use("/api/assets/saveAssets", route18);
app.use("/api/assets/updateAssets", route19);
app.use("/api/assets/updateAudioAssets", route20);
app.use("/api/assets/uploadClip", route21);
app.use("/api/assetsGenerate/batchGenerateImageAssets", route22);
app.use("/api/assetsGenerate/batchPolishAssetsPrompt", route23);
app.use("/api/assetsGenerate/cancelGenerate", route24);
app.use("/api/assetsGenerate/generateAssets", route25);
app.use("/api/assetsGenerate/polishAssetsPrompt", route26);
app.use("/api/cornerScape/batchBindAudio", route27);
app.use("/api/cornerScape/getAllAssets", route28);
app.use("/api/cornerScape/updateAssetsAudio", route29);
app.use("/api/general/generalStatistics", route30);
app.use("/api/general/getSingleProject", route31);
app.use("/api/general/updateProject", route32);
app.use("/api/login/login", route33);
app.use("/api/migrate/migrateData", route34);
app.use("/api/modelSelect/getModelDetail", route35);
app.use("/api/modelSelect/getModelList", route36);
app.use("/api/novel/addNovel", route37);
app.use("/api/novel/batchDeleteNovel", route38);
app.use("/api/novel/delNovel", route39);
app.use("/api/novel/event/batchDeleteEvent", route40);
app.use("/api/novel/event/deletEvent", route41);
app.use("/api/novel/event/generateEvents", route42);
app.use("/api/novel/event/getEvent", route43);
app.use("/api/novel/getNovel", route44);
app.use("/api/novel/getNovelData", route45);
app.use("/api/novel/getNovelEventState", route46);
app.use("/api/novel/getNovelIndex", route47);
app.use("/api/novel/updateNovel", route48);
app.use("/api/other/deleteAllData", route49);
app.use("/api/other/getVersion", route50);
app.use("/api/production/assets/batchGenerateAssetsImage", route51);
app.use("/api/production/assets/deleteAssetsDireve", route52);
app.use("/api/production/assets/getAssetsData", route53);
app.use("/api/production/assets/pollingImage", route54);
app.use("/api/production/assets/updateAssetsUrl", route55);
app.use("/api/production/editImage/generateFlowImage", route56);
app.use("/api/production/editImage/getImageDefaultModle", route57);
app.use("/api/production/editImage/getImageFlow", route58);
app.use("/api/production/editImage/saveImageFlow", route59);
app.use("/api/production/editImage/updateImageFlow", route60);
app.use("/api/production/editImage/uploadImage", route61);
app.use("/api/production/getFlowData", route62);
app.use("/api/production/getStoryboardData", route63);
app.use("/api/production/saveFlowData", route64);
app.use("/api/production/storyboard/addStoryboard", route65);
app.use("/api/production/storyboard/batchAddStoryboardInfo", route66);
app.use("/api/production/storyboard/batchGenerateImage", route67);
app.use("/api/production/storyboard/downPreviewImage", route68);
app.use("/api/production/storyboard/editStoryboardInfo", route69);
app.use("/api/production/storyboard/getStoryboardData", route70);
app.use("/api/production/storyboard/pollingImage", route71);
app.use("/api/production/storyboard/previewImage", route72);
app.use("/api/production/storyboard/removeFrame", route73);
app.use("/api/production/storyboard/updateStoryboardUrl", route74);
app.use("/api/production/workbench/addTrack", route75);
app.use("/api/production/workbench/deleteTrack", route76);
app.use("/api/production/workbench/delVideo", route77);
app.use("/api/production/workbench/generateVideo", route78);
app.use("/api/production/workbench/generateVideoPrompt", route79);
app.use("/api/production/workbench/getGenerateData", route80);
app.use("/api/production/workbench/getVideoList", route81);
app.use("/api/production/workbench/selectVideo", route82);
app.use("/api/production/workbench/updateVideoPrompt", route83);
app.use("/api/project/addDirectorManual", route84);
app.use("/api/project/addProject", route85);
app.use("/api/project/addVisualManual", route86);
app.use("/api/project/deleteDirectorManual", route87);
app.use("/api/project/deleteVisualManual", route88);
app.use("/api/project/delProject", route89);
app.use("/api/project/editDirectorlManual", route90);
app.use("/api/project/editProject", route91);
app.use("/api/project/editVisualManual", route92);
app.use("/api/project/getProject", route93);
app.use("/api/project/getVisualManual", route94);
app.use("/api/project/queryDirectorManual", route95);
app.use("/api/project/visualManual", route96);
app.use("/api/script/addScript", route97);
app.use("/api/script/batchAddScript", route98);
app.use("/api/script/delScript", route99);
app.use("/api/script/exportScript", route100);
app.use("/api/script/extractAssets", route101);
app.use("/api/script/getScrptApi", route102);
app.use("/api/script/pollScriptAssets", route103);
app.use("/api/script/updateScript", route104);
app.use("/api/scriptAgent/getPlanData", route105);
app.use("/api/scriptAgent/setPlanData", route106);
app.use("/api/scriptAgent/updateData", route107);
app.use("/api/setting/about/checkUpdate", route108);
app.use("/api/setting/about/downloadApp", route109);
app.use("/api/setting/agentDeploy/agentSetKey", route110);
app.use("/api/setting/agentDeploy/deployAgentModel", route111);
app.use("/api/setting/agentDeploy/getAgentDeploy", route112);
app.use("/api/setting/dbConfig/clearData", route113);
app.use("/api/setting/dev/getSwitchAiDevTool", route114);
app.use("/api/setting/dev/updateSwitchAiDevTool", route115);
app.use("/api/setting/fileManagement/openFolder", route116);
app.use("/api/setting/getTextModel", route117);
app.use("/api/setting/loginConfig/getUser", route118);
app.use("/api/setting/loginConfig/updateUserPwd", route119);
app.use("/api/setting/memoryConfig/delAllMemory", route120);
app.use("/api/setting/memoryConfig/getMemory", route121);
app.use("/api/setting/memoryConfig/sureMemory", route122);
app.use("/api/setting/promptManage/getPrompt", route123);
app.use("/api/setting/promptManage/updatePrompt", route124);
app.use("/api/setting/skillManagement/getSkillContent", route125);
app.use("/api/setting/skillManagement/getSkillList", route126);
app.use("/api/setting/skillManagement/saveSkillContent", route127);
app.use("/api/setting/vendorConfig/addVendor", route128);
app.use("/api/setting/vendorConfig/addVendorModel", route129);
app.use("/api/setting/vendorConfig/deleteVendor", route130);
app.use("/api/setting/vendorConfig/delVendorModel", route131);
app.use("/api/setting/vendorConfig/enableVendor", route132);
app.use("/api/setting/vendorConfig/getCodeByLink", route133);
app.use("/api/setting/vendorConfig/getVendorList", route134);
app.use("/api/setting/vendorConfig/modelTest", route135);
app.use("/api/setting/vendorConfig/updateCode", route136);
app.use("/api/setting/vendorConfig/updateVendorInputs", route137);
app.use("/api/setting/vendorConfig/upVendorModel", route138);
app.use("/api/task/getProject", route139);
app.use("/api/task/getTaskApi", route140);
app.use("/api/task/getTaskCategories", route141);
app.use("/api/task/taskDetails", route142);
app.use("/api/test/test", route143);
}

View File

@ -0,0 +1,66 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 新增资产
export default router.post(
"/",
validateFields({
name: z.string(),
describe: z.string(),
projectId: z.number(),
assetsItem: z.array(
z.object({
base64: z.string(),
prompt: z.string(),
describe: z.string(),
name: z.string(),
}),
),
}),
async (req, res) => {
const { name, describe, projectId, assetsItem } = req.body;
await Promise.all(
assetsItem.map(async (i: { src?: string; base64: string; prompt: string }) => {
if (i.base64) {
const savePath = `/${projectId}/assets/audio/${u.uuid()}.mp4`;
await u.oss.writeFile(savePath, i.base64);
i.src = savePath;
}
}),
);
const [id] = await u.db("o_assets").insert({
name,
describe,
type: "audio",
projectId,
startTime: Date.now(),
});
for (const item of assetsItem) {
const [assetsId] = await u.db("o_assets").insert({
prompt: item.prompt,
assetsId: id,
type: "audio",
describe: item.describe,
name: item.name,
projectId,
startTime: Date.now(),
});
const [imageId] = await u.db("o_image").insert({
filePath: item.src,
type: "audio",
assetsId,
state: "已完成",
});
await u.db("o_assets").where("id", assetsId).update({
imageId,
});
}
res.status(200).send(success({ message: "新增资产成功" }));
},
);

View File

@ -28,6 +28,7 @@ export default router.post(
}
await u.db("o_image").where({ assetsId: id }).delete();
await u.db("o_assets").where({ id }).delete();
await u.db("o_assets").where("assetsId", id).delete();
res.status(200).send(success({ message: "删除资产成功" }));
},
);

View File

@ -57,6 +57,7 @@ export default router.post(
...parent,
sonAssets: childAssetsWithSrc.filter((child) => child.assetsId === parent.id),
src: parent.filePath && (await u.oss.getFileUrl(parent.filePath!)),
...(parent.type == "audio" ? { sex: parent.describe?.split("|")[0], describe: parent.describe?.split("|")[1] } : {}),
})),
);

View File

@ -0,0 +1,98 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 新增资产
export default router.post(
"/",
validateFields({
id: z.number(),
name: z.string(),
describe: z.string(),
projectId: z.number(),
assetsItem: z.array(
z.object({
src: z.string().optional(),
id: z.number().optional(),
base64: z.string().optional(),
prompt: z.string(),
describe: z.string(),
name: z.string(),
}),
),
}),
async (req, res) => {
const { id, name, describe, projectId, assetsItem } = req.body;
await Promise.all(
assetsItem.map(async (i: { src?: string; id?: number; base64: string; prompt: string }) => {
if (i.src) {
i.src = u.replaceUrl(i.src);
}
if (i.base64) {
const savePath = `/${projectId}/assets/audio/${u.uuid()}.mp4`;
await u.oss.writeFile(savePath, i.base64);
i.src = savePath;
}
}),
);
await u.db("o_assets").where("id", id).update({
name,
describe,
});
// 删除不在 assetsItem 中的子项
const existingItems = await u.db("o_assets").where("assetsId", id).select("id");
const existingIds = existingItems.map((i: { id?: number }) => i.id!);
const incomingIds = assetsItem.filter((i: { id?: number }) => i.id).map((i: { id?: number }) => i.id);
const toDeleteIds = existingIds.filter((eid: number) => !incomingIds.includes(eid));
if (toDeleteIds.length > 0) {
const deleteItems = await u.db("o_assets").whereIn("id", toDeleteIds).select("imageId");
const deleteImageIds = deleteItems.map((i: { imageId?: number | null }) => i.imageId!).filter(Boolean);
// 先将 o_assets.imageId 置空,解除外键约束,再删除 o_image最后删除 o_assets
await u.db("o_assets").whereIn("id", toDeleteIds).update({ imageId: null });
if (deleteImageIds.length > 0) {
await u.db("o_image").whereIn("id", deleteImageIds).delete();
}
await u.db("o_assets").whereIn("id", toDeleteIds).delete();
}
for (const item of assetsItem) {
if (item.id) {
await u.db("o_assets").where("id", item.id).update({
prompt: item.prompt,
describe: item.describe,
name: item.name,
});
const itemData = await u.db("o_assets").where("id", item.id).select("imageId").first();
await u.db("o_image").where("id", itemData?.imageId).update({
filePath: item.src,
});
} else {
const [assetsId] = await u.db("o_assets").insert({
prompt: item.prompt,
assetsId: id,
type: "audio",
projectId,
describe: item.describe,
name: item.name,
startTime: Date.now(),
});
const [imageId] = await u.db("o_image").insert({
filePath: item.src,
type: "audio",
assetsId,
state: "已完成",
});
await u.db("o_assets").where("id", assetsId).update({
imageId,
});
}
}
res.status(200).send(success({ message: "新增资产成功" }));
},
);

View File

@ -116,7 +116,7 @@ export default router.post("/", validateFields(requestSchema), async (req, res)
await aiImage.run(
{
prompt: userPrompt,
imageBase64: item.base64 ? [item.base64] : [],
referenceList: item.base64 ? [{ base64: item.base64, type: "image" }] : [],
size: resolution,
aspectRatio: "16:9",
},

View File

@ -100,7 +100,7 @@ export default router.post("/", validateFields(requestSchema), async (req, res)
await aiImage.run(
{
prompt: userPrompt,
imageBase64: base64 ? [base64] : [],
referenceList: base64 ? [{ type: "image", base64 }] : [],
size: resolution,
aspectRatio: "16:9",
},

View File

@ -0,0 +1,75 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import { tool } from "ai";
const router = express.Router();
// 获取资产
export default router.post(
"/",
validateFields({
projectId: z.number(),
assetsIds: z.array(z.number()),
concurrentCount: z.number().min(1).optional(),
}),
async (req, res) => {
const { projectId, assetsIds, concurrentCount } = req.body;
const assetsData = await u
.db("o_assets")
.where("type", "audio")
.whereIn("id", assetsIds)
.andWhere("projectId", projectId)
.select("id", "name", "describe");
console.log("%c Line:20 🍎 assetsData", "background:#b03734", assetsData);
const audioData = await u.db("o_assets").where("type", "audio").whereNull("assetsId").andWhere("projectId", projectId).select("id", "name", "describe");
console.log("%c Line:26 🍋 audioData", "background:#ea7e5c", audioData);
async function processGroup() {
try {
const resultTool = tool({
description: "返回结果时必须调用这个工具",
inputSchema: z.object({
result: z.array(z.object({
id: z.number(),
audioIds: z.array(z.number()).describe("适配的音频id 无适配内容可以为 空数组")
})).describe("适配的音色列表id为资产idaudioIds为适配的音频id 无适配内容可以为 空数组")
}),
execute: async ({ result }) => {
console.log("[tools] extractAssets result", result);
for (const item of result) {
await u.db("o_assetsRole2Audio").where("assetsRoleId", item.id).delete()
if (item.audioIds.length)
await u.db("o_assetsRole2Audio").insert(item.audioIds.map(i => ({ assetsRoleId: item.id, assetsAudioId: i })))
}
return "无需回复用户任何内容";
},
});
const { text } = await u.Ai.Text("universalAi").invoke({
messages: [
{
role: "system",
content: `请根据提供的资产内容描述 与 提供的音色 进行匹配,返回适配的音色,结果必须调用 resultTool 工具返回, 调用工具之后你无需回复用户任何内容。`,
},
{
role: "user",
content: `音频内容:${audioData.map(i => `Id:${i.id},音色名称:${i.name},描述:${i.describe}`).join("\n")}\n\n
${assetsData.map(i => `ID:${i.id},名称:${i.name},描述:${i.describe}`).join("\n")}\n\n
`,
},
],
tools: { resultTool },
});
console.log("%c Line:44 🍞 text", "background:#f5ce50", text);
} catch (e) {
console.error(`提取失败:`, e);
return;
}
}
await processGroup()
res.status(200).send(success());
},
);

View File

@ -32,6 +32,20 @@ export default router.post(
if (type && type.length > 0) qb.whereIn("o_assets.type", type);
})
.orderByRaw(`CASE o_assets.type WHEN 'role' THEN 1 WHEN 'scene' THEN 2 WHEN 'tool' THEN 3 ELSE 4 END`);
const assets2AudioData = await u
.db("o_assetsRole2Audio")
.leftJoin("o_assets", "o_assets.id", "o_assetsRole2Audio.assetsRoleId")
.whereIn(
"assetsRoleId",
data.map((i:any) => i.id!),
)
.select( "o_assets.id", "o_assets.name");
console.log("%c Line:36 🍎 assets2AudioData", "background:#3f7cff", assets2AudioData);
const repleAssets:Record<number,{id:number;name:string}[]> = {};
assets2AudioData.forEach((item) => {
if (!repleAssets[item.id]) repleAssets[item.id] = [item];
else repleAssets[item.id].push(item);
});
const result = await Promise.all(
data.map(async (parent: any) => {
const historyImages = await u.db("o_image").where("assetsId", parent.id).andWhere("state", "已完成").select("id", "filePath");
@ -45,6 +59,7 @@ export default router.post(
...parent,
filePath: parent.filePath && (await u.oss.getFileUrl(parent.filePath!)),
historyImages: historyImagesWithUrl,
relepedAudio:repleAssets[parent.id] ?? []
};
}),
);

View File

@ -0,0 +1,23 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
// 新增资产
export default router.post(
"/",
validateFields({
assetsId: z.number(),
audioIds: z.array(z.number()),
}),
async (req, res) => {
const { assetsId, audioIds } = req.body;
await u.db("o_assetsRole2Audio").where("assetsRoleId", assetsId).delete();
if (audioIds.length) {
await u.db("o_assetsRole2Audio").insert(audioIds.map((i: number) => ({ assetsRoleId: assetsId, assetsAudioId: i })));
}
res.status(200).send(success({ message: "更新音频成功" }));
},
);

View File

@ -13,11 +13,7 @@ export default router.post(
async (req, res) => {
const { modelId } = req.body;
const [id, name] = modelId.split(":");
const data = await u.db("o_vendorConfig").where("id", id).andWhere("enable", 1).select("models").first();
if (!data) {
return res.status(404).send({ error: "模型未找到" });
}
const models = JSON.parse(data.models!);
const models = await u.vendor.getModelList(id);
const findData = models.find((i: any) => i.modelName == name);
res.status(200).send(success(findData));
},

View File

@ -12,24 +12,28 @@ export default router.post(
}),
async (req, res) => {
const { type } = req.body;
const dataList = await u.db("o_vendorConfig").select("id", "models", "name").where("enable", 1);
const dataList = await u.db("o_vendorConfig").select("id").where("enable", 1);
if (!dataList || dataList.length === 0) {
return res.status(404).send({ error: "模型未找到" });
}
const result = dataList.flatMap((data) => {
const models = JSON.parse(data.models!);
const filtered =
type === "all"
? models.filter((item: { type: string }) => item.type !== "video")
: models.filter((item: { type: string }) => item.type === type);
return filtered.map((item: { name: string; modelName: string; type: string }) => ({
id: data.id,
label: item.name,
value: item.modelName,
type: item.type,
name: data.name,
}));
});
res.status(200).send(success(result));
const modelList = await Promise.all(dataList.map((i) => u.vendor.getModelList(i.id!)));
const result = await Promise.all(
dataList.map(async (data, index) => {
const vendorData = await u.vendor.getVendor(data.id!);
const models = modelList[index];
const filtered =
type === "all"
? models.filter((item: { type: string }) => item.type !== "video")
: models.filter((item: { type: string }) => item.type === type);
return filtered.map((item: { name: string; modelName: string; type: string }) => ({
id: data.id,
label: item.name,
value: item.modelName,
type: item.type,
name: vendorData.name,
}));
}),
);
res.status(200).send(success(result.flat()));
},
);

View File

@ -92,7 +92,7 @@ export default router.post(
};
const imageCls = await u.Ai.Image(projectSettingData?.imageModel as `${string}:${string}`).run(
{
imageBase64: imageBase64 ? [imageBase64] : [],
referenceList: imageBase64 ? [{ type: "image", base64: imageBase64 }] : [],
...repeloadObj,
},
{

View File

@ -31,7 +31,13 @@ export default router.post(
const imageClass = await u.Ai.Image(model).run(
{
prompt: prompt,
imageBase64: references && references.length ? await Promise.all(references.map((url: string) => urlToBase64(url))) : [],
referenceList: await (async () => {
const list: { type: "image"; base64: string }[] = [];
for (const url of references) {
list.push({ type: "image" as const, base64: await urlToBase64(url) });
}
return list;
})(),
size: quality,
aspectRatio: ratio,
},

View File

@ -73,6 +73,7 @@ export default router.post(
storyboardTable: "",
storyboard: [],
//todo矫正workbench数据
//@ts-ignore
workbench: {
videoList: [],
},
@ -99,7 +100,8 @@ export default router.post(
}),
);
const storyboardIds = storyboardData.map((i) => i.id);
const assetsIds = await u.db("o_assets2Storyboard").whereIn("storyboardId", storyboardIds);
const assetsIds = await u.db("o_assets2Storyboard").whereIn("storyboardId", storyboardIds).orderBy("rowid");
const assets2StoryboardMap: Record<number, number[]> = {};
assetsIds.forEach((i) => {
if (!assets2StoryboardMap[i.storyboardId!]) {

View File

@ -1,74 +0,0 @@
import express from "express";
import u from "@/utils";
import { z } from "zod";
import { success } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
const router = express.Router();
export default router.post(
"/",
validateFields({
ids: z.array(z.number()),
}),
async (req, res) => {
const { ids } = req.body;
//查询分镜配置
const storyboardConfigs = await u.db("o_videoConfig").whereIn("storyboardId", ids).select("*");
//查询视频数据
const videos = await u.db("o_video").whereIn("storyboardId", ids).select("*");
//组装数据
const data = await Promise.all(
ids.map(async (storyboardId: number) => {
// 处理配置
const configRow = storyboardConfigs.find((item) => item.storyboardId === storyboardId) || null;
let config = null;
if (configRow?.data) {
const parsedData = JSON.parse(configRow.data);
const dataWithFilePath = await Promise.all(
parsedData.map(async (d: { type: string; id: number }) => {
if (d.type === "assets" && d.id) {
const row = await u
.db("o_assets")
.where("o_assets.id", d.id)
.leftJoin("o_image", "o_assets.imageId", "o_image.id")
.select("o_image.filePath as imageFilePath")
.first();
if (row?.imageFilePath) {
return { id: d.id, type: "assets", url: await u.oss.getFileUrl(row.imageFilePath) };
}
return null;
}
if (d.type === "storyboard" && d.id) {
const row = await u.db("o_storyboard").where("id", d.id).select("filePath").first();
if (row?.filePath) {
return { id: d.id, type: "storyboard", url: await u.oss.getFileUrl(row.filePath) };
}
return null;
}
return null;
}),
);
config = { ...configRow, data: dataWithFilePath };
}
// 处理视频
const storyboardVideos = videos.filter((v) => v.storyboardId === storyboardId);
const videosList = await Promise.all(
storyboardVideos.map(async (item) => ({
...item,
filePath: item.filePath ? await u.oss.getFileUrl(item.filePath) : null,
})),
);
return {
id: storyboardId,
config,
videos: videosList,
};
}),
);
return res.status(200).send(success(data));
},
);

View File

@ -54,7 +54,6 @@ export default router.post(
.leftJoin("o_assets2Storyboard", "o_assets.id", "o_assets2Storyboard.assetId")
.whereIn("o_assets2Storyboard.storyboardId", finalStoryboardIds)
.select("o_assets2Storyboard.storyboardId", "o_assets.imageId");
console.log("%c Line:42 🥪 assetData", "background:#ea7e5c", assetData);
const assetRecord: Record<number, number[]> = {};
assetData.forEach((item: any) => {
@ -87,7 +86,7 @@ export default router.post(
await u.Ai.Image(projectSettingData?.imageModel as `${string}:${string}`)
.run(
{
imageBase64: await getAssetsImageBase64(assetRecord[item.id!] || []),
referenceList: await getAssetsImageBase64(assetRecord[item.id!] || []),
...repeloadObj,
},
{
@ -151,5 +150,5 @@ async function getAssetsImageBase64(imageIds: number[]) {
}),
);
// 保留顺序,并且过滤掉无效项
return imageUrls.filter(Boolean) as string[];
return (imageUrls.filter(Boolean) as string[]).map((url) => ({ type: "image" as const, base64: url }));
}

View File

@ -13,7 +13,6 @@ export default router.post(
async (req, res) => {
const { id } = req.body;
await u.db("o_videoTrack").where("id", id).delete();
await u.db("o_storyboard").where("trackId", id).delete();
res.status(200).send(success({ message: "视频段删除成功" }));
},
);

View File

@ -95,10 +95,10 @@ export default router.post(
await aiVideo.run(
{
prompt,
imageBase64: base64.filter((item) => item !== null) as string[],
referenceList: base64.filter((item) => item !== null).map((item) => ({ type: "image" as const, base64: item! })),
mode: modeData.length > 0 ? modeData : mode,
duration,
aspectRatio: (ratio?.videoRatio as `${number}:${number}`) || "16:9",
aspectRatio: (ratio?.videoRatio as "16:9" | "9:16") || "16:9",
resolution,
audio,
},

View File

@ -42,7 +42,7 @@ export default router.post(
}
if (item.sources === "assets") {
// 查询素材
const assetsData = await u.db("o_assets").where("o_assets.id", item.id).select("id", "type", "name").first();
const assetsData = await u.db("o_assets").leftJoin("o_image","o_image.id","o_assets.imageId").where("o_assets.id", item.id).select("o_assets.id", "o_assets.type", "o_assets.name","o_image.filePath").first();
return {
...assetsData,
_type: "assets", // 标记类型
@ -61,6 +61,7 @@ export default router.post(
id: item.id,
type: item.type,
name: item.name,
filePath:item.filePath
});
if (item._type === "storyboard")
storyboard.push({
@ -85,18 +86,16 @@ export default router.post(
const visualManual = u.getArtPrompt(artStyle, "art_skills", "art_storyboard_video");
const content = `
****${modelData},
****):${assets.map((i) => `[${i.id},${i.type},${i.name}]`).join("")},
****):${assets.filter(i => i.filePath).map((i) => `[${i.id},${i.type},${i.name}]`).join("")},
****${storyboard.map(
(i) => `<storyboardItem
videoDesc='${i.videoDesc}'
prompt='${i.prompt}'
track='${i.track}'
duration='${i.duration}'
associateAssetsIds='[${i.associateAssetsIds}]'
shouldGenerateImage='${i.shouldGenerateImage == 1 ? "true" : "false"}'
></storyboardItem>`,
)},
`;
console.log("%c Line:87 🌮 content", "background:#2eafb0", content);
try {
const { text } = await u.Ai.Text("universalAi").invoke({
system: videoPromptGeneration,

View File

@ -37,15 +37,18 @@ export default router.post(
}),
async (req, res) => {
const { projectId, scriptId } = req.body;
const projectData = await u.db("o_project").where("id", projectId).select("id", "videoModel").first();
const projectData = await u.db("o_project").where("id", projectId).select("id", "videoModel","mode").first();
if (!projectData?.videoModel) {
return res.status(400).json(success("项目未配置视频模型"));
}
const [videoId, videoModelName] = projectData.videoModel.split(":");
const vendorData = await u.db("o_vendorConfig").where("id", videoId).select("models").first();
const models = JSON.parse(vendorData!.models!);
const findData = models.find((i: any) => i.modelName == videoModelName);
const isRef = findData.mode.every((i: any) => Array.isArray(i));
let videoMode = ""
try{
videoMode = JSON.parse(projectData?.mode ?? "")
}catch(e){
videoMode = projectData?.mode ?? ""
}
const isRef = Array.isArray(videoMode) ? true : false;
const storyboardList = await u.db("o_storyboard").where({ scriptId, projectId }).orderBy("index", "asc");
await Promise.all(
@ -62,6 +65,7 @@ export default router.post(
sources: "storyboard",
...(i.prompt != null ? { prompt: i.videoDesc } : {}),
...(i.id != null ? { id: i.id } : {}),
index: i.index,
});
} else {
storyboardTrackRecord[i.trackId!] = [
@ -71,6 +75,7 @@ export default router.post(
sources: "storyboard",
...(i.prompt != null ? { prompt: i.videoDesc } : {}),
...(i.id != null ? { id: i.id } : {}),
index: i.index,
},
];
}
@ -104,7 +109,6 @@ export default router.post(
);
}
const id = await u.db("o_project").where({ id: projectId }).select("id").first();
const trackData = await u.db("o_videoTrack").where({ projectId, scriptId });
const videoList = await u.db("o_video").whereIn(
"videoTrackId",
@ -130,7 +134,10 @@ export default router.post(
seenAssetIds.add(a.id);
return true;
});
return [...storyboardMedias, ...uniqueAssets];
const hasImageAssetData = uniqueAssets.filter(i => i.src)
const notHasImageAssetData = uniqueAssets.filter(i => !i.src)
return [...hasImageAssetData, ...storyboardMedias, ...notHasImageAssetData];
})(),
videoList: await Promise.all(
videoList

View File

@ -1,17 +0,0 @@
import express from "express";
import u from "@/utils";
const router = express.Router();
export default router.post("/", async (req, res) => {
const { type } = req.body;
const vendorData = await u.db("o_vendorConfig").select("id", "models", "name");
if (!vendorData) {
return res.status(404).send({ error: "模型未找到" });
}
for (const item of vendorData) {
const modelsData = JSON.parse(item.models! ?? "[]");
const filterData = modelsData.filter((item: { type: string }) => item.type === type);
if (filterData.length > 0) {
}
}
});

View File

@ -4,6 +4,6 @@ import u from "@/utils";
const router = express.Router();
export default router.post("/", async (req, res) => {
const data = await u.db("o_agentDeploy").leftJoin("o_vendorConfig", "o_vendorConfig.id", "o_agentDeploy.vendorId").select("o_agentDeploy.*", "o_vendorConfig.icon");
const data = await u.db("o_agentDeploy").leftJoin("o_vendorConfig", "o_vendorConfig.id", "o_agentDeploy.vendorId").select("o_agentDeploy.*");
res.status(200).send(success(data));
});

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,26 +75,39 @@ export default router.post(
const vendor = exports.vendor;
const result = vendorConfigSchema.safeParse(vendor);
if (!result.success) {
const errorMsg = result.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
return res.status(400).send(error(`vendor配置校验失败: ${errorMsg}`));
const issueLines = result.error.issues.map((issue, index) => {
const path = issue.path.length ? issue.path.join(".") : "root";
let detail = issue.message;
if (issue.code === "invalid_union") {
const unionDetails = [
...new Set(
issue.errors
.flat()
.map((e) => e.message)
.filter(Boolean),
),
];
if (unionDetails.length > 0) {
detail = `${issue.message}${unionDetails.join("")}`;
}
}
return `${index + 1}. ${path}: ${detail}`;
});
return res.status(400).send(error(`vendor配置校验失败${issueLines.length} 处:\n${issueLines.join("\n")}`));
}
if ((vendor.id as string).includes(":")) return res.status(400).send(error("id不能包含英文冒号"));
const data = await u.db("o_vendorConfig").where("id", vendor.id).first();
if (data) return res.status(500).send(error("供应商id已存在"));
await u.db("o_vendorConfig").insert({
const [id] = await u.db("o_vendorConfig").insert({
id: vendor.id,
author: vendor.author,
description: vendor.description || "",
name: vendor.name,
icon: vendor.icon || "",
inputs: JSON.stringify(vendor.inputs ?? []),
inputValues: JSON.stringify(vendor.inputValues ?? {}),
models: JSON.stringify(vendor.models ?? []),
code: tsCode,
createTime: Date.now(),
models: JSON.stringify([]),
enable: vendor.id == "toonflow" ? 1 : 0,
});
u.vendor.writeCode(vendor.id, tsCode);
res.status(200).send(success(result.data));
},
);

View File

@ -0,0 +1,61 @@
import express from "express";
import { success, error } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import u from "@/utils";
import { z } from "zod";
const router = express.Router();
export default router.post(
"/",
validateFields({
id: z.string(),
model: z.discriminatedUnion("type", [
z.object({
name: z.string(),
modelName: z.string(),
type: z.literal("text"),
think: z.boolean(),
}),
z.object({
name: z.string(),
modelName: z.string(),
type: z.literal("image"),
mode: z.array(z.enum(["text", "singleImage", "multiReference"])),
}),
z.object({
name: z.string(),
modelName: z.string(),
type: z.literal("video"),
mode: z.array(
z.union([
z.enum(["singleImage", "startEndRequired", "endFrameOptional", "startFrameOptional", "text", "audioReference", "videoReference"]),
z.array(z.string().regex(/^(videoReference|imageReference|audioReference):\d+$/)),
]),
),
audio: z.union([z.literal("optional"), z.boolean()]),
durationResolutionMap: z.array(
z.object({
duration: z.array(z.number()),
resolution: z.array(z.string()),
}),
),
}),
]),
}),
async (req, res) => {
const { id, model } = req.body;
const models = await u.db("o_vendorConfig").where("id", id).first("models");
if (models?.models) {
const existingModels = JSON.parse(models.models);
existingModels.push(model);
await u
.db("o_vendorConfig")
.where("id", id)
.update({
models: JSON.stringify(existingModels),
});
}
res.status(200).send(success("更新成功"));
},
);

View File

@ -0,0 +1,33 @@
import express from "express";
import { success, error } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import u from "@/utils";
import { z } from "zod";
const router = express.Router();
export default router.post(
"/",
validateFields({
id: z.string(),
modelName: z.string(),
}),
async (req, res) => {
const { id, modelName } = req.body;
const models = await u.db("o_vendorConfig").where("id", id).first("models");
if (models?.models) {
const existingModels = JSON.parse(models.models);
if (!existingModels.some((model: any) => model.modelName === modelName)) {
return res.status(400).send(error("基本模型不允许删除"));
}
const updatedModels = existingModels.filter((model: any) => model.modelName !== modelName);
await u
.db("o_vendorConfig")
.where("id", id)
.update({
models: JSON.stringify(updatedModels),
});
}
res.status(200).send(success("更新成功"));
},
);

View File

@ -1,6 +1,8 @@
import express from "express";
import { success, error } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import path from "path";
import fs from "fs";
import u from "@/utils";
import { z } from "zod";
const router = express.Router();
@ -16,6 +18,7 @@ export default router.post(
model: null,
vendorId: null,
});
fs.rmSync(path.join(u.getPath("vendor"), `${id}.ts`), { recursive: true, force: true });
res.status(200).send(success("删除成功"));
},
);

View File

@ -12,9 +12,7 @@ export default router.post(
}),
async (req, res) => {
const { id, enable } = req.body;
await u.db("o_vendorConfig").where("id", id).update({
enable,
});
await u.db("o_vendorConfig").where("id", id).update({ enable });
res.status(200).send(success("更新成功"));
},
);

View File

@ -6,11 +6,23 @@ const router = express.Router();
export default router.post("/", async (req, res) => {
const data = await u.db("o_vendorConfig").select("*");
const list = data.map((item) => ({
...item,
inputs: JSON.parse(item.inputs ?? "{}"),
inputValues: JSON.parse(item.inputValues ?? "{}"),
models: JSON.parse(item.models ?? "[]"),
}));
const list = await Promise.all(
data.map(async (item) => {
const vendor = u.vendor.getVendor(item.id!);
return {
...item,
inputValues: JSON.parse(item.inputValues ?? "{}"),
models: await u.vendor.getModelList(item.id!),
code: u.vendor.getCode(item.id!),
description: vendor.description,
inputs: vendor.inputs,
author: vendor.author,
name: vendor.name,
version: vendor.version ?? "1.0",
};
}),
);
list.sort((a, b) => (a.id === "toonflow" ? -1 : b.id === "toonflow" ? 1 : 0));
res.status(200).send(success(list));
});

View File

@ -25,7 +25,7 @@ export default router.post(
modelData: {
prompt:
"一张16:9比例的图片完美等分为2x2四宫格布局各区域无缝衔接\n左上宫格一只可爱的猫毛发蓬松眼睛明亮姿态俏皮\n右上宫格一只友善的狗金毛犬表情愉悦摇着尾巴\n左下宫格一头健壮的牛田园背景目光温和皮毛光泽\n右下宫格一匹骏马姿态优雅鬃毛飘逸肌肉健美\n风格要求四个宫格风格统一色彩鲜艳饱和高清画质细节清晰锐利专业插画风格线条干净统一的左上方光源柔和阴影和谐配色卡通/半写实风格,宫格间用白色或浅灰细线分隔", //图片提示词
imageBase64: [], //输入的图片提示词
referenceList: [], //输入的图片提示词
size: "1K", // 图片尺寸
aspectRatio: "16:9",
},
@ -37,7 +37,7 @@ export default router.post(
if (!vendorConfigData) return res.status(500).send(error("未找到该供应商配置"));
if (!vendorConfigData.models) return res.status(500).send(error("未找到模型列表"));
const modelList = JSON.parse(vendorConfigData.models);
const modelList = await u.vendor.getModelList(vendorConfigData.id!);
const selectedModel = modelList.find((i: any) => i.modelName == modelName);
if (type == "video") {
@ -46,8 +46,9 @@ export default router.post(
duration: selectedModel.durationResolutionMap[0].duration[0],
resolution: selectedModel.durationResolutionMap[0].resolution[0],
aspectRatio: "16:9",
prompt: "生成一个卖火柴的小女孩,保持镜头稳定,从远景到近景",
imageBase64: [],
prompt:
"A shirtless middle-aged man with a horse head is standing in a supermarket, carefully comparing two identical bottles of shampoo for 3 seconds, then suddenly bursts into tears, drops to his knees dramatically, a flock of pigeons explodes out of nowhere from behind him, the supermarket lights flicker, an old grandma nearby continues shopping completely unbothered, the horse head man instantly stops crying, puts both shampoo bottles back, and moonwalks away disappearing into the vegetable section. Security camera footage style, slightly grainy, 5 seconds.",
referenceList: [],
audio: false,
mode: "text",
};

View File

@ -0,0 +1,66 @@
import express from "express";
import { success, error } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import u from "@/utils";
import { z } from "zod";
const router = express.Router();
export default router.post(
"/",
validateFields({
id: z.string(),
modelName: z.string(),
model: z.discriminatedUnion("type", [
z.object({
name: z.string(),
modelName: z.string(),
type: z.literal("text"),
think: z.boolean(),
}),
z.object({
name: z.string(),
modelName: z.string(),
type: z.literal("image"),
mode: z.array(z.enum(["text", "singleImage", "multiReference"])),
}),
z.object({
name: z.string(),
modelName: z.string(),
type: z.literal("video"),
mode: z.array(
z.union([
z.enum(["singleImage", "startEndRequired", "endFrameOptional", "startFrameOptional", "text", "audioReference", "videoReference"]),
z.array(z.string().regex(/^(videoReference|imageReference|audioReference):\d+$/)),
]),
),
audio: z.union([z.literal("optional"), z.boolean()]),
durationResolutionMap: z.array(
z.object({
duration: z.array(z.number()),
resolution: z.array(z.string()),
}),
),
}),
]),
}),
async (req, res) => {
const { id, modelName, model } = req.body;
const models = await u.db("o_vendorConfig").where("id", id).first("models");
if (models?.models) {
const existingModels = JSON.parse(models.models);
const modelIndex = existingModels.findIndex((m: any) => m.modelName !== modelName);
if (modelIndex === -1) {
existingModels.push(model);
}
existingModels[modelIndex] = model;
await u
.db("o_vendorConfig")
.where("id", id)
.update({
models: JSON.stringify(existingModels),
});
}
res.status(200).send(success("更新成功"));
},
);

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()]),
@ -85,16 +85,11 @@ export default router.post(
.db("o_vendorConfig")
.where("id", id)
.update({
author: vendor.author,
description: vendor.description || "",
name: vendor.name,
icon: vendor.icon || "",
inputs: JSON.stringify(vendor.inputs ?? []),
inputValues: JSON.stringify(vendor.inputValues ?? {}),
models: JSON.stringify(vendor.models ?? []),
code: tsCode,
createTime: Date.now(),
});
u.vendor.writeCode(id, tsCode);
res.status(200).send(success(result.data));
} catch (err) {
console.log(err);

View File

@ -1,71 +0,0 @@
import express from "express";
import { success, error } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import u from "@/utils";
import { z } from "zod";
import { transform } from "sucrase";
const router = express.Router();
export default router.post(
"/",
validateFields({
id: z.string(),
inputValues: z.record(z.string(), z.string()),
inputs: z.array(
z.object({
key: z.string(),
label: z.string(),
type: z.enum(["text", "password", "url"]),
required: z.boolean(),
placeholder: z.string().optional(),
}),
),
models: z.array(
z.discriminatedUnion("type", [
z.object({
name: z.string(),
modelName: z.string(),
type: z.literal("text"),
think: z.boolean(),
}),
z.object({
name: z.string(),
modelName: z.string(),
type: z.literal("image"),
mode: z.array(z.enum(["text", "singleImage", "multiReference"])),
}),
z.object({
name: z.string(),
modelName: z.string(),
type: z.literal("video"),
mode: z.array(
z.union([
z.enum(["singleImage", "startEndRequired", "endFrameOptional", "startFrameOptional", "text"]),
z.array(z.enum(["audioReference", "videoReference", "textReference", "imageReference"])),
]),
),
audio: z.union([z.literal("optional"), z.boolean()]),
durationResolutionMap: z.array(
z.object({
duration: z.array(z.number()),
resolution: z.array(z.string()),
}),
),
}),
]),
),
}),
async (req, res) => {
const { id, models, inputs, inputValues } = req.body;
await u
.db("o_vendorConfig")
.where("id", id)
.update({
inputs: JSON.stringify(inputs),
inputValues: JSON.stringify(inputValues),
models: JSON.stringify(models),
});
res.status(200).send(success("更新成功"));
},
);

View File

@ -0,0 +1,26 @@
import express from "express";
import { success, error } from "@/lib/responseFormat";
import { validateFields } from "@/middleware/middleware";
import u from "@/utils";
import { z } from "zod";
import { transform } from "sucrase";
const router = express.Router();
export default router.post(
"/",
validateFields({
id: z.string(),
inputValues: z.record(z.string(), z.string()),
}),
async (req, res) => {
const { id, inputValues } = req.body;
await u
.db("o_vendorConfig")
.where("id", id)
.update({
inputValues: JSON.stringify(inputValues),
});
res.status(200).send(success("更新成功"));
},
);

View File

@ -59,6 +59,10 @@ export interface o_assets2Storyboard {
'assetId'?: number;
'storyboardId'?: number;
}
export interface o_assetsRole2Audio {
'assetsAudioId'?: number;
'assetsRoleId'?: number;
}
export interface o_event {
'createTime'?: number | null;
'detail'?: string | null;
@ -197,17 +201,10 @@ export interface o_user {
'password'?: string | null;
}
export interface o_vendorConfig {
'author'?: string | null;
'code'?: string | null;
'createTime'?: number | null;
'description'?: string | null;
'enable'?: number | null;
'icon'?: string | null;
'id'?: string;
'inputs'?: string | null;
'inputValues'?: string | null;
'models'?: string | null;
'name'?: string | null;
}
export interface o_video {
'errorReason'?: string | null;
@ -238,6 +235,7 @@ export interface DB {
"o_artStyle": o_artStyle;
"o_assets": o_assets;
"o_assets2Storyboard": o_assets2Storyboard;
"o_assetsRole2Audio": o_assetsRole2Audio;
"o_event": o_event;
"o_eventChapter": o_eventChapter;
"o_image": o_image;

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,73 @@ 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]);
}
}
function referenceList2imageBase642(id: string, input: any) {
const version = u.vendor.getVendor(id).version;
if (!version || isNaN(parseFloat(version)) || parseFloat(version) < 2.0) {
input.imageBase64 = input.referenceList.map((item: any) => item.base64);
return input;
}
return input;
}
type ReferenceList = { type: "image"; base64: string } | { type: "audio"; base64: string } | { type: "video"; base64: string };
interface ImageConfig {
prompt: string; //图片提示词
imageBase64: string[]; //输入的图片提示词
size: "1K" | "2K" | "4K"; // 图片尺寸
aspectRatio: `${number}:${number}`; // 长宽比
prompt: string;
referenceList?: Extract<ReferenceList, { type: "image" }>[];
size: "1K" | "2K" | "4K";
aspectRatio: `${number}:${number}`;
}
interface TaskRecord {
@ -131,6 +168,7 @@ class AiImage {
const modelName = await resolveModelName(this.key);
const exec = async (mn: `${string}:${string}`) => {
const fn = await getVendorTemplateFn("imageRequest", mn);
await referenceList2imageBase642(mn.split(":")[0], input);
this.result = await fn(input);
if (this.result.startsWith("http")) this.result = await urlToBase64(this.result);
return this;
@ -145,14 +183,23 @@ class AiImage {
return this;
}
}
type VideoMode =
| "singleImage" //单图参考
| "startEndRequired" //首尾帧(两张都得有)
| "endFrameOptional" //首尾帧(尾帧可选)
| "startFrameOptional" //首尾帧(首帧可选)
| "text" //文本
| (`videoReference:${number}` | `imageReference:${number}` | `audioReference:${number}`)[]; //多参考(数字代表限制数量)
interface VideoConfig {
prompt: string; //视频提示词
imageBase64: string[]; //输入的图片提示词
aspectRatio: `${number}:${number}`; // 长宽比
mode: string; //模式
duration: number; // 视频时长,单位秒
resolution: string; // 视频分辨率
audio: boolean; // 是否需要配音
duration: number;
resolution: string;
aspectRatio: "16:9" | "9:16";
prompt: string;
referenceList?: ReferenceList[];
audio?: boolean;
mode: VideoMode[];
}
class AiVideo {
@ -165,6 +212,7 @@ class AiVideo {
const modelName = await resolveModelName(this.key);
const exec = async (mn: `${string}:${string}`) => {
const fn = await getVendorTemplateFn("videoRequest", mn);
await referenceList2imageBase642(mn.split(":")[0], input);
this.result = await fn(input);
if (this.result.startsWith("http")) this.result = await urlToBase64(this.result);
return this;
@ -189,6 +237,7 @@ class AiAudio {
const modelName = await resolveModelName(this.key);
const exec = async (mn: `${string}:${string}`) => {
const fn = await getVendorTemplateFn("ttsRequest", mn);
await referenceList2imageBase642(mn.split(":")[0], input);
this.result = await fn(input);
if (this.result.startsWith("http")) this.result = await urlToBase64(this.result);
return this;
@ -205,7 +254,7 @@ class AiAudio {
}
export default {
Text: (AiType: AiType | `${string}:${string}`) => new AiText(AiType),
Text: (AiType: AiType | `${string}:${string}`, think?: boolean, thinkLevel?: 0 | 1 | 2 | 3) => new AiText(AiType, think, thinkLevel),
Image: (key: `${string}:${string}`) => new AiImage(key),
Video: (key: `${string}:${string}`) => new AiVideo(key),
Audio: (key: `${string}:${string}`) => new AiAudio(key),

View File

@ -1,88 +0,0 @@
import "../type";
import axios from "axios";
import { pollTask, validateVideoConfig } from "@/utils/ai/utils";
export default async (input: VideoConfig, config: AIConfig) => {
if (!config.apiKey) throw new Error("缺少API Key");
// const { owned, images, hasStartEndType } = validateVideoConfig(input, config);
const hasStartEndType = input.mode === "startEnd";
const authorization = "Bearer " + config.apiKey.replace(/^Bearer\s*/i, "").trim();
const baseUrl = config.baseURL ?? "https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks";
const images = input.imageBase64 || [];
// 构建图片内容
const imageContent = images.map((base64, index) => {
const item: Record<string, any> = {
type: "image_url",
image_url: { url: base64 },
};
if (hasStartEndType) {
item.role = index === 0 ? "first_frame" : "last_frame";
} else {
item.role = "reference_image";
}
return item;
});
// 构建请求体
const requestBody: Record<string, any> = {
model: config.model,
content: [{ type: "text", text: input.prompt }, ...imageContent],
duration: input.duration,
resolution: input.resolution,
watermark: false,
};
// 仅当模型支持音频时才添加 generate_audio 字段
if (typeof input?.audio == "boolean") {
requestBody.generate_audio = input.audio ?? false;
}
// 创建视频生成任务
const createResponse = await axios.post(baseUrl, requestBody, {
headers: {
"Content-Type": "application/json",
Authorization: authorization,
},
});
console.log("%c Line:44 🍡 createResponse", "background:#2eafb0", createResponse.data);
const taskId = createResponse.data.id;
if (!taskId) throw new Error("视频任务创建失败");
// 轮询任务状态
return await pollTask(async () => {
const data = await axios.get(`${baseUrl}/${taskId}`, {
headers: { Authorization: authorization },
});
console.log("%c Line:62 🥕 data.data", "background:#e41a6a", data.data);
const { status, content, error } = data.data;
switch (status) {
case "succeeded":
case "completed":
return { completed: true, url: content?.video_url };
case "failed":
case "cancelled":
case "expired":
let errorMsg = "";
try {
errorMsg = typeof error === "string" ? error : JSON.stringify(error);
} catch (e) {
errorMsg = error || "";
}
return { completed: false, error: `任务${status}: ${errorMsg}` };
case "queued":
case "running":
case "unknown":
case "submit":
case "in_progress":
return { completed: false };
default:
return { completed: false, error: `未知状态: ${status}` };
}
});
};

41
src/utils/vendor.ts Normal file
View File

@ -0,0 +1,41 @@
import { transform } from "sucrase";
import fs from "fs";
import path from "path";
import u from "@/utils";
export function writeCode(id: string | number, tsCode: string) {
const rootDir = u.getPath("vendor")
fs.mkdirSync(rootDir, { recursive: true })
if (fs.existsSync(path.join(rootDir, `${id}.ts`))) {
fs.writeFileSync(path.join(rootDir, `${id}.ts`), tsCode);
}
fs.writeFileSync(path.join(rootDir, `${id}.ts`), tsCode);
}
export function getCode(id: string): string {
const rootDir = u.getPath("vendor");
const targetFile = path.join(rootDir, `${id}.ts`);
if (!fs.existsSync(targetFile)) return "";
return fs.readFileSync(targetFile, "utf-8");
}
export async function getModelList(id: string): Promise<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 = [...JSON.parse(JSON.stringify(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

@ -14,6 +14,7 @@ import FormData from "form-data";
import jsonwebtoken from "jsonwebtoken";
import u from "@/utils";
export default function runCode(code: string, vendor?: Record<string, any>) {
code = code.replace(/export\s*\{\s*\};?/g, ""); // 去掉 export {} 以免沙盒环境报错
// 创建一个沙盒
const exports = {};
const sandbox: Record<string, any> = {
@ -31,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,5 +1,6 @@
{
"compilerOptions": {
"ignoreDeprecations": "5.0",
"target": "ESNext",
"module": "CommonJS",
"moduleResolution": "Node",
@ -12,10 +13,21 @@
"outDir": "build",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
"@/*": [
"src/*"
]
},
"incremental": true,
"typeRoots": ["./node_modules/@types", "./src/types"],
"typeRoots": [
"./node_modules/@types",
"./src/types"
],
"resolveJsonModule": true
}
}
},
"exclude": [
"node_modules",
"data/**/*.ts",
"dist",
"build"
]
}

View File

@ -1869,11 +1869,6 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.6:
shebang-command "^2.0.0"
which "^2.0.1"
custom-electron-titlebar@^4.2.8:
version "4.4.1"
resolved "https://registry.npmmirror.com/custom-electron-titlebar/-/custom-electron-titlebar-4.4.1.tgz#aea64f009697c9771cb2a67d2eb5ac8059696906"
integrity sha512-I+sOGBdslrGpuCWlhda8P0vtRAZK+W2NzjHLsxTiE2bNmhAIs9YLDe6iRBExwU1xVZt+J1hSXzUT67BlAuMWLA==
debug@2.6.9:
version "2.6.9"
resolved "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"