feat: 初步支持 Gemini Business 并重构和整理

This commit is contained in:
foxhui
2025-11-28 17:53:48 +08:00
Unverified
parent 811e175054
commit 3a08ccadf4
18 changed files with 2590 additions and 1031 deletions
+374
View File
@@ -0,0 +1,374 @@
import fs from 'fs';
import path from 'path';
import { initBrowserBase } from '../browser/launcher.js';
import {
random,
sleep,
getRealViewport,
clamp,
queryDeep,
safeClick,
humanType,
pasteImages
} from '../browser/utils.js';
import { logger } from '../logger.js';
// --- 配置常量 ---
const USER_DATA_DIR = path.join(process.cwd(), 'data', 'chromeUserDataGeminiBiz');
const TEMP_DIR = path.join(process.cwd(), 'data', 'temp');
// 确保临时目录存在
if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, { recursive: true });
}
/**
* 查找 Shadow DOM 中的输入框
* @param {import('puppeteer').Page} page
* @returns {Promise<ElementHandle|null>}
*/
async function findInput(page) {
return await page.evaluateHandle(() => {
function queryDeep(root, selector) {
let found = root.querySelector(selector);
if (found) return found;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null, false);
while (walker.nextNode()) {
const node = walker.currentNode;
if (node.shadowRoot) {
found = queryDeep(node.shadowRoot, selector);
if (found) return found;
}
}
return null;
}
const editor = queryDeep(document.body, 'ucs-prosemirror-editor');
if (!editor) return null;
return queryDeep(editor.shadowRoot, '.ProseMirror');
});
}
/**
* 初始化浏览器
* @param {object} config - 配置对象
* @param {object} [config.chrome] - Chrome 配置
* @param {boolean} [config.chrome.headless] - 是否开启 Headless 模式
* @param {string} [config.chrome.path] - Chrome 可执行文件路径
* @param {object} [config.chrome.proxy] - 代理配置
* @param {object} [config.backend] - 后端配置
* @param {object} [config.backend.geminiBiz] - Gemini Biz 配置
* @param {string} config.backend.geminiBiz.entryUrl - Gemini entry URL (必需)
* @returns {Promise<{browser: import('puppeteer').Browser, page: import('puppeteer').Page, client: import('puppeteer').CDPSession}>}
*/
async function initBrowser(config) {
// 从配置读取 Gemini Biz entry URL
const backendCfg = config.backend || {};
const geminiCfg = backendCfg.geminiBiz || {};
const targetUrl = geminiCfg.entryUrl;
if (!targetUrl) {
throw new Error('GeminiBiz backend missing entry URL: backend.geminiBiz.entryUrl');
}
// Gemini Biz 特定的输入框验证
const waitInputValidator = async (page) => {
let inputHandle = null;
let retries = 0;
const maxRetries = 20;
logger.info('适配器', '正在寻找输入框 (如果您需要登录,请使用登录模式)...');
while (retries < maxRetries) {
try {
inputHandle = await findInput(page);
if (inputHandle && inputHandle.asElement()) {
logger.info('适配器', '已找到输入框');
break;
}
} catch (err) {
if (err.message.includes('Execution context was destroyed')) {
logger.info('适配器', '页面跳转中,继续等待...');
}
}
await sleep(1000, 1500);
retries++;
if (retries % 10 === 0) logger.info('适配器', `仍在寻找输入框... (${retries}/${maxRetries})`);
}
if (!inputHandle || !inputHandle.asElement()) {
logger.error('适配器', '等待超时,未找到输入框');
}
if (inputHandle && inputHandle.asElement()) {
const box = await inputHandle.boundingBox();
if (box) {
if (page.cursor) {
await page.cursor.moveTo({ x: box.x + box.width / 2, y: box.y + box.height / 2 });
}
await sleep(500, 1000);
}
}
};
return await initBrowserBase(config, {
userDataDir: USER_DATA_DIR,
targetUrl,
productName: 'Gemini Enterprise Business',
reuseExistingTab: false,
waitInputValidator
});
}
/**
* 生成图片
* @param {object} context - 浏览器上下文 { page, client, config }
* @param {string} prompt - 提示词
* @param {string[]} imgPaths - 参考图片路径数组
* @param {string} modelId - 模型 ID (目前未使用,固定为 gemini-3-pro-preview)
* @returns {Promise<{image?: string, error?: string}>} 生成结果
*/
async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
const { page, client } = context;
let fetchPausedHandler = null;
try {
// 获取配置 (通过闭包或全局)
// 这里需要从 context 或其他方式获取 config
const { loadConfig } = await import('../config.js');
const config = loadConfig();
const targetUrl = config.backend?.geminiBiz?.entryUrl;
if (!targetUrl) {
throw new Error('GeminiBiz backend missing entry URL');
}
// 开启新对话
await page.goto(targetUrl, { waitUntil: 'networkidle2' });
// 1. 查找输入框
logger.debug('适配器', '正在寻找输入框...', meta);
let inputHandle = await findInput(page);
let retries = 0;
while ((!inputHandle || !inputHandle.asElement()) && retries < 15) {
await sleep(1000, 1500);
inputHandle = await findInput(page);
retries++;
}
if (!inputHandle || !inputHandle.asElement()) {
throw new Error('未找到输入框 (.ProseMirror)');
}
// 2. 粘贴图片 (使用自定义验证器)
if (imgPaths && imgPaths.length > 0) {
const expectedUploads = imgPaths.length;
let uploadedCount = 0;
let metadataCount = 0;
await pasteImages(page, inputHandle, imgPaths, {
uploadValidator: (response) => {
const url = response.url();
if (response.status() === 200) {
if (url.includes('global/widgetAddContextFile')) {
uploadedCount++;
logger.debug('适配器', `图片上传进度 (Add): ${uploadedCount}/${expectedUploads}`, meta);
return false; // 未完成,继续等待
} else if (url.includes('global/widgetListSessionFileMetadata')) {
metadataCount++;
logger.info('适配器', `图片上传进度: ${metadataCount}/${expectedUploads}`, meta);
// 两个检查都满足才算完成
if (uploadedCount >= expectedUploads && metadataCount >= expectedUploads) {
return true;
}
}
}
return false;
}
});
await sleep(1000, 2000); // 额外缓冲
}
// 3. 输入文字
logger.info('适配器', '正在输入提示词...', meta);
await humanType(page, inputHandle, prompt);
await sleep(1000, 2000);
// 4. 设置拦截器
logger.debug('适配器', '已启用请求拦截', meta);
await client.send('Fetch.enable', {
patterns: [{
urlPattern: '*global/widgetStreamAssist*',
requestStage: 'Request'
}]
});
fetchPausedHandler = async (event) => {
const { requestId, request } = event;
if (request.method === 'POST' && request.postData) {
try {
let rawBody = request.postData;
let data;
try {
data = JSON.parse(rawBody);
} catch (e) {
try {
rawBody = Buffer.from(rawBody, 'base64').toString('utf8');
data = JSON.parse(rawBody);
} catch (e2) { }
}
if (data) {
logger.debug('适配器', '已拦截请求,正在修改...', meta);
if (!data.streamAssistRequest) data.streamAssistRequest = {};
if (!data.streamAssistRequest.assistGenerationConfig) data.streamAssistRequest.assistGenerationConfig = {};
//data.streamAssistRequest.assistGenerationConfig.modelId = "gemini-3-pro-preview";
data.streamAssistRequest.toolsSpec = { imageGenerationSpec: {} };
const newBody = JSON.stringify(data);
const newBodyBase64 = Buffer.from(newBody).toString('base64');
logger.info('适配器', '已拦截请求,强制使用 Nano Banana Pro', meta);
await client.send('Fetch.continueRequest', {
requestId,
postData: newBodyBase64
});
return;
}
} catch (e) {
logger.error('适配器', '请求拦截处理失败', { ...meta, error: e.message });
}
}
try {
await client.send('Fetch.continueRequest', { requestId });
} catch (e) { }
};
client.on('Fetch.requestPaused', fetchPausedHandler);
// 5. 点击发送
logger.debug('适配器', '点击发送...', meta);
const sendBtnHandle = await page.evaluateHandle(() => {
function queryDeep(root, selector) {
let found = root.querySelector(selector);
if (found) return found;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null, false);
while (walker.nextNode()) {
const node = walker.currentNode;
if (node.shadowRoot) {
found = queryDeep(node.shadowRoot, selector);
if (found) return found;
}
}
return null;
}
// 精准匹配发送按钮
return queryDeep(document.body, 'md-icon-button.send-button.submit, button[aria-label="提交"], button[aria-label="Send"], .send-button');
});
if (sendBtnHandle && sendBtnHandle.asElement()) {
await safeClick(page, sendBtnHandle);
} else {
logger.warn('适配器', '未找到发送按钮,尝试回车提交', meta);
await inputHandle.focus();
await page.keyboard.press('Enter');
}
logger.info('适配器', '等待生成结果中...', meta);
// 6. 等待结果
const result = await new Promise((resolve, reject) => {
const requestMethods = new Map(); // Store request methods by requestId
const cleanup = () => {
client.off('Network.requestWillBeSent', onRequest);
client.off('Network.responseReceived', onRes);
client.off('Network.loadingFinished', onLoad);
if (fetchPausedHandler) {
client.off('Fetch.requestPaused', fetchPausedHandler);
client.send('Fetch.disable').catch(() => { });
}
};
let targetRequestId = null;
const onRequest = (e) => {
requestMethods.set(e.requestId, e.request.method);
};
const onRes = (e) => {
// 1. 监听生图接口错误 (如 429 Too Many Requests)
if (e.response.url.includes('global/widgetStreamAssist')) {
if (e.response.status !== 200) {
logger.error('适配器', `请求返回错误状态码: ${e.response.status}`, meta);
cleanup();
resolve({ error: `API Error: ${e.response.status}` });
return;
}
}
if (e.response.url.includes('download/v1alpha/projects')) {
const method = requestMethods.get(e.requestId);
if (method === 'GET') {
logger.info('适配器', '捕获到图片下载亲求', meta);
targetRequestId = e.requestId;
} else {
logger.debug('适配器', `忽略非 GET 请求: ${method} - ${e.response.url}`, meta);
}
}
};
const onLoad = async (e) => {
if (e.requestId === targetRequestId) {
try {
const { body } = await client.send('Network.getResponseBody', { requestId: targetRequestId });
// GeminiBiz 返回的 body 已经是不带前缀的 base64 字符串,直接使用
const dataUri = `data:image/png;base64,${body}`;
logger.info('适配器', '生图成功', meta);
cleanup();
resolve({ image: dataUri });
} catch (err) {
logger.error('适配器', '生图失败 (提取图片失败)', { ...meta, error: err.message });
cleanup();
resolve({ error: err.message });
}
}
};
client.on('Network.requestWillBeSent', onRequest);
client.on('Network.responseReceived', onRes);
client.on('Network.loadingFinished', onLoad);
// 超时保护 (180秒)
setTimeout(() => {
cleanup();
resolve({ error: 'Timeout' });
}, 180000);
});
// 任务结束,移开鼠标
if (page.cursor) {
const currentVp = await getRealViewport(page);
const relativeX = currentVp.safeWidth * random(0.85, 0.95);
const relativeY = currentVp.height * random(0.3, 0.7);
const finalX = clamp(relativeX, 0, currentVp.safeWidth);
const finalY = clamp(relativeY, 0, currentVp.safeHeight);
await page.cursor.moveTo({ x: finalX, y: finalY });
}
return result;
} catch (err) {
logger.error('适配器', '生成任务失败', { ...meta, error: err.message });
return { error: err.message };
} finally {
if (fetchPausedHandler) {
client.off('Fetch.requestPaused', fetchPausedHandler);
try {
await client.send('Fetch.disable');
} catch (e) { }
}
}
}
export { initBrowser, generateImage, TEMP_DIR };
+27
View File
@@ -0,0 +1,27 @@
import { loadConfig } from '../config.js';
import * as lmarenaBackend from './lmarena.js';
import * as geminiBackend from './gemini_biz.js';
const config = loadConfig();
let activeBackend;
if (config.backend?.type === 'gemini_biz') {
activeBackend = {
name: 'gemini_biz',
initBrowser: (cfg) => geminiBackend.initBrowser(cfg),
generateImage: (ctx, prompt, paths, model, meta) => geminiBackend.generateImage(ctx, prompt, paths, model, meta),
TEMP_DIR: geminiBackend.TEMP_DIR
};
} else {
activeBackend = {
name: 'lmarena',
initBrowser: (cfg) => lmarenaBackend.initBrowser(cfg),
generateImage: (ctx, prompt, paths, model, meta) => lmarenaBackend.generateImage(ctx, prompt, paths, model, meta),
TEMP_DIR: lmarenaBackend.TEMP_DIR
};
}
export function getBackend() {
return { config, ...activeBackend };
}
+285
View File
@@ -0,0 +1,285 @@
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
} from '../browser/utils.js';
import { logger } from '../logger.js';
// --- 配置常量 ---
const USER_DATA_DIR = path.join(process.cwd(), 'data', 'chromeUserData');
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 配置对象 (包含 chrome 配置)
* @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) {
await page.cursor.moveTo({ x: box.x + box.width / 2, y: box.y + box.height / 2 });
}
await sleep(500, 1000);
}
};
return await initBrowserBase(config, {
userDataDir: USER_DATA_DIR,
targetUrl: TARGET_URL,
productName: 'LMArena',
reuseExistingTab: true,
waitInputValidator
});
}
/**
* 执行生图任务
* @param {object} context 浏览器上下文 {page, client}
* @param {string} prompt 提示词
* @param {string[]} imgPaths 图片路径数组
* @param {string|null} modelId 模型 UUID (可选)
* @returns {Promise<{image?: string, text?: string, error?: string}>}
*/
async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
const { page, client } = context;
const textareaSelector = 'textarea';
let fetchPausedHandler = null;
try {
// 1. 强制开启新会话 (通过URL跳转)
logger.info('适配器', '开启新会话', meta);
await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded' });
// 等待输入框出现
await page.waitForSelector(textareaSelector, { timeout: 30000 });
await sleep(1500, 2500); // 等页面稳一点
// 2. 粘贴图片
if (imgPaths && imgPaths.length > 0) {
await pasteImages(page, textareaSelector, imgPaths);
// 如果没有图片,也点击一下输入框获取焦点
await safeClick(page, textareaSelector);
}
// 3. 输入 Prompt
logger.info('适配器', '正在输入提示词...', meta);
await humanType(page, textareaSelector, prompt);
await sleep(800, 1500);
// 注入 CDP 拦截器
if (modelId) {
// 1. 启用 Fetch 域拦截,仅拦截特定 URL
await client.send('Fetch.enable', {
patterns: [{
urlPattern: '*nextjs-api/stream*',
requestStage: 'Request'
}]
});
// 2. 定义拦截处理函数
fetchPausedHandler = async (event) => {
const { requestId, request } = event;
if (request.method === 'POST' && request.postData) {
try {
// 尝试解码可能是 Base64 编码的postData
let rawBody = request.postData;
// 尝试解析 JSON
let data;
try {
data = JSON.parse(rawBody);
} catch (e) {
// 尝试 Base64 解码
try {
rawBody = Buffer.from(rawBody, 'base64').toString('utf8');
data = JSON.parse(rawBody);
} catch (e2) {
// 无法解析,跳过
}
}
if (data && data.modelAId) {
logger.debug('适配器', `已拦截请求,原始模型UUID: ${data.modelAId}`, meta);
// 修改 modelAId
data.modelAId = modelId;
// 重新序列化并转为 Base64 (Fetch.continueRequest 需要 base64)
const newBody = JSON.stringify(data);
const newBodyBase64 = Buffer.from(newBody).toString('base64');
logger.debug('适配器', `已拦截请求,修改模型UUID为: ${data.modelAId}`, meta);
logger.info('适配器', '已拦截请求,修改为指定模型', meta);
await client.send('Fetch.continueRequest', {
requestId,
postData: newBodyBase64
});
return;
}
} catch (e) {
logger.error('适配器', '请求拦截处理出错', { ...meta, error: e.message });
}
}
// 如果不匹配或出错,直接放行
try {
await client.send('Fetch.continueRequest', { requestId });
} catch (e) { }
};
// 3. 监听拦截事件
client.on('Fetch.requestPaused', fetchPausedHandler);
logger.debug('适配器', `已启用请求拦截`, meta);
}
// 4. 发送
logger.debug('适配器', '点击发送...', meta);
const btnSelector = 'button[type="submit"]';
await safeClick(page, btnSelector);
logger.info('适配器', '等待生成结果中...', meta);
// 5. 监听网络响应
let targetRequestId = null;
const result = await new Promise((resolve) => {
const cleanup = () => {
client.off('Network.responseReceived', onRes);
client.off('Network.loadingFinished', onLoad);
};
const onRes = (e) => {
// 监听流式响应接口
if (e.response.url.includes('/nextjs-api/stream/')) targetRequestId = e.requestId;
};
const onLoad = async (e) => {
if (e.requestId === targetRequestId) {
try {
const { body, base64Encoded } = await client.send('Network.getResponseBody', { requestId: targetRequestId });
const content = base64Encoded ? Buffer.from(body, 'base64').toString('utf8') : body;
// 检查是否包含 reCAPTCHA 错误
if (content.includes('recaptcha validation failed')) {
cleanup();
resolve({ error: 'recaptcha validation failed' });
return;
}
const img = extractImage(content);
if (img) {
logger.info('适配器', '已获取生图结果,正在下载图片...', meta);
// 下载图片并转换为 Base64
try {
const response = await gotScraping({
url: img,
responseType: 'buffer',
http2: true,
headerGeneratorOptions: {
browsers: [{ name: 'chrome', minVersion: 110 }],
devices: ['desktop'],
locales: ['en-US'],
operatingSystems: ['windows'],
}
});
const base64 = response.body.toString('base64');
const dataUri = `data:image/png;base64,${base64}`;
logger.info('适配器', '生图成功', meta);
cleanup();
resolve({ image: dataUri });
} catch (e) {
logger.error('适配器', '图片下载失败', { ...meta, error: e.message });
cleanup();
resolve({ error: `Image download failed: ${e.message}` });
}
} else {
logger.info('适配器', 'AI 返回文本回复', { ...meta, preview: content.substring(0, 150) });
cleanup();
resolve({ text: content });
}
} catch (err) {
cleanup();
resolve({ error: err.message });
}
}
};
client.on('Network.responseReceived', onRes);
client.on('Network.loadingFinished', onLoad);
// 超时保护 (120秒)
setTimeout(() => {
cleanup();
resolve({ error: 'Timeout' });
}, 120000);
});
// 任务结束,基于当前窗口比例智能移开鼠标
if (page.cursor) {
// 1. 再次获取最新窗口大小 (用户可能在生成过程中改变了窗口大小)
const currentVp = await getRealViewport(page);
// 2. 计算相对坐标:停靠在屏幕右侧 85% ~ 95% 的位置
const relativeX = currentVp.safeWidth * random(0.85, 0.95);
const relativeY = currentVp.height * random(0.3, 0.7); // 高度居中随机
// 3. 再次检查
const finalX = clamp(relativeX, 0, currentVp.safeWidth);
const finalY = clamp(relativeY, 0, currentVp.safeHeight);
await page.cursor.moveTo({ x: finalX, y: finalY });
}
return result;
} catch (err) {
logger.error('适配器', '生成任务失败', { ...meta, error: err.message });
return { error: err.message };
} finally {
if (fetchPausedHandler) {
client.off('Fetch.requestPaused', fetchPausedHandler);
try {
await client.send('Fetch.disable');
} catch (e) { }
}
}
}
export { initBrowser, generateImage, TEMP_DIR };
+94
View File
@@ -0,0 +1,94 @@
// LMArena 完整模型映射 (模型名 -> UUID)
export const LMARENA_MODEL_MAPPING = {
"gemini-3-pro-image-preview": "019aa208-5c19-7162-ae3b-0a9ddbb1e16a",
"seedream-4-high-res-fal": "32974d8d-333c-4d2e-abf3-f258c0ac1310",
"hunyuan-image-3.0": "7766a45c-1b6b-4fb8-9823-2557291e1ddd",
"gemini-2.5-flash-image-preview": "0199ef2a-583f-7088-b704-b75fd169401d",
"imagen-4.0-ultra-generate-preview-06-06": "f8aec69d-e077-4ed1-99be-d34f48559bbf",
"imagen-4.0-generate-preview-06-06": "2ec9f1a6-126f-4c65-a102-15ac401dcea4",
"wan2.5-t2i-preview": "019a5050-2875-78ed-ae3a-d9a51a438685",
"gpt-image-1": "6e855f13-55d7-4127-8656-9168a9f4dcc0",
"gpt-image-mini": "0199c238-f8ee-7f7d-afc1-7e28fcfd21cf",
"mai-image-1": "1b407d5c-1806-477c-90a5-e5c5a114f3bc",
"seedream-3": "d8771262-8248-4372-90d5-eb41910db034",
"qwen-image-prompt-extend": "9fe82ee1-c84f-417f-b0e7-cab4ae4cf3f3",
"flux-1-kontext-pro": "28a8f330-3554-448c-9f32-2c0a08ec6477",
"imagen-3.0-generate-002": "51ad1d79-61e2-414c-99e3-faeb64bb6b1b",
"ideogram-v3-quality": "73378be5-cdba-49e7-b3d0-027949871aa6",
"photon": "e7c9fa2d-6f5d-40eb-8305-0980b11c7cab",
"lucid-origin": "5a3b3520-c87d-481f-953c-1364687b6e8f",
"recraft-v3": "b88d5814-1d20-49cc-9eb6-e362f5851661",
"gemini-2.0-flash-preview-image-generation": "69bbf7d4-9f44-447e-a868-abc4f7a31810",
"dall-e-3": "bb97bc68-131c-4ea4-a59e-03a6252de0d2",
"flux-1-kontext-dev": "eb90ae46-a73a-4f27-be8b-40f090592c9a",
"imagen-4.0-fast-generate-001": "f44fd4f8-af30-480f-8ce2-80b2bdfea55e",
"hunyuan-image-2.1": "a9a26426-5377-4efa-bef9-de71e29ad943"
};
// GeminiBiz 支持的模型列表 (仅需验证模型 ID,不需要 UUID)
export const GEMINI_BIZ_SUPPORTED_MODELS = [
"gemini-3-pro-image-preview"
];
/**
* 获取后端对应的模型映射或列表
* @param {string} backendName - 后端名称 ('lmarena' 或 'gemini_biz')
* @returns {Object|Array} LMArena 返回映射对象,GeminiBiz 返回支持的模型数组
* @private
*/
function getMapForBackend(backendName) {
if (backendName === 'gemini_biz') {
return GEMINI_BIZ_SUPPORTED_MODELS;
}
return LMARENA_MODEL_MAPPING;
}
/**
* 获取指定后端的模型列表 (OpenAI格式)
* @param {string} backendName - 后端名称
* @returns {Object} OpenAI 格式的模型列表
*/
export function getModelsForBackend(backendName) {
const map = getMapForBackend(backendName);
let modelIds;
if (backendName === 'gemini_biz') {
// GeminiBiz: 直接使用支持的模型列表
modelIds = map;
} else {
// LMArena: 从映射对象中提取键
modelIds = Object.keys(map);
}
return {
object: 'list',
data: modelIds.map(id => ({
id,
object: 'model',
created: Math.floor(Date.now() / 1000),
owned_by: backendName === 'gemini_biz' ? 'gemini_biz' : 'lmarena'
}))
};
}
/**
* 解析模型 ID
* @param {string} backendName - 后端名称
* @param {string} modelKey - 请求的模型键
* @returns {string|null} LMArena 返回 UUID,GeminiBiz 返回模型 ID (验证通过) 或 null
*/
export function resolveModelId(backendName, modelKey) {
if (backendName === 'gemini_biz') {
// GeminiBiz: 只验证模型是否在支持列表中
return GEMINI_BIZ_SUPPORTED_MODELS.includes(modelKey) ? modelKey : null;
}
// LMArena: 返回 UUID
return LMARENA_MODEL_MAPPING[modelKey] || null;
}
// 保留旧的导出以兼容 (如果有其他地方还在使用)
export const MODEL_MAPPING = LMARENA_MODEL_MAPPING;
export function getModels() {
return getModelsForBackend('lmarena');
}