From edd9336bb23789ee567367fe6bbee204ed03d84a Mon Sep 17 00:00:00 2001 From: foxhui Date: Wed, 28 Jan 2026 04:45:15 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=B0=9D=E8=AF=95=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=85=83=E7=B4=A0=E6=97=A0=E6=B3=95=E7=82=B9=E5=87=BB=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/backend/adapter/gemini_text.js | 35 +++++++++++---------------- src/backend/engine/utils.js | 39 ++++++++++++++++++++---------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/backend/adapter/gemini_text.js b/src/backend/adapter/gemini_text.js index 3c4ca47..e298dcd 100644 --- a/src/backend/adapter/gemini_text.js +++ b/src/backend/adapter/gemini_text.js @@ -86,23 +86,20 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { await page.keyboard.press('Enter'); await sleep(300, 500); - // 获取所有 menuitemradio 选项 - const menuItems = await page.getByRole('menuitemradio').all(); + // 获取所有 menuitemradio 选项的文本 + const menuItemsLocator = page.getByRole('menuitemradio'); + const menuItemsCount = await menuItemsLocator.count(); - if (menuItems.length === 0) { + if (menuItemsCount === 0) { logger.warn('适配器', '未找到模型选项,使用默认模型', meta); } else { // 获取所有选项的文本(去除前后空白) - const itemTexts = []; - for (const item of menuItems) { - const text = await item.textContent(); - itemTexts.push((text || '').trim()); - } + const itemTexts = await menuItemsLocator.allTextContents(); - logger.debug('适配器', `可用模型选项: [${itemTexts.join('], [')}]`, meta); + logger.debug('适配器', `可用模型选项: [${itemTexts.map(t => t.trim()).join('], [')}]`, meta); // 判断是否有 Pro 选项 - const hasPro = itemTexts.some(text => text.startsWith('Pro')); + const hasPro = itemTexts.some(text => text.trim().startsWith('Pro')); // 确定要选择的目标选项文本前缀 let targetPrefix = null; @@ -127,18 +124,14 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) { logger.debug('适配器', `目标模型前缀: "${targetPrefix}"`, meta); - // 查找并点击对应的选项 - let found = false; - for (let i = 0; i < menuItems.length; i++) { - if (itemTexts[i].startsWith(targetPrefix)) { - await safeClick(page, menuItems[i], { bias: 'button' }); - logger.info('适配器', `已选择模型: "${itemTexts[i]}"`, meta); - found = true; - break; - } - } + // 使用 locator 直接定位目标选项(避免缓存元素引用导致 detached 错误) + const targetItem = menuItemsLocator.filter({ hasText: new RegExp(`^\\s*${targetPrefix}`) }).first(); - if (!found) { + if (await targetItem.count() > 0) { + const selectedText = (await targetItem.textContent() || '').trim(); + await safeClick(page, targetItem, { bias: 'button' }); + logger.info('适配器', `已选择模型: "${selectedText}"`, meta); + } else { logger.warn('适配器', `未找到匹配的模型选项 (${targetPrefix}),使用默认模型`, meta); // 按 Escape 关闭菜单 await page.keyboard.press('Escape'); diff --git a/src/backend/engine/utils.js b/src/backend/engine/utils.js index 9fd92a3..0a349e7 100644 --- a/src/backend/engine/utils.js +++ b/src/backend/engine/utils.js @@ -241,37 +241,50 @@ export async function safeClick(page, target, options = {}) { ? baseTimeout + Math.ceil(50000 / cursorSpeed) : baseTimeout; - const doClick = async () => { - let el; - - // 判断输入类型 - logger.debug('浏览器', `[safeClick] 开始查找: ${selector}`); + // 元素定位函数(可重复调用以获取新鲜的 ElementHandle) + const resolveElement = async () => { if (typeof target === 'string') { // CSS selector - el = await page.$(target); + const el = await page.$(target); if (!el) throw new Error(`未找到: ${target}`); + return el; } else if (typeof target.elementHandle === 'function') { // Locator (来自 page.getByRole, page.getByText 等) - el = await target.elementHandle(); + const el = await target.elementHandle(); if (!el) throw new Error(`Locator 未匹配到元素`); + return el; } else { // ElementHandle - el = target; - if (!el || !el.asElement()) throw new Error(`Element handle invalid`); + if (!target || !target.asElement()) throw new Error(`Element handle invalid`); + return target; } + }; + + const doClick = async () => { + // 1. 首次获取元素(用于滚动和等待稳定) + logger.debug('浏览器', `[safeClick] 开始查找: ${selector}`); + let el = await resolveElement(); logger.debug('浏览器', `[safeClick] 已找到元素`); - // 确保元素在可视区域内 + // 2. 确保元素在可视区域内 logger.debug('浏览器', `[safeClick] 滚动到可视区域...`); await el.scrollIntoViewIfNeeded().catch(() => { }); - // 如果开启了布局稳定等待,等待元素位置稳定 + // 3. 如果开启了布局稳定等待,等待元素位置稳定 if (waitStable) { logger.debug('浏览器', `[safeClick] 等待元素稳定...`); await waitForElementStable(el); logger.debug('浏览器', `[safeClick] 元素已稳定`); + + // 4. 重新获取元素引用(防止等待期间 DOM 变化导致 detached 错误) + // 仅对 Locator 类型重新获取,ElementHandle 无法刷新 + if (typeof target.elementHandle === 'function') { + logger.debug('浏览器', `[safeClick] 重新获取元素引用...`); + el = await resolveElement(); + } } - // 使用自维护 ghost-cursor 拟人鼠标轨迹 (仅当 humanizeCursor=true) + + // 5. 使用自维护 ghost-cursor 拟人鼠标轨迹 (仅当 humanizeCursor=true) if (useGhostCursor) { const box = await el.boundingBox(); logger.debug('浏览器', `[safeClick] boundingBox: ${JSON.stringify(box)}`); @@ -289,7 +302,7 @@ export async function safeClick(page, target, options = {}) { return; } - // 使用原生点击 (humanizeCursor=false 或 "camou") + // 6. 使用原生点击 (humanizeCursor=false 或 "camou") const mode = page?._humanizeCursorMode; logger.debug('浏览器', `[safeClick] humanizeCursor=${mode} 使用原生点击`); // force: true 跳过可操作性检查(遮挡检测等),避免在复杂页面卡住