From aae185106d903ac83104dd02b11075d782b08b78 Mon Sep 17 00:00:00 2001 From: foxhui Date: Sun, 11 Jan 2026 03:09:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E8=B1=86=E5=8C=85?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=20LMA=20=E6=A8=A1=E5=9E=8B=E9=80=89?= =?UTF-8?q?=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 + README.md | 1 + src/backend/adapter/doubao.js | 253 ++++++++++++++++++++++++++ src/backend/adapter/doubao_text.js | 273 ++++++++++++++++++++++++++++ src/backend/adapter/lmarena.js | 95 +++++----- src/backend/adapter/lmarena_text.js | 73 +++++--- src/backend/engine/utils.js | 31 ++-- src/backend/utils/error.js | 2 +- 8 files changed, 641 insertions(+), 93 deletions(-) create mode 100644 src/backend/adapter/doubao.js create mode 100644 src/backend/adapter/doubao_text.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 5185109..d3c6f13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [3.4.4] - 2026-01-10 +### ✨ Added +- **新增适配器** + - 支持豆包图片生成与文本生成适配器 + ### 🐛 Fixed - **未捕获的超时错误** - 修复因未捕获的超时错误导致的程序崩溃 +- **模型选择** + - 修复 LMArena 模型选择的问题并同步模型列表 ## [3.4.3] - 2025-12-26 diff --git a/README.md b/README.md index c8aadd2..db2c5a9 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ | [**DeepSeek**](https://chat.deepseek.com/) | ✅ | 🚫 | 🚫 | | [**Sora**](https://sora.chatgpt.com/) | 🚫 | 🚫 | ✅ | | [**Google Flow**](https://labs.google/fx/zh/tools/flow) | 🚫 | ✅ | ❌ | +| [**豆包**](https://www.doubao.com/) | ✅ | ✅ | ❌ | | 待续... | - | - | - | > [!NOTE] diff --git a/src/backend/adapter/doubao.js b/src/backend/adapter/doubao.js new file mode 100644 index 0000000..3c2ad7c --- /dev/null +++ b/src/backend/adapter/doubao.js @@ -0,0 +1,253 @@ +/** + * @fileoverview 豆包 (Doubao) 图片生成适配器 + */ + +import { + sleep, + safeClick, + uploadFilesViaChooser +} from '../engine/utils.js'; +import { + fillPrompt, + normalizePageError, + moveMouseAway, + waitForInput, + gotoWithCheck, + useContextDownload +} from '../utils/index.js'; +import { logger } from '../../utils/logger.js'; + +// --- 配置常量 --- +const TARGET_URL = 'https://www.doubao.com/chat/'; + +/** + * 执行图片生成任务 + * @param {object} context - 浏览器上下文 { page, config } + * @param {string} prompt - 提示词 + * @param {string[]} imgPaths - 图片路径数组 + * @param {string} [modelId] - 模型 ID + * @param {object} [meta={}] - 日志元数据 + * @returns {Promise<{image?: string, error?: string}>} + */ +async function generate(context, prompt, imgPaths, modelId, meta = {}) { + const { page } = context; + + // 获取模型配置 + const modelConfig = manifest.models.find(m => m.id === modelId) || manifest.models[0]; + const { codeName } = modelConfig; + + try { + logger.info('适配器', '开启新会话...', meta); + await gotoWithCheck(page, TARGET_URL); + await sleep(1500, 2500); + + // 1. 点击进入图片生成模式 + logger.debug('适配器', '进入图片生成模式...', meta); + const skillBtn = page.locator('button[data-testid="skill_bar_button_3"]'); + await skillBtn.waitFor({ state: 'visible', timeout: 30000 }); + await safeClick(page, skillBtn, { bias: 'button' }); + await sleep(1000, 1500); + + // 2. 选择模型 + logger.debug('适配器', `选择模型: ${codeName}...`, meta); + const modelBtn = page.locator('button[data-testid="image-creation-chat-input-picture-model-button"]'); + await modelBtn.waitFor({ state: 'visible', timeout: 10000 }); + await safeClick(page, modelBtn, { bias: 'button' }); + await sleep(500, 800); + + const modelOption = page.getByRole('menuitem', { name: codeName }); + await modelOption.waitFor({ state: 'visible', timeout: 5000 }); + await safeClick(page, modelOption, { bias: 'button' }); + await sleep(500, 800); + + // 3. 上传参考图片 (如果有) + if (imgPaths && imgPaths.length > 0) { + logger.info('适配器', `开始上传 ${imgPaths.length} 张参考图片...`, meta); + + const uploadBtn = page.locator('button[data-testid="image-creation-chat-input-picture-reference-button"]'); + await uploadBtn.waitFor({ state: 'visible', timeout: 10000 }); + + await uploadFilesViaChooser(page, uploadBtn, imgPaths, { + uploadValidator: (response) => { + const url = response.url(); + return response.status() === 200 && + url.includes('bytedanceapi.com') && + url.includes('Action=CommitImageUpload'); + } + }); + + logger.info('适配器', '参考图片上传完成', meta); + await sleep(1000, 1500); + } + + // 4. 填写提示词 + const inputLocator = page.locator('div[data-testid="chat_input_input"][role="textbox"]'); + await waitForInput(page, inputLocator, { click: true }); + await fillPrompt(page, inputLocator, prompt, meta); + await sleep(500, 1000); + + // 5. 设置 SSE 监听 + logger.debug('适配器', '启动 SSE 监听...', meta); + + let imageUrl = null; + let isResolved = false; + + const resultPromise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + if (!isResolved) { + isResolved = true; + reject(new Error('API_TIMEOUT: 响应超时 (180秒)')); + } + }, 180000); + + const handleResponse = async (response) => { + try { + const url = response.url(); + if (!url.includes('chat/completion')) return; + + const contentType = response.headers()['content-type'] || ''; + if (!contentType.includes('text/event-stream')) return; + + const body = await response.text(); + const extractedUrl = parseSSEForImage(body); + + if (extractedUrl) { + imageUrl = extractedUrl; + if (!isResolved) { + isResolved = true; + clearTimeout(timeout); + page.off('response', handleResponse); + resolve(); + } + } + } catch (e) { + // 忽略解析错误 + } + }; + + page.on('response', handleResponse); + }); + + // 6. 点击发送 + const sendBtn = page.locator('button[data-testid="chat_input_send_button"]'); + await sendBtn.waitFor({ state: 'visible', timeout: 10000 }); + logger.info('适配器', '点击发送...', meta); + await safeClick(page, sendBtn, { bias: 'button' }); + + // 7. 等待响应 + logger.info('适配器', '等待图片生成...', meta); + await resultPromise; + + if (!imageUrl) { + return { error: '未能从响应中提取图片链接' }; + } + + logger.info('适配器', '已获取图片链接,开始下载...', meta); + + // 8. 下载图片 + const downloadResult = await useContextDownload(imageUrl, page); + if (downloadResult.error) { + logger.error('适配器', downloadResult.error, meta); + return downloadResult; + } + + logger.info('适配器', '图片生成完成', meta); + return { image: downloadResult.image }; + + } catch (err) { + const pageError = normalizePageError(err, meta); + if (pageError) return pageError; + + logger.error('适配器', '生成任务失败', { ...meta, error: err.message }); + return { error: `生成任务失败: ${err.message}` }; + } finally { + await moveMouseAway(page); + } +} + +/** + * 解析 SSE 响应,提取图片链接 + * @param {string} body - SSE 响应体 + * @returns {string|null} 图片 URL + */ +function parseSSEForImage(body) { + const lines = body.split('\n'); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + if (line.startsWith('data:')) { + const dataLine = line.substring(5).trim(); + if (!dataLine || dataLine === '{}') continue; + + try { + const data = JSON.parse(dataLine); + const url = extractRawImage(data); + if (url) return url; + } catch (e) { + // JSON 解析失败,跳过 + } + } + } + + return null; +} + +/** + * 从 SSE 消息数据中提取原图 Raw 链接 + * @param {Object} sseData - 解析后的 data JSON 对象 + * @returns {string|null} - 返回图片 URL 或 null + */ +function extractRawImage(sseData) { + if (!sseData || !sseData.patch_op || !Array.isArray(sseData.patch_op)) { + return null; + } + + for (const op of sseData.patch_op) { + const contentBlocks = op.patch_value?.content_block; + + if (Array.isArray(contentBlocks)) { + for (const block of contentBlocks) { + // block_type 2074 代表生成卡片 + if (block.block_type === 2074) { + const creations = block.content?.creation_block?.creations; + + if (Array.isArray(creations)) { + for (const creation of creations) { + // 提取 image_ori_raw,只有图片生成完成时才会出现 + const rawUrl = creation.image?.image_ori_raw?.url; + if (rawUrl) { + return rawUrl; + } + } + } + } + } + } + } + + return null; +} + +/** + * 适配器 manifest + */ +export const manifest = { + id: 'doubao', + displayName: '豆包 (图片生成)', + description: '使用字节跳动豆包生成图片,支持多种模型和参考图片上传。需要已登录的豆包账户。', + + getTargetUrl(config, workerConfig) { + return TARGET_URL; + }, + + models: [ + { id: 'seedream-4.5', codeName: 'Seedream 4.5', imagePolicy: 'optional' }, + { id: 'seedream-4.0', codeName: 'Seedream 4.0', imagePolicy: 'optional' }, + { id: 'seedream-3.0', codeName: 'Seedream 3.0', imagePolicy: 'optional' } + ], + + navigationHandlers: [], + + generate +}; diff --git a/src/backend/adapter/doubao_text.js b/src/backend/adapter/doubao_text.js new file mode 100644 index 0000000..032ecdb --- /dev/null +++ b/src/backend/adapter/doubao_text.js @@ -0,0 +1,273 @@ +/** + * @fileoverview 豆包 (Doubao) 文本生成适配器 + */ + +import { + sleep, + safeClick, + uploadFilesViaChooser +} from '../engine/utils.js'; +import { + fillPrompt, + normalizePageError, + moveMouseAway, + waitForInput, + gotoWithCheck +} from '../utils/index.js'; +import { logger } from '../../utils/logger.js'; + +// --- 配置常量 --- +const TARGET_URL = 'https://www.doubao.com/chat/'; + +/** + * 执行文本生成任务 + * @param {object} context - 浏览器上下文 { page, config } + * @param {string} prompt - 提示词 + * @param {string[]} imgPaths - 图片路径数组 + * @param {string} [modelId] - 模型 ID + * @param {object} [meta={}] - 日志元数据 + * @returns {Promise<{text?: string, reasoning?: string, error?: string}>} + */ +async function generate(context, prompt, imgPaths, modelId, meta = {}) { + const { page } = context; + + // 是否使用深度思考模式 + const useThinking = modelId === 'seed-thinking'; + + try { + logger.info('适配器', '开启新会话...', meta); + await gotoWithCheck(page, TARGET_URL); + await sleep(1500, 2500); + + // 1. 等待输入框加载 + const inputLocator = page.locator('textarea[data-testid="chat_input_input"]'); + await waitForInput(page, inputLocator, { click: false }); + await sleep(500, 1000); + + // 2. 上传图片 (如果有) + if (imgPaths && imgPaths.length > 0) { + logger.info('适配器', `开始上传 ${imgPaths.length} 张图片...`, meta); + + // 点击上传菜单按钮 + const uploadMenuBtn = page.locator('button[aria-haspopup="menu"]').first(); + await safeClick(page, uploadMenuBtn, { bias: 'button' }); + await sleep(500, 1000); + + // 点击上传文件选项 + const uploadItem = page.locator('div[data-testid="upload_file_panel_upload_item"][role="menuitem"]'); + await uploadFilesViaChooser(page, uploadItem, imgPaths, { + uploadValidator: (response) => { + const url = response.url(); + return response.status() === 200 && + url.includes('bytedanceapi.com') && + url.includes('Action=CommitImageUpload'); + } + }); + + logger.info('适配器', '图片上传完成', meta); + await sleep(1000, 1500); + } + + // 3. 切换深度思考模式 (如需) + const deepThinkBtn = page.locator('div[data-testid="use-deep-thinking-switch-btn"] button'); + const btnExists = await deepThinkBtn.count() > 0; + + if (btnExists) { + const isChecked = await deepThinkBtn.getAttribute('data-checked') === 'true'; + + if (useThinking && !isChecked) { + logger.debug('适配器', '启用深度思考模式...', meta); + await safeClick(page, deepThinkBtn, { bias: 'button' }); + await sleep(500, 800); + } else if (!useThinking && isChecked) { + logger.debug('适配器', '关闭深度思考模式...', meta); + await safeClick(page, deepThinkBtn, { bias: 'button' }); + await sleep(500, 800); + } + } + + // 4. 填写提示词 + await safeClick(page, inputLocator, { bias: 'input' }); + await fillPrompt(page, inputLocator, prompt, meta); + await sleep(500, 1000); + + // 5. 设置 SSE 监听 + logger.debug('适配器', '启动 SSE 监听...', meta); + + let resultText = ''; + let reasoningText = ''; + let isResolved = false; + + const resultPromise = new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + if (!isResolved) { + isResolved = true; + reject(new Error('API_TIMEOUT: 响应超时 (120秒)')); + } + }, 120000); + + // 监听页面响应 + const handleResponse = async (response) => { + try { + const url = response.url(); + // 只处理 chat/completion 接口的 SSE 响应 + if (!url.includes('chat/completion')) return; + + const contentType = response.headers()['content-type'] || ''; + if (!contentType.includes('text/event-stream')) return; + + // 读取响应体并解析 SSE + const body = await response.text(); + const result = parseSSEResponse(body, useThinking); + + if (result.text) { + resultText = result.text; + reasoningText = result.reasoning || ''; + + if (!isResolved) { + isResolved = true; + clearTimeout(timeout); + page.off('response', handleResponse); + resolve(); + } + } + } catch (e) { + // 忽略解析错误,继续等待 + } + }; + + page.on('response', handleResponse); + }); + + // 6. 点击发送 + const sendBtn = page.locator('button[data-testid="chat_input_send_button"]'); + await sendBtn.waitFor({ state: 'visible', timeout: 10000 }); + logger.info('适配器', '点击发送...', meta); + await safeClick(page, sendBtn, { bias: 'button' }); + + // 7. 等待响应 + logger.info('适配器', '等待生成结果...', meta); + await resultPromise; + + if (resultText) { + logger.info('适配器', `生成完成,文本长度: ${resultText.length}`, meta); + const result = { text: resultText }; + if (reasoningText) { + result.reasoning = reasoningText; + } + return result; + } else { + return { error: '未能从响应中提取文本' }; + } + + } catch (err) { + const pageError = normalizePageError(err, meta); + if (pageError) return pageError; + + logger.error('适配器', '生成任务失败', { ...meta, error: err.message }); + return { error: `生成任务失败: ${err.message}` }; + } finally { + await moveMouseAway(page); + } +} + +/** + * 解析 SSE 响应体,提取最终文本 + * @param {string} body - SSE 响应体 + * @param {boolean} useThinking - 是否使用深度思考模式 + * @returns {{text: string, reasoning?: string}} + */ +function parseSSEResponse(body, useThinking) { + const lines = body.split('\n'); + let resultText = ''; + let reasoningText = ''; + let inThinkingBlock = false; + let thinkingBlockId = null; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + + // 解析事件类型 + if (line.startsWith('event:')) { + const eventType = line.substring(6).trim(); + + // 找到对应的 data 行 + if (i + 1 < lines.length && lines[i + 1].startsWith('data:')) { + const dataLine = lines[i + 1].substring(5).trim(); + if (!dataLine || dataLine === '{}') continue; + + try { + const data = JSON.parse(dataLine); + + // SSE_REPLY_END with end_type: 1 包含完整回复 + if (eventType === 'SSE_REPLY_END' && data.end_type === 1) { + resultText = data.msg_finish_attr?.brief || ''; + } + + // STREAM_MSG_NOTIFY 检测深度思考块 + if (eventType === 'STREAM_MSG_NOTIFY' && useThinking) { + const blocks = data.content?.content_block || []; + for (const block of blocks) { + if (block.block_type === 10040 && block.content?.thinking_block) { + inThinkingBlock = true; + thinkingBlockId = block.block_id; + } + } + } + + // STREAM_CHUNK 处理内容块 + if (eventType === 'STREAM_CHUNK' && useThinking && data.patch_op) { + for (const op of data.patch_op) { + if (op.patch_object === 1 && op.patch_value?.content_block) { + for (const block of op.patch_value.content_block) { + // 如果有 parent_id 指向 thinking_block,则是思考内容 + if (block.parent_id === thinkingBlockId) { + const text = block.content?.text_block?.text || ''; + if (text) reasoningText += text; + } + // 思考块结束标记 + if (block.block_type === 10040 && block.is_finish) { + inThinkingBlock = false; + } + } + } + } + } + + // CHUNK_DELTA 增量文本 (思考过程中的增量) + if (eventType === 'CHUNK_DELTA' && useThinking && inThinkingBlock) { + const text = data.text || ''; + if (text) reasoningText += text; + } + + } catch (e) { + // JSON 解析失败,跳过 + } + } + } + } + + return { text: resultText, reasoning: reasoningText }; +} + +/** + * 适配器 manifest + */ +export const manifest = { + id: 'doubao_text', + displayName: '豆包 (文本生成)', + description: '使用字节跳动豆包生成文本,支持深度思考模式和图片上传。需要已登录的豆包账户。', + + getTargetUrl(config, workerConfig) { + return TARGET_URL; + }, + + models: [ + { id: 'seed', imagePolicy: 'optional', type: 'text' }, + { id: 'seed-thinking', imagePolicy: 'optional', type: 'text' } + ], + + navigationHandlers: [], + + generate +}; diff --git a/src/backend/adapter/lmarena.js b/src/backend/adapter/lmarena.js index ead3796..827967b 100644 --- a/src/backend/adapter/lmarena.js +++ b/src/backend/adapter/lmarena.js @@ -57,8 +57,8 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { const textareaSelector = 'textarea'; // Worker 已验证,直接解析模型配置 - const modelConfig = manifest.models.find(m => m.id === modelId); - const codeName = modelConfig?.codeName; + //const modelConfig = manifest.models.find(m => m.id === modelId); + //const codeName = modelConfig?.codeName; try { logger.info('适配器', '开启新会话...', meta); @@ -77,28 +77,24 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { await safeClick(page, textareaSelector, { bias: 'input' }); await fillPrompt(page, textareaSelector, prompt, meta); - // 4. 配置请求拦截 (用于修改模型 ID 为 codeName) - await page.unroute('**/*').catch(() => { }); + // 4. 选择模型 + if (modelId) { + logger.debug('适配器', `选择模型: ${modelId}`, meta); + const modelCombobox = page.locator('#chat-area') + .locator('button[role="combobox"][aria-haspopup="dialog"]') + .last(); - if (codeName) { - 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(); + await modelCombobox.waitFor({ state: 'visible', timeout: 10000 }); + await safeClick(page, modelCombobox, { bias: 'button' }); + await sleep(500, 800); - try { - const postData = request.postDataJSON(); - if (postData && postData.modelAId) { - logger.info('适配器', `已拦截请求并修改模型: ${postData.modelAId} -> ${codeName}`, meta); - postData.modelAId = codeName; - await route.continue({ postData: JSON.stringify(postData) }); - return; - } - } catch (e) { - logger.error('适配器', '拦截处理异常', { ...meta, error: e.message }); - } - await route.continue(); - }); + // 模拟粘贴输入模型 ID 并回车 + await page.evaluate((text) => { + document.execCommand('insertText', false, text); + }, modelId); + await sleep(300, 500); + await page.keyboard.press('Enter'); + await sleep(500, 800); } // 5. 提交表单 (submit) @@ -166,9 +162,6 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { logger.error('适配器', '生成任务失败', { ...meta, error: err.message }); return { error: `生成任务失败: ${err.message}` }; } finally { - // 清理拦截器 - if (codeName) await page.unroute('**/*').catch(() => { }); - // 任务结束,将鼠标移至安全区域 await moveMouseAway(page); } @@ -198,41 +191,43 @@ export const manifest = { return TARGET_URL; }, - // 模型列表(从 models.js 迁移) + // 模型列表 models: [ { id: 'gemini-3-pro-image-preview-2k', codeName: '019abc10-e78d-7932-b725-7f1563ed8a12', imagePolicy: 'optional' }, { id: 'gemini-3-pro-image-preview', codeName: '019aa208-5c19-7162-ae3b-0a9ddbb1e16a', imagePolicy: 'optional' }, - { id: 'flux-2-flex', codeName: '019abed6-d96e-7a2b-bf69-198c28bef281', imagePolicy: 'optional' }, - { id: 'gemini-2.5-flash-image-preview', codeName: '0199ef2a-583f-7088-b704-b75fd169401d', imagePolicy: 'optional' }, { id: 'hunyuan-image-3.0', codeName: '7766a45c-1b6b-4fb8-9823-2557291e1ddd', imagePolicy: 'forbidden' }, - { id: 'flux-2-pro', codeName: '019abcf4-5600-7a8b-864d-9b8ab7ab7328', imagePolicy: 'optional' }, - { id: 'seedream-4.5', codeName: '019abd43-b052-7eec-aa57-e895e45c9723', imagePolicy: 'optional' }, - { id: 'seedream-4-high-res-fal', codeName: '32974d8d-333c-4d2e-abf3-f258c0ac1310', imagePolicy: 'optional' }, - { id: 'wan2.5-t2i-preview', codeName: '019a5050-2875-78ed-ae3a-d9a51a438685', imagePolicy: 'forbidden' }, - { id: 'gpt-image-1', codeName: '6e855f13-55d7-4127-8656-9168a9f4dcc0', imagePolicy: 'optional' }, - { id: 'gpt-image-mini', codeName: '0199c238-f8ee-7f7d-afc1-7e28fcfd21cf', imagePolicy: 'optional' }, - { id: 'mai-image-1', codeName: '1b407d5c-1806-477c-90a5-e5c5a114f3bc', imagePolicy: 'forbidden' }, - { id: 'seedream-3', codeName: 'd8771262-8248-4372-90d5-eb41910db034', imagePolicy: 'forbidden' }, - { id: 'qwen-image-prompt-extend', codeName: '9fe82ee1-c84f-417f-b0e7-cab4ae4cf3f3', imagePolicy: 'forbidden' }, - { id: 'flux-1-kontext-pro', codeName: '28a8f330-3554-448c-9f32-2c0a08ec6477', imagePolicy: 'optional' }, - { id: 'imagen-3.0-generate-002', codeName: '51ad1d79-61e2-414c-99e3-faeb64bb6b1b', imagePolicy: 'forbidden' }, - { id: 'ideogram-v3-quality', codeName: '73378be5-cdba-49e7-b3d0-027949871aa6', imagePolicy: 'forbidden' }, - { id: 'photon', codeName: 'e7c9fa2d-6f5d-40eb-8305-0980b11c7cab', imagePolicy: 'forbidden' }, - { id: 'recraft-v3', codeName: 'b88d5814-1d20-49cc-9eb6-e362f5851661', imagePolicy: 'forbidden' }, - { id: 'lucid-origin', codeName: '5a3b3520-c87d-481f-953c-1364687b6e8f', imagePolicy: 'forbidden' }, - { id: 'gemini-2.0-flash-preview-image-generation', codeName: '69bbf7d4-9f44-447e-a868-abc4f7a31810', imagePolicy: 'optional' }, - { id: 'dall-e-3', codeName: 'bb97bc68-131c-4ea4-a59e-03a6252de0d2', imagePolicy: 'forbidden' }, - { id: 'flux-1-kontext-dev', codeName: 'eb90ae46-a73a-4f27-be8b-40f090592c9a', imagePolicy: 'optional' }, { id: 'vidu-q2-image', codeName: '019adb32-afa4-749e-9992-39653b52fe13', imagePolicy: 'optional' }, + { id: 'mai-image-1', codeName: '1b407d5c-1806-477c-90a5-e5c5a114f3bc', imagePolicy: 'forbidden' }, { id: 'imagen-4.0-fast-generate-001', codeName: 'f44fd4f8-af30-480f-8ce2-80b2bdfea55e', imagePolicy: 'forbidden' }, + { id: 'flux-2-pro', codeName: '019abcf4-5600-7a8b-864d-9b8ab7ab7328', imagePolicy: 'optional' }, + { id: 'recraft-v3', codeName: 'b88d5814-1d20-49cc-9eb6-e362f5851661', imagePolicy: 'forbidden' }, + { id: 'flux-2-flex', codeName: '019abed6-d96e-7a2b-bf69-198c28bef281', imagePolicy: 'optional' }, + { id: 'imagen-3.0-generate-002', codeName: '51ad1d79-61e2-414c-99e3-faeb64bb6b1b', imagePolicy: 'forbidden' }, + { id: 'photon', codeName: 'e7c9fa2d-6f5d-40eb-8305-0980b11c7cab', imagePolicy: 'forbidden' }, { id: 'imagen-4.0-ultra-generate-001', codeName: '019ae6da-6438-7077-9d2d-b311a35645f8', imagePolicy: 'forbidden' }, { id: 'flux-2-dev', codeName: '019ae6a0-4773-77d5-8ffb-cc35813e063c', imagePolicy: 'optional' }, { id: 'imagen-4.0-generate-001', codeName: '019ae6da-6788-761a-8253-e0bb2bf2e3a9', imagePolicy: 'forbidden' }, - { id: 'wan2.5-i2i-preview', codeName: '019aeb62-c6ea-788e-88f9-19b1b48325b5', imagePolicy: 'required' }, - { id: 'hunyuan-image-2.1', codeName: 'a9a26426-5377-4efa-bef9-de71e29ad943', imagePolicy: 'forbidden' }, + { id: 'flux-2-max', codeName: '', imagePolicy: 'optional' }, + { id: 'qwen-image-prompt-extend', codeName: '9fe82ee1-c84f-417f-b0e7-cab4ae4cf3f3', imagePolicy: 'forbidden' }, { id: 'qwen-image-edit', codeName: '995cf221-af30-466d-a809-8e0985f83649', imagePolicy: 'required' }, - { id: 'reve-v1', codeName: '0199e980-ba42-737b-9436-927b6e7ca73e', imagePolicy: 'required' }, - { id: 'reve-fast-edit', codeName: '019a5675-0a56-7835-abdd-1cb9e7870afa', imagePolicy: 'required' } + { id: 'ideogram-v3-quality', codeName: '73378be5-cdba-49e7-b3d0-027949871aa6', imagePolicy: 'forbidden' }, + { id: 'hunyuan-image-2.1', codeName: 'a9a26426-5377-4efa-bef9-de71e29ad943', imagePolicy: 'forbidden' }, + { id: 'qwen-image-2512', codeName: '', imagePolicy: 'optional' }, + { id: 'wan2.5-t2i-preview', codeName: '019a5050-2875-78ed-ae3a-d9a51a438685', imagePolicy: 'forbidden' }, + { id: 'reve-v1.1', codeName: '', imagePolicy: 'required' }, + { id: 'chatgpt-image-latest', codeName: '', imagePolicy: 'optional' }, + { id: 'seedream-4.5', codeName: '019abd43-b052-7eec-aa57-e895e45c9723', imagePolicy: 'optional' }, + { id: 'gpt-image-1-mini', codeName: '0199c238-f8ee-7f7d-afc1-7e28fcfd21cf', imagePolicy: 'optional' }, + { id: 'gpt-image-1', codeName: '6e855f13-55d7-4127-8656-9168a9f4dcc0', imagePolicy: 'optional' }, + { id: 'gemini-2.0-flash-preview-image-generation', codeName: '69bbf7d4-9f44-447e-a868-abc4f7a31810', imagePolicy: 'optional' }, + { id: 'gemini-2.5-flash-image-preview', codeName: '0199ef2a-583f-7088-b704-b75fd169401d', imagePolicy: 'optional' }, + { id: 'seedream-3', codeName: 'd8771262-8248-4372-90d5-eb41910db034', imagePolicy: 'forbidden' }, + { id: 'seedream-4-high-res-fal', codeName: '32974d8d-333c-4d2e-abf3-f258c0ac1310', imagePolicy: 'optional' }, + { id: 'gpt-image-1.5', codeName: '', imagePolicy: 'optional' }, + { id: 'flux-1-kontext-pro', codeName: '28a8f330-3554-448c-9f32-2c0a08ec6477', imagePolicy: 'optional' }, + { id: 'wan2.5-i2i-preview', codeName: '019aeb62-c6ea-788e-88f9-19b1b48325b5', imagePolicy: 'required' }, + { id: 'flux-1-kontext-dev', codeName: 'eb90ae46-a73a-4f27-be8b-40f090592c9a', imagePolicy: 'optional' }, + { id: 'lucid-origin', codeName: '5a3b3520-c87d-481f-953c-1364687b6e8f', imagePolicy: 'forbidden' } ], // 无需导航处理器 diff --git a/src/backend/adapter/lmarena_text.js b/src/backend/adapter/lmarena_text.js index 1852436..a46e270 100644 --- a/src/backend/adapter/lmarena_text.js +++ b/src/backend/adapter/lmarena_text.js @@ -58,28 +58,24 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { await safeClick(page, textareaSelector, { bias: 'input' }); await fillPrompt(page, textareaSelector, prompt, meta); - // 4. 配置请求拦截 (用于修改模型 ID 为 codeName) - await page.unroute('**/*').catch(() => { }); + // 4. 选择模型 + if (modelId) { + logger.debug('适配器', `选择模型: ${modelId}`, meta); + const modelCombobox = page.locator('#chat-area') + .locator('button[role="combobox"][aria-haspopup="dialog"]') + .last(); - if (codeName) { - 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(); + await modelCombobox.waitFor({ state: 'visible', timeout: 10000 }); + await safeClick(page, modelCombobox, { bias: 'button' }); + await sleep(500, 800); - try { - const postData = request.postDataJSON(); - if (postData && postData.modelAId) { - logger.info('适配器', `已拦截请求并修改模型: ${postData.modelAId} -> ${codeName}`, meta); - postData.modelAId = codeName; - await route.continue({ postData: JSON.stringify(postData) }); - return; - } - } catch (e) { - logger.error('适配器', '拦截处理异常', { ...meta, error: e.message }); - } - await route.continue(); - }); + // 模拟粘贴输入模型 ID 并回车 + await page.evaluate((text) => { + document.execCommand('insertText', false, text); + }, modelId); + await sleep(300, 500); + await page.keyboard.press('Enter'); + await sleep(500, 800); } // 5. 提交表单 (submit) @@ -157,9 +153,6 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { logger.error('适配器', '生成任务失败', { ...meta, error: err.message }); return { error: `生成任务失败: ${err.message}` }; } finally { - // 清理拦截器 - if (codeName) await page.unroute('**/*').catch(() => { }); - // 任务结束,将鼠标移至安全区域 await moveMouseAway(page); } @@ -178,10 +171,11 @@ export const manifest = { return TARGET_URL; }, - // 模型列表(从 models.js 迁移) + // 模型列表(根据最新支持列表整理) models: [ + // --- 文本模型 --- { id: 'claude-opus-4-5-20251101-thinking-32k', codeName: '019ab8b2-9bcf-79b5-9fb5-149a7c67b7c0', imagePolicy: 'forbidden', type: 'text' }, - { id: 'claude-opus-4-5-20251101', 'codeName': '019adbec-8396-71cc-87d5-b47f8431a6a6', 'imagePolicy': 'forbidden', "type": "text" }, + { id: 'claude-opus-4-5-20251101', codeName: '019adbec-8396-71cc-87d5-b47f8431a6a6', imagePolicy: 'forbidden', type: 'text' }, { id: 'gemini-3-pro', codeName: '019a98f7-afcd-779f-8dcb-856cc3b3f078', imagePolicy: 'optional', type: 'text' }, { id: 'grok-4.1-thinking', codeName: '019a9389-a9d3-77a8-afbb-4fe4dd3d8630', imagePolicy: 'forbidden', type: 'text' }, { id: 'grok-4.1', codeName: '019a9389-a4d8-748d-9939-b4640198302e', imagePolicy: 'forbidden', type: 'text' }, @@ -219,7 +213,6 @@ export const manifest = { { id: 'gemini-2.5-flash', codeName: '0199f059-3877-7cfe-bc80-e01b1a4a83de', imagePolicy: 'optional', type: 'text' }, { id: 'gemini-2.5-flash-preview-09-2025', codeName: 'fc700d46-c4c1-4fec-88b5-f086876ae0bb', imagePolicy: 'optional', type: 'text' }, { id: 'claude-haiku-4-5-20251001', codeName: '0199e8e9-01ed-73e0-96ba-cf43b286bf10', imagePolicy: 'forbidden', type: 'text' }, - { id: 'grok-4-fast-reasoning', codeName: '19b3730a-0369-49ba-ad9c-09e7337937f0', imagePolicy: 'forbidden', type: 'text' }, { id: 'qwen3-next-80b-a3b-instruct', codeName: '351fe482-eb6c-4536-857b-909e16c0bf52', imagePolicy: 'forbidden', type: 'text' }, { id: 'longcat-flash-chat', codeName: '6fcbe051-f521-4dc7-8986-c429eb6191bf', imagePolicy: 'forbidden', type: 'text' }, { id: 'qwen3-235b-a22b-no-thinking', codeName: '1a400d9a-f61c-4bc2-89b4-a9b7e77dff12', imagePolicy: 'forbidden', type: 'text' }, @@ -270,7 +263,6 @@ export const manifest = { { id: 'gpt-oss-20b', codeName: 'ec3beb4b-7229-4232-bab9-670ee52dd711', imagePolicy: 'forbidden', type: 'text' }, { id: 'mercury', codeName: '019a6f77-e20d-7c1d-a7cd-8bd926e7395d', imagePolicy: 'forbidden', type: 'text' }, { id: 'olmo-3-32b-think', codeName: '019ac2ef-27e1-769f-8258-d131f79e28ef', imagePolicy: 'forbidden', type: 'text' }, - { id: 'magistral-medium-2506', codeName: '6337f479-2fc8-4311-a76b-8c957765cd68', imagePolicy: 'forbidden', type: 'text' }, { id: 'mistral-small-3.1-24b-instruct-2503', codeName: '69f5d38a-45f5-4d3a-9320-b866a4035ed9', imagePolicy: 'optional', type: 'text' }, { id: 'ibm-granite-h-small', codeName: '4ddb69f5-391a-4f78-af92-7d7328c18ab1', imagePolicy: 'forbidden', type: 'text' }, { id: 'qwen3-vl-8b-thinking', codeName: '0199e3d1-a308-77b9-a650-41453e8ef2fb', imagePolicy: 'optional', type: 'text' }, @@ -280,9 +272,29 @@ export const manifest = { { id: 'gpt-5.2-high', codeName: '019b1448-dafa-7f92-90c3-50e159c2263c', imagePolicy: 'optional', type: 'text' }, { id: 'gpt-5.2', codeName: '019b1448-d548-78f4-8b98-788d72cbd057', imagePolicy: 'optional', type: 'text' }, { id: 'glm-4.6v-flash', codeName: '019b1536-49c0-73b2-8d45-403b8571568d', imagePolicy: 'optional', type: 'text' }, - { id: 'qwen3-omni-flash', codeName: '0199c9dc-e157-7458-bd49-5942363be215', imagePolicy: 'optional', type: 'text' }, { id: 'mimo-vl-7b-rl-2508', codeName: '1c0259b5-dff7-48ce-bca1-b6957675463b', imagePolicy: 'optional', type: 'text' }, - { id: 'mimo-7b', codeName: 'ee3588cd-1fe1-484a-bcc9-f92065b8380c', imagePolicy: 'forbidden', type: 'text' }, + { id: 'minimax-m2.1-preview', codeName: '', imagePolicy: 'forbidden', type: 'text' }, + { id: 'mimo-v2-flash (thinking)', codeName: '', imagePolicy: 'forbidden', type: 'text' }, + { id: 'glm-4.7', codeName: '', imagePolicy: 'forbidden', type: 'text' }, + { id: 'amazon-nova-experimental-chat-11-10', codeName: '', imagePolicy: 'forbidden', type: 'text' }, + { id: 'grok-4-1-fast-non-reasoning', codeName: '', imagePolicy: 'forbidden', type: 'text' }, + { id: 'gemini-3-flash', codeName: '', imagePolicy: 'optional', type: 'text' }, + { id: 'nvidia-nemotron-3-nano-30b-a3b-bf16', codeName: '', imagePolicy: 'forbidden', type: 'text' }, + { id: 'olmo-3.1-32b-instruct', codeName: '', imagePolicy: 'forbidden', type: 'text' }, + { id: 'olmo-3.1-32b-think', codeName: '', imagePolicy: 'forbidden', type: 'text' }, + { id: 'gemini-3-flash (thinking-minimal)', codeName: '', imagePolicy: 'forbidden', type: 'text' }, + { id: 'mimo-v2-flash', codeName: '', imagePolicy: 'optional', type: 'text' }, + { id: 'ernie-5.0-preview-1220', codeName: '', imagePolicy: 'forbidden', type: 'text' }, + { id: 'qwen3-max-2025-09-26', codeName: '', imagePolicy: 'forbidden', type: 'text' }, + { id: 'ernie-5.0-preview-1203', codeName: '', imagePolicy: 'forbidden', type: 'text' }, + { id: 'mimo-7b', codeName: '', imagePolicy: 'forbidden', type: 'text' }, + { id: 'qwen-vl-max-2025-08-13', codeName: '', imagePolicy: 'optional', type: 'text' }, + { id: 'claude-sonnet-4-20250514-thinking-32k', codeName: '', imagePolicy: 'forbidden', type: 'text' }, + { id: 'minimax-m2-preview', codeName: '', imagePolicy: 'forbidden', type: 'text' }, + { id: 'ernie-5.0-preview-1120', codeName: '', imagePolicy: 'forbidden', type: 'text' }, + { id: 'gpt-5-high-new-system-prompt', codeName: '', imagePolicy: 'optional', type: 'text' }, + + // --- 搜索模型 --- { id: 'gemini-3-pro-grounding', codeName: '019abdb7-6957-71c1-96a2-bfa79e8a094f', imagePolicy: 'forbidden', type: 'text', search: true }, { id: 'gpt-5.1-search', codeName: '019abdb7-50a5-7c05-9308-4491d069578b', imagePolicy: 'forbidden', type: 'text', search: true }, { id: 'grok-4-fast-search', codeName: '9217ac2d-91bc-4391-aa07-b8f9e2cf11f2', imagePolicy: 'forbidden', type: 'text', search: true }, @@ -295,7 +307,8 @@ export const manifest = { { id: 'claude-opus-4-search', codeName: '25bcb878-749e-49f4-ac05-de84d964bcee', imagePolicy: 'forbidden', type: 'text', search: true }, { id: 'diffbot-small-xl', codeName: '0862885e-ef53-4d0d-b9c4-4c8f68f453ce', imagePolicy: 'forbidden', type: 'text', search: true }, { id: 'grok-4-1-fast-search', codeName: '019af19c-0658-7566-9c60-112ae5bdb8db', imagePolicy: 'forbidden', type: 'text', search: true }, - { id: 'gpt-5.2-search', codeName: '019b1448-f74a-72de-b25d-8666618f8c5a', imagePolicy: 'forbidden', type: 'text', search: true } + { id: 'gpt-5.2-search', codeName: '019b1448-f74a-72de-b25d-8666618f8c5a', imagePolicy: 'forbidden', type: 'text', search: true }, + { id: 'gpt-5.1-search-sp', codeName: '', imagePolicy: 'forbidden', type: 'text', search: true } ], // 无需导航处理器 diff --git a/src/backend/engine/utils.js b/src/backend/engine/utils.js index bbcbf26..0cefaac 100644 --- a/src/backend/engine/utils.js +++ b/src/backend/engine/utils.js @@ -260,24 +260,31 @@ export async function safeScroll(page, target, options = {}) { * 模拟人类键盘输入 * 支持 CSS selector 和 ElementHandle 两种输入 * @param {import('playwright-core').Page} page - Playwright 页面对象 - * @param {string|import('playwright-core').ElementHandle} target - CSS 选择器或元素句柄 + * @param {string|import('playwright-core').ElementHandle|null} target - CSS 选择器、元素句柄,或 null(需配合 skipFocus 使用) * @param {string} text - 要输入的文本 + * @param {object} [options] - 可选配置 + * @param {boolean} [options.skipFocus=false] - 跳过元素定位和 focus,直接输入(适用于已获得焦点的场景) * @returns {Promise} */ -export async function humanType(page, target, text) { - let el; +export async function humanType(page, target, text, options = {}) { + const { skipFocus = false } = options; - // 判断是 selector 还是 ElementHandle - if (typeof target === 'string') { - el = await page.$(target); - if (!el) throw new Error(`Element not found: ${target}`); - } else { - el = target; - if (!el) throw new Error(`Element handle invalid`); + // 如果不跳过 focus,需要定位并聚焦元素 + if (!skipFocus) { + let el; + + // 判断是 selector 还是 ElementHandle + if (typeof target === 'string') { + el = await page.$(target); + if (!el) throw new Error(`Element not found: ${target}`); + } else { + el = target; + if (!el) throw new Error(`Element handle invalid`); + } + + await el.focus(); } - await el.focus(); - // 智能输入策略 if (text.length < 50) { // 短文本: 保持拟人化逐字输入 diff --git a/src/backend/utils/error.js b/src/backend/utils/error.js index 9d243cb..0e265d4 100644 --- a/src/backend/utils/error.js +++ b/src/backend/utils/error.js @@ -66,7 +66,7 @@ export function normalizePageError(err, meta = {}) { // 兼容原生 TimeoutError (其他地方抛出的) if (err.name === 'TimeoutError' || err.message?.includes('Timeout')) { logger.error('适配器', '请求超时', meta); - return { error: '请求超时 (120秒), 请检查网络或稍后重试', code: ADAPTER_ERRORS.TIMEOUT_ERROR, retryable: true }; + return { error: '请求超时, 请检查网络或稍后重试', code: ADAPTER_ERRORS.TIMEOUT_ERROR, retryable: true }; } return null; }