diff --git a/.gitignore b/.gitignore index 04fff5a..f0a606c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ data/ -client.js \ No newline at end of file +client.js +config.yaml \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d24b8b9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,52 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.1.0] - 2024-11-24 + +### Added +- **模型选择功能**:新增 `model` 参数支持,允许用户指定使用的图像生成模型 + - 支持 23+ 种模型,包括 Seedream、Gemini、Imagen、DALL-E 等 + - 新增 `/v1/models` API 端点,用于查询可用模型列表 + - 模型映射配置文件 `lib/models.js`,便于维护和扩展 + - 在浏览器页面注入拦截脚本,动态修改请求体中的 `modelAId` + +- **CLI 测试工具增强**:`lib/test.js` 新增交互式模型选择 + - 支持在命令行中输入模型名称 + - 回车跳过则使用默认模型 + +- **API 接口更新**: + - OpenAI 兼容模式 (`/v1/chat/completions`) 现在支持 `model` 参数 + - Queue 队列模式 (`/v1/queue/join`) 现在支持 `model` 参数 + - 未指定 `model` 时,使用 LMArena 网页默认模型 + +--- + +## [1.0.1] - 2024-11-23 + +### Fixed +- **浏览器代理** + - 修复需要鉴权的Socks5代理无法连接 + +--- + +## [1.0.0] - 2024-11-23 + +### Added +- **初始版本发布** + - 基于 Puppeteer 的自动化图像生成功能 + - 支持两种运行模式: + - OpenAI 兼容模式 + - Queue 队列模式(SSE) + - 拟人化操作特性: + - 贝塞尔曲线鼠标移动 + - 智能键盘输入模拟 + - 随机延迟和抖动 + - 多图上传支持(最多 5 张) + - Bearer Token 认证 + - 代理支持(HTTP 和 SOCKS5) + - CLI 测试工具 + - 完整的配置文件系统 \ No newline at end of file diff --git a/README.md b/README.md index 1df73d6..9f53248 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ curl -X POST http://127.0.0.1:3000/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer your-secret-key" \ -d '{ + "model": "gemini-3-pro-image-preview", "messages": [ { "type": "text", @@ -155,6 +156,14 @@ curl -X POST http://127.0.0.1:3000/v1/chat/completions \ }' ``` +> **关于 `model` 参数**: +> - **必填**:必须填写支持的模型名称,否则将使用 LMArena 网页默认模型 +> - **查看可用模型**: +> - 方式 1:访问 `/v1/models` 接口查询 +> - 方式 2:直接查看 `lib/models.js` 文件 +> - **示例模型**:`seedream-4-high-res-fal`、`gemini-3-pro-image-preview`、`dall-e-3` 等 + + **响应格式** ```json { @@ -173,6 +182,45 @@ curl -X POST http://127.0.0.1:3000/v1/chat/completions \ } ``` +#### 获取可用模型列表 + +**请求端点** +``` +GET http://127.0.0.1:3000/v1/models +``` + +**请求示例** +```bash +curl -X GET http://127.0.0.1:3000/v1/models \ + -H "Authorization: Bearer your-secret-key" +``` + +**响应格式** +```json +{ + "object": "list", + "data": [ + { + "id": "seedream-4-high-res-fal", + "object": "model", + "created": 1732456789, + "owned_by": "lmarena" + }, + { + "id": "gemini-3-pro-image-preview", + "object": "model", + "created": 1732456789, + "owned_by": "lmarena" + } + ] +} +``` + +> **说明**: +> - 此接口在 **OpenAI 兼容模式** 和 **Queue 队列模式** 下均可用 +> - `created` 字段为当前请求时的时间戳 +> - 完整模型列表可在 `lib/models.js` 文件中查看 + #### Queue 队列模式(SSE)(推荐) **配置文件设置** @@ -228,11 +276,14 @@ const req = http.request(options, (res) => { }); req.write(JSON.stringify({ + model: "gemini-3-pro-image-preview", messages: [{ role: "user", content: "a cute cat" }] })); req.end(); ``` +> **提示**:Queue 模式同样支持 `model` 参数,用法与 OpenAI 兼容模式一致。 + #### 带图片的请求 **支持格式**:PNG、JPEG、GIF、WebP @@ -242,6 +293,7 @@ req.end(); **请求示例** ```json { + "model": "gemini-3-pro-image-preview", "messages": [{ "role": "user", "content": [ @@ -279,6 +331,7 @@ lmarena/ ├── package.json # 项目依赖 ├── lib/ │ ├── lmarena.js # 核心生图逻辑 (Puppeteer 操作) +│ ├── models.js # 模型映射配置 │ ├── config.js # 配置加载器 │ ├── genApiKey.js # API 密钥生成工具 │ └── test.js # 功能测试脚本 diff --git a/config.yaml b/config.yaml index 2567577..0850b3f 100644 --- a/config.yaml +++ b/config.yaml @@ -10,7 +10,7 @@ chrome: # 浏览器可执行文件路径 (留空则使用Puppeteer默认) # Windows系统示例 "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" # Linux系统示例 "/usr/bin/chromium" - #path: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" + path: "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" # 是否启用无头模式 headless: false # 是否启用 GPU (无GPU设备运行请使用false) diff --git a/lib/lmarena.js b/lib/lmarena.js index ede3024..e65e28b 100644 --- a/lib/lmarena.js +++ b/lib/lmarena.js @@ -399,9 +399,10 @@ async function initBrowser(config) { * @param {object} context 浏览器上下文 {page, client, width, height} * @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) { +async function generateImage(context, prompt, imgPaths, modelId) { const { page, client, width, height } = context; const textareaSelector = 'textarea'; @@ -417,7 +418,6 @@ async function generateImage(context, prompt, imgPaths) { // 2. 粘贴图片 if (imgPaths && imgPaths.length > 0) { await pasteImages(page, textareaSelector, imgPaths); - } else { // 如果没有图片,也点击一下输入框获取焦点 await safeClick(page, textareaSelector); } @@ -427,6 +427,35 @@ async function generateImage(context, prompt, imgPaths) { await humanType(page, textareaSelector, prompt); await sleep(800, 1500); + // --- 注入 Fetch 拦截器 --- + if (modelId) { + await page.evaluate((targetModelId) => { + const originalFetch = window.fetch; + window.fetch = async (...args) => { + let [resource, config] = args; + // 简单判断 URL + const url = resource instanceof Request ? resource.url : resource.toString(); + + if (url.includes('/nextjs-api/stream/') && config && config.method === 'POST' && config.body) { + try { + const data = JSON.parse(config.body); + console.log(`[Browser] 正在拦截请求。原始 modelAId: ${data.modelAId}`); + + // 修改 modelAId + data.modelAId = targetModelId; + config.body = JSON.stringify(data); + + console.log(`[Browser] 请求已修改。新 modelAId: ${data.modelAId}`); + } catch (e) { + console.error('[Browser] 拦截失败:', e); + } + } + return originalFetch.apply(window, args); + }; + }, modelId); + console.log(`>>> [Test] 已注入 Fetch 拦截器,目标模型: ${modelId}`); + } + // 4. 发送 const btnSelector = 'button[type="submit"]'; await safeClick(page, btnSelector); diff --git a/lib/models.js b/lib/models.js new file mode 100644 index 0000000..03832c9 --- /dev/null +++ b/lib/models.js @@ -0,0 +1,40 @@ +export const MODEL_MAPPING = { + "gemini-3-pro-image-preview": "019aa208-5c19-7162-ae3b-0a9ddbble16a", + "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" +}; + +/** + * 获取模型列表 + */ +export function getModels() { + return { + object: "list", + data: Object.keys(MODEL_MAPPING).map(id => ({ + id: id, + object: "model", + created: Math.floor(Date.now() / 1000), + owned_by: "lmarena" + })) + }; +} diff --git a/lib/test.js b/lib/test.js index 628580b..ae7eaa3 100644 --- a/lib/test.js +++ b/lib/test.js @@ -1,6 +1,7 @@ import readline from 'readline'; import config from './config.js'; import { initBrowser, generateImage } from './lmarena.js'; +import { MODEL_MAPPING } from './models.js'; /** * 创建命令行交互接口 @@ -47,12 +48,28 @@ async function main() { continue; } + // 3. 获取模型 ID + const modelInput = await ask('>>> [CLI] 请输入模型 ID (回车跳过使用默认): '); + const modelName = modelInput.trim(); + let modelId = null; + + if (modelName) { + if (MODEL_MAPPING[modelName]) { + modelId = MODEL_MAPPING[modelName]; + console.log(`>>> [CLI] 使用模型: ${modelName} (${modelId})`); + } else { + console.log(`>>> [Warn] 未找到模型 "${modelName}",将尝试直接使用默认模型。`); + } + } else { + console.log('>>> [CLI] 未指定模型,使用默认值。'); + } + console.log(`>>> [CLI] 开始任务: Prompt="${prompt}", Images=${imagePaths.length}`); - // 3. 调用生图逻辑 - const result = await generateImage(browserContext, prompt, imagePaths); + // 4. 调用生图逻辑 + const result = await generateImage(browserContext, prompt, imagePaths, modelId); - // 4. 显示结果 + // 5. 显示结果 if (result.error) { console.error('>>> [Error]', result.error); } else if (result.image) { diff --git a/server.js b/server.js index 663aa29..72f023a 100644 --- a/server.js +++ b/server.js @@ -5,6 +5,7 @@ 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'; const PORT = config.server.port || 3000; const AUTH_TOKEN = config.server.auth; @@ -41,10 +42,10 @@ async function processQueue() { browserContext = await initBrowser(config); } - const { req, res, prompt, imagePaths } = task; + const { req, res, prompt, imagePaths, modelId } = task; // 调用核心生图逻辑 - const result = await generateImage(browserContext, prompt, imagePaths); + const result = await generateImage(browserContext, prompt, imagePaths, modelId); // 清理临时图片 for (const p of imagePaths) { @@ -173,6 +174,13 @@ async function startServer() { const isQueueMode = SERVER_MODE === 'queue'; const targetPath = isQueueMode ? '/v1/queue/join' : '/v1/chat/completions'; + // 1. 模型列表接口 (OpenAI & Queue 模式通用) + if (req.method === 'GET' && req.url === '/v1/models') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(getModels())); + return; + } + if (req.method === 'POST' && req.url.startsWith(targetPath)) { // --- SSE 设置 (仅 Queue 模式) --- let sseHelper = null; @@ -275,6 +283,24 @@ async function startServer() { } prompt = prompt.trim(); + + // 解析模型参数 + let modelId = null; + if (data.model) { + if (MODEL_MAPPING[data.model]) { + modelId = MODEL_MAPPING[data.model]; + console.log(`>>> [Server] 触发模型: ${data.model}, UUID: ${modelId}`); + } else { + const errorMsg = `Invalid model: ${data.model}`; + console.warn(`>>> [Server] ${errorMsg}`); + if (isQueueMode) { sseHelper.send('error', { msg: errorMsg }); sseHelper.end(); } + else { res.writeHead(400); res.end(JSON.stringify({ error: errorMsg })); } + return; + } + } else { + console.log('>>> [Server] 未指定模型,使用网页默认值'); + } + console.log(`>>> [Queue] 请求入队 - Prompt: ${prompt}, Images: ${imagePaths.length}`); if (isQueueMode) { @@ -282,7 +308,7 @@ async function startServer() { } // 将任务加入队列 - queue.push({ req, res, prompt, imagePaths, sse: sseHelper }); + queue.push({ req, res, prompt, imagePaths, sse: sseHelper, modelId }); // 触发队列处理 processQueue();