From 1a5f569d16caef81a35601b34e0a982b029d7fd5 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Fri, 17 Apr 2026 03:54:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E9=82=AE=E7=AE=B1?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E9=A1=B5=E9=9D=A2=E5=A4=84=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E4=BC=98=E5=8C=96=E6=AD=A5=E9=AA=A4=202=20?= =?UTF-8?q?=E8=B7=B3=E8=BF=87=E5=AF=86=E7=A0=81=E6=AD=A5=E9=AA=A4=E7=9A=84?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E6=9B=B4=E6=96=B0=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 +- background.js | 36 ++++- background/message-router.js | 13 ++ background/navigation-utils.js | 8 ++ background/signup-flow-helpers.js | 67 +++++++-- background/steps/submit-signup-email.js | 14 +- ...ckground-message-router-step2-skip.test.js | 127 +++++++++++++++++ .../background-signup-step2-branching.test.js | 134 ++++++++++++++++++ 8 files changed, 388 insertions(+), 15 deletions(-) create mode 100644 tests/background-message-router-step2-skip.test.js create mode 100644 tests/background-signup-step2-branching.test.js diff --git a/.gitignore b/.gitignore index 59f4367..830d0c2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ /.github /_metadata/ -.vscode \ No newline at end of file +.vscode + +/data/account-run-history.txt \ No newline at end of file diff --git a/background.js b/background.js index 1d15a77..eea794d 100644 --- a/background.js +++ b/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); } diff --git a/background/message-router.js b/background/message-router.js index 4e2ac53..d2c4bb4 100644 --- a/background/message-router.js +++ b/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) { diff --git a/background/navigation-utils.js b/background/navigation-utils.js index ec5133d..4926e63 100644 --- a/background/navigation-utils.js +++ b/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, diff --git a/background/signup-flow-helpers.js b/background/signup-flow-helpers.js index 6fe97d7..9a82c40 100644 --- a/background/signup-flow-helpers.js +++ b/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,17 +58,46 @@ 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, { @@ -78,9 +105,19 @@ 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, diff --git a/background/steps/submit-signup-email.js b/background/steps/submit-signup-email.js index 669b828..77e6b65 100644 --- a/background/steps/submit-signup-email.js +++ b/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 }; diff --git a/tests/background-message-router-step2-skip.test.js b/tests/background-message-router-step2-skip.test.js new file mode 100644 index 0000000..a16d79e --- /dev/null +++ b/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, []); +}); diff --git a/tests/background-signup-step2-branching.test.js b/tests/background-signup-step2-branching.test.js new file mode 100644 index 0000000..31feb5d --- /dev/null +++ b/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); +});