Эх сурвалжийг харах

feat: 添加邮箱验证页面处理逻辑,优化步骤 2 跳过密码步骤的逻辑,更新相关测试用例

QLHazyCoder 4 сар өмнө
parent
commit
1a5f569d16

+ 3 - 1
.gitignore

@@ -2,4 +2,6 @@
 
 /.github
 /_metadata/
-.vscode
+.vscode
+
+/data/account-run-history.txt

+ 34 - 2
background.js

@@ -3274,6 +3274,16 @@ function isSignupPasswordPageUrl(rawUrl) {
     && /\/create-account\/password(?:[/?#]|$)/i.test(parsed.pathname || '');
 }
 
+function isSignupEmailVerificationPageUrl(rawUrl) {
+  if (typeof navigationUtils !== 'undefined' && navigationUtils?.isSignupEmailVerificationPageUrl) {
+    return navigationUtils.isSignupEmailVerificationPageUrl(rawUrl);
+  }
+  const parsed = parseUrlSafely(rawUrl);
+  if (!parsed) return false;
+  return isSignupPageHost(parsed.hostname)
+    && /\/email-verification(?:[/?#]|$)/i.test(parsed.pathname || '');
+}
+
 function is163MailHost(hostname = '') {
   if (typeof navigationUtils !== 'undefined' && navigationUtils?.is163MailHost) {
     return navigationUtils.is163MailHost(hostname);
@@ -4306,6 +4316,17 @@ async function handleStepData(step, payload) {
       }
       break;
     }
+    case 2:
+      if (payload.email) await setEmailState(payload.email);
+      if (payload.skippedPasswordStep) {
+        const latestState = await getState();
+        const step3Status = latestState.stepStatuses?.[3];
+        if (step3Status !== 'running' && step3Status !== 'completed' && step3Status !== 'manual_completed') {
+          await setStepStatus(3, 'skipped');
+          await addLog('步骤 2:提交邮箱后页面直接进入邮箱验证码页,已自动跳过步骤 3。', 'warn');
+        }
+      }
+      break;
     case 3:
       if (payload.email) await setEmailState(payload.email);
       if (payload.signupVerificationRequestedAt) {
@@ -4994,13 +5015,19 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
   }
 
   if (startStep <= 3) {
+    const latestState = await getState();
+    const step3Status = latestState.stepStatuses?.[3] || 'pending';
     await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:阶段 2,填写密码、验证、登录并完成授权(第 ${attemptRuns} 次尝试)===`, 'info');
     await broadcastAutoRunStatus('running', {
       currentRun: targetRun,
       totalRuns,
       attemptRun: attemptRuns,
     });
-    await executeStepAndWait(3, AUTO_STEP_DELAYS[3]);
+    if (isStepDoneStatus(step3Status)) {
+      await addLog(`自动运行:步骤 3 当前状态为 ${step3Status},将直接继续后续流程。`, 'info');
+    } else {
+      await executeStepAndWait(3, AUTO_STEP_DELAYS[3]);
+    }
   } else {
     await addLog(`=== 目标 ${targetRun}/${totalRuns} 轮:继续执行剩余流程(第 ${attemptRuns} 次尝试)===`, 'info');
   }
@@ -5177,6 +5204,7 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe
   ensureLuckmailPurchaseForFlow,
   getTabId,
   isGeneratedAliasProvider,
+  isSignupEmailVerificationPageUrl,
   isHotmailProvider,
   isLuckmailProvider,
   isSignupPasswordPageUrl,
@@ -5228,7 +5256,7 @@ const step2Executor = self.MultiPageBackgroundStep2?.createStep2Executor({
   completeStepFromBackground,
   ensureContentScriptReadyOnTab,
   ensureSignupEntryPageReady,
-  ensureSignupPasswordPageReadyInTab,
+  ensureSignupPostEmailPageReadyInTab,
   getTabId,
   isTabAlive,
   resolveSignupEmailForFlow,
@@ -5457,6 +5485,10 @@ async function ensureSignupPasswordPageReadyInTab(tabId, step = 2, options = {})
   return signupFlowHelpers.ensureSignupPasswordPageReadyInTab(tabId, step, options);
 }
 
+async function ensureSignupPostEmailPageReadyInTab(tabId, step = 2, options = {}) {
+  return signupFlowHelpers.ensureSignupPostEmailPageReadyInTab(tabId, step, options);
+}
+
 async function resolveSignupEmailForFlow(state) {
   return signupFlowHelpers.resolveSignupEmailForFlow(state);
 }

+ 13 - 0
background/message-router.js

@@ -109,6 +109,19 @@
           }
           break;
         }
+        case 2:
+          if (payload.email) {
+            await setEmailState(payload.email);
+          }
+          if (payload.skippedPasswordStep) {
+            const latestState = await getState();
+            const step3Status = latestState.stepStatuses?.[3];
+            if (step3Status !== 'running' && step3Status !== 'completed' && step3Status !== 'manual_completed') {
+              await setStepStatus(3, 'skipped');
+              await addLog('步骤 2:提交邮箱后页面直接进入邮箱验证码页,已自动跳过步骤 3。', 'warn');
+            }
+          }
+          break;
         case 3:
           if (payload.email) await setEmailState(payload.email);
           if (payload.signupVerificationRequestedAt) {

+ 8 - 0
background/navigation-utils.js

@@ -52,6 +52,13 @@
         && /\/create-account\/password(?:[/?#]|$)/i.test(parsed.pathname || '');
     }
 
+    function isSignupEmailVerificationPageUrl(rawUrl) {
+      const parsed = parseUrlSafely(rawUrl);
+      if (!parsed) return false;
+      return isSignupPageHost(parsed.hostname)
+        && /\/email-verification(?:[/?#]|$)/i.test(parsed.pathname || '');
+    }
+
     function is163MailHost(hostname = '') {
       return hostname === 'mail.163.com'
         || hostname.endsWith('.mail.163.com')
@@ -154,6 +161,7 @@
       is163MailHost,
       isLocalCpaUrl,
       isLocalhostOAuthCallbackUrl,
+      isSignupEmailVerificationPageUrl,
       isSignupEntryHost,
       isSignupPageHost,
       isSignupPasswordPageUrl,

+ 59 - 8
background/signup-flow-helpers.js

@@ -3,18 +3,16 @@
 })(typeof self !== 'undefined' ? self : globalThis, function createSignupFlowHelpersModule() {
   function createSignupFlowHelpers(deps = {}) {
     const {
-      addLog,
       buildGeneratedAliasEmail,
       chrome,
       ensureContentScriptReadyOnTab,
       ensureHotmailAccountForFlow,
       ensureLuckmailPurchaseForFlow,
-      getTabId,
       isGeneratedAliasProvider,
       isHotmailProvider,
       isLuckmailProvider,
+      isSignupEmailVerificationPageUrl,
       isSignupPasswordPageUrl,
-      isTabAlive,
       reuseOrCreateTab,
       sendToContentScriptResilient,
       setEmailState,
@@ -60,27 +58,66 @@
       return { tabId, result: result || {} };
     }
 
-    async function ensureSignupPasswordPageReadyInTab(tabId, step = 2, options = {}) {
+    function resolveSignupPostEmailState(rawUrl) {
+      if (isSignupPasswordPageUrl(rawUrl)) {
+        return 'password_page';
+      }
+      if (isSignupEmailVerificationPageUrl(rawUrl)) {
+        return 'verification_page';
+      }
+      return '';
+    }
+
+    async function ensureSignupPostEmailPageReadyInTab(tabId, step = 2, options = {}) {
       const { skipUrlWait = false } = options;
+      let landingUrl = '';
+      let landingState = '';
 
       if (!skipUrlWait) {
-        const matchedTab = await waitForTabUrlMatch(tabId, (url) => isSignupPasswordPageUrl(url), {
+        const matchedTab = await waitForTabUrlMatch(tabId, (url) => Boolean(resolveSignupPostEmailState(url)), {
           timeoutMs: 45000,
           retryDelayMs: 300,
         });
         if (!matchedTab) {
-          throw new Error('等待进入密码页超时,请检查邮箱提交后页面是否仍停留在官网或邮箱页。');
+          throw new Error('等待邮箱提交后的页面跳转超时,请检查页面是否仍停留在邮箱输入页。');
+        }
+
+        landingUrl = matchedTab.url || '';
+        landingState = resolveSignupPostEmailState(landingUrl);
+      }
+
+      if (!landingState) {
+        try {
+          const currentTab = await chrome.tabs.get(tabId);
+          landingUrl = landingUrl || currentTab?.url || '';
+          landingState = resolveSignupPostEmailState(landingUrl);
+        } catch {
+          landingUrl = landingUrl || '';
         }
       }
 
+      if (!landingState) {
+        throw new Error(`邮箱提交后未能识别当前页面,既不是密码页也不是邮箱验证码页。URL: ${landingUrl || 'unknown'}`);
+      }
+
       await ensureContentScriptReadyOnTab('signup-page', tabId, {
         inject: SIGNUP_PAGE_INJECT_FILES,
         injectSource: 'signup-page',
         timeoutMs: 45000,
         retryDelayMs: 900,
-        logMessage: `步骤 ${step}:密码页仍在加载,正在重试连接内容脚本...`,
+        logMessage: landingState === 'verification_page'
+          ? `步骤 ${step}:邮箱验证码页仍在加载,正在等待页面恢复...`
+          : `步骤 ${step}:密码页仍在加载,正在重试连接内容脚本...`,
       });
 
+      if (landingState === 'verification_page') {
+        return {
+          ready: true,
+          state: landingState,
+          url: landingUrl,
+        };
+      }
+
       const result = await sendToContentScriptResilient('signup-page', {
         type: 'ENSURE_SIGNUP_PASSWORD_PAGE_READY',
         step,
@@ -96,7 +133,20 @@
         throw new Error(result.error);
       }
 
-      return result || {};
+      return {
+        ...(result || {}),
+        ready: true,
+        state: landingState,
+        url: landingUrl,
+      };
+    }
+
+    async function ensureSignupPasswordPageReadyInTab(tabId, step = 2, options = {}) {
+      const result = await ensureSignupPostEmailPageReadyInTab(tabId, step, options);
+      if (result.state !== 'password_page') {
+        throw new Error(`当前页面不是密码页,实际落地为 ${result.state || 'unknown'}。URL: ${result.url || 'unknown'}`);
+      }
+      return result;
     }
 
     async function resolveSignupEmailForFlow(state) {
@@ -128,6 +178,7 @@
 
     return {
       ensureSignupEntryPageReady,
+      ensureSignupPostEmailPageReadyInTab,
       ensureSignupPasswordPageReadyInTab,
       openSignupEntryTab,
       resolveSignupEmailForFlow,

+ 10 - 4
background/steps/submit-signup-email.js

@@ -8,7 +8,7 @@
       completeStepFromBackground,
       ensureContentScriptReadyOnTab,
       ensureSignupEntryPageReady,
-      ensureSignupPasswordPageReadyInTab,
+      ensureSignupPostEmailPageReadyInTab,
       getTabId,
       isTabAlive,
       resolveSignupEmailForFlow,
@@ -50,13 +50,19 @@
       }
 
       if (!step2Result?.alreadyOnPasswordPage) {
-        await addLog(`步骤 2:邮箱 ${resolvedEmail} 已提交,正在等待进入密码页...`);
+        await addLog(`步骤 2:邮箱 ${resolvedEmail} 已提交,正在等待页面加载并确认下一步入口...`);
       }
 
-      await ensureSignupPasswordPageReadyInTab(signupTabId, 2, {
+      const landingResult = await ensureSignupPostEmailPageReadyInTab(signupTabId, 2, {
         skipUrlWait: Boolean(step2Result?.alreadyOnPasswordPage),
       });
-      await completeStepFromBackground(2, {});
+
+      await completeStepFromBackground(2, {
+        email: resolvedEmail,
+        nextSignupState: landingResult?.state || 'password_page',
+        nextSignupUrl: landingResult?.url || step2Result?.url || '',
+        skippedPasswordStep: landingResult?.state === 'verification_page',
+      });
     }
 
     return { executeStep2 };

+ 127 - 0
tests/background-message-router-step2-skip.test.js

@@ -0,0 +1,127 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+const source = fs.readFileSync('background/message-router.js', 'utf8');
+const globalScope = {};
+const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
+
+function createRouter(overrides = {}) {
+  const events = {
+    logs: [],
+    stepStatuses: [],
+    emailStates: [],
+  };
+
+  const router = api.createMessageRouter({
+    addLog: async (message, level) => {
+      events.logs.push({ message, level });
+    },
+    appendAccountRunRecord: async () => null,
+    batchUpdateLuckmailPurchases: async () => {},
+    buildLocalhostCleanupPrefix: () => '',
+    buildLuckmailSessionSettingsPayload: () => ({}),
+    buildPersistentSettingsPayload: () => ({}),
+    broadcastDataUpdate: () => {},
+    cancelScheduledAutoRun: async () => {},
+    checkIcloudSession: async () => {},
+    clearAutoRunTimerAlarm: async () => {},
+    clearLuckmailRuntimeState: async () => {},
+    clearStopRequest: () => {},
+    closeLocalhostCallbackTabs: async () => {},
+    closeTabsByUrlPrefix: async () => {},
+    deleteHotmailAccount: async () => {},
+    deleteHotmailAccounts: async () => {},
+    deleteIcloudAlias: async () => {},
+    deleteUsedIcloudAliases: async () => {},
+    disableUsedLuckmailPurchases: async () => {},
+    doesStepUseCompletionSignal: () => false,
+    ensureManualInteractionAllowed: async () => ({}),
+    executeStep: async () => {},
+    executeStepViaCompletionSignal: async () => {},
+    exportSettingsBundle: async () => ({}),
+    fetchGeneratedEmail: async () => '',
+    finalizeIcloudAliasAfterSuccessfulFlow: async () => {},
+    findHotmailAccount: async () => null,
+    flushCommand: async () => {},
+    getCurrentLuckmailPurchase: () => null,
+    getPendingAutoRunTimerPlan: () => null,
+    getSourceLabel: () => '',
+    getState: async () => overrides.state || { stepStatuses: { 3: 'pending' } },
+    getStopRequested: () => false,
+    handleAutoRunLoopUnhandledError: async () => {},
+    importSettingsBundle: async () => {},
+    invalidateDownstreamAfterStepRestart: async () => {},
+    isAutoRunLockedState: () => false,
+    isHotmailProvider: () => false,
+    isLocalhostOAuthCallbackUrl: () => true,
+    isLuckmailProvider: () => false,
+    isStopError: () => false,
+    launchAutoRunTimerPlan: async () => {},
+    listIcloudAliases: async () => [],
+    listLuckmailPurchasesForManagement: async () => [],
+    normalizeHotmailAccounts: (items) => items,
+    normalizeRunCount: (value) => value,
+    AUTO_RUN_TIMER_KIND_SCHEDULED_START: 'scheduled',
+    notifyStepComplete: () => {},
+    notifyStepError: () => {},
+    patchHotmailAccount: async () => {},
+    registerTab: async () => {},
+    requestStop: async () => {},
+    resetState: async () => {},
+    resumeAutoRun: async () => {},
+    scheduleAutoRun: async () => {},
+    selectLuckmailPurchase: async () => {},
+    setCurrentHotmailAccount: async () => {},
+    setEmailState: async (email) => {
+      events.emailStates.push(email);
+    },
+    setEmailStateSilently: async () => {},
+    setIcloudAliasPreservedState: async () => {},
+    setIcloudAliasUsedState: async () => {},
+    setLuckmailPurchaseDisabledState: async () => {},
+    setLuckmailPurchasePreservedState: async () => {},
+    setLuckmailPurchaseUsedState: async () => {},
+    setPersistentSettings: async () => {},
+    setState: async () => {},
+    setStepStatus: async (step, status) => {
+      events.stepStatuses.push({ step, status });
+    },
+    skipAutoRunCountdown: async () => false,
+    skipStep: async () => {},
+    startAutoRunLoop: async () => {},
+    syncHotmailAccounts: async () => {},
+    testHotmailAccountMailAccess: async () => {},
+    upsertHotmailAccount: async () => {},
+    verifyHotmailAccount: async () => {},
+  });
+
+  return { router, events };
+}
+
+test('message router skips step 3 when step 2 lands on verification page', async () => {
+  const { router, events } = createRouter({
+    state: { stepStatuses: { 3: 'pending' } },
+  });
+
+  await router.handleStepData(2, {
+    email: 'user@example.com',
+    skippedPasswordStep: true,
+  });
+
+  assert.deepStrictEqual(events.emailStates, ['user@example.com']);
+  assert.deepStrictEqual(events.stepStatuses, [{ step: 3, status: 'skipped' }]);
+  assert.equal(events.logs[0]?.message, '步骤 2:提交邮箱后页面直接进入邮箱验证码页,已自动跳过步骤 3。');
+});
+
+test('message router does not overwrite a completed step 3 when step 2 is replayed', async () => {
+  const { router, events } = createRouter({
+    state: { stepStatuses: { 3: 'completed' } },
+  });
+
+  await router.handleStepData(2, {
+    skippedPasswordStep: true,
+  });
+
+  assert.deepStrictEqual(events.stepStatuses, []);
+});

+ 134 - 0
tests/background-signup-step2-branching.test.js

@@ -0,0 +1,134 @@
+const test = require('node:test');
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+
+const step2Source = fs.readFileSync('background/steps/submit-signup-email.js', 'utf8');
+const step2GlobalScope = {};
+const step2Api = new Function('self', `${step2Source}; return self.MultiPageBackgroundStep2;`)(step2GlobalScope);
+
+const signupFlowSource = fs.readFileSync('background/signup-flow-helpers.js', 'utf8');
+const signupFlowGlobalScope = {};
+const signupFlowApi = new Function('self', `${signupFlowSource}; return self.MultiPageSignupFlowHelpers;`)(signupFlowGlobalScope);
+
+test('step 2 completes with password step skipped when landing on email verification page', async () => {
+  const completedPayloads = [];
+
+  const executor = step2Api.createStep2Executor({
+    addLog: async () => {},
+    chrome: { tabs: { update: async () => {} } },
+    completeStepFromBackground: async (step, payload) => {
+      completedPayloads.push({ step, payload });
+    },
+    ensureContentScriptReadyOnTab: async () => {},
+    ensureSignupEntryPageReady: async () => ({ tabId: 11 }),
+    ensureSignupPostEmailPageReadyInTab: async () => ({
+      state: 'verification_page',
+      url: 'https://auth.openai.com/email-verification',
+    }),
+    getTabId: async () => 11,
+    isTabAlive: async () => true,
+    resolveSignupEmailForFlow: async () => 'user@example.com',
+    sendToContentScriptResilient: async () => ({ submitted: true }),
+    SIGNUP_PAGE_INJECT_FILES: [],
+  });
+
+  await executor.executeStep2({ email: 'user@example.com' });
+
+  assert.deepStrictEqual(completedPayloads, [
+    {
+      step: 2,
+      payload: {
+        email: 'user@example.com',
+        nextSignupState: 'verification_page',
+        nextSignupUrl: 'https://auth.openai.com/email-verification',
+        skippedPasswordStep: true,
+      },
+    },
+  ]);
+});
+
+test('step 2 keeps password flow when landing on password page', async () => {
+  const completedPayloads = [];
+
+  const executor = step2Api.createStep2Executor({
+    addLog: async () => {},
+    chrome: { tabs: { update: async () => {} } },
+    completeStepFromBackground: async (step, payload) => {
+      completedPayloads.push({ step, payload });
+    },
+    ensureContentScriptReadyOnTab: async () => {},
+    ensureSignupEntryPageReady: async () => ({ tabId: 12 }),
+    ensureSignupPostEmailPageReadyInTab: async () => ({
+      state: 'password_page',
+      url: 'https://auth.openai.com/create-account/password',
+    }),
+    getTabId: async () => 12,
+    isTabAlive: async () => true,
+    resolveSignupEmailForFlow: async () => 'user@example.com',
+    sendToContentScriptResilient: async () => ({ submitted: true }),
+    SIGNUP_PAGE_INJECT_FILES: [],
+  });
+
+  await executor.executeStep2({ email: 'user@example.com' });
+
+  assert.deepStrictEqual(completedPayloads, [
+    {
+      step: 2,
+      payload: {
+        email: 'user@example.com',
+        nextSignupState: 'password_page',
+        nextSignupUrl: 'https://auth.openai.com/create-account/password',
+        skippedPasswordStep: false,
+      },
+    },
+  ]);
+});
+
+test('signup flow helper recognizes email verification page as post-email landing page', async () => {
+  let ensureCalls = 0;
+  let passwordReadyChecks = 0;
+
+  const helpers = signupFlowApi.createSignupFlowHelpers({
+    buildGeneratedAliasEmail: () => '',
+    chrome: {
+      tabs: {
+        get: async () => ({
+          id: 21,
+          url: 'https://auth.openai.com/email-verification',
+        }),
+      },
+    },
+    ensureContentScriptReadyOnTab: async () => {
+      ensureCalls += 1;
+    },
+    ensureHotmailAccountForFlow: async () => ({}),
+    ensureLuckmailPurchaseForFlow: async () => ({}),
+    isGeneratedAliasProvider: () => false,
+    isHotmailProvider: () => false,
+    isLuckmailProvider: () => false,
+    isSignupEmailVerificationPageUrl: (url) => /\/email-verification(?:[/?#]|$)/i.test(url || ''),
+    isSignupPasswordPageUrl: (url) => /\/create-account\/password(?:[/?#]|$)/i.test(url || ''),
+    reuseOrCreateTab: async () => 21,
+    sendToContentScriptResilient: async () => {
+      passwordReadyChecks += 1;
+      return {};
+    },
+    setEmailState: async () => {},
+    SIGNUP_ENTRY_URL: 'https://chatgpt.com/',
+    SIGNUP_PAGE_INJECT_FILES: [],
+    waitForTabUrlMatch: async () => ({
+      id: 21,
+      url: 'https://auth.openai.com/email-verification',
+    }),
+  });
+
+  const result = await helpers.ensureSignupPostEmailPageReadyInTab(21, 2);
+
+  assert.deepStrictEqual(result, {
+    ready: true,
+    state: 'verification_page',
+    url: 'https://auth.openai.com/email-verification',
+  });
+  assert.equal(ensureCalls, 1);
+  assert.equal(passwordReadyChecks, 0);
+});