diff --git a/lib/backend/adapter/gemini_biz.js b/lib/backend/adapter/gemini_biz.js index e9e9cdf..be4ba4d 100644 --- a/lib/backend/adapter/gemini_biz.js +++ b/lib/backend/adapter/gemini_biz.js @@ -17,15 +17,21 @@ import { logger } from '../../utils/logger.js'; // Gemini Biz 输入框选择器 const INPUT_SELECTOR = 'ucs-prosemirror-editor .ProseMirror'; -// 防止重复处理登录的锁 -let isHandlingAuth = false; - /** * 处理账户选择页面跳转 * @param {import('puppeteer').Page} page * @param {string} targetUrl - 目标 URL,用于判断跳转完成 * @returns {Promise} 是否处理了跳转 */ +let isHandlingAuth = false; + +/** 等待登录处理完成 */ +async function waitForAuthComplete() { + while (isHandlingAuth) { + await sleep(500, 1000); + } +} + async function handleAccountChooser(page) { // 防止重复处理 if (isHandlingAuth) return false; @@ -68,8 +74,12 @@ async function handleAccountChooser(page) { isHandlingAuth = false; return true; } else { - logger.warn('适配器', '[登录器] 未找到确认按钮 button[type="submit"]'); - isHandlingAuth = false; + // 按钮还没加载出来,保持锁,等待下次检查 + // 不要释放 isHandlingAuth,让全局监听器下次再试 + logger.debug('适配器', '[登录器] 按钮尚未加载,等待中...'); + await sleep(500, 1000); + isHandlingAuth = false; // 释放锁让下次尝试 + return true; // 返回 true 表示"仍在处理中" } } } catch (err) { @@ -91,59 +101,43 @@ async function handleAccountChooser(page) { async function waitForInputWithAccountChooser(page, options = {}) { const { timeout = 60000, click = true } = options; - // 设置导航监听器,自动处理账户选择页面跳转 - const navigationHandler = async () => { - await handleAccountChooser(page); - }; - page.on('framenavigated', navigationHandler); + // 先检查一次当前页面 (全局监听器也会处理,但显式调用确保首次检查) + await handleAccountChooser(page); - try { - // 先检查一次当前页面 - await handleAccountChooser(page); - - // 轮询等待输入框,同时处理账户选择 - const startTime = Date.now(); - while (Date.now() - startTime < timeout) { - // 如果正在处理跳转,暂停检测输入框 - if (isHandlingAuth) { - await sleep(500, 1000); - continue; - } - - if (await handleAccountChooser(page)) { - // 处理了账户选择,重置等待 - continue; - } - - let inputHandle = null; - try { - inputHandle = await page.$(INPUT_SELECTOR); - } catch (e) { - // 忽略执行上下文销毁错误 - if (e.message.includes('Execution context was destroyed')) { - inputHandle = null; - } else { - throw e; - } - } - - if (inputHandle) break; - - await sleep(1000, 1500); - } - - // 最终确认输入框存在 - await page.waitForSelector(INPUT_SELECTOR, { timeout: 5000 }).catch(() => { - throw new Error('未找到输入框 (.ProseMirror)'); - }); - - if (click) { - await safeClick(page, INPUT_SELECTOR, { bias: 'input' }); + // 轮询等待输入框 + const startTime = Date.now(); + while (Date.now() - startTime < timeout) { + // 如果正在处理跳转,暂停检测输入框 + if (isHandlingAuth) { await sleep(500, 1000); + continue; } - } finally { - // 清理监听器 - page.off('framenavigated', navigationHandler); + + let inputHandle = null; + try { + inputHandle = await page.$(INPUT_SELECTOR); + } catch (e) { + // 忽略执行上下文销毁错误 + if (e.message.includes('Execution context was destroyed')) { + inputHandle = null; + } else { + throw e; + } + } + + if (inputHandle) break; + + await sleep(1000, 1500); + } + + // 最终确认输入框存在 + await page.waitForSelector(INPUT_SELECTOR, { timeout: 5000 }).catch(() => { + throw new Error('未找到输入框 (.ProseMirror)'); + }); + + if (click) { + await safeClick(page, INPUT_SELECTOR, { bias: 'input' }); + await sleep(500, 1000); } } @@ -179,7 +173,8 @@ async function initBrowser(config) { userDataDir: config.paths.userDataDir, targetUrl, productName: 'Gemini Enterprise Business', - waitInputValidator + waitInputValidator, + navigationHandler: handleAccountChooser }); return { ...base, config }; } @@ -202,10 +197,15 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) { throw new Error('GeminiBiz backend missing entry URL'); } - // 开启新对话 + // 开启新对话 - 先等待可能正在进行的登录处理完成 + await waitForAuthComplete(); + logger.info('适配器', '开启新会话', meta); await page.goto(targetUrl, { waitUntil: 'domcontentloaded' }); + // 如果触发了账户选择跳转,等待全局处理器完成 + await waitForAuthComplete(); + // 1. 等待输入框加载(使用公共函数处理账户选择) logger.debug('适配器', '正在寻找输入框...', meta); await waitForInputWithAccountChooser(page, { click: false }); diff --git a/lib/backend/adapter/zai_is.js b/lib/backend/adapter/zai_is.js index 42f6f73..cbaa66c 100644 --- a/lib/backend/adapter/zai_is.js +++ b/lib/backend/adapter/zai_is.js @@ -21,13 +21,20 @@ const INPUT_SELECTOR = '.tiptap.ProseMirror'; // 入口 URL const TARGET_URL = 'https://zai.is/'; - /** * 处理 Discord OAuth2 登录流程 * @param {import('playwright-core').Page} page * @returns {Promise} 是否处理了登录 */ let isHandlingAuth = false; + +/** 等待登录处理完成 */ +async function waitForAuthComplete() { + while (isHandlingAuth) { + await sleep(500, 1000); + } +} + async function handleDiscordAuth(page) { // 防止重复处理 if (isHandlingAuth) return false; @@ -112,57 +119,43 @@ async function handleDiscordAuth(page) { async function waitForInputWithAuth(page, options = {}) { const { timeout = 60000, click = true } = options; - // 设置导航监听器,自动处理登录页面跳转 - const navigationHandler = async () => { - await handleDiscordAuth(page); - }; - page.on('framenavigated', navigationHandler); + // 先检查一次当前页面 (全局监听器也会处理,但显式调用确保首次检查) + await handleDiscordAuth(page); - try { - // 先检查一次当前页面 - await handleDiscordAuth(page); - - // 轮询等待输入框,同时处理登录 - const startTime = Date.now(); - while (Date.now() - startTime < timeout) { - // 如果正在处理登录,暂停检测输入框,避免冲突 - if (isHandlingAuth) { - await sleep(500, 1000); - continue; - } - - if (await handleDiscordAuth(page)) { - continue; - } - - let inputHandle = null; - try { - inputHandle = await page.$(INPUT_SELECTOR); - } catch (e) { - // 忽略执行上下文销毁错误 (通常发生在页面刷新/跳转时) - if (e.message.includes('Execution context was destroyed')) { - inputHandle = null; - } else { - throw e; - } - } - - if (inputHandle) break; - - await sleep(1000, 1500); - } - - // 最终确认输入框存在 - await page.waitForSelector(INPUT_SELECTOR, { timeout: 5000 }).catch(() => { - throw new Error('未找到输入框 (.tiptap.ProseMirror)'); - }); - - if (click) { - await safeClick(page, INPUT_SELECTOR, { bias: 'input' }); + // 轮询等待输入框 + const startTime = Date.now(); + while (Date.now() - startTime < timeout) { + // 如果正在处理登录,暂停检测输入框,避免冲突 + if (isHandlingAuth) { await sleep(500, 1000); + continue; } - } finally { - page.off('framenavigated', navigationHandler); + + let inputHandle = null; + try { + inputHandle = await page.$(INPUT_SELECTOR); + } catch (e) { + // 忽略执行上下文销毁错误 (通常发生在页面刷新/跳转时) + if (e.message.includes('Execution context was destroyed')) { + inputHandle = null; + } else { + throw e; + } + } + + if (inputHandle) break; + + await sleep(1000, 1500); + } + + // 最终确认输入框存在 + await page.waitForSelector(INPUT_SELECTOR, { timeout: 5000 }).catch(() => { + throw new Error('未找到输入框 (.tiptap.ProseMirror)'); + }); + + if (click) { + await safeClick(page, INPUT_SELECTOR, { bias: 'input' }); + await sleep(500, 1000); } } @@ -181,7 +174,8 @@ async function initBrowser(config) { userDataDir: config.paths.userDataDir, targetUrl: TARGET_URL, productName: 'Zai.is', - waitInputValidator + waitInputValidator, + navigationHandler: handleDiscordAuth }); return { ...base, config }; } @@ -199,10 +193,15 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) { const { page, config } = context; try { - // 开启新对话 + // 开启新对话 - 先等待可能正在进行的登录处理完成 + await waitForAuthComplete(); + logger.info('适配器', '开启新会话', meta); await page.goto(TARGET_URL, { waitUntil: 'domcontentloaded' }); + // 如果触发了登录跳转,等待全局处理器完成 + await waitForAuthComplete(); + // 1. 等待输入框加载(使用公共函数处理登录) logger.debug('适配器', '正在寻找输入框...', meta); await waitForInputWithAuth(page, { click: false }); diff --git a/lib/browser/launcher.js b/lib/browser/launcher.js index 47796f1..5eba273 100644 --- a/lib/browser/launcher.js +++ b/lib/browser/launcher.js @@ -174,6 +174,7 @@ function getPersistentFingerprint(filePath) { * @param {string} options.productName - 产品名称(用于日志) * @param {boolean} [options.reuseExistingTab=false] - 是否复用已有特定域名的 tab * @param {Function} [options.waitInputValidator] - 自定义输入框等待验证函数 + * @param {Function} [options.navigationHandler] - 全局导航处理器,用于自动处理登录等跳转 * @returns {Promise<{browser: object, page: object, client: object}>} */ export async function initBrowserBase(config, options) { @@ -181,7 +182,8 @@ export async function initBrowserBase(config, options) { userDataDir, targetUrl, productName, - waitInputValidator = null + waitInputValidator = null, + navigationHandler = null } = options; // 检测登录模式和 Xvfb 模式 @@ -275,6 +277,18 @@ export async function initBrowserBase(config, options) { await page.setViewportSize(camoufoxLaunchOptions.viewport); } + // 注册全局导航处理器(用于自动处理登录等跳转) + if (navigationHandler) { + page.on('framenavigated', async () => { + try { + await navigationHandler(page); + } catch (e) { + logger.warn('浏览器', `全局导航处理器出错: ${e.message}`); + } + }); + logger.debug('浏览器', '已注册全局导航处理器'); + } + // 登录模式挂起逻辑 if (isLoginMode) { // 尝试导航到目标页面方便用户登录