feat: 增加指定模型和查看可指定模型功能

This commit is contained in:
foxhui
2025-11-24 19:07:02 +08:00
Unverified
parent 55377de092
commit 4e37ff272e
8 changed files with 228 additions and 10 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
node_modules/
data/
client.js
client.js
config.yaml
+52
View File
@@ -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 测试工具
- 完整的配置文件系统
+53
View File
@@ -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 # 功能测试脚本
+1 -1
View File
@@ -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)
+31 -2
View File
@@ -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);
+40
View File
@@ -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"
}))
};
}
+20 -3
View File
@@ -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) {
+29 -3
View File
@@ -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();