修正供应商获取代码
This commit is contained in:
parent
baee581a83
commit
9fcc6e5e02
427
data/vendor/yunwu.ts
vendored
427
data/vendor/yunwu.ts
vendored
@ -1,427 +0,0 @@
|
||||
// ==================== 类型定义 ====================
|
||||
// 文本模型
|
||||
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;
|
||||
name: string;
|
||||
icon?: string;
|
||||
inputs: { key: string; label: string; type: "text" | "password" | "url"; required: boolean; placeholder?: string }[];
|
||||
inputValues: Record<string, string>;
|
||||
models: (TextModel | ImageModel | VideoModel)[];
|
||||
}
|
||||
|
||||
// ==================== 全局工具函数声明 ====================
|
||||
declare const zipImage: (completeBase64: string, size: number) => Promise<string>;
|
||||
declare const zipImageResolution: (completeBase64: string, width: number, height: number) => Promise<string>;
|
||||
declare const mergeImages: (completeBase64: string[], maxSize?: string) => Promise<string>;
|
||||
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: "yunwu",
|
||||
author: "Toonflow",
|
||||
description: "OpenAI标准格式接口,您可以修改请求地址并手动添加缺失的模型。",
|
||||
name: "云雾中转",
|
||||
icon: "",
|
||||
inputs: [
|
||||
{ key: "apiKey", label: "API密钥", type: "password", required: true },
|
||||
{ key: "baseUrl", label: "请求地址", type: "url", required: true, placeholder: "以v1结束,示例:https://yunwu.ai/v1" },
|
||||
],
|
||||
inputValues: {
|
||||
apiKey: "",
|
||||
baseUrl: "https://yunwu.ai/v1",
|
||||
},
|
||||
models: [
|
||||
{
|
||||
name: "Doubao-Seedream-5.0-lite",
|
||||
type: "image",
|
||||
modelName: "doubao-seedream-5-0-260128",
|
||||
mode: ["text", "singleImage", "multiReference"],
|
||||
},
|
||||
{
|
||||
name: "Gemini-3-Pro-Image-Preview",
|
||||
type: "image",
|
||||
modelName: "gemini-3.1-flash-image-preview",
|
||||
mode: ["text", "singleImage", "multiReference"],
|
||||
associationSkills: "高质量图像生成,支持文本生成图像、图像编辑",
|
||||
},
|
||||
{
|
||||
name: "Claude-sonnet-4.6",
|
||||
type: "text",
|
||||
modelName: "claude-sonnet-4-6",
|
||||
think: false,
|
||||
},
|
||||
{
|
||||
name: "Claude-haiku-4.5-20251001",
|
||||
type: "text",
|
||||
modelName: "claude-haiku-4-5-20251001",
|
||||
think: false,
|
||||
},
|
||||
{
|
||||
name: "Grok-Video-3",
|
||||
type: "video",
|
||||
modelName: "grok-video-3",
|
||||
mode: ["text", "singleImage"],
|
||||
audio: false,
|
||||
durationResolutionMap: [
|
||||
{ duration: [6, 10], resolution: ["720P", "1080P"] }
|
||||
],
|
||||
associationSkills: "文本生成视频,支持图片垫图"
|
||||
}
|
||||
],
|
||||
};
|
||||
exports.vendor = vendor;
|
||||
|
||||
// ==================== 适配器函数 ====================
|
||||
|
||||
// 文本请求函数
|
||||
const textRequest = (textModel: TextModel) => {
|
||||
if (!vendor.inputValues.apiKey) throw new Error("缺少API Key");
|
||||
if (!vendor.inputValues.baseUrl) throw new Error("缺少请求地址(baseUrl)");
|
||||
|
||||
const apiKey = vendor.inputValues.apiKey.replace(/^Bearer\s+/i, "");
|
||||
const baseURL = vendor.inputValues.baseUrl;
|
||||
|
||||
return createOpenAI({
|
||||
baseURL: baseURL,
|
||||
apiKey: apiKey,
|
||||
}).chat(textModel.modelName);
|
||||
};
|
||||
exports.textRequest = textRequest;
|
||||
|
||||
// 图片请求函数(修正版:使用 /v1/chat/completions 兼容接口)
|
||||
interface ImageConfig {
|
||||
prompt: string;
|
||||
imageBase64: string[];
|
||||
size: "1K" | "2K" | "4K";
|
||||
aspectRatio: `${number}:${number}`;
|
||||
}
|
||||
|
||||
const imageRequest = async (imageConfig: ImageConfig, imageModel: ImageModel) => {
|
||||
const { apiKey, baseUrl } = vendor.inputValues;
|
||||
if (!apiKey) throw new Error("缺少API Key");
|
||||
if (!baseUrl) throw new Error("缺少请求地址(baseUrl)");
|
||||
|
||||
const cleanApiKey = apiKey.replace(/^Bearer\s+/i, "");
|
||||
const baseURL = baseUrl.replace(/\/$/, "");
|
||||
const endpoint = baseURL + "/chat/completions";
|
||||
|
||||
// 构建用户消息内容(支持多图垫图)
|
||||
const content: any[] = [
|
||||
{
|
||||
type: "text",
|
||||
text: imageConfig.prompt,
|
||||
},
|
||||
];
|
||||
|
||||
// 添加参考图片(垫图)
|
||||
if (imageConfig.imageBase64 && imageConfig.imageBase64.length > 0) {
|
||||
for (const imgBase64 of imageConfig.imageBase64) {
|
||||
let dataUrl = imgBase64;
|
||||
if (!imgBase64.startsWith("data:image")) {
|
||||
dataUrl = `data:image/png;base64,${imgBase64}`;
|
||||
}
|
||||
content.push({
|
||||
type: "image_url",
|
||||
image_url: { url: dataUrl },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 注意:云雾中转站可能支持通过额外参数传递图像尺寸/比例,
|
||||
// 若不确定,可将 size 和 aspectRatio 拼接到 prompt 中(推荐)。
|
||||
// 这里采用追加提示词的方式,确保模型理解期望的分辨率和比例。
|
||||
let finalPrompt = imageConfig.prompt;
|
||||
const sizeMap: Record<string, string> = { "1K": "1024x1024", "2K": "2048x2048", "4K": "4096x4096" };
|
||||
const resolution = sizeMap[imageConfig.size] || "1024x1024";
|
||||
finalPrompt += `\n请生成一张比例为 ${imageConfig.aspectRatio}、分辨率不低于 ${resolution} 的图片。`;
|
||||
content[0].text = finalPrompt;
|
||||
|
||||
const requestBody = {
|
||||
model: imageModel.modelName,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: content,
|
||||
},
|
||||
],
|
||||
max_tokens: 4096, // 防止输出截断
|
||||
response_format: { type: "json_object" }, // 部分中转站需要 JSON 输出
|
||||
};
|
||||
|
||||
logger(`[图像生成] 请求URL: ${endpoint}`);
|
||||
logger(`[图像生成] 模型: ${imageModel.modelName}`);
|
||||
logger(`[图像生成] 参考图片数量: ${imageConfig.imageBase64?.length || 0}`);
|
||||
|
||||
try {
|
||||
const response = await axios.post(endpoint, requestBody, {
|
||||
headers: {
|
||||
"Authorization": `Bearer ${cleanApiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout: 120000,
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`HTTP ${response.status}: ${JSON.stringify(response.data)}`);
|
||||
}
|
||||
|
||||
const assistantMessage = response.data?.choices?.[0]?.message?.content;
|
||||
if (!assistantMessage) {
|
||||
throw new Error("响应中没有 assistant 消息内容");
|
||||
}
|
||||
|
||||
// 提取图像数据(支持直接返回 base64 data URL 或普通 URL)
|
||||
let imageBase64: string | null = null;
|
||||
// 情况1:消息内容本身就是 data:image 开头
|
||||
if (assistantMessage.startsWith("data:image")) {
|
||||
imageBase64 = assistantMessage;
|
||||
}
|
||||
// 情况2:包含 Markdown 图片链接 
|
||||
else {
|
||||
const markdownMatch = assistantMessage.match(/!\[.*?\]\((.*?)\)/);
|
||||
if (markdownMatch && markdownMatch[1]) {
|
||||
const url = markdownMatch[1];
|
||||
if (url.startsWith("data:image")) {
|
||||
imageBase64 = url;
|
||||
} else {
|
||||
imageBase64 = await urlToBase64(url);
|
||||
}
|
||||
}
|
||||
// 情况3:直接是纯文本 URL
|
||||
else if (assistantMessage.match(/^https?:\/\/[^\s]+\.(png|jpg|jpeg|gif|webp)/i)) {
|
||||
imageBase64 = await urlToBase64(assistantMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (!imageBase64) {
|
||||
// 最后尝试:也许整个 content 就是 base64 字符串(无前缀)
|
||||
if (/^[A-Za-z0-9+/=]+$/.test(assistantMessage) && assistantMessage.length > 100) {
|
||||
imageBase64 = `data:image/png;base64,${assistantMessage}`;
|
||||
} else {
|
||||
throw new Error(`无法从响应中提取图像数据: ${assistantMessage.substring(0, 200)}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger(`[图像生成] 成功,图片大小: ${(imageBase64.length / 1024).toFixed(2)} KB`);
|
||||
return imageBase64;
|
||||
} catch (error: any) {
|
||||
logger(`[图像生成] 失败: ${error.message}`);
|
||||
if (error.response) {
|
||||
logger(`[图像生成] API 错误详情: ${JSON.stringify(error.response.data)}`);
|
||||
throw new Error(`图像生成失败: ${error.response.data?.error?.message || error.message}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
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")[];
|
||||
}
|
||||
|
||||
const videoRequest = async (videoConfig: VideoConfig, videoModel: VideoModel) => {
|
||||
const { apiKey, baseUrl: rawBaseUrl } = vendor.inputValues;
|
||||
if (!apiKey) throw new Error("缺少API Key");
|
||||
const baseUrl = rawBaseUrl?.trim();
|
||||
if (!baseUrl) throw new Error("缺少请求地址(baseUrl)");
|
||||
|
||||
const createEndpoint = baseUrl.replace(/\/$/, "") + "/video/create";
|
||||
const queryEndpoint = baseUrl.replace(/\/$/, "") + "/video/query";
|
||||
|
||||
let images: string[] | undefined;
|
||||
if (videoConfig.imageBase64 && videoConfig.imageBase64.length > 0) {
|
||||
logger(`[视频生成] 原始图片数组: ${JSON.stringify(videoConfig.imageBase64)}`);
|
||||
images = videoConfig.imageBase64
|
||||
.filter(img => img && typeof img === 'string' && img.length > 0)
|
||||
.map(img => {
|
||||
if (img.startsWith("data:image")) return img;
|
||||
return `data:image/png;base64,${img}`;
|
||||
});
|
||||
if (images.length === 0) {
|
||||
logger(`[视频生成] 警告: 所有图片都无效,将忽略图片参数`);
|
||||
images = undefined;
|
||||
} else {
|
||||
logger(`[视频生成] 有效图片数量: ${images.length}`);
|
||||
}
|
||||
}
|
||||
|
||||
let aspectRatioParam: string;
|
||||
switch (videoConfig.aspectRatio) {
|
||||
case "16:9": aspectRatioParam = "3:2"; break;
|
||||
case "9:16": aspectRatioParam = "2:3"; break;
|
||||
default: aspectRatioParam = "1:1";
|
||||
}
|
||||
|
||||
let sizeParam: string = "720P";
|
||||
if (videoConfig.resolution && videoConfig.resolution.includes("1080")) sizeParam = "1080P";
|
||||
|
||||
const createBody: any = {
|
||||
model: videoModel.modelName,
|
||||
prompt: videoConfig.prompt,
|
||||
aspect_ratio: aspectRatioParam,
|
||||
size: sizeParam,
|
||||
};
|
||||
if (images && images.length > 0) createBody.images = images;
|
||||
|
||||
try {
|
||||
logger(`[视频生成] 创建请求体: ${JSON.stringify({ ...createBody, images: images ? `${images.length}张图片` : undefined })}`);
|
||||
const createResp = await axios.post(createEndpoint, createBody, {
|
||||
headers: {
|
||||
"Authorization": `Bearer ${apiKey.replace(/^Bearer\s+/i, "")}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
if (createResp.status !== 200 || !createResp.data?.id) {
|
||||
throw new Error(`创建任务失败: ${JSON.stringify(createResp.data)}`);
|
||||
}
|
||||
|
||||
const taskId = createResp.data.id;
|
||||
logger(`[视频生成] 任务已创建,ID: ${taskId}`);
|
||||
|
||||
const pollResult = await pollTask(
|
||||
async () => {
|
||||
try {
|
||||
const queryResp = await axios.get(queryEndpoint, {
|
||||
params: { id: taskId },
|
||||
headers: {
|
||||
"Authorization": `Bearer ${apiKey.replace(/^Bearer\s+/i, "")}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
if (queryResp.status !== 200) {
|
||||
return { completed: false, error: `查询失败: HTTP ${queryResp.status}` };
|
||||
}
|
||||
|
||||
const data = queryResp.data;
|
||||
const status = data.status;
|
||||
logger(`[视频生成] 任务状态: ${status}`);
|
||||
|
||||
if (status === "succeeded" || status === "completed" || status === "success") {
|
||||
if (data.video_url) {
|
||||
return { completed: true, data: data.video_url };
|
||||
} else {
|
||||
return { completed: false, error: "任务成功但未返回视频URL" };
|
||||
}
|
||||
} else if (status === "failed" || status === "error") {
|
||||
return { completed: false, error: `视频生成失败: ${data.error || "未知错误"}` };
|
||||
} else {
|
||||
return { completed: false };
|
||||
}
|
||||
} catch (err: any) {
|
||||
logger(`[视频生成] 轮询出错: ${err.message}`);
|
||||
return { completed: false, error: err.message };
|
||||
}
|
||||
},
|
||||
3000,
|
||||
300000
|
||||
);
|
||||
|
||||
if (!pollResult.completed) {
|
||||
throw new Error(pollResult.error || "视频生成超时或失败");
|
||||
}
|
||||
|
||||
const videoUrl = pollResult.data;
|
||||
logger(`[视频生成] 成功,视频URL: ${videoUrl}`);
|
||||
return videoUrl;
|
||||
} catch (error: any) {
|
||||
logger(`[视频生成] 失败: ${error.message}`);
|
||||
if (error.response) {
|
||||
logger(`[视频生成] API 错误详情: ${JSON.stringify(error.response.data)}`);
|
||||
throw new Error(`视频生成失败: ${error.response.data?.error?.message || error.message}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
exports.videoRequest = videoRequest;
|
||||
|
||||
// TTS 请求函数(占位)
|
||||
interface TTSConfig {
|
||||
text: string;
|
||||
voice: string;
|
||||
speechRate: number;
|
||||
pitchRate: number;
|
||||
volume: number;
|
||||
}
|
||||
const ttsRequest = async (ttsConfig: TTSConfig, ttsModel: TTSModel) => {
|
||||
return null;
|
||||
};
|
||||
exports.ttsRequest = ttsRequest;
|
||||
@ -6,23 +6,29 @@ const router = express.Router();
|
||||
export default router.post("/", async (req, res) => {
|
||||
const data = await u.db("o_vendorConfig").select("*");
|
||||
|
||||
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",
|
||||
};
|
||||
}),
|
||||
);
|
||||
const list = (
|
||||
await Promise.all(
|
||||
data.map(async (item) => {
|
||||
const vendor = u.vendor.getVendor(item.id!);
|
||||
if (!vendor) {
|
||||
await u.db("o_vendorConfig").where("id", item.id).delete();
|
||||
return null
|
||||
};
|
||||
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",
|
||||
};
|
||||
}),
|
||||
)
|
||||
).filter((i) => Boolean(i));
|
||||
|
||||
list.sort((a, b) => (a.id === "toonflow" ? -1 : b.id === "toonflow" ? 1 : 0));
|
||||
list.sort((a, b) => (a!.id === "toonflow" ? -1 : b!.id === "toonflow" ? 1 : 0));
|
||||
res.status(200).send(success(list));
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user