mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
317 lines
13 KiB
JavaScript
317 lines
13 KiB
JavaScript
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';
|
|
|
|
const PORT = config.server.port || 3000;
|
|
const AUTH_TOKEN = config.server.auth;
|
|
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)
|
|
|
|
/**
|
|
* 处理队列中的任务
|
|
*/
|
|
async function processQueue() {
|
|
// 如果正在处理的任务已满,或队列为空,则停止
|
|
if (processingCount >= MAX_CONCURRENT || queue.length === 0) return;
|
|
|
|
// 取出下一个任务
|
|
const task = queue.shift();
|
|
processingCount++;
|
|
|
|
// 如果是 Queue 模式,通知客户端状态变更
|
|
if (SERVER_MODE === 'queue' && task.sse) {
|
|
task.sse.send('status', { status: 'processing' });
|
|
}
|
|
|
|
try {
|
|
console.log(`>>> [Queue] 开始处理任务。剩余排队: ${queue.length}`);
|
|
|
|
// 确保浏览器已初始化
|
|
if (!browserContext) {
|
|
browserContext = await initBrowser(config);
|
|
}
|
|
|
|
const { req, res, prompt, imagePaths } = task;
|
|
|
|
// 调用核心生图逻辑
|
|
const result = await generateImage(browserContext, prompt, imagePaths);
|
|
|
|
// 清理临时图片
|
|
for (const p of imagePaths) {
|
|
try { fs.unlinkSync(p); } catch (e) { }
|
|
}
|
|
|
|
// 处理结果
|
|
let finalContent = '';
|
|
let queueResult = {};
|
|
|
|
if (result.error) {
|
|
// 特殊错误处理:reCAPTCHA
|
|
if (result.error === 'recaptcha validation failed') {
|
|
if (SERVER_MODE === 'openai') {
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'recaptcha validation failed' }));
|
|
} else {
|
|
task.sse.send('result', { status: 'error', image: null, msg: 'recaptcha validation failed' });
|
|
task.sse.send('done', '[DONE]');
|
|
task.sse.end();
|
|
}
|
|
return;
|
|
}
|
|
finalContent = `[生成错误] ${result.error}`;
|
|
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;
|
|
|
|
// 检测图片格式并转 Base64
|
|
const metadata = await sharp(imgBuffer).metadata();
|
|
const mimeType = metadata.format === 'png' ? 'image/png' : 'image/jpeg';
|
|
const base64 = imgBuffer.toString('base64');
|
|
|
|
finalContent = ``;
|
|
queueResult = { status: 'completed', image: base64, msg: '' };
|
|
console.log('>>> [Response] 图片已转换为 Base64');
|
|
} catch (e) {
|
|
console.error('>>> [Error] 图片下载失败:', e.message);
|
|
finalContent = `[图片下载失败] ${result.image}`;
|
|
queueResult = { status: 'error', image: null, msg: `Download failed: ${e.message}` };
|
|
}
|
|
} else {
|
|
finalContent = result.text || '生成失败';
|
|
queueResult = { status: 'completed', image: null, msg: result.text };
|
|
}
|
|
|
|
// 发送响应
|
|
if (SERVER_MODE === 'openai') {
|
|
const response = {
|
|
id: 'chatcmpl-' + Date.now(),
|
|
object: 'chat.completion',
|
|
created: Math.floor(Date.now() / 1000),
|
|
model: 'lmarena-image',
|
|
choices: [{
|
|
index: 0,
|
|
message: {
|
|
role: 'assistant',
|
|
content: finalContent
|
|
},
|
|
finish_reason: 'stop'
|
|
}]
|
|
};
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(response));
|
|
} else {
|
|
// Queue Mode
|
|
task.sse.send('result', queueResult);
|
|
task.sse.send('done', '[DONE]');
|
|
task.sse.end();
|
|
}
|
|
|
|
} catch (err) {
|
|
console.error('>>> [Error] 任务处理失败:', err);
|
|
if (SERVER_MODE === 'openai') {
|
|
if (!task.res.writableEnded) {
|
|
task.res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
task.res.end(JSON.stringify({ error: err.message }));
|
|
}
|
|
} else {
|
|
task.sse.send('result', { status: 'error', image: null, msg: err.message });
|
|
task.sse.send('done', '[DONE]');
|
|
task.sse.end();
|
|
}
|
|
} finally {
|
|
processingCount--;
|
|
// 递归处理下一个任务
|
|
processQueue();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 启动 HTTP 服务器
|
|
*/
|
|
async function startServer() {
|
|
// 预先启动浏览器
|
|
try {
|
|
browserContext = await initBrowser(config);
|
|
} catch (err) {
|
|
console.error('>>> [Error] 浏览器初始化失败:', err);
|
|
process.exit(1);
|
|
}
|
|
|
|
const server = http.createServer(async (req, res) => {
|
|
// --- 鉴权中间件 ---
|
|
const authHeader = req.headers['authorization'];
|
|
if (!authHeader || authHeader !== `Bearer ${AUTH_TOKEN}`) {
|
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
return;
|
|
}
|
|
|
|
// --- 路由分发 ---
|
|
const isQueueMode = SERVER_MODE === 'queue';
|
|
const targetPath = isQueueMode ? '/v1/queue/join' : '/v1/chat/completions';
|
|
|
|
if (req.method === 'POST' && req.url.startsWith(targetPath)) {
|
|
// --- SSE 设置 (仅 Queue 模式) ---
|
|
let sseHelper = null;
|
|
if (isQueueMode) {
|
|
res.writeHead(200, {
|
|
'Content-Type': 'text/event-stream',
|
|
'Cache-Control': 'no-cache',
|
|
'Connection': 'keep-alive'
|
|
});
|
|
|
|
sseHelper = {
|
|
send: (event, data) => {
|
|
res.write(`event: ${event}\n`);
|
|
res.write(`data: ${typeof data === 'object' ? JSON.stringify(data) : data}\n\n`);
|
|
},
|
|
end: () => res.end()
|
|
};
|
|
|
|
// 启动心跳
|
|
const heartbeat = setInterval(() => {
|
|
if (res.writableEnded) {
|
|
clearInterval(heartbeat);
|
|
return;
|
|
}
|
|
sseHelper.send('heartbeat', Date.now());
|
|
}, 3000);
|
|
}
|
|
|
|
const chunks = [];
|
|
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] 请求过多,已拒绝(限流)');
|
|
if (isQueueMode) {
|
|
sseHelper.send('error', { msg: 'Too Many Requests' });
|
|
sseHelper.end();
|
|
} else {
|
|
res.writeHead(429, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: 'Too Many Requests. Server is busy.' }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
const body = Buffer.concat(chunks).toString();
|
|
const data = JSON.parse(body);
|
|
const messages = data.messages;
|
|
|
|
if (!messages || messages.length === 0) {
|
|
if (isQueueMode) { sseHelper.send('error', { msg: 'No messages' }); sseHelper.end(); }
|
|
else { res.writeHead(400); res.end(JSON.stringify({ error: 'No messages' })); }
|
|
return;
|
|
}
|
|
|
|
// 筛选用户消息
|
|
const userMessages = messages.filter(m => m.role === 'user');
|
|
if (userMessages.length === 0) {
|
|
if (isQueueMode) { sseHelper.send('error', { msg: 'No user messages' }); sseHelper.end(); }
|
|
else { res.writeHead(400); res.end(JSON.stringify({ error: 'No user messages' })); }
|
|
return;
|
|
}
|
|
const lastMessage = userMessages[userMessages.length - 1];
|
|
|
|
let prompt = '';
|
|
const imagePaths = [];
|
|
let imageCount = 0;
|
|
|
|
// 解析内容 (拼接文本 + 处理图片)
|
|
if (Array.isArray(lastMessage.content)) {
|
|
for (const item of lastMessage.content) {
|
|
if (item.type === 'text') {
|
|
prompt += item.text + ' ';
|
|
} else if (item.type === 'image_url' && item.image_url && item.image_url.url) {
|
|
imageCount++;
|
|
if (imageCount > 5) {
|
|
return;
|
|
}
|
|
|
|
const url = item.image_url.url;
|
|
if (url.startsWith('data:image')) {
|
|
const matches = url.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
|
|
if (matches && matches.length === 3) {
|
|
const buffer = Buffer.from(matches[2], 'base64');
|
|
// 压缩图片
|
|
const processedBuffer = await sharp(buffer)
|
|
.jpeg({ quality: 90 })
|
|
.toBuffer();
|
|
|
|
const filename = `img_${Date.now()}_${Math.random().toString(36).substring(7)}.jpg`;
|
|
const filePath = path.join(TEMP_DIR, filename);
|
|
fs.writeFileSync(filePath, processedBuffer);
|
|
imagePaths.push(filePath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
prompt = lastMessage.content; // 回落保留
|
|
}
|
|
|
|
prompt = prompt.trim();
|
|
console.log(`>>> [Queue] 请求入队 - Prompt: ${prompt}, Images: ${imagePaths.length}`);
|
|
|
|
if (isQueueMode) {
|
|
sseHelper.send('status', { status: 'queued', position: queue.length + 1 });
|
|
}
|
|
|
|
// 将任务加入队列
|
|
queue.push({ req, res, prompt, imagePaths, sse: sseHelper });
|
|
|
|
// 触发队列处理
|
|
processQueue();
|
|
|
|
} catch (err) {
|
|
console.error('>>> [Error] 服务器处理失败:', err);
|
|
if (isQueueMode && sseHelper) {
|
|
sseHelper.send('error', { msg: err.message });
|
|
sseHelper.end();
|
|
} else if (!res.writableEnded) {
|
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify({ error: err.message }));
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
res.writeHead(404);
|
|
res.end();
|
|
}
|
|
});
|
|
|
|
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}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
startServer();
|