From b1d5d2d8d41eee9eca05b5f35964e8dceb8d21e3 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Tue, 14 Apr 2026 23:50:15 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E7=AC=AC?= =?UTF-8?q?=E5=85=AD=E4=B8=83=E6=AD=A5=E7=99=BB=E5=BD=95=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E7=A0=81=E8=81=8C=E8=B4=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 16 +- background.js | 282 +++++++++------- content/signup-page.js | 711 ++++++++++++++++++++++++++++++++--------- 3 files changed, 754 insertions(+), 255 deletions(-) diff --git a/README.md b/README.md index 87d60c3..b6aca3f 100644 --- a/README.md +++ b/README.md @@ -492,13 +492,27 @@ Cloudflare 模式下,插件不会再调用 Cloudflare API 创建路由。 在登录前会先重新获取一遍最新的 CPA OAuth 链接,再使用刚注册的账号登录。 +当前 Step 6 的完成标准不是“邮箱/密码已提交”,而是: + +- 认证页已经真正进入登录验证码页面 +- 如遇登录超时报错或登录页长时间停滞,会由后台刷新 OAuth 后重跑整个 Step 6 + 支持: - 邮箱 + 密码登录 -- 提交后进入验证码验证流程 +- 必要时切换到一次性验证码登录 +- 直到登录验证码页就绪才算步骤完成 ### Step 7: Get Login Code +Step 7 默认要求当前认证页已经处于登录验证码页。 + +它只负责: + +- 打开邮箱并轮询登录验证码 +- 填写并提交登录验证码 +- 验证码链路失败后按有限次数回退到 Step 6 + 与 Step 4 类似,但会使用稍微不同的关键词组合去找登录验证码邮件。 ### Step 8: Manual OAuth Confirm diff --git a/background.js b/background.js index 1585fc1..8160a76 100644 --- a/background.js +++ b/background.js @@ -2368,6 +2368,10 @@ function getContentScriptResponseTimeoutMs(message) { return 30000; } + if (message.type === 'EXECUTE_STEP' && Number(message.step) === 6) { + return 75000; + } + if (message.type === 'POLL_EMAIL') { const maxAttempts = Math.max(1, Number(message.payload?.maxAttempts) || 1); const intervalMs = Math.max(0, Number(message.payload?.intervalMs) || 0); @@ -2851,50 +2855,23 @@ function isVerificationMailPollingError(error) { return /未在 .*邮箱中找到新的匹配邮件|未在 Hotmail 收件箱中找到新的匹配验证码|邮箱轮询结束,但未获取到验证码|无法获取新的(?:注册|登录)验证码|页面未能重新就绪|页面通信异常|did not respond in \d+s/i.test(message); } -const STEP7_RESTART_FROM_STEP6_ERROR_CODE = 'STEP7_RESTART_FROM_STEP6'; -const STEP7_RESTART_FROM_STEP6_MARKER_PATTERN = /^STEP7_RESTART_FROM_STEP6::([^:]+)::(.*)$/; - -function createStep7RestartFromStep6Error(details = {}) { - const { reason = 'unknown', url = '' } = details || {}; - const reasonLabel = reason === 'login_timeout_error_page' - ? '检测到登录页超时报错' - : '步骤 7 请求回到步骤 6'; - const error = new Error(`步骤 7:${reasonLabel}。${url ? `URL: ${url}` : ''}`.trim()); - error.code = STEP7_RESTART_FROM_STEP6_ERROR_CODE; - error.restartReason = reason; - error.restartUrl = url; - return error; -} - -function parseStep7RestartFromStep6Marker(message) { - const normalized = getErrorMessage(message); - const match = normalized.match(STEP7_RESTART_FROM_STEP6_MARKER_PATTERN); - if (!match) { - return null; +function getLoginAuthStateLabel(state) { + switch (state) { + case 'verification_page': + return '登录验证码页'; + case 'password_page': + return '密码页'; + case 'email_page': + return '邮箱输入页'; + case 'login_timeout_error_page': + return '登录超时报错页'; + case 'oauth_consent_page': + return 'OAuth 授权页'; + case 'add_phone_page': + return '手机号页'; + default: + return '未知页面'; } - - return { - reason: match[1] || 'unknown', - url: match[2] || '', - }; -} - -function getStep7RestartFromStep6Error(result) { - if (result?.restartFromStep6) { - return createStep7RestartFromStep6Error(result); - } - - const parsed = parseStep7RestartFromStep6Marker(result?.error); - if (!parsed) { - return null; - } - - return createStep7RestartFromStep6Error(parsed); -} - -function isStep7RestartFromStep6Error(error) { - return error?.code === STEP7_RESTART_FROM_STEP6_ERROR_CODE - || Boolean(parseStep7RestartFromStep6Marker(error)); } function isRestartCurrentAttemptError(error) { @@ -3886,8 +3863,8 @@ async function handleStepData(step, payload) { const stepWaiters = new Map(); let resumeWaiter = null; const AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS = 120000; -const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([4, 7, 8]); -const STEP_COMPLETION_SIGNAL_STEPS = new Set([1, 2, 3, 5, 6, 9]); +const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([4, 6, 7, 8]); +const STEP_COMPLETION_SIGNAL_STEPS = new Set([1, 2, 3, 5, 9]); function waitForStepComplete(step, timeoutMs = 120000) { return new Promise((resolve, reject) => { @@ -5710,17 +5687,15 @@ async function requestVerificationCodeResend(step) { payload: {}, }); - if (step === 7) { - const restartError = getStep7RestartFromStep6Error(result); - if (restartError) { - throw restartError; - } - } - if (result && result.error) { throw new Error(result.error); } + const requestedAt = Date.now(); + if (step === 7) { + await setState({ loginVerificationRequestedAt: requestedAt }); + } + const currentState = await getState(); if (currentState.mailProvider === '2925') { const mailTabId = await getTabId('mail-2925'); @@ -5730,16 +5705,18 @@ async function requestVerificationCodeResend(step) { } } - return Date.now(); + return requestedAt; } async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) { + const { onResendRequestedAt, ...cleanPollOverrides } = pollOverrides; + if (mail.provider === HOTMAIL_PROVIDER) { const hotmailPollConfig = getHotmailVerificationPollConfig(step); return pollHotmailVerificationCode(step, state, { ...getVerificationPollPayload(step, state), ...hotmailPollConfig, - ...pollOverrides, + ...cleanPollOverrides, }); } if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) { @@ -5763,17 +5740,23 @@ async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) } let lastError = null; - const filterAfterTimestamp = pollOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp; + let filterAfterTimestamp = cleanPollOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp; const maxRounds = pollOverrides.maxRounds || VERIFICATION_POLL_MAX_ROUNDS; for (let round = 1; round <= maxRounds; round++) { throwIfStopped(); if (round > 1) { - await requestVerificationCodeResend(step); + const requestedAt = await requestVerificationCodeResend(step); + if (typeof onResendRequestedAt === 'function') { + const nextFilterAfterTimestamp = await onResendRequestedAt(requestedAt); + if (nextFilterAfterTimestamp !== undefined) { + filterAfterTimestamp = nextFilterAfterTimestamp; + } + } } const payload = getVerificationPollPayload(step, state, { - ...pollOverrides, + ...cleanPollOverrides, filterAfterTimestamp, excludeCodes: [...rejectedCodes], }); @@ -5835,10 +5818,14 @@ async function pollFreshVerificationCodeWithResendInterval(step, state, mail, po maxRounds: _ignoredMaxRounds, resendIntervalMs: _ignoredResendIntervalMs, lastResendAt: _ignoredLastResendAt, + onResendRequestedAt: _ignoredOnResendRequestedAt, ...payloadOverrides } = pollOverrides; + const onResendRequestedAt = typeof pollOverrides.onResendRequestedAt === 'function' + ? pollOverrides.onResendRequestedAt + : null; let lastError = null; - const filterAfterTimestamp = payloadOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp; + let filterAfterTimestamp = payloadOverrides.filterAfterTimestamp ?? getVerificationPollPayload(step, state).filterAfterTimestamp; const maxRounds = pollOverrides.maxRounds || VERIFICATION_POLL_MAX_ROUNDS; const resendIntervalMs = Math.max(0, Number(pollOverrides.resendIntervalMs) || 0); let lastResendAt = Number(pollOverrides.lastResendAt) || 0; @@ -5847,6 +5834,12 @@ async function pollFreshVerificationCodeWithResendInterval(step, state, mail, po throwIfStopped(); if (round > 1) { lastResendAt = await requestVerificationCodeResend(step); + if (onResendRequestedAt) { + const nextFilterAfterTimestamp = await onResendRequestedAt(lastResendAt); + if (nextFilterAfterTimestamp !== undefined) { + filterAfterTimestamp = nextFilterAfterTimestamp; + } + } } while (true) { @@ -5938,13 +5931,6 @@ async function submitVerificationCode(step, code) { payload: { code }, }); - if (step === 7) { - const restartError = getStep7RestartFromStep6Error(result); - if (restartError) { - throw restartError; - } - } - if (result && result.error) { throw new Error(result.error); } @@ -5963,7 +5949,7 @@ async function resolveVerificationStep(step, state, mail, options = {}) { rejectedCodes.add(state[stateKey]); } - const nextFilterAfterTimestamp = options.filterAfterTimestamp ?? null; + let nextFilterAfterTimestamp = options.filterAfterTimestamp ?? null; const requestFreshCodeFirst = options.requestFreshCodeFirst !== undefined ? Boolean(options.requestFreshCodeFirst) : (hotmailPollConfig?.requestFreshCodeFirst ?? false); @@ -5971,12 +5957,30 @@ async function resolveVerificationStep(step, state, mail, options = {}) { const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0); let lastResendAt = Number(options.lastResendAt) || 0; + const updateFilterAfterTimestampForStep7 = async (requestedAt) => { + if (step !== 7 || !requestedAt) { + return nextFilterAfterTimestamp; + } + + if (mail.provider === HOTMAIL_PROVIDER) { + nextFilterAfterTimestamp = getHotmailVerificationRequestTimestamp(7, { + ...state, + loginVerificationRequestedAt: requestedAt, + }); + } else { + nextFilterAfterTimestamp = Math.max(0, Number(requestedAt) - 60000); + } + + return nextFilterAfterTimestamp; + }; + if (requestFreshCodeFirst) { try { lastResendAt = await requestVerificationCodeResend(step); + await updateFilterAfterTimestampForStep7(lastResendAt); await addLog(`步骤 ${step}:已先请求一封新的${getVerificationCodeLabel(step)}验证码,再开始轮询邮箱。`, 'warn'); } catch (err) { - if (isStopError(err) || (step === 7 && isStep7RestartFromStep6Error(err))) { + if (isStopError(err)) { throw err; } await addLog(`步骤 ${step}:首次重新获取验证码失败:${err.message},将继续使用当前时间窗口轮询。`, 'warn'); @@ -5997,6 +6001,7 @@ async function resolveVerificationStep(step, state, mail, options = {}) { filterAfterTimestamp: nextFilterAfterTimestamp ?? undefined, resendIntervalMs, lastResendAt, + onResendRequestedAt: updateFilterAfterTimestampForStep7, }); lastResendAt = Number(result?.lastResendAt) || lastResendAt; @@ -6025,6 +6030,7 @@ async function resolveVerificationStep(step, state, mail, options = {}) { } lastResendAt = await requestVerificationCodeResend(step); + await updateFilterAfterTimestampForStep7(lastResendAt); await addLog(`步骤 ${step}:提交失败后已请求新验证码(${attempt + 1}/${maxSubmitAttempts})...`, 'warn'); continue; } @@ -6072,9 +6078,6 @@ async function executeStep4(state) { if (prepareResult && prepareResult.error) { throw new Error(prepareResult.error); } - if (prepareResult?.verificationRequestedAt) { - await setState({ loginVerificationRequestedAt: prepareResult.verificationRequestedAt }); - } if (prepareResult?.alreadyVerified) { await completeStepFromBackground(4, {}); return; @@ -6138,7 +6141,7 @@ async function executeStep5(state) { } // ============================================================ -// Step 6: Login ChatGPT (Background opens tab, chatgpt.js handles login) +// Step 6: Login and ensure the auth page reaches the login verification page // ============================================================ async function refreshOAuthUrlBeforeStep6(state) { @@ -6159,28 +6162,110 @@ async function refreshOAuthUrlBeforeStep6(state) { return latestState.oauthUrl; } +function isStep6SuccessResult(result) { + return result?.step6Outcome === 'success'; +} + +function isStep6RecoverableResult(result) { + return result?.step6Outcome === 'recoverable'; +} + +async function getLoginAuthStateFromContent() { + const result = await sendToContentScriptResilient( + 'signup-page', + { + type: 'GET_LOGIN_AUTH_STATE', + source: 'background', + payload: {}, + }, + { + timeoutMs: 15000, + retryDelayMs: 600, + logMessage: '步骤 7:认证页正在切换,等待页面重新就绪后继续确认验证码页状态...', + } + ); + + if (result?.error) { + throw new Error(result.error); + } + + return result || {}; +} + +async function ensureStep7VerificationPageReady() { + const pageState = await getLoginAuthStateFromContent(); + if (pageState.state === 'verification_page') { + return pageState; + } + + const stateLabel = getLoginAuthStateLabel(pageState.state); + const urlPart = pageState.url ? ` URL: ${pageState.url}` : ''; + throw new Error(`当前未进入登录验证码页面,请先重新完成步骤 6。当前状态:${stateLabel}.${urlPart}`.trim()); +} + async function executeStep6(state) { if (!state.email) { throw new Error('缺少邮箱地址,请先完成步骤 3。'); } + let attempt = 0; - const oauthUrl = await refreshOAuthUrlBeforeStep6(state); + while (true) { + throwIfStopped(); + attempt += 1; + const currentState = attempt === 1 ? state : await getState(); + const password = currentState.password || currentState.customPassword || ''; + const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState); - await addLog('步骤 6:正在打开最新 OAuth 链接并登录...'); - // Reuse the signup-page tab — navigate it to the OAuth URL - await reuseOrCreateTab('signup-page', oauthUrl); + if (attempt === 1) { + await addLog('步骤 6:正在打开最新 OAuth 链接并登录...'); + } else { + await addLog(`步骤 6:上一轮登录未进入验证码页,正在重新发起第 ${attempt} 轮登录尝试...`, 'warn'); + } - // signup-page.js will inject (same auth.openai.com domain) and handle login - await sendToContentScript('signup-page', { - type: 'EXECUTE_STEP', - step: 6, - source: 'background', - payload: { email: state.email, password: state.password }, - }); + await reuseOrCreateTab('signup-page', oauthUrl); + + const result = await sendToContentScriptResilient( + 'signup-page', + { + type: 'EXECUTE_STEP', + step: 6, + source: 'background', + payload: { + email: currentState.email, + password, + }, + }, + { + timeoutMs: 180000, + retryDelayMs: 700, + logMessage: '步骤 6:认证页正在切换,等待页面重新就绪后继续登录...', + } + ); + + if (result?.error) { + throw new Error(result.error); + } + + if (isStep6SuccessResult(result)) { + await completeStepFromBackground(6, { + loginVerificationRequestedAt: result.loginVerificationRequestedAt || null, + }); + return; + } + + if (isStep6RecoverableResult(result)) { + const reasonMessage = result.message + || `当前停留在${getLoginAuthStateLabel(result.state)},准备重新执行步骤 6。`; + await addLog(`步骤 6:${reasonMessage}`, 'warn'); + continue; + } + + throw new Error('步骤 6:认证页未返回可识别的登录结果。'); + } } // ============================================================ -// Step 7: Get Login Verification Code (qq-mail.js polls, then fills in chatgpt.js) +// Step 7: Poll login verification mail and submit the login code // ============================================================ async function runStep7Attempt(state) { @@ -6199,22 +6284,8 @@ async function runStep7Attempt(state) { } throwIfStopped(); - await addLog('步骤 7:正在准备认证页,必要时切换到一次性验证码登录...'); - const prepareResult = await sendToContentScript('signup-page', { - type: 'PREPARE_LOGIN_CODE', - step: 7, - source: 'background', - payload: {}, - }); - - const restartError = getStep7RestartFromStep6Error(prepareResult); - if (restartError) { - throw restartError; - } - - if (prepareResult && prepareResult.error) { - throw new Error(prepareResult.error); - } + await ensureStep7VerificationPageReady(); + await addLog('步骤 7:登录验证码页面已就绪,开始获取验证码。', 'info'); if (shouldUseCustomRegistrationEmail(state)) { await confirmCustomVerificationStepBypass(7); @@ -6247,18 +6318,16 @@ async function runStep7Attempt(state) { } await resolveVerificationStep(7, state, mail, { - filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : stepStartedAt, - requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true, + filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : Math.max(0, stepStartedAt - 60000), + requestFreshCodeFirst: false, resendIntervalMs: mail.provider === HOTMAIL_PROVIDER ? 0 : STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS, }); } async function rerunStep6ForStep7Recovery() { const currentState = await getState(); - const waitForStep6 = waitForStepComplete(6, 120000); await addLog('步骤 7:正在回到步骤 6,重新发起登录验证码流程...', 'warn'); await executeStep6(currentState); - await waitForStep6; await sleepWithStop(3000); } @@ -6272,13 +6341,6 @@ async function executeStep7(state) { await runStep7Attempt(currentState); return; } catch (err) { - if (isStep7RestartFromStep6Error(err)) { - await addLog('步骤 7:检测到登录页超时报错,准备从步骤 6 重新开始...', 'warn'); - await rerunStep6ForStep7Recovery(); - currentState = await getState(); - continue; - } - if (!isVerificationMailPollingError(err)) { throw err; } diff --git a/content/signup-page.js b/content/signup-page.js index ea2de6b..6c56621 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -11,7 +11,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { || message.type === 'STEP8_FIND_AND_CLICK' || message.type === 'STEP8_GET_STATE' || message.type === 'STEP8_TRIGGER_CONTINUE' - || message.type === 'PREPARE_LOGIN_CODE' + || message.type === 'GET_LOGIN_AUTH_STATE' || message.type === 'PREPARE_SIGNUP_VERIFICATION' || message.type === 'RESEND_VERIFICATION_CODE' ) { @@ -52,10 +52,10 @@ async function handleCommand(message) { case 'FILL_CODE': // Step 4 = signup code, Step 7 = login code (same handler) return await fillVerificationCode(message.step, message.payload); + case 'GET_LOGIN_AUTH_STATE': + return serializeLoginAuthState(inspectLoginAuthState()); case 'PREPARE_SIGNUP_VERIFICATION': return await prepareSignupVerificationFlow(message.payload); - case 'PREPARE_LOGIN_CODE': - return await prepareLoginCodeFlow(); case 'RESEND_VERIFICATION_CODE': return await resendVerificationCode(message.step); case 'STEP8_FIND_AND_CLICK': @@ -176,86 +176,9 @@ function isEmailVerificationPage() { return /\/email-verification(?:[/?#]|$)/i.test(location.pathname || ''); } -async function prepareLoginCodeFlow(timeout = 15000) { - const readyTarget = getVerificationCodeTarget(); - if (readyTarget) { - log('步骤 7:验证码输入框已就绪。'); - return { ready: true, mode: readyTarget.type }; - } - - if (isEmailVerificationPage() && isVerificationPageStillVisible()) { - log('步骤 7:已进入邮箱验证码页面,正在等待验证码输入框或重发入口稳定。'); - return { ready: true, mode: 'verification_page' }; - } - - const initialRestartSignal = getStep7RestartFromStep6Signal(); - if (initialRestartSignal) { - log('步骤 7:检测到登录页超时报错,准备回到步骤 6 重新发起登录验证码流程...', 'warn'); - return initialRestartSignal; - } - - const start = Date.now(); - let switchClickCount = 0; - let lastSwitchAttemptAt = 0; - let loggedPasswordPage = false; - let loggedVerificationPage = false; - - while (Date.now() - start < timeout) { - throwIfStopped(); - - const target = getVerificationCodeTarget(); - if (target) { - log('步骤 7:验证码页面已就绪。'); - return { ready: true, mode: target.type }; - } - - if (isEmailVerificationPage() && isVerificationPageStillVisible()) { - if (!loggedVerificationPage) { - loggedVerificationPage = true; - log('步骤 7:页面已进入邮箱验证码流程,继续等待验证码输入框渲染...'); - } - await sleep(250); - continue; - } - - const restartSignal = getStep7RestartFromStep6Signal(); - if (restartSignal) { - log('步骤 7:检测到登录页超时报错,准备回到步骤 6 重新发起登录验证码流程...', 'warn'); - return restartSignal; - } - - const passwordInput = document.querySelector('input[type="password"]'); - const switchTrigger = findOneTimeCodeLoginTrigger(); - - if (switchTrigger && (switchClickCount === 0 || Date.now() - lastSwitchAttemptAt > 1500)) { - switchClickCount += 1; - lastSwitchAttemptAt = Date.now(); - loggedPasswordPage = false; - log('步骤 7:检测到密码页,正在切换到一次性验证码登录...'); - await humanPause(350, 900); - const verificationRequestedAt = Date.now(); - simulateClick(switchTrigger); - await sleep(1200); - return { ready: true, mode: 'verification_switch', verificationRequestedAt }; - } - - if (passwordInput && !loggedPasswordPage) { - loggedPasswordPage = true; - log('步骤 7:正在等待密码页上的一次性验证码登录入口...'); - } - - await sleep(200); - } - - throw new Error('无法切换到一次性验证码验证页面。URL: ' + location.href); -} - async function resendVerificationCode(step, timeout = 45000) { if (step === 7) { - const prepareResult = await prepareLoginCodeFlow(); - if (prepareResult?.restartFromStep6) { - return prepareResult; - } + await waitForLoginVerificationPageReady(); } const start = Date.now(); @@ -730,26 +653,248 @@ function getLoginTimeoutErrorPageState() { }); } -function isSignupPasswordErrorPage() { - return Boolean(getSignupPasswordTimeoutErrorPageState()); +function getLoginEmailInput() { + const input = document.querySelector( + 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i], input[placeholder*="Email"]' + ); + return input && isVisibleElement(input) ? input : null; } -function buildStep7RestartFromStep6Marker(reason, url = location.href) { - return `STEP7_RESTART_FROM_STEP6::${reason || 'unknown'}::${url || ''}`; +function getLoginPasswordInput() { + const input = document.querySelector('input[type="password"]'); + return input && isVisibleElement(input) ? input : null; } -function getStep7RestartFromStep6Signal() { - const timeoutPage = getLoginTimeoutErrorPageState(); - if (!timeoutPage) { - return null; +function getLoginSubmitButton({ allowDisabled = false } = {}) { + const direct = document.querySelector('button[type="submit"], input[type="submit"]'); + if (direct && isVisibleElement(direct) && (allowDisabled || isActionEnabled(direct))) { + return direct; } - return { - error: buildStep7RestartFromStep6Marker('login_timeout_error_page', timeoutPage.url), - restartFromStep6: true, - reason: 'login_timeout_error_page', - url: timeoutPage.url, + const candidates = document.querySelectorAll( + 'button, a, [role="button"], [role="link"], input[type="button"], input[type="submit"]' + ); + return Array.from(candidates).find((el) => { + if (!isVisibleElement(el) || (!allowDisabled && !isActionEnabled(el))) return false; + const text = getActionText(el); + if (!text || ONE_TIME_CODE_LOGIN_PATTERN.test(text)) return false; + return /continue|next|submit|sign\s*in|log\s*in|继续|下一步|登录/i.test(text); + }) || null; +} + +function inspectLoginAuthState() { + const retryState = getLoginTimeoutErrorPageState(); + const verificationTarget = getVerificationCodeTarget(); + const passwordInput = getLoginPasswordInput(); + const emailInput = getLoginEmailInput(); + const switchTrigger = findOneTimeCodeLoginTrigger(); + const submitButton = getLoginSubmitButton({ allowDisabled: true }); + const verificationVisible = isVerificationPageStillVisible(); + const addPhonePage = isAddPhonePageReady(); + const consentReady = isStep8Ready(); + const oauthConsentPage = isOAuthConsentPage(); + const baseState = { + state: 'unknown', + url: location.href, + path: location.pathname || '', + retryButton: retryState?.retryButton || null, + retryEnabled: Boolean(retryState?.retryEnabled), + titleMatched: Boolean(retryState?.titleMatched), + detailMatched: Boolean(retryState?.detailMatched), + verificationTarget, + passwordInput, + emailInput, + submitButton, + switchTrigger, + verificationVisible, + addPhonePage, + oauthConsentPage, + consentReady, }; + + if (verificationTarget || verificationVisible) { + return { + ...baseState, + state: 'verification_page', + }; + } + + if (retryState) { + return { + ...baseState, + state: 'login_timeout_error_page', + }; + } + + if (addPhonePage) { + return { + ...baseState, + state: 'add_phone_page', + }; + } + + if (oauthConsentPage) { + return { + ...baseState, + state: 'oauth_consent_page', + }; + } + + if (passwordInput || switchTrigger) { + return { + ...baseState, + state: 'password_page', + }; + } + + if (emailInput) { + return { + ...baseState, + state: 'email_page', + }; + } + + return baseState; +} + +function serializeLoginAuthState(snapshot) { + return { + state: snapshot?.state || 'unknown', + url: snapshot?.url || location.href, + path: snapshot?.path || location.pathname || '', + retryEnabled: Boolean(snapshot?.retryEnabled), + titleMatched: Boolean(snapshot?.titleMatched), + detailMatched: Boolean(snapshot?.detailMatched), + hasVerificationTarget: Boolean(snapshot?.verificationTarget), + hasPasswordInput: Boolean(snapshot?.passwordInput), + hasEmailInput: Boolean(snapshot?.emailInput), + hasSubmitButton: Boolean(snapshot?.submitButton), + hasSwitchTrigger: Boolean(snapshot?.switchTrigger), + verificationVisible: Boolean(snapshot?.verificationVisible), + addPhonePage: Boolean(snapshot?.addPhonePage), + oauthConsentPage: Boolean(snapshot?.oauthConsentPage), + consentReady: Boolean(snapshot?.consentReady), + }; +} + +function getLoginAuthStateLabel(snapshot) { + switch (snapshot?.state) { + case 'verification_page': + return '登录验证码页'; + case 'password_page': + return '密码页'; + case 'email_page': + return '邮箱输入页'; + case 'login_timeout_error_page': + return '登录超时报错页'; + case 'oauth_consent_page': + return 'OAuth 授权页'; + case 'add_phone_page': + return '手机号页'; + default: + return '未知页面'; + } +} + +async function waitForKnownLoginAuthState(timeout = 15000) { + const start = Date.now(); + let snapshot = inspectLoginAuthState(); + + while (Date.now() - start < timeout) { + throwIfStopped(); + snapshot = inspectLoginAuthState(); + if (snapshot.state !== 'unknown') { + return snapshot; + } + await sleep(200); + } + + return snapshot; +} + +async function waitForLoginVerificationPageReady(timeout = 10000) { + const start = Date.now(); + let snapshot = inspectLoginAuthState(); + + while (Date.now() - start < timeout) { + throwIfStopped(); + snapshot = inspectLoginAuthState(); + if (snapshot.state === 'verification_page') { + return snapshot; + } + if (snapshot.state !== 'unknown') { + break; + } + await sleep(200); + } + + throw new Error( + `当前未进入登录验证码页面,请先重新完成步骤 6。当前状态:${getLoginAuthStateLabel(snapshot)}。URL: ${snapshot?.url || location.href}` + ); +} + +function createStep6SuccessResult(snapshot, options = {}) { + return { + step6Outcome: 'success', + state: snapshot?.state || 'verification_page', + url: snapshot?.url || location.href, + via: options.via || '', + loginVerificationRequestedAt: options.loginVerificationRequestedAt || null, + }; +} + +function createStep6RecoverableResult(reason, snapshot, options = {}) { + return { + step6Outcome: 'recoverable', + reason, + state: snapshot?.state || 'unknown', + url: snapshot?.url || location.href, + message: options.message || '', + loginVerificationRequestedAt: options.loginVerificationRequestedAt || null, + }; +} + +function throwForStep6FatalState(snapshot) { + switch (snapshot?.state) { + case 'oauth_consent_page': + throw new Error(`当前页面已进入 OAuth 授权页,未经过登录验证码页,无法完成步骤 6。URL: ${snapshot.url}`); + case 'add_phone_page': + throw new Error(`当前页面已进入手机号页面,未经过登录验证码页,无法完成步骤 6。URL: ${snapshot.url}`); + case 'unknown': + throw new Error(`无法识别当前登录页面状态。URL: ${snapshot?.url || location.href}`); + default: + return; + } +} + +async function triggerLoginSubmitAction(button, fallbackField) { + const form = button?.form || fallbackField?.form || button?.closest?.('form') || fallbackField?.closest?.('form') || null; + + await humanPause(400, 1100); + if (button && isActionEnabled(button)) { + simulateClick(button); + return; + } + + if (form && typeof form.requestSubmit === 'function') { + if (button && button.form === form) { + form.requestSubmit(button); + } else { + form.requestSubmit(); + } + return; + } + + if (button && typeof button.click === 'function') { + button.click(); + return; + } + + throw new Error('未找到可用的登录提交按钮。URL: ' + location.href); +} + +function isSignupPasswordErrorPage() { + return Boolean(getSignupPasswordTimeoutErrorPageState()); } function isSignupEmailAlreadyExistsPage() { @@ -922,10 +1067,7 @@ async function fillVerificationCode(step, payload) { log(`步骤 ${step}:正在填写验证码:${code}`); if (step === 7) { - const prepareResult = await prepareLoginCodeFlow(); - if (prepareResult?.restartFromStep6) { - return prepareResult; - } + await waitForLoginVerificationPageReady(); } // Find code input — could be a single input or multiple separate inputs @@ -986,63 +1128,344 @@ async function fillVerificationCode(step, payload) { // Step 6: Login with registered account (on OAuth auth page) // ============================================================ +async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 12000) { + const start = Date.now(); + let snapshot = inspectLoginAuthState(); + + while (Date.now() - start < timeout) { + throwIfStopped(); + snapshot = inspectLoginAuthState(); + + if (snapshot.state === 'verification_page') { + return { + action: 'done', + result: createStep6SuccessResult(snapshot, { + via: 'email_submit', + loginVerificationRequestedAt: emailSubmittedAt, + }), + }; + } + + if (snapshot.state === 'password_page') { + return { action: 'password', snapshot }; + } + + if (snapshot.state === 'login_timeout_error_page') { + return { + action: 'recoverable', + result: createStep6RecoverableResult('login_timeout_error_page', snapshot, { + message: '提交邮箱后进入登录超时报错页。', + }), + }; + } + + if (snapshot.state === 'oauth_consent_page') { + throw new Error(`提交邮箱后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`); + } + + if (snapshot.state === 'add_phone_page') { + throw new Error(`提交邮箱后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`); + } + + await sleep(250); + } + + snapshot = inspectLoginAuthState(); + if (snapshot.state === 'verification_page') { + return { + action: 'done', + result: createStep6SuccessResult(snapshot, { + via: 'email_submit', + loginVerificationRequestedAt: emailSubmittedAt, + }), + }; + } + if (snapshot.state === 'password_page') { + return { action: 'password', snapshot }; + } + if (snapshot.state === 'login_timeout_error_page') { + return { + action: 'recoverable', + result: createStep6RecoverableResult('login_timeout_error_page', snapshot, { + message: '提交邮箱后进入登录超时报错页。', + }), + }; + } + if (snapshot.state === 'oauth_consent_page') { + throw new Error(`提交邮箱后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`); + } + if (snapshot.state === 'add_phone_page') { + throw new Error(`提交邮箱后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`); + } + + return { + action: 'recoverable', + result: createStep6RecoverableResult('email_submit_stalled', snapshot, { + message: '提交邮箱后长时间未进入密码页或登录验证码页。', + }), + }; +} + +async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout = 10000) { + const start = Date.now(); + let snapshot = inspectLoginAuthState(); + + while (Date.now() - start < timeout) { + throwIfStopped(); + snapshot = inspectLoginAuthState(); + + if (snapshot.state === 'verification_page') { + return { + action: 'done', + result: createStep6SuccessResult(snapshot, { + via: 'password_submit', + loginVerificationRequestedAt: passwordSubmittedAt, + }), + }; + } + + if (snapshot.state === 'login_timeout_error_page') { + return { + action: 'recoverable', + result: createStep6RecoverableResult('login_timeout_error_page', snapshot, { + message: '提交密码后进入登录超时报错页。', + }), + }; + } + + if (snapshot.state === 'oauth_consent_page') { + throw new Error(`提交密码后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`); + } + + if (snapshot.state === 'add_phone_page') { + throw new Error(`提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`); + } + + await sleep(250); + } + + snapshot = inspectLoginAuthState(); + if (snapshot.state === 'verification_page') { + return { + action: 'done', + result: createStep6SuccessResult(snapshot, { + via: 'password_submit', + loginVerificationRequestedAt: passwordSubmittedAt, + }), + }; + } + if (snapshot.state === 'login_timeout_error_page') { + return { + action: 'recoverable', + result: createStep6RecoverableResult('login_timeout_error_page', snapshot, { + message: '提交密码后进入登录超时报错页。', + }), + }; + } + if (snapshot.state === 'oauth_consent_page') { + throw new Error(`提交密码后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`); + } + if (snapshot.state === 'add_phone_page') { + throw new Error(`提交密码后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`); + } + if (snapshot.state === 'password_page' && snapshot.switchTrigger) { + return { action: 'switch', snapshot }; + } + + return { + action: 'recoverable', + result: createStep6RecoverableResult('password_submit_stalled', snapshot, { + message: '提交密码后仍未进入登录验证码页。', + }), + }; +} + +async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeout = 10000) { + const start = Date.now(); + let snapshot = inspectLoginAuthState(); + + while (Date.now() - start < timeout) { + throwIfStopped(); + snapshot = inspectLoginAuthState(); + + if (snapshot.state === 'verification_page') { + return createStep6SuccessResult(snapshot, { + via: 'switch_to_one_time_code_login', + loginVerificationRequestedAt, + }); + } + + if (snapshot.state === 'login_timeout_error_page') { + return createStep6RecoverableResult('login_timeout_error_page', snapshot, { + message: '切换到一次性验证码登录后进入登录超时报错页。', + }); + } + + if (snapshot.state === 'oauth_consent_page') { + throw new Error(`切换到一次性验证码登录后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`); + } + + if (snapshot.state === 'add_phone_page') { + throw new Error(`切换到一次性验证码登录后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`); + } + + await sleep(250); + } + + snapshot = inspectLoginAuthState(); + if (snapshot.state === 'verification_page') { + return createStep6SuccessResult(snapshot, { + via: 'switch_to_one_time_code_login', + loginVerificationRequestedAt, + }); + } + if (snapshot.state === 'login_timeout_error_page') { + return createStep6RecoverableResult('login_timeout_error_page', snapshot, { + message: '切换到一次性验证码登录后进入登录超时报错页。', + }); + } + if (snapshot.state === 'oauth_consent_page') { + throw new Error(`切换到一次性验证码登录后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`); + } + if (snapshot.state === 'add_phone_page') { + throw new Error(`切换到一次性验证码登录后页面直接进入手机号页面,未经过登录验证码页。URL: ${snapshot.url}`); + } + + return createStep6RecoverableResult('one_time_code_switch_stalled', snapshot, { + message: '点击一次性验证码登录后仍未进入登录验证码页。', + }); +} + +async function step6SwitchToOneTimeCodeLogin(snapshot) { + const switchTrigger = snapshot?.switchTrigger || findOneTimeCodeLoginTrigger(); + if (!switchTrigger || !isActionEnabled(switchTrigger)) { + return createStep6RecoverableResult('missing_one_time_code_trigger', inspectLoginAuthState(), { + message: '当前登录页没有可用的一次性验证码登录入口。', + }); + } + + log('步骤 6:已检测到一次性验证码登录入口,准备切换...'); + const loginVerificationRequestedAt = Date.now(); + await humanPause(350, 900); + simulateClick(switchTrigger); + log('步骤 6:已点击一次性验证码登录'); + await sleep(1200); + return waitForStep6SwitchTransition(loginVerificationRequestedAt); +} + +async function step6LoginFromPasswordPage(payload, snapshot) { + const currentSnapshot = snapshot || inspectLoginAuthState(); + + if (currentSnapshot.passwordInput) { + if (!payload.password) { + throw new Error('登录时缺少密码,步骤 6 无法继续。'); + } + + log('步骤 6:已进入密码页,准备填写密码...'); + await humanPause(550, 1450); + fillInput(currentSnapshot.passwordInput, payload.password); + log('步骤 6:已填写密码'); + + await sleep(500); + const passwordSubmittedAt = Date.now(); + await triggerLoginSubmitAction(currentSnapshot.submitButton, currentSnapshot.passwordInput); + log('步骤 6:已提交密码'); + + const transition = await waitForStep6PasswordSubmitTransition(passwordSubmittedAt); + if (transition.action === 'done') { + log('步骤 6:已进入登录验证码页面。', 'ok'); + return transition.result; + } + if (transition.action === 'recoverable') { + log(`步骤 6:${transition.result.message || '提交密码后仍未进入登录验证码页面,准备重新执行步骤 6。'}`, 'warn'); + return transition.result; + } + if (transition.action === 'switch') { + return step6SwitchToOneTimeCodeLogin(transition.snapshot); + } + + return createStep6RecoverableResult('password_submit_unknown', inspectLoginAuthState(), { + message: '提交密码后未得到可用的下一步状态。', + }); + } + + if (currentSnapshot.switchTrigger) { + return step6SwitchToOneTimeCodeLogin(currentSnapshot); + } + + return createStep6RecoverableResult('password_page_unactionable', currentSnapshot, { + message: '当前停留在登录页,但没有可提交密码的输入框,也没有一次性验证码登录入口。', + }); +} + +async function step6LoginFromEmailPage(payload, snapshot) { + const currentSnapshot = snapshot || inspectLoginAuthState(); + const emailInput = currentSnapshot.emailInput || getLoginEmailInput(); + if (!emailInput) { + throw new Error('在登录页未找到邮箱输入框。URL: ' + location.href); + } + + if ((emailInput.value || '').trim() !== payload.email) { + await humanPause(500, 1400); + fillInput(emailInput, payload.email); + log('步骤 6:已填写邮箱'); + } else { + log('步骤 6:邮箱已在输入框中,准备提交...'); + } + + await sleep(500); + const emailSubmittedAt = Date.now(); + await triggerLoginSubmitAction(currentSnapshot.submitButton, emailInput); + log('步骤 6:已提交邮箱'); + + const transition = await waitForStep6EmailSubmitTransition(emailSubmittedAt); + if (transition.action === 'done') { + log('步骤 6:已进入登录验证码页面。', 'ok'); + return transition.result; + } + if (transition.action === 'recoverable') { + log(`步骤 6:${transition.result.message || '提交邮箱后仍未进入目标页面,准备重新执行步骤 6。'}`, 'warn'); + return transition.result; + } + if (transition.action === 'password') { + return step6LoginFromPasswordPage(payload, transition.snapshot); + } + + return createStep6RecoverableResult('email_submit_unknown', inspectLoginAuthState(), { + message: '提交邮箱后未得到可用的下一步状态。', + }); +} + async function step6_login(payload) { - const { email, password } = payload; + const { email } = payload; if (!email) throw new Error('登录时缺少邮箱地址。'); log(`步骤 6:正在使用 ${email} 登录...`); - // Wait for email input on the auth page - let emailInput = null; - try { - emailInput = await waitForElement( - 'input[type="email"], input[name="email"], input[name="username"], input[id*="email"], input[placeholder*="email" i], input[placeholder*="Email"]', - 15000 - ); - } catch { - throw new Error('在登录页未找到邮箱输入框。URL: ' + location.href); + const snapshot = await waitForKnownLoginAuthState(15000); + + if (snapshot.state === 'verification_page') { + log('步骤 6:登录验证码页面已就绪。', 'ok'); + return createStep6SuccessResult(snapshot, { via: 'already_on_verification_page' }); } - await humanPause(500, 1400); - fillInput(emailInput, email); - log('步骤 6:邮箱已填写'); - - // Submit email - await sleep(500); - const submitBtn1 = document.querySelector('button[type="submit"]') - || await waitForElementByText('button', /continue|next|submit|继续|下一步/i, 5000).catch(() => null); - if (submitBtn1) { - await humanPause(400, 1100); - simulateClick(submitBtn1); - log('步骤 6:邮箱已提交'); + if (snapshot.state === 'login_timeout_error_page') { + log('步骤 6:检测到登录超时报错,准备重新执行步骤 6。', 'warn'); + return createStep6RecoverableResult('login_timeout_error_page', snapshot, { + message: '当前页面处于登录超时报错页。', + }); } - await sleep(2000); - - // Check for password field - const passwordInput = document.querySelector('input[type="password"]'); - if (passwordInput) { - log('步骤 6:已找到密码输入框,正在填写密码...'); - await humanPause(550, 1450); - fillInput(passwordInput, password); - - await sleep(500); - const submitBtn2 = document.querySelector('button[type="submit"]') - || await waitForElementByText('button', /continue|log\s*in|submit|sign\s*in|登录|继续/i, 5000).catch(() => null); - // Report complete BEFORE submit in case page navigates - reportComplete(6, { needsOTP: true }); - - if (submitBtn2) { - await humanPause(450, 1200); - simulateClick(submitBtn2); - log('步骤 6:密码已提交,可能还需要验证码(步骤 7)'); - } - return; + if (snapshot.state === 'email_page') { + return step6LoginFromEmailPage(payload, snapshot); } - // No password field — OTP flow - log('步骤 6:未发现密码输入框,可能进入验证码流程或自动跳转。'); - reportComplete(6, { needsOTP: true }); + if (snapshot.state === 'password_page') { + return step6LoginFromPasswordPage(payload, snapshot); + } + + throwForStep6FatalState(snapshot); + throw new Error(`无法识别当前登录页面状态。URL: ${snapshot?.url || location.href}`); } // ============================================================