diff --git a/src/backend/adapter/gemini.js b/src/backend/adapter/gemini.js index e83c120..8dc3ab2 100644 --- a/src/backend/adapter/gemini.js +++ b/src/backend/adapter/gemini.js @@ -5,6 +5,7 @@ import { sleep, safeClick, + safeScroll, uploadFilesViaChooser } from '../engine/utils.js'; import { @@ -173,9 +174,8 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { meta }); - // 等待图片元素出现并滚动到可视范围,触发懒加载 - await scrollToElement(page, 'model-response', { timeout: 20000 }); - + // 将图片滚动到可视范围,触发懒加载 + await scrollToElement(page, 'generated-image', { timeout: 120000 }); imageResponse = await imageResponsePromise; } catch (e) { const pageError = normalizePageError(e, meta); diff --git a/src/backend/adapter/google_flow.js b/src/backend/adapter/google_flow.js index 1db69c6..f0bc37b 100644 --- a/src/backend/adapter/google_flow.js +++ b/src/backend/adapter/google_flow.js @@ -8,6 +8,7 @@ import { uploadFilesViaChooser } from '../engine/utils.js'; import { + fillPrompt, normalizePageError, moveMouseAway, waitForInput, @@ -123,7 +124,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { if (await modelCombobox.count() > 0) { await safeClick(page, modelCombobox.first(), { bias: 'button' }); await sleep(300, 500); - await safeClick(page, page.getByRole('option', { name: codeName }), { bias: 'button' }); + await safeClick(page, page.getByRole('option', { name: codeName, exact: true }), { bias: 'button' }); await sleep(300, 500); logger.debug('适配器', `模型已设置为 ${codeName}`, meta); } @@ -185,7 +186,7 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { logger.info('适配器', '输入提示词...', meta); const textarea = page.locator('textarea[placeholder]'); await waitForInput(page, textarea, { click: true }); - await textarea.fill(prompt); + await fillPrompt(page, textarea, prompt, meta); await sleep(500, 1000); // 7. 先启动 API 监听,再点击发送 diff --git a/src/backend/engine/utils.js b/src/backend/engine/utils.js index 91907b5..bbcbf26 100644 --- a/src/backend/engine/utils.js +++ b/src/backend/engine/utils.js @@ -5,7 +5,7 @@ * 职责边界: * - 浏览器原子操作(点击、输入、上传等) * - 页面状态检测(isPageValid、createPageCloseWatcher) - * - 拟人化交互(humanType、safeClick) + * - 拟人化交互(humanType、safeClick、safeScroll) * - 工具函数(random、sleep、getMimeType) * * 注意:业务逻辑应放在 backend/utils.js @@ -200,6 +200,62 @@ export async function safeClick(page, target, options = {}) { } } +/** + * 安全滚动 (包含拟人化移动和滚轮滚动) + * 支持 CSS selector、ElementHandle 和 Locator 三种输入 + * @param {import('playwright-core').Page} page - Playwright 页面对象 + * @param {string|import('playwright-core').ElementHandle|import('playwright-core').Locator} target - CSS 选择器、元素句柄或 Locator + * @param {object} [options] - 滚动选项 + * @param {number} [options.deltaX=0] - 水平滚动距离 (正值向右) + * @param {number} [options.deltaY=0] - 垂直滚动距离 (正值向下) + * @param {string} [options.bias='random'] - 偏移偏好: 'input' 或 'random' + * @returns {Promise} + */ +export async function safeScroll(page, target, options = {}) { + try { + let el; + + // 判断输入类型 + if (typeof target === 'string') { + // CSS selector + el = await page.$(target); + if (!el) throw new Error(`未找到: ${target}`); + } else if (typeof target.elementHandle === 'function') { + // Locator (来自 page.getByRole, page.getByText 等) + el = await target.elementHandle(); + if (!el) throw new Error(`Locator 未匹配到元素`); + } else { + // ElementHandle + el = target; + if (!el || !el.asElement()) throw new Error(`Element handle invalid`); + } + + const deltaX = options.deltaX || 0; + const deltaY = options.deltaY || 0; + + // 使用 ghost-cursor hover 后滚动 + if (page.cursor) { + const box = await el.boundingBox(); + if (box) { + const { x, y } = getHumanClickPoint(box, options.bias || 'random'); + await page.cursor.moveTo({ x, y }); + await page.mouse.wheel(deltaX, deltaY); + return; + } + // 如果无法获取 box,降级到元素中心点滚动 + await page.cursor.move(el); + await page.mouse.wheel(deltaX, deltaY); + return; + } + + // 降级逻辑: 直接在元素上 hover 并滚动 + await el.hover(); + await page.mouse.wheel(deltaX, deltaY); + } catch (err) { + throw err; + } +} + /** * 模拟人类键盘输入 * 支持 CSS selector 和 ElementHandle 两种输入