diff --git a/lib/backend/adapter/gemini.js b/lib/backend/adapter/gemini.js index 8a1c978..5b83eca 100644 --- a/lib/backend/adapter/gemini.js +++ b/lib/backend/adapter/gemini.js @@ -2,7 +2,7 @@ import { initBrowserBase } from '../../browser/launcher.js'; import { sleep, safeClick, - pasteImages + uploadFilesViaChooser } from '../../browser/utils.js'; import { fillPrompt, @@ -58,23 +58,22 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) { await inputLocator.waitFor({ timeout: 30000 }); await sleep(1500, 2500); - // 2. 上传图片 + // 2. 上传图片 (使用 filechooser 事件,因为 Firefox 不会创建 DOM input 元素) if (imgPaths && imgPaths.length > 0) { - const expectedUploads = imgPaths.length; - let uploadedCount = 0; + // 点击加号按钮打开菜单 + logger.debug('适配器', '点击加号按钮...', meta); + const uploadMenuBtn = page.getByRole('button', { name: 'Open upload file menu' }); + await safeClick(page, uploadMenuBtn, { bias: 'button' }); + await sleep(500, 1000); - await pasteImages(page, inputLocator, imgPaths, { + // 使用公共函数上传文件 + const uploadFilesBtn = page.getByRole('button', { name: /Upload files/ }); + await uploadFilesViaChooser(page, uploadFilesBtn, imgPaths, { uploadValidator: (response) => { const url = response.url(); - // 检测上传成功:google.com/upload/?upload_id= 的 POST 请求 - if (response.status() === 200 && + return response.status() === 200 && url.includes('google.com/upload/') && - url.includes('upload_id=')) { - uploadedCount++; - logger.info('适配器', `图片上传进度: ${uploadedCount}/${expectedUploads}`, meta); - return uploadedCount >= expectedUploads; - } - return false; + url.includes('upload_id='); } }); diff --git a/lib/browser/utils.js b/lib/browser/utils.js index 84de815..9e0dad4 100644 --- a/lib/browser/utils.js +++ b/lib/browser/utils.js @@ -404,6 +404,76 @@ export async function pasteImages(page, target, filePaths, options = {}) { } } +/** + * 通过 filechooser 事件上传文件 (适用于无 DOM input 元素的场景,如 Firefox) + * @param {import('playwright-core').Page} page - Playwright 页面对象 + * @param {string|import('playwright-core').ElementHandle|import('playwright-core').Locator} triggerTarget - 触发文件选择的按钮 + * @param {string[]} filePaths - 文件路径数组 + * @param {Object} [options] - 可选配置 + * @param {Function} [options.uploadValidator] - 自定义上传确认回调函数, 接收 response 参数,返回 true 表示该响应代表一次成功上传 + * @param {number} [options.timeout=60000] - 上传超时时间 (毫秒) + * @returns {Promise} + */ +export async function uploadFilesViaChooser(page, triggerTarget, filePaths, options = {}) { + if (!filePaths || filePaths.length === 0) return; + + const timeout = options.timeout || 60000; + const expectedUploads = filePaths.length; + let uploadedCount = 0; + + logger.info('浏览器', `正在处理 ${filePaths.length} 张图片 (filechooser 模式)...`); + + // 设置上传确认监听 + const uploadPromise = new Promise((resolve) => { + if (!options.uploadValidator) { + // 无验证器,直接 resolve + resolve(); + return; + } + + const timeoutId = setTimeout(() => { + cleanup(); + logger.warn('浏览器', `图片上传等待超时 (已确认: ${uploadedCount}/${expectedUploads})`); + resolve(); + }, timeout); + + const onResponse = (response) => { + if (options.uploadValidator(response)) { + uploadedCount++; + logger.info('浏览器', `图片上传进度: ${uploadedCount}/${expectedUploads}`); + if (uploadedCount >= expectedUploads) { + cleanup(); + resolve(); + } + } + }; + + const cleanup = () => { + clearTimeout(timeoutId); + page.off('response', onResponse); + }; + + page.on('response', onResponse); + }); + + // 设置等待 filechooser 事件(在点击之前) + const fileChooserPromise = page.waitForEvent('filechooser'); + + // 点击触发按钮 + await safeClick(page, triggerTarget, { bias: 'button' }); + + // 等待 filechooser 事件并设置文件 + const fileChooser = await fileChooserPromise; + await fileChooser.setFiles(filePaths); + logger.debug('浏览器', '已通过 filechooser 提交文件'); + + // 等待上传完成(如果有验证器) + if (options.uploadValidator) { + await uploadPromise; + logger.info('浏览器', '所有图片上传完成'); + } +} + /** * 检查页面是否有效 * @param {import('playwright-core').Page} page