From 078a5457c565878e8c00a9fe252e87426a0e00c1 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Sat, 11 Apr 2026 23:03:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E6=AD=A5=E9=AA=A4=20?= =?UTF-8?q?8=20=E7=9A=84=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86=E5=92=8C?= =?UTF-8?q?=E7=82=B9=E5=87=BB=E7=AD=96=E7=95=A5=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=20OAuth=20=E6=8E=88=E6=9D=83=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 203 ++++++++++++++++++++++++++++++++++++----- content/signup-page.js | 129 ++++++++++++++++++++++++-- 2 files changed, 301 insertions(+), 31 deletions(-) diff --git a/background.js b/background.js index c0e8458..49697ba 100644 --- a/background.js +++ b/background.js @@ -2443,6 +2443,138 @@ async function executeStep7(state) { // ============================================================ let webNavListener = null; +const STEP8_CLICK_EFFECT_TIMEOUT_MS = 3500; +const STEP8_CLICK_RETRY_DELAY_MS = 500; +const STEP8_READY_WAIT_TIMEOUT_MS = 30000; +const STEP8_STRATEGIES = [ + { mode: 'content', strategy: 'requestSubmit', label: 'form.requestSubmit' }, + { mode: 'debugger', label: 'debugger click' }, + { mode: 'content', strategy: 'nativeClick', label: 'element.click' }, + { mode: 'content', strategy: 'dispatchClick', label: 'dispatch click' }, + { mode: 'debugger', label: 'debugger click retry' }, +]; + +async function getStep8PageState(tabId, responseTimeoutMs = 1500) { + try { + const result = await sendTabMessageWithTimeout(tabId, 'signup-page', { + type: 'STEP8_GET_STATE', + source: 'background', + payload: {}, + }, responseTimeoutMs); + if (result?.error) { + throw new Error(result.error); + } + return result; + } catch (err) { + if (isRetryableContentScriptTransportError(err)) { + return null; + } + throw err; + } +} + +async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS) { + const start = Date.now(); + + while (Date.now() - start < timeoutMs) { + throwIfStopped(); + const pageState = await getStep8PageState(tabId); + if (pageState?.addPhonePage) { + throw new Error('步骤 8:认证页进入了手机号页面,当前不是 OAuth 同意页,无法继续自动授权。'); + } + if (pageState?.consentReady) { + return pageState; + } + await sleepWithStop(250); + } + + throw new Error('步骤 8:长时间未进入 OAuth 同意页,无法定位“继续”按钮。'); +} + +async function prepareStep8DebuggerClick() { + const result = await sendToContentScriptResilient('signup-page', { + type: 'STEP8_FIND_AND_CLICK', + source: 'background', + payload: {}, + }, { + timeoutMs: 15000, + retryDelayMs: 600, + logMessage: '步骤 8:认证页正在切换,等待 OAuth 同意页按钮重新就绪...', + }); + + if (result?.error) { + throw new Error(result.error); + } + + return result; +} + +async function triggerStep8ContentStrategy(strategy) { + const result = await sendToContentScriptResilient('signup-page', { + type: 'STEP8_TRIGGER_CONTINUE', + source: 'background', + payload: { + strategy, + findTimeoutMs: 4000, + enabledTimeoutMs: 3000, + }, + }, { + timeoutMs: 15000, + retryDelayMs: 600, + logMessage: '步骤 8:认证页正在切换,等待“继续”按钮重新就绪...', + }); + + if (result?.error) { + throw new Error(result.error); + } + + return result; +} + +async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLICK_EFFECT_TIMEOUT_MS) { + const start = Date.now(); + + while (Date.now() - start < timeoutMs) { + throwIfStopped(); + + const tab = await chrome.tabs.get(tabId).catch(() => null); + if (!tab) { + throw new Error('步骤 8:认证页面标签页已关闭,无法继续自动授权。'); + } + + if (baselineUrl && typeof tab.url === 'string' && tab.url !== baselineUrl) { + return { progressed: true, reason: 'url_changed', url: tab.url }; + } + + const pageState = await getStep8PageState(tabId); + if (pageState?.addPhonePage) { + throw new Error('步骤 8:点击“继续”后页面跳到了手机号页面,当前流程无法继续自动授权。'); + } + if (pageState === null) { + return { progressed: true, reason: 'page_reloading' }; + } + if (pageState && !pageState.consentPage) { + return { progressed: true, reason: 'left_consent_page', url: pageState.url }; + } + + await sleepWithStop(200); + } + + return { progressed: false, reason: 'no_effect' }; +} + +function getStep8EffectLabel(effect) { + switch (effect?.reason) { + case 'url_changed': + return `URL 已变化:${effect.url}`; + case 'page_reloading': + return '页面正在跳转或重载'; + case 'left_consent_page': + return `页面已离开 OAuth 同意页:${effect.url || 'unknown'}`; + default: + return '页面仍停留在 OAuth 同意页'; + } +} async function executeStep8(state) { if (!state.oauthUrl) { @@ -2451,7 +2583,6 @@ async function executeStep8(state) { await addLog('步骤 8:正在监听 localhost 回调地址...'); - // 只在当前步骤内注册 webNavigation 监听 return new Promise((resolve, reject) => { let resolved = false; let signupTabId = null; @@ -2463,9 +2594,16 @@ async function executeStep8(state) { } }; - const timeout = setTimeout(() => { + const failStep8 = (error) => { + if (resolved) return; + resolved = true; + clearTimeout(timeout); cleanupListener(); - reject(new Error('120 秒内未捕获到 localhost 回调跳转,步骤 8 的点击可能被拦截了。')); + reject(error); + }; + + const timeout = setTimeout(() => { + failStep8(new Error('120 秒内未捕获到 localhost 回调跳转,步骤 8 已持续重试点击“继续”但页面仍未完成授权。')); }, 120000); webNavListener = (details) => { @@ -2487,40 +2625,61 @@ async function executeStep8(state) { } }; - - // 步骤 7 之后,认证页会进入 OAuth 同意页,出现“继续”按钮。 - // 这里先在页面内定位按钮,再通过 debugger 输入事件发起点击。 (async () => { try { signupTabId = await getTabId('signup-page'); if (signupTabId) { await chrome.tabs.update(signupTabId, { active: true }); - await addLog('步骤 8:已切回认证页,正在准备调试器点击...'); + await addLog('步骤 8:已切回认证页,准备循环确认“继续”按钮直到页面真正跳转...'); } else { signupTabId = await reuseOrCreateTab('signup-page', state.oauthUrl); - await addLog('步骤 8:已重新打开认证页,正在准备调试器点击...'); + await addLog('步骤 8:已重新打开认证页,准备循环确认“继续”按钮直到页面真正跳转...'); } chrome.webNavigation.onBeforeNavigate.addListener(webNavListener); - const clickResult = await sendToContentScript('signup-page', { - type: 'STEP8_FIND_AND_CLICK', - source: 'background', - payload: {}, - }); + let attempt = 0; + while (!resolved) { + const pageState = await waitForStep8Ready(signupTabId); + if (!pageState?.consentReady) { + await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS); + continue; + } - if (clickResult?.error) { - throw new Error(clickResult.error); - } + const strategy = STEP8_STRATEGIES[attempt % STEP8_STRATEGIES.length]; + const round = attempt + 1; + attempt += 1; - if (!resolved) { - await clickWithDebugger(signupTabId, clickResult?.rect); - await addLog('步骤 8:已发送调试器点击,正在等待跳转...'); + await addLog(`步骤 8:第 ${round} 次尝试点击“继续”(${strategy.label})...`); + + if (strategy.mode === 'debugger') { + const clickTarget = await prepareStep8DebuggerClick(); + if (!resolved) { + await clickWithDebugger(signupTabId, clickTarget?.rect); + } + } else { + await triggerStep8ContentStrategy(strategy.strategy); + } + + if (resolved) { + return; + } + + const effect = await waitForStep8ClickEffect(signupTabId, pageState.url); + if (resolved) { + return; + } + + if (effect.progressed) { + await addLog(`步骤 8:检测到本次点击已生效,${getStep8EffectLabel(effect)},继续等待 localhost 回调...`, 'info'); + break; + } + + await addLog(`步骤 8:${strategy.label} 本次未触发页面离开同意页,准备继续重试。`, 'warn'); + await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS); } } catch (err) { - clearTimeout(timeout); - cleanupListener(); - reject(err); + failStep8(err); } })(); }); diff --git a/content/signup-page.js b/content/signup-page.js index 7fe1f0d..9934753 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -9,6 +9,8 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { message.type === 'EXECUTE_STEP' || message.type === 'FILL_CODE' || message.type === 'STEP8_FIND_AND_CLICK' + || message.type === 'STEP8_GET_STATE' + || message.type === 'STEP8_TRIGGER_CONTINUE' || message.type === 'PREPARE_LOGIN_CODE' || message.type === 'PREPARE_SIGNUP_VERIFICATION' || message.type === 'RESEND_VERIFICATION_CODE' @@ -58,6 +60,10 @@ async function handleCommand(message) { return await resendVerificationCode(message.step); case 'STEP8_FIND_AND_CLICK': return await step8_findAndClick(); + case 'STEP8_GET_STATE': + return getStep8State(); + case 'STEP8_TRIGGER_CONTINUE': + return await step8_triggerContinue(message.payload); } } @@ -928,13 +934,7 @@ async function step6_login(payload) { async function step8_findAndClick() { log('步骤 8:正在查找 OAuth 同意页的“继续”按钮...'); - const continueBtn = await findContinueButton(); - await waitForButtonEnabled(continueBtn); - - await humanPause(350, 900); - continueBtn.scrollIntoView({ behavior: 'smooth', block: 'center' }); - continueBtn.focus(); - await sleep(250); + const continueBtn = await prepareStep8ContinueButton(); const rect = getSerializableRect(continueBtn); log('步骤 8:已找到“继续”按钮并准备好调试器点击坐标。'); @@ -945,9 +945,82 @@ async function step8_findAndClick() { }; } -async function findContinueButton() { +function getStep8State() { + const pageText = getPageTextSnapshot(); + const continueBtn = getPrimaryContinueButton(); + const state = { + url: location.href, + consentPage: OAUTH_CONSENT_PAGE_PATTERN.test(pageText), + consentReady: isStep8Ready(), + verificationPage: isVerificationPageStillVisible(), + addPhonePage: isAddPhonePageReady(), + buttonFound: Boolean(continueBtn), + buttonEnabled: isButtonEnabled(continueBtn), + buttonText: continueBtn ? getActionText(continueBtn) : '', + }; + + if (continueBtn) { + try { + state.rect = getSerializableRect(continueBtn); + } catch { + state.rect = null; + } + } + + return state; +} + +async function step8_triggerContinue(payload = {}) { + const strategy = payload?.strategy || 'requestSubmit'; + const continueBtn = await prepareStep8ContinueButton({ + findTimeoutMs: payload?.findTimeoutMs, + enabledTimeoutMs: payload?.enabledTimeoutMs, + }); + const form = continueBtn.form || continueBtn.closest('form'); + + switch (strategy) { + case 'requestSubmit': + if (!form || typeof form.requestSubmit !== 'function') { + throw new Error('“继续”按钮当前不在可提交的 form 中,无法使用 requestSubmit。URL: ' + location.href); + } + form.requestSubmit(continueBtn); + break; + case 'nativeClick': + continueBtn.click(); + break; + case 'dispatchClick': + simulateClick(continueBtn); + break; + default: + throw new Error(`未知的 Step 8 触发策略:${strategy}`); + } + + log(`Step 8: continue button triggered via ${strategy}.`); + return { + strategy, + ...getStep8State(), + }; +} + +async function prepareStep8ContinueButton(options = {}) { + const { + findTimeoutMs = 10000, + enabledTimeoutMs = 8000, + } = options; + + const continueBtn = await findContinueButton(findTimeoutMs); + await waitForButtonEnabled(continueBtn, enabledTimeoutMs); + + await humanPause(250, 700); + continueBtn.scrollIntoView({ behavior: 'auto', block: 'center' }); + continueBtn.focus(); + await waitForStableButtonRect(continueBtn); + return continueBtn; +} + +async function findContinueButton(timeout = 10000) { const start = Date.now(); - while (Date.now() - start < 10000) { + while (Date.now() - start < timeout) { throwIfStopped(); if (isAddPhonePageReady()) { throw new Error('当前页面已进入手机号页面,不是 OAuth 授权同意页。URL: ' + location.href); @@ -978,6 +1051,44 @@ function isButtonEnabled(button) { && button.getAttribute('aria-disabled') !== 'true'; } +async function waitForStableButtonRect(button, timeout = 1500) { + let previous = null; + let stableSamples = 0; + const start = Date.now(); + + while (Date.now() - start < timeout) { + throwIfStopped(); + const rect = button?.getBoundingClientRect?.(); + if (rect && rect.width > 0 && rect.height > 0) { + const snapshot = { + left: rect.left, + top: rect.top, + width: rect.width, + height: rect.height, + }; + + if ( + previous + && Math.abs(snapshot.left - previous.left) < 1 + && Math.abs(snapshot.top - previous.top) < 1 + && Math.abs(snapshot.width - previous.width) < 1 + && Math.abs(snapshot.height - previous.height) < 1 + ) { + stableSamples += 1; + if (stableSamples >= 2) { + return; + } + } else { + stableSamples = 0; + } + + previous = snapshot; + } + + await sleep(80); + } +} + function getSerializableRect(el) { const rect = el.getBoundingClientRect(); if (!rect.width || !rect.height) {