feat: 修改监听跳转的逻辑为任何时候被触发都执行自动登录

This commit is contained in:
foxhui
2025-12-11 23:29:04 +08:00
Unverified
parent 8cdf410ddd
commit 02e6cf0142
3 changed files with 122 additions and 109 deletions
+57 -57
View File
@@ -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<boolean>} 是否处理了跳转
*/
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 });
+50 -51
View File
@@ -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<boolean>} 是否处理了登录
*/
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 });
+15 -1
View File
@@ -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) {
// 尝试导航到目标页面方便用户登录