mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat: 初步支持 Gemini Business 并重构和整理
This commit is contained in:
@@ -2,10 +2,14 @@ import http from 'http';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import sharp from 'sharp';
|
||||
import { gotScraping } from 'got-scraping';
|
||||
import config from './lib/config.js';
|
||||
import { initBrowser, generateImage, TEMP_DIR } from './lib/lmarena.js';
|
||||
import { MODEL_MAPPING, getModels } from './lib/models.js';
|
||||
import { getBackend } from './lib/backend/index.js';
|
||||
import { getModelsForBackend, resolveModelId } from './lib/backend/models.js';
|
||||
import { logger } from './lib/logger.js';
|
||||
import crypto from 'crypto';
|
||||
|
||||
// 使用统一后端获取配置和函数
|
||||
const { config, name, initBrowser, generateImage, TEMP_DIR } = getBackend();
|
||||
|
||||
|
||||
const PORT = config.server.port || 3000;
|
||||
const AUTH_TOKEN = config.server.auth;
|
||||
@@ -15,37 +19,37 @@ const SERVER_MODE = config.server.type || 'openai'; // 'openai' 或 'queue'
|
||||
let browserContext = null; // 浏览器上下文 {browser, page, client, width, height}
|
||||
const queue = []; // 请求队列
|
||||
let processingCount = 0; // 当前正在处理的任务数
|
||||
const MAX_CONCURRENT = 1; // 同时处理的任务数 (Puppeteer 只能单线程操作)
|
||||
const MAX_QUEUE_SIZE = 2; // 最大排队数 (总容量 = MAX_CONCURRENT + MAX_QUEUE_SIZE = 3)
|
||||
const MAX_CONCURRENT = config.queue?.maxConcurrent || 1; // 从配置读取
|
||||
const MAX_QUEUE_SIZE = config.queue?.maxQueueSize || 2; // 从配置读取
|
||||
const IMAGE_LIMIT = config.queue?.imageLimit || 5; // 图片数量上限
|
||||
|
||||
/**
|
||||
* 处理队列中的任务
|
||||
*/
|
||||
async function processQueue() {
|
||||
// 如果正在处理的任务已满,或队列为空,则停止
|
||||
// 如果正在处理的任务已满,或队列为空,则停止
|
||||
if (processingCount >= MAX_CONCURRENT || queue.length === 0) return;
|
||||
|
||||
// 取出下一个任务
|
||||
const task = queue.shift();
|
||||
processingCount++;
|
||||
|
||||
// 如果是 Queue 模式,通知客户端状态变更
|
||||
// 如果是 Queue 模式,通知客户端状态变更
|
||||
if (SERVER_MODE === 'queue' && task.sse) {
|
||||
task.sse.send('status', { status: 'processing' });
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`>>> [Queue] 开始处理任务。剩余排队: ${queue.length}`);
|
||||
const { req, res, prompt, imagePaths, modelId, modelName, id, sse } = task;
|
||||
logger.info('服务器', '[队列] 开始处理任务', { id, remaining: queue.length });
|
||||
|
||||
// 确保浏览器已初始化
|
||||
if (!browserContext) {
|
||||
browserContext = await initBrowser(config);
|
||||
}
|
||||
|
||||
const { req, res, prompt, imagePaths, modelId } = task;
|
||||
|
||||
// 调用核心生图逻辑
|
||||
const result = await generateImage(browserContext, prompt, imagePaths, modelId);
|
||||
const result = await generateImage(browserContext, prompt, imagePaths, modelId, { id });
|
||||
|
||||
// 清理临时图片
|
||||
for (const p of imagePaths) {
|
||||
@@ -57,7 +61,7 @@ async function processQueue() {
|
||||
let queueResult = {};
|
||||
|
||||
if (result.error) {
|
||||
// 特殊错误处理:reCAPTCHA
|
||||
// 特殊错误处理:reCAPTCHA
|
||||
if (result.error === 'recaptcha validation failed') {
|
||||
if (SERVER_MODE === 'openai') {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
@@ -73,32 +77,20 @@ async function processQueue() {
|
||||
queueResult = { status: 'error', image: null, msg: result.error };
|
||||
} else if (result.image) {
|
||||
try {
|
||||
console.log('>>> [Download] 正在下载生成结果...');
|
||||
const response = await gotScraping({
|
||||
url: result.image,
|
||||
responseType: 'buffer',
|
||||
http2: true,
|
||||
headerGeneratorOptions: {
|
||||
browsers: [{ name: 'chrome', minVersion: 110 }],
|
||||
devices: ['desktop'],
|
||||
locales: ['en-US'],
|
||||
operatingSystems: ['windows'],
|
||||
}
|
||||
});
|
||||
const imgBuffer = response.body;
|
||||
// result.image 已经是 "data:image/png;base64,..." 格式
|
||||
// 提取纯 Base64 部分用于 b64_json
|
||||
const base64Data = result.image.split(',')[1];
|
||||
|
||||
// 检测图片格式并转 Base64
|
||||
const metadata = await sharp(imgBuffer).metadata();
|
||||
const mimeType = metadata.format === 'png' ? 'image/png' : 'image/jpeg';
|
||||
const base64 = imgBuffer.toString('base64');
|
||||
// 构造 Markdown 图片展示 (Data URI)
|
||||
finalContent = ``;
|
||||
|
||||
finalContent = ``;
|
||||
queueResult = { status: 'completed', image: base64, msg: '' };
|
||||
console.log('>>> [Response] 图片已转换为 Base64');
|
||||
queueResult = { status: 'completed', image: base64Data, msg: '' };
|
||||
queueResult = { status: 'completed', image: base64Data, msg: '' };
|
||||
logger.info('服务器', '图片已准备就绪 (Base64)', { id });
|
||||
} catch (e) {
|
||||
console.error('>>> [Error] 图片下载失败:', e.message);
|
||||
finalContent = `[图片下载失败] ${result.image}`;
|
||||
queueResult = { status: 'error', image: null, msg: `Download failed: ${e.message}` };
|
||||
logger.error('服务器', '图片处理失败', { id, error: e.message });
|
||||
finalContent = `[图片处理失败] ${e.message}`;
|
||||
queueResult = { status: 'error', image: null, msg: `Processing failed: ${e.message}` };
|
||||
}
|
||||
} else {
|
||||
finalContent = result.text || '生成失败';
|
||||
@@ -111,7 +103,7 @@ async function processQueue() {
|
||||
id: 'chatcmpl-' + Date.now(),
|
||||
object: 'chat.completion',
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model: 'lmarena-image',
|
||||
model: modelName || 'default-model',
|
||||
choices: [{
|
||||
index: 0,
|
||||
message: {
|
||||
@@ -131,7 +123,7 @@ async function processQueue() {
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('>>> [Error] 任务处理失败:', err);
|
||||
logger.error('服务器', '任务处理失败', { id: task.id, error: err.message });
|
||||
if (SERVER_MODE === 'openai') {
|
||||
if (!task.res.writableEnded) {
|
||||
task.res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
@@ -157,11 +149,14 @@ async function startServer() {
|
||||
try {
|
||||
browserContext = await initBrowser(config);
|
||||
} catch (err) {
|
||||
console.error('>>> [Error] 浏览器初始化失败:', err);
|
||||
logger.error('服务器', '浏览器初始化失败', { error: err.message });
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
// 为每个请求生成唯一 ID
|
||||
const id = crypto.randomUUID().slice(0, 8);
|
||||
|
||||
// --- 鉴权中间件 ---
|
||||
const authHeader = req.headers['authorization'];
|
||||
if (!authHeader || authHeader !== `Bearer ${AUTH_TOKEN}`) {
|
||||
@@ -176,8 +171,9 @@ async function startServer() {
|
||||
|
||||
// 1. 模型列表接口 (OpenAI & Queue 模式通用)
|
||||
if (req.method === 'GET' && req.url === '/v1/models') {
|
||||
const models = getModelsForBackend(name);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(getModels()));
|
||||
res.end(JSON.stringify(models));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -213,9 +209,9 @@ async function startServer() {
|
||||
req.on('data', chunk => chunks.push(chunk));
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
// --- 限流检查 (仅 OpenAI 模式) ---
|
||||
// --- 限流检查 ---
|
||||
if (!isQueueMode && processingCount + queue.length >= MAX_CONCURRENT + MAX_QUEUE_SIZE) {
|
||||
console.warn('>>> [Server] 请求过多,已拒绝(限流)');
|
||||
logger.warn('服务器', '请求过多,已拒绝 (最大队列限制)', { id });
|
||||
if (isQueueMode) {
|
||||
sseHelper.send('error', { msg: 'Too Many Requests' });
|
||||
sseHelper.end();
|
||||
@@ -256,8 +252,25 @@ async function startServer() {
|
||||
prompt += item.text + ' ';
|
||||
} else if (item.type === 'image_url' && item.image_url && item.image_url.url) {
|
||||
imageCount++;
|
||||
if (imageCount > 5) {
|
||||
return;
|
||||
|
||||
// 逻辑:
|
||||
// 1. 如果配置限制 <= 10 (浏览器硬限制), 则严格执行, 超过报错
|
||||
// 2. 如果配置限制 > 10, 则视为用户想"尽力而为", 自动截断到 10 张, 忽略多余的
|
||||
|
||||
if (IMAGE_LIMIT <= 10) {
|
||||
if (imageCount > IMAGE_LIMIT) {
|
||||
const errorMsg = `Too many images. Maximum ${IMAGE_LIMIT} images allowed.`;
|
||||
logger.warn('server', errorMsg, { id });
|
||||
if (isQueueMode) { sseHelper.send('error', { msg: errorMsg }); sseHelper.end(); }
|
||||
else { res.writeHead(400); res.end(JSON.stringify({ error: errorMsg })); }
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// IMAGE_LIMIT > 10
|
||||
if (imageCount > 10) {
|
||||
// 超过浏览器硬限制, 忽略该图片
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const url = item.image_url.url;
|
||||
@@ -287,34 +300,34 @@ async function startServer() {
|
||||
// 解析模型参数
|
||||
let modelId = null;
|
||||
if (data.model) {
|
||||
if (MODEL_MAPPING[data.model]) {
|
||||
modelId = MODEL_MAPPING[data.model];
|
||||
console.log(`>>> [Server] 触发模型: ${data.model}, UUID: ${modelId}`);
|
||||
modelId = resolveModelId(name, data.model);
|
||||
if (modelId) {
|
||||
logger.info('服务器', `触发模型: ${data.model} (${modelId})`, { id });
|
||||
} else {
|
||||
const errorMsg = `Invalid model: ${data.model}`;
|
||||
console.warn(`>>> [Server] ${errorMsg}`);
|
||||
const errorMsg = `Invalid model for backend ${name}: ${data.model}`;
|
||||
logger.warn('服务器', errorMsg, { id });
|
||||
if (isQueueMode) { sseHelper.send('error', { msg: errorMsg }); sseHelper.end(); }
|
||||
else { res.writeHead(400); res.end(JSON.stringify({ error: errorMsg })); }
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
console.log('>>> [Server] 未指定模型,使用网页默认值');
|
||||
logger.info('服务器', '未指定模型,使用网页默认', { id });
|
||||
}
|
||||
|
||||
console.log(`>>> [Queue] 请求入队 - Prompt: ${prompt}, Images: ${imagePaths.length}`);
|
||||
logger.info('服务器', `[队列] 请求入队: ${prompt.slice(0, 10)}...`, { id, images: imagePaths.length });
|
||||
|
||||
if (isQueueMode) {
|
||||
sseHelper.send('status', { status: 'queued', position: queue.length + 1 });
|
||||
}
|
||||
|
||||
// 将任务加入队列
|
||||
queue.push({ req, res, prompt, imagePaths, sse: sseHelper, modelId });
|
||||
queue.push({ req, res, prompt, imagePaths, sse: sseHelper, modelId, modelName: data.model || null, id });
|
||||
|
||||
// 触发队列处理
|
||||
processQueue();
|
||||
|
||||
} catch (err) {
|
||||
console.error('>>> [Error] 服务器处理失败:', err);
|
||||
logger.error('服务器', '服务器处理失败', { id, error: err.message });
|
||||
if (isQueueMode && sseHelper) {
|
||||
sseHelper.send('error', { msg: err.message });
|
||||
sseHelper.end();
|
||||
@@ -331,11 +344,9 @@ async function startServer() {
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`>>> [Server] HTTP 服务器启动成功,监听端口 ${PORT}`);
|
||||
console.log(`>>> [Server] 运行模式: ${SERVER_MODE === 'openai' ? 'OpenAI 兼容模式' : 'Queue 队列模式'}`);
|
||||
if (SERVER_MODE === 'openai') {
|
||||
console.log(`>>> [Server] 最大并发: ${MAX_CONCURRENT}, 最大排队: ${MAX_QUEUE_SIZE}`);
|
||||
}
|
||||
logger.info('服务器', `HTTP 服务器启动成功,监听端口 ${PORT}`);
|
||||
logger.info('服务器', `运行模式: ${SERVER_MODE === 'openai' ? 'OpenAI 兼容模式' : 'Queue 队列模式'}`);
|
||||
logger.info('服务器', `最大队列: ${MAX_QUEUE_SIZE},最大图片数量: ${IMAGE_LIMIT}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user