mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
231 lines
8.2 KiB
JavaScript
231 lines
8.2 KiB
JavaScript
import fs from 'fs';
|
|
import path from 'path';
|
|
import { gotScraping } from 'got-scraping';
|
|
import { initBrowserBase } from '../browser/launcher.js';
|
|
import {
|
|
random,
|
|
sleep,
|
|
getRealViewport,
|
|
clamp,
|
|
safeClick,
|
|
humanType,
|
|
pasteImages,
|
|
getHumanClickPoint
|
|
} from '../browser/utils.js';
|
|
import { logger } from '../utils/logger.js';
|
|
import { loadConfig } from '../utils/config.js';
|
|
import { getProxyConfig, getHttpProxy } from '../utils/proxy.js';
|
|
|
|
// --- 配置常量 ---
|
|
const USER_DATA_DIR = path.join(process.cwd(), 'data', 'camoufoxUserData');
|
|
const TARGET_URL = 'https://lmarena.ai/c/new?mode=direct&chat-modality=image';
|
|
const TEMP_DIR = path.join(process.cwd(), 'data', 'temp');
|
|
|
|
// 确保临时目录存在
|
|
if (!fs.existsSync(TEMP_DIR)) {
|
|
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
|
}
|
|
|
|
/**
|
|
* 从响应文本中提取图片 URL
|
|
* @param {string} text - 响应文本内容
|
|
* @returns {string|null} 提取到的图片 URL,如果未找到则返回 null
|
|
*/
|
|
function extractImage(text) {
|
|
if (!text) return null;
|
|
const lines = text.split('\n');
|
|
for (const line of lines) {
|
|
if (line.startsWith('a2:')) {
|
|
try {
|
|
const data = JSON.parse(line.substring(3));
|
|
if (data?.[0]?.image) return data[0].image;
|
|
} catch (e) { }
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 初始化浏览器会话
|
|
* @param {object} config - 全局配置对象
|
|
* @returns {Promise<{browser: object, page: object, client: object}>} 初始化后的浏览器上下文
|
|
*/
|
|
async function initBrowser(config) {
|
|
// LMArena 特定的输入框验证逻辑
|
|
const waitInputValidator = async (page) => {
|
|
const textareaSelector = 'textarea';
|
|
await page.waitForSelector(textareaSelector, { timeout: 60000 });
|
|
const box = await (await page.$(textareaSelector)).boundingBox();
|
|
if (box) {
|
|
if (page.cursor) {
|
|
const { x, y } = getHumanClickPoint(box, 'input');
|
|
await page.cursor.moveTo({ x, y });
|
|
}
|
|
await sleep(500, 1000);
|
|
}
|
|
};
|
|
|
|
return await initBrowserBase(config, {
|
|
userDataDir: USER_DATA_DIR,
|
|
targetUrl: TARGET_URL,
|
|
productName: 'LMArena',
|
|
waitInputValidator
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 执行生图任务
|
|
* @param {object} context - 浏览器上下文 { page, client }
|
|
* @param {string} prompt - 提示词
|
|
* @param {string[]} imgPaths - 图片路径数组
|
|
* @param {string} [modelId] - 指定的模型 ID (可选)
|
|
* @param {object} [meta={}] - 日志元数据
|
|
* @returns {Promise<{image?: string, text?: string, error?: string}>} 生成结果
|
|
*/
|
|
async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
|
|
const { page } = context;
|
|
const textareaSelector = 'textarea';
|
|
|
|
try {
|
|
logger.info('适配器', '开启新会话', meta);
|
|
await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded' });
|
|
|
|
// 等待输入框加载
|
|
await page.waitForSelector(textareaSelector, { timeout: 30000 });
|
|
await sleep(1500, 2500);
|
|
|
|
// 1. 上传图片
|
|
if (imgPaths && imgPaths.length > 0) {
|
|
await pasteImages(page, textareaSelector, imgPaths);
|
|
// 确保焦点在输入框
|
|
await safeClick(page, textareaSelector, { bias: 'input' });
|
|
}
|
|
|
|
// 2. 输入提示词
|
|
logger.info('适配器', '正在输入提示词...', meta);
|
|
await humanType(page, textareaSelector, prompt);
|
|
await sleep(800, 1500);
|
|
|
|
// 3. 配置请求拦截 (用于修改模型 ID)
|
|
await page.unroute('**/*').catch(() => { });
|
|
|
|
if (modelId) {
|
|
logger.debug('适配器', `准备拦截请求`, meta);
|
|
await page.route(url => url.href.includes('/nextjs-api/stream'), async (route) => {
|
|
const request = route.request();
|
|
if (request.method() !== 'POST') return route.continue();
|
|
|
|
try {
|
|
const postData = request.postDataJSON();
|
|
if (postData && postData.modelAId) {
|
|
logger.info('适配器', `已拦截请求并修改模型: ${postData.modelAId} -> ${modelId}`, meta);
|
|
postData.modelAId = modelId;
|
|
await route.continue({ postData: JSON.stringify(postData) });
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
logger.error('适配器', '拦截处理异常', { ...meta, error: e.message });
|
|
}
|
|
await route.continue();
|
|
});
|
|
}
|
|
|
|
// 4. 建立响应监听器
|
|
// 只要 URL 匹配且是 POST,无论状态码是 200 还是 429,都立即返回,防止超时死等
|
|
const responsePromise = page.waitForResponse(response =>
|
|
response.url().includes('/nextjs-api/stream') &&
|
|
response.request().method() === 'POST' &&
|
|
(response.status() === 200 || response.status() >= 400),
|
|
{ timeout: 120000 }
|
|
).catch(e => e);
|
|
|
|
logger.debug('适配器', '点击发送...', meta);
|
|
const btnSelector = 'button[type="submit"]';
|
|
await safeClick(page, btnSelector, { bias: 'button' });
|
|
|
|
logger.info('适配器', '等待生成结果...', meta);
|
|
|
|
// 5. 等待并处理响应
|
|
const response = await responsePromise;
|
|
|
|
if (response instanceof Error) {
|
|
throw response;
|
|
}
|
|
|
|
// 检查状态码
|
|
if (response.status() === 429) {
|
|
logger.warn('适配器', '触发限流/人机验证', meta);
|
|
return { error: 'Rate limit exceeded or CAPTCHA triggered (HTTP 429)' };
|
|
}
|
|
|
|
if (response.status() !== 200) {
|
|
logger.warn('适配器', `返回异常状态码: ${response.status()}`, meta);
|
|
return { error: `Server error: HTTP ${response.status()}` };
|
|
}
|
|
|
|
// 解析成功响应
|
|
const content = await response.text();
|
|
|
|
// 检查业务错误
|
|
if (content.includes('recaptcha validation failed')) {
|
|
return { error: 'recaptcha validation failed' };
|
|
}
|
|
|
|
const img = extractImage(content);
|
|
if (img) {
|
|
logger.info('适配器', '已获取生图结果,正在下载图片...', meta);
|
|
try {
|
|
// 获取代理配置
|
|
const config = loadConfig();
|
|
const proxyConfig = getProxyConfig(config);
|
|
const proxyUrl = await getHttpProxy(proxyConfig);
|
|
|
|
const options = {
|
|
url: img,
|
|
responseType: 'buffer',
|
|
http2: true,
|
|
headerGeneratorOptions: {
|
|
browsers: [{ name: 'firefox', minVersion: 100 }],
|
|
devices: ['desktop'],
|
|
locales: ['en-US'],
|
|
operatingSystems: ['windows'],
|
|
}
|
|
};
|
|
|
|
if (proxyUrl) {
|
|
options.proxyUrl = proxyUrl;
|
|
}
|
|
|
|
const imgRes = await gotScraping(options);
|
|
const base64 = imgRes.body.toString('base64');
|
|
return { image: `data:image/png;base64,${base64}` };
|
|
} catch (e) {
|
|
return { error: `Image download failed: ${e.message}` };
|
|
}
|
|
} else {
|
|
logger.info('适配器', 'AI 返回文本回复', { ...meta, preview: content.substring(0, 150) });
|
|
return { text: content };
|
|
}
|
|
|
|
} catch (err) {
|
|
if (err.name === 'TimeoutError') return { error: 'Timeout: 生成响应超时' };
|
|
logger.error('适配器', '生成任务失败', { ...meta, error: err.message });
|
|
return { error: err.message };
|
|
} finally {
|
|
// 清理拦截器
|
|
if (modelId) await page.unroute('**/*').catch(() => { });
|
|
|
|
// 任务结束,将鼠标移至安全区域
|
|
if (page.cursor) {
|
|
try {
|
|
const vp = await getRealViewport(page);
|
|
await page.cursor.moveTo({
|
|
x: clamp(vp.safeWidth * random(0.85, 0.95), 0, vp.safeWidth),
|
|
y: clamp(vp.height * random(0.3, 0.7), 0, vp.safeHeight)
|
|
});
|
|
} catch (e) { }
|
|
}
|
|
}
|
|
}
|
|
|
|
export { initBrowser, generateImage, TEMP_DIR }; |