修正供应商获取代码

This commit is contained in:
ACT丶流星雨 2026-04-19 11:32:30 +08:00
parent baee581a83
commit 9fcc6e5e02
2 changed files with 23 additions and 444 deletions

427
data/vendor/yunwu.ts vendored
View File

@ -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 图片链接 ![alt](url)
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;

View File

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