Bladeren bron

refactor: 重构第六七步登录验证码职责

QLHazyCoder 4 maanden geleden
bovenliggende
commit
b1d5d2d8d4
3 gewijzigde bestanden met toevoegingen van 750 en 251 verwijderingen
  1. 15 1
      README.md
  2. 172 110
      background.js
  3. 563 140
      content/signup-page.js

+ 15 - 1
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

+ 172 - 110
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;
-  }
-
-  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;
+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 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;
       }

+ 563 - 140
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,28 +653,250 @@ 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;
+  }
+
+  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 {
-    error: buildStep7RestartFromStep6Marker('login_timeout_error_page', timeoutPage.url),
-    restartFromStep6: true,
-    reason: 'login_timeout_error_page',
-    url: timeoutPage.url,
+    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() {
   return isSignupPasswordPage() && SIGNUP_EMAIL_EXISTS_PATTERN.test(getPageTextSnapshot());
 }
@@ -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 step6_login(payload) {
-  const { email, password } = payload;
-  if (!email) throw new Error('登录时缺少邮箱地址。');
+async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 12000) {
+  const start = Date.now();
+  let snapshot = inspectLoginAuthState();
 
-  log(`步骤 6:正在使用 ${email} 登录...`);
+  while (Date.now() - start < timeout) {
+    throwIfStopped();
+    snapshot = inspectLoginAuthState();
 
-  // 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);
+    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);
   }
 
-  await humanPause(500, 1400);
-  fillInput(emailInput, email);
-  log('步骤 6:邮箱已填写');
+  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}`);
+  }
 
-  // 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:邮箱已提交');
+  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 };
   }
 
-  await sleep(2000);
+  return {
+    action: 'recoverable',
+    result: createStep6RecoverableResult('password_submit_stalled', snapshot, {
+      message: '提交密码后仍未进入登录验证码页。',
+    }),
+  };
+}
 
-  // Check for password field
-  const passwordInput = document.querySelector('input[type="password"]');
-  if (passwordInput) {
-    log('步骤 6:已找到密码输入框,正在填写密码...');
+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(passwordInput, password);
+    fillInput(currentSnapshot.passwordInput, payload.password);
+    log('步骤 6:已填写密码');
 
     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)');
+    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;
     }
-    return;
+    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 } = payload;
+  if (!email) throw new Error('登录时缺少邮箱地址。');
+
+  log(`步骤 6:正在使用 ${email} 登录...`);
+
+  const snapshot = await waitForKnownLoginAuthState(15000);
+
+  if (snapshot.state === 'verification_page') {
+    log('步骤 6:登录验证码页面已就绪。', 'ok');
+    return createStep6SuccessResult(snapshot, { via: 'already_on_verification_page' });
+  }
+
+  if (snapshot.state === 'login_timeout_error_page') {
+    log('步骤 6:检测到登录超时报错,准备重新执行步骤 6。', 'warn');
+    return createStep6RecoverableResult('login_timeout_error_page', snapshot, {
+      message: '当前页面处于登录超时报错页。',
+    });
+  }
+
+  if (snapshot.state === 'email_page') {
+    return step6LoginFromEmailPage(payload, snapshot);
+  }
+
+  if (snapshot.state === 'password_page') {
+    return step6LoginFromPasswordPage(payload, snapshot);
   }
 
-  // No password field — OTP flow
-  log('步骤 6:未发现密码输入框,可能进入验证码流程或自动跳转。');
-  reportComplete(6, { needsOTP: true });
+  throwForStep6FatalState(snapshot);
+  throw new Error(`无法识别当前登录页面状态。URL: ${snapshot?.url || location.href}`);
 }
 
 // ============================================================