feat: 支持豆包,修复 LMA 模型选择

This commit is contained in:
foxhui
2026-01-11 03:09:31 +08:00
Unverified
parent eb1d4ee693
commit aae185106d
8 changed files with 641 additions and 93 deletions
+6
View File
@@ -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
+1
View File
@@ -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]
+253
View File
@@ -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
};
+273
View File
@@ -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
};
+45 -50
View File
@@ -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' }
],
// 无需导航处理器
+43 -30
View File
@@ -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 }
],
// 无需导航处理器
+19 -12
View File
@@ -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<void>}
*/
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) {
// 短文本: 保持拟人化逐字输入
+1 -1
View File
@@ -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;
}