diff --git a/background.js b/background.js index 37ec740..217e695 100644 --- a/background.js +++ b/background.js @@ -4,6 +4,7 @@ importScripts( 'background/panel-bridge.js', 'background/generated-email-helpers.js', 'background/signup-flow-helpers.js', + 'background/verification-flow.js', 'background/steps/registry.js', 'background/steps/open-chatgpt.js', 'background/steps/submit-signup-email.js', @@ -6405,6 +6406,33 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe SIGNUP_PAGE_INJECT_FILES, waitForTabUrlMatch, }); +const verificationFlowHelpers = self.MultiPageBackgroundVerificationFlow?.createVerificationFlowHelpers({ + addLog, + chrome, + CLOUDFLARE_TEMP_EMAIL_PROVIDER, + completeStepFromBackground, + confirmCustomVerificationStepBypassRequest: (step) => chrome.runtime.sendMessage({ + type: 'REQUEST_CUSTOM_VERIFICATION_BYPASS_CONFIRMATION', + payload: { step }, + }), + getHotmailVerificationPollConfig, + getHotmailVerificationRequestTimestamp, + getState, + getTabId, + HOTMAIL_PROVIDER, + isStopError, + LUCKMAIL_PROVIDER, + pollCloudflareTempEmailVerificationCode, + pollHotmailVerificationCode, + pollLuckmailVerificationCode, + sendToContentScript, + sendToMailContentScriptResilient, + setState, + setStepStatus, + sleepWithStop, + throwIfStopped, + VERIFICATION_POLL_MAX_ROUNDS, +}); const step1Executor = self.MultiPageBackgroundStep1?.createStep1Executor({ addLog, completeStepFromBackground, @@ -6439,14 +6467,14 @@ const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({ addLog, chrome, completeStepFromBackground, - confirmCustomVerificationStepBypass, + confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass, getMailConfig, getTabId, HOTMAIL_PROVIDER, isTabAlive, LUCKMAIL_PROVIDER, CLOUDFLARE_TEMP_EMAIL_PROVIDER, - resolveVerificationStep, + resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep, reuseOrCreateTab, sendToContentScriptResilient, shouldUseCustomRegistrationEmail, @@ -6483,7 +6511,7 @@ const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({ addLog, chrome, CLOUDFLARE_TEMP_EMAIL_PROVIDER, - confirmCustomVerificationStepBypass, + confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass, ensureStep7VerificationPageReady, executeStep6: (...args) => executeStep6(...args), getMailConfig, @@ -6493,7 +6521,7 @@ const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({ isTabAlive, isVerificationMailPollingError, LUCKMAIL_PROVIDER, - resolveVerificationStep, + resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep, reuseOrCreateTab, setState, setStepStatus, @@ -6677,452 +6705,39 @@ function normalizeInbucketOrigin(rawValue) { } function getVerificationCodeStateKey(step) { - return step === 4 ? 'lastSignupCode' : 'lastLoginCode'; + return verificationFlowHelpers.getVerificationCodeStateKey(step); } function getVerificationCodeLabel(step) { - return step === 4 ? '注册' : '登录'; + return verificationFlowHelpers.getVerificationCodeLabel(step); } async function confirmCustomVerificationStepBypass(step) { - const verificationLabel = getVerificationCodeLabel(step); - await addLog(`步骤 ${step}:当前为自定义邮箱模式,请手动在页面中输入${verificationLabel}验证码并进入下一页面。`, 'warn'); - - let response = null; - try { - response = await chrome.runtime.sendMessage({ - type: 'REQUEST_CUSTOM_VERIFICATION_BYPASS_CONFIRMATION', - payload: { step }, - }); - } catch { - throw new Error(`步骤 ${step}:无法打开确认弹窗,请先保持侧边栏打开后重试。`); - } - - if (response?.error) { - throw new Error(response.error); - } - if (!response?.confirmed) { - throw new Error(`步骤 ${step}:已取消手动${verificationLabel}验证码确认。`); - } - - await setState({ - lastEmailTimestamp: null, - signupVerificationRequestedAt: null, - loginVerificationRequestedAt: null, - }); - await setStepStatus(step, 'skipped'); - await addLog(`步骤 ${step}:已确认手动完成${verificationLabel}验证码输入,当前步骤已跳过。`, 'warn'); + return verificationFlowHelpers.confirmCustomVerificationStepBypass(step); } function getVerificationPollPayload(step, state, overrides = {}) { - if (step === 4) { - return { - filterAfterTimestamp: getHotmailVerificationRequestTimestamp(4, state), - senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'], - subjectFilters: ['verify', 'verification', 'code', '楠岃瘉', 'confirm'], - targetEmail: state.email, - maxAttempts: 5, - intervalMs: 3000, - ...overrides, - }; - } - - return { - filterAfterTimestamp: getHotmailVerificationRequestTimestamp(7, state), - senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'], - subjectFilters: ['verify', 'verification', 'code', '楠岃瘉', 'confirm', 'login'], - targetEmail: state.email, - maxAttempts: 5, - intervalMs: 3000, - ...overrides, - }; + return verificationFlowHelpers.getVerificationPollPayload(step, state, overrides); } async function requestVerificationCodeResend(step) { - throwIfStopped(); - const signupTabId = await getTabId('signup-page'); - if (!signupTabId) { - throw new Error('认证页面标签页已关闭,无法重新请求验证码。'); - } - - throwIfStopped(); - await chrome.tabs.update(signupTabId, { active: true }); - throwIfStopped(); - await addLog(`步骤 ${step}:正在请求新的${getVerificationCodeLabel(step)}验证码...`, 'warn'); - throwIfStopped(); - - const result = await sendToContentScript('signup-page', { - type: 'RESEND_VERIFICATION_CODE', - step, - source: 'background', - payload: {}, - }); - - 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'); - if (mailTabId) { - await chrome.tabs.update(mailTabId, { active: true }); - await addLog(`步骤 ${step}:已切换到 2925 邮箱标签页等待新邮件。`, 'info'); - } - } - - return requestedAt; + return verificationFlowHelpers.requestVerificationCodeResend(step); } 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, - ...cleanPollOverrides, - }); - } - if (mail.provider === LUCKMAIL_PROVIDER) { - return pollLuckmailVerificationCode(step, state, { - ...getVerificationPollPayload(step, state), - ...pollOverrides, - }); - } - if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) { - return pollCloudflareTempEmailVerificationCode(step, state, { - ...getVerificationPollPayload(step, state), - ...pollOverrides, - }); - } - - if (Number(pollOverrides.resendIntervalMs) > 0) { - return pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides); - } - - const stateKey = getVerificationCodeStateKey(step); - const rejectedCodes = new Set(); - if (state[stateKey]) { - rejectedCodes.add(state[stateKey]); - } - for (const code of (pollOverrides.excludeCodes || [])) { - if (code) rejectedCodes.add(code); - } - - let lastError = null; - 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) { - const requestedAt = await requestVerificationCodeResend(step); - if (typeof onResendRequestedAt === 'function') { - const nextFilterAfterTimestamp = await onResendRequestedAt(requestedAt); - if (nextFilterAfterTimestamp !== undefined) { - filterAfterTimestamp = nextFilterAfterTimestamp; - } - } - } - - const payload = getVerificationPollPayload(step, state, { - ...cleanPollOverrides, - filterAfterTimestamp, - excludeCodes: [...rejectedCodes], - }); - - try { - const result = await sendToMailContentScriptResilient( - mail, - { - type: 'POLL_EMAIL', - step, - source: 'background', - payload, - }, - { - timeoutMs: 45000, - maxRecoveryAttempts: 2, - } - ); - - if (result && result.error) { - throw new Error(result.error); - } - - if (!result || !result.code) { - throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到验证码。`); - } - - if (rejectedCodes.has(result.code)) { - throw new Error(`步骤 ${step}:再次收到了相同的${getVerificationCodeLabel(step)}验证码:${result.code}`); - } - - return result; - } catch (err) { - if (isStopError(err)) { - throw err; - } - lastError = err; - await addLog(`步骤 ${step}:${err.message}`, 'warn'); - if (round < maxRounds) { - await addLog(`步骤 ${step}:将重新发送验证码后重试(${round + 1}/${maxRounds})...`, 'warn'); - } - } - } - - throw lastError || new Error(`步骤 ${step}:无法获取新的${getVerificationCodeLabel(step)}验证码。`); + return verificationFlowHelpers.pollFreshVerificationCode(step, state, mail, pollOverrides); } async function pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides = {}) { - const stateKey = getVerificationCodeStateKey(step); - const rejectedCodes = new Set(); - if (state[stateKey]) { - rejectedCodes.add(state[stateKey]); - } - for (const code of (pollOverrides.excludeCodes || [])) { - if (code) rejectedCodes.add(code); - } - - const { - maxRounds: _ignoredMaxRounds, - resendIntervalMs: _ignoredResendIntervalMs, - lastResendAt: _ignoredLastResendAt, - onResendRequestedAt: _ignoredOnResendRequestedAt, - ...payloadOverrides - } = pollOverrides; - const onResendRequestedAt = typeof pollOverrides.onResendRequestedAt === 'function' - ? pollOverrides.onResendRequestedAt - : null; - let lastError = null; - 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; - - for (let round = 1; round <= maxRounds; round++) { - throwIfStopped(); - if (round > 1) { - lastResendAt = await requestVerificationCodeResend(step); - if (onResendRequestedAt) { - const nextFilterAfterTimestamp = await onResendRequestedAt(lastResendAt); - if (nextFilterAfterTimestamp !== undefined) { - filterAfterTimestamp = nextFilterAfterTimestamp; - } - } - } - - while (true) { - throwIfStopped(); - const payload = getVerificationPollPayload(step, state, { - ...payloadOverrides, - filterAfterTimestamp, - excludeCodes: [...rejectedCodes], - }); - - if (lastResendAt > 0) { - const remainingBeforeResendMs = Math.max(0, resendIntervalMs - (Date.now() - lastResendAt)); - const baseMaxAttempts = Math.max(1, Number(payload.maxAttempts) || 5); - const intervalMs = Math.max(1, Number(payload.intervalMs) || 3000); - payload.maxAttempts = Math.max(1, Math.min(baseMaxAttempts, Math.floor(remainingBeforeResendMs / intervalMs) + 1)); - } - - try { - const result = await sendToMailContentScriptResilient( - mail, - { - type: 'POLL_EMAIL', - step, - source: 'background', - payload, - }, - { - timeoutMs: 45000, - maxRecoveryAttempts: 2, - } - ); - - if (result && result.error) { - throw new Error(result.error); - } - - if (!result || !result.code) { - throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到验证码。`); - } - - if (rejectedCodes.has(result.code)) { - throw new Error(`步骤 ${step}:再次收到了相同的${getVerificationCodeLabel(step)}验证码:${result.code}`); - } - - return { - ...result, - lastResendAt, - }; - } catch (err) { - if (isStopError(err)) { - throw err; - } - lastError = err; - await addLog(`步骤 ${step}:${err.message}`, 'warn'); - } - - const remainingBeforeResendMs = lastResendAt > 0 - ? Math.max(0, resendIntervalMs - (Date.now() - lastResendAt)) - : 0; - if (remainingBeforeResendMs > 0) { - await addLog( - `步骤 ${step}:距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,继续刷新邮箱(第 ${round}/${maxRounds} 轮)...`, - 'info' - ); - continue; - } - - if (round < maxRounds) { - await addLog(`步骤 ${step}:已到 25 秒重发间隔,准备重新发送验证码(第 ${round + 1}/${maxRounds} 轮)...`, 'warn'); - } - break; - } - } - - throw lastError || new Error(`步骤 ${step}:无法获取新的${getVerificationCodeLabel(step)}验证码。`); + return verificationFlowHelpers.pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides); } async function submitVerificationCode(step, code) { - const signupTabId = await getTabId('signup-page'); - if (!signupTabId) { - throw new Error('认证页面标签页已关闭,无法填写验证码。'); - } - - await chrome.tabs.update(signupTabId, { active: true }); - const result = await sendToContentScript('signup-page', { - type: 'FILL_CODE', - step, - source: 'background', - payload: { code }, - }); - - if (result && result.error) { - throw new Error(result.error); - } - - return result || {}; + return verificationFlowHelpers.submitVerificationCode(step, code); } async function resolveVerificationStep(step, state, mail, options = {}) { - const stateKey = getVerificationCodeStateKey(step); - const rejectedCodes = new Set(); - const hotmailPollConfig = mail.provider === HOTMAIL_PROVIDER - ? getHotmailVerificationPollConfig(step) - : null; - const ignorePersistedLastCode = Boolean(hotmailPollConfig?.ignorePersistedLastCode); - if (state[stateKey] && !ignorePersistedLastCode) { - rejectedCodes.add(state[stateKey]); - } - - let nextFilterAfterTimestamp = options.filterAfterTimestamp ?? null; - const requestFreshCodeFirst = options.requestFreshCodeFirst !== undefined - ? Boolean(options.requestFreshCodeFirst) - : (hotmailPollConfig?.requestFreshCodeFirst ?? false); - const maxSubmitAttempts = 3; - 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)) { - throw err; - } - await addLog(`步骤 ${step}:首次重新获取验证码失败:${err.message},将继续使用当前时间窗口轮询。`, 'warn'); - } - } - - if (mail.provider === HOTMAIL_PROVIDER) { - const initialDelayMs = Number(options.initialDelayMs ?? hotmailPollConfig.initialDelayMs) || 0; - if (initialDelayMs > 0) { - await addLog(`步骤 ${step}:等待 ${Math.round(initialDelayMs / 1000)} 秒,让 Hotmail 验证码邮件先到达...`, 'info'); - await sleepWithStop(initialDelayMs); - } - } - - for (let attempt = 1; attempt <= maxSubmitAttempts; attempt++) { - const result = await pollFreshVerificationCode(step, state, mail, { - excludeCodes: [...rejectedCodes], - filterAfterTimestamp: nextFilterAfterTimestamp ?? undefined, - resendIntervalMs, - lastResendAt, - onResendRequestedAt: updateFilterAfterTimestampForStep7, - }); - lastResendAt = Number(result?.lastResendAt) || lastResendAt; - - throwIfStopped(); - await addLog(`步骤 ${step}:已获取${getVerificationCodeLabel(step)}验证码:${result.code}`); - throwIfStopped(); - const submitResult = await submitVerificationCode(step, result.code); - - if (submitResult.invalidCode) { - rejectedCodes.add(result.code); - await addLog(`步骤 ${step}:验证码被页面拒绝:${submitResult.errorText || result.code}`, 'warn'); - - if (attempt >= maxSubmitAttempts) { - throw new Error(`步骤 ${step}:验证码连续失败,已达到 ${maxSubmitAttempts} 次重试上限。`); - } - - const remainingBeforeResendMs = resendIntervalMs > 0 && lastResendAt > 0 - ? Math.max(0, resendIntervalMs - (Date.now() - lastResendAt)) - : 0; - if (remainingBeforeResendMs > 0) { - await addLog( - `步骤 ${step}:提交失败后距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,先继续刷新邮箱(${attempt + 1}/${maxSubmitAttempts})...`, - 'warn' - ); - continue; - } - - lastResendAt = await requestVerificationCodeResend(step); - await updateFilterAfterTimestampForStep7(lastResendAt); - await addLog(`步骤 ${step}:提交失败后已请求新验证码(${attempt + 1}/${maxSubmitAttempts})...`, 'warn'); - continue; - } - - await setState({ - lastEmailTimestamp: result.emailTimestamp, - [stateKey]: result.code, - }); - - await completeStepFromBackground(step, { - emailTimestamp: result.emailTimestamp, - code: result.code, - }); - return; - } + return verificationFlowHelpers.resolveVerificationStep(step, state, mail, options); } async function executeStep4(state) { diff --git a/background/verification-flow.js b/background/verification-flow.js new file mode 100644 index 0000000..ad504a9 --- /dev/null +++ b/background/verification-flow.js @@ -0,0 +1,491 @@ +(function attachBackgroundVerificationFlow(root, factory) { + root.MultiPageBackgroundVerificationFlow = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundVerificationFlowModule() { + function createVerificationFlowHelpers(deps = {}) { + const { + addLog, + chrome, + CLOUDFLARE_TEMP_EMAIL_PROVIDER, + completeStepFromBackground, + confirmCustomVerificationStepBypassRequest, + getHotmailVerificationPollConfig, + getHotmailVerificationRequestTimestamp, + getState, + getTabId, + HOTMAIL_PROVIDER, + isStopError, + LUCKMAIL_PROVIDER, + pollCloudflareTempEmailVerificationCode, + pollHotmailVerificationCode, + pollLuckmailVerificationCode, + sendToContentScript, + sendToMailContentScriptResilient, + setState, + sleepWithStop, + throwIfStopped, + VERIFICATION_POLL_MAX_ROUNDS, + } = deps; + + function getVerificationCodeStateKey(step) { + return step === 4 ? 'lastSignupCode' : 'lastLoginCode'; + } + + function getVerificationCodeLabel(step) { + return step === 4 ? '注册' : '登录'; + } + + async function confirmCustomVerificationStepBypass(step) { + const verificationLabel = getVerificationCodeLabel(step); + await addLog(`步骤 ${step}:当前为自定义邮箱模式,请手动在页面中输入${verificationLabel}验证码并进入下一页面。`, 'warn'); + + let response = null; + try { + response = await confirmCustomVerificationStepBypassRequest(step); + } catch { + throw new Error(`步骤 ${step}:无法打开确认弹窗,请先保持侧边栏打开后重试。`); + } + + if (response?.error) { + throw new Error(response.error); + } + if (!response?.confirmed) { + throw new Error(`步骤 ${step}:已取消手动${verificationLabel}验证码确认。`); + } + + await setState({ + lastEmailTimestamp: null, + signupVerificationRequestedAt: null, + loginVerificationRequestedAt: null, + }); + await deps.setStepStatus(step, 'skipped'); + await addLog(`步骤 ${step}:已确认手动完成${verificationLabel}验证码输入,当前步骤已跳过。`, 'warn'); + } + + function getVerificationPollPayload(step, state, overrides = {}) { + if (step === 4) { + return { + filterAfterTimestamp: getHotmailVerificationRequestTimestamp(4, state), + senderFilters: ['openai', 'noreply', 'verify', 'auth', 'duckduckgo', 'forward'], + subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm'], + targetEmail: state.email, + maxAttempts: 5, + intervalMs: 3000, + ...overrides, + }; + } + + return { + filterAfterTimestamp: getHotmailVerificationRequestTimestamp(7, state), + senderFilters: ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'], + subjectFilters: ['verify', 'verification', 'code', '验证码', 'confirm', 'login'], + targetEmail: state.email, + maxAttempts: 5, + intervalMs: 3000, + ...overrides, + }; + } + + async function requestVerificationCodeResend(step) { + throwIfStopped(); + const signupTabId = await getTabId('signup-page'); + if (!signupTabId) { + throw new Error('认证页面标签页已关闭,无法重新请求验证码。'); + } + + throwIfStopped(); + await chrome.tabs.update(signupTabId, { active: true }); + throwIfStopped(); + + const result = await sendToContentScript('signup-page', { + type: 'RESEND_VERIFICATION_CODE', + step, + source: 'background', + payload: {}, + }); + + if (result && result.error) { + throw new Error(result.error); + } + + await addLog(`步骤 ${step}:已请求新的${getVerificationCodeLabel(step)}验证码。`, 'warn'); + + 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'); + if (mailTabId) { + await chrome.tabs.update(mailTabId, { active: true }); + await addLog(`步骤 ${step}:已切换到 2925 邮箱标签页等待新邮件。`, 'info'); + } + } + + return requestedAt; + } + + async function pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides = {}) { + const stateKey = getVerificationCodeStateKey(step); + const rejectedCodes = new Set(); + if (state[stateKey]) { + rejectedCodes.add(state[stateKey]); + } + for (const code of (pollOverrides.excludeCodes || [])) { + if (code) rejectedCodes.add(code); + } + + const { + maxRounds: _ignoredMaxRounds, + resendIntervalMs: _ignoredResendIntervalMs, + lastResendAt: _ignoredLastResendAt, + onResendRequestedAt: _ignoredOnResendRequestedAt, + ...payloadOverrides + } = pollOverrides; + const onResendRequestedAt = typeof pollOverrides.onResendRequestedAt === 'function' + ? pollOverrides.onResendRequestedAt + : null; + let lastError = null; + 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; + + for (let round = 1; round <= maxRounds; round++) { + throwIfStopped(); + if (round > 1) { + lastResendAt = await requestVerificationCodeResend(step); + if (onResendRequestedAt) { + const nextFilterAfterTimestamp = await onResendRequestedAt(lastResendAt); + if (nextFilterAfterTimestamp !== undefined) { + filterAfterTimestamp = nextFilterAfterTimestamp; + } + } + } + + while (true) { + throwIfStopped(); + const payload = getVerificationPollPayload(step, state, { + ...payloadOverrides, + filterAfterTimestamp, + excludeCodes: [...rejectedCodes], + }); + + if (lastResendAt > 0) { + const remainingBeforeResendMs = Math.max(0, resendIntervalMs - (Date.now() - lastResendAt)); + const baseMaxAttempts = Math.max(1, Number(payload.maxAttempts) || 5); + const intervalMs = Math.max(1, Number(payload.intervalMs) || 3000); + payload.maxAttempts = Math.max(1, Math.min(baseMaxAttempts, Math.floor(remainingBeforeResendMs / intervalMs) + 1)); + } + + try { + const result = await sendToMailContentScriptResilient( + mail, + { + type: 'POLL_EMAIL', + step, + source: 'background', + payload, + }, + { + timeoutMs: 45000, + maxRecoveryAttempts: 2, + } + ); + + if (result && result.error) { + throw new Error(result.error); + } + + if (!result || !result.code) { + throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到验证码。`); + } + + if (rejectedCodes.has(result.code)) { + throw new Error(`步骤 ${step}:再次收到了相同的${getVerificationCodeLabel(step)}验证码:${result.code}`); + } + + return { + ...result, + lastResendAt, + }; + } catch (err) { + if (isStopError(err)) { + throw err; + } + lastError = err; + await addLog(`步骤 ${step}:${err.message}`, 'warn'); + } + + const remainingBeforeResendMs = lastResendAt > 0 + ? Math.max(0, resendIntervalMs - (Date.now() - lastResendAt)) + : 0; + if (remainingBeforeResendMs > 0) { + await addLog( + `步骤 ${step}:距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,继续刷新邮箱(第 ${round}/${maxRounds} 轮)...`, + 'info' + ); + continue; + } + + if (round < maxRounds) { + await addLog(`步骤 ${step}:已到 25 秒重发间隔,准备重新发送验证码(第 ${round + 1}/${maxRounds} 轮)...`, 'warn'); + } + break; + } + } + + throw lastError || new Error(`步骤 ${step}:无法获取新的${getVerificationCodeLabel(step)}验证码。`); + } + + 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, + ...cleanPollOverrides, + }); + } + if (mail.provider === LUCKMAIL_PROVIDER) { + return pollLuckmailVerificationCode(step, state, { + ...getVerificationPollPayload(step, state), + ...pollOverrides, + }); + } + if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) { + return pollCloudflareTempEmailVerificationCode(step, state, { + ...getVerificationPollPayload(step, state), + ...pollOverrides, + }); + } + + if (Number(pollOverrides.resendIntervalMs) > 0) { + return pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides); + } + + const stateKey = getVerificationCodeStateKey(step); + const rejectedCodes = new Set(); + if (state[stateKey]) { + rejectedCodes.add(state[stateKey]); + } + for (const code of (pollOverrides.excludeCodes || [])) { + if (code) rejectedCodes.add(code); + } + + let lastError = null; + 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) { + const requestedAt = await requestVerificationCodeResend(step); + if (typeof onResendRequestedAt === 'function') { + const nextFilterAfterTimestamp = await onResendRequestedAt(requestedAt); + if (nextFilterAfterTimestamp !== undefined) { + filterAfterTimestamp = nextFilterAfterTimestamp; + } + } + } + + const payload = getVerificationPollPayload(step, state, { + ...cleanPollOverrides, + filterAfterTimestamp, + excludeCodes: [...rejectedCodes], + }); + + try { + const result = await sendToMailContentScriptResilient( + mail, + { + type: 'POLL_EMAIL', + step, + source: 'background', + payload, + }, + { + timeoutMs: 45000, + maxRecoveryAttempts: 2, + } + ); + + if (result && result.error) { + throw new Error(result.error); + } + + if (!result || !result.code) { + throw new Error(`步骤 ${step}:邮箱轮询结束,但未获取到验证码。`); + } + + if (rejectedCodes.has(result.code)) { + throw new Error(`步骤 ${step}:再次收到了相同的${getVerificationCodeLabel(step)}验证码:${result.code}`); + } + + return result; + } catch (err) { + if (isStopError(err)) { + throw err; + } + lastError = err; + await addLog(`步骤 ${step}:${err.message}`, 'warn'); + if (round < maxRounds) { + await addLog(`步骤 ${step}:将重新发送验证码后重试(${round + 1}/${maxRounds})...`, 'warn'); + } + } + } + + throw lastError || new Error(`步骤 ${step}:无法获取新的${getVerificationCodeLabel(step)}验证码。`); + } + + async function submitVerificationCode(step, code) { + const signupTabId = await getTabId('signup-page'); + if (!signupTabId) { + throw new Error('认证页面标签页已关闭,无法填写验证码。'); + } + + await chrome.tabs.update(signupTabId, { active: true }); + const result = await sendToContentScript('signup-page', { + type: 'FILL_CODE', + step, + source: 'background', + payload: { code }, + }); + + if (result && result.error) { + throw new Error(result.error); + } + + return result || {}; + } + + async function resolveVerificationStep(step, state, mail, options = {}) { + const stateKey = getVerificationCodeStateKey(step); + const rejectedCodes = new Set(); + const hotmailPollConfig = mail.provider === HOTMAIL_PROVIDER + ? getHotmailVerificationPollConfig(step) + : null; + const ignorePersistedLastCode = Boolean(hotmailPollConfig?.ignorePersistedLastCode); + if (state[stateKey] && !ignorePersistedLastCode) { + rejectedCodes.add(state[stateKey]); + } + + let nextFilterAfterTimestamp = options.filterAfterTimestamp ?? null; + const requestFreshCodeFirst = options.requestFreshCodeFirst !== undefined + ? Boolean(options.requestFreshCodeFirst) + : (hotmailPollConfig?.requestFreshCodeFirst ?? false); + const maxSubmitAttempts = 3; + 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)) { + throw err; + } + await addLog(`步骤 ${step}:首次重新获取验证码失败:${err.message},将继续使用当前时间窗口轮询。`, 'warn'); + } + } + + if (mail.provider === HOTMAIL_PROVIDER) { + const initialDelayMs = Number(options.initialDelayMs ?? hotmailPollConfig.initialDelayMs) || 0; + if (initialDelayMs > 0) { + await addLog(`步骤 ${step}:等待 ${Math.round(initialDelayMs / 1000)} 秒,让 Hotmail 验证码邮件先到达...`, 'info'); + await sleepWithStop(initialDelayMs); + } + } + + for (let attempt = 1; attempt <= maxSubmitAttempts; attempt++) { + const result = await pollFreshVerificationCode(step, state, mail, { + excludeCodes: [...rejectedCodes], + filterAfterTimestamp: nextFilterAfterTimestamp ?? undefined, + resendIntervalMs, + lastResendAt, + onResendRequestedAt: updateFilterAfterTimestampForStep7, + }); + lastResendAt = Number(result?.lastResendAt) || lastResendAt; + + throwIfStopped(); + await addLog(`步骤 ${step}:已获取${getVerificationCodeLabel(step)}验证码:${result.code}`); + throwIfStopped(); + const submitResult = await submitVerificationCode(step, result.code); + + if (submitResult.invalidCode) { + rejectedCodes.add(result.code); + await addLog(`步骤 ${step}:验证码被页面拒绝:${submitResult.errorText || result.code}`, 'warn'); + + if (attempt >= maxSubmitAttempts) { + throw new Error(`步骤 ${step}:验证码连续失败,已达到 ${maxSubmitAttempts} 次重试上限。`); + } + + const remainingBeforeResendMs = resendIntervalMs > 0 && lastResendAt > 0 + ? Math.max(0, resendIntervalMs - (Date.now() - lastResendAt)) + : 0; + if (remainingBeforeResendMs > 0) { + await addLog( + `步骤 ${step}:提交失败后距离下次重新发送验证码还差 ${Math.ceil(remainingBeforeResendMs / 1000)} 秒,先继续刷新邮箱(${attempt + 1}/${maxSubmitAttempts})...`, + 'warn' + ); + continue; + } + + lastResendAt = await requestVerificationCodeResend(step); + await updateFilterAfterTimestampForStep7(lastResendAt); + await addLog(`步骤 ${step}:提交失败后已请求新验证码(${attempt + 1}/${maxSubmitAttempts})...`, 'warn'); + continue; + } + + await setState({ + lastEmailTimestamp: result.emailTimestamp, + [stateKey]: result.code, + }); + + await completeStepFromBackground(step, { + emailTimestamp: result.emailTimestamp, + code: result.code, + }); + return; + } + } + + return { + confirmCustomVerificationStepBypass, + getVerificationCodeLabel, + getVerificationCodeStateKey, + getVerificationPollPayload, + pollFreshVerificationCode, + pollFreshVerificationCodeWithResendInterval, + requestVerificationCodeResend, + resolveVerificationStep, + submitVerificationCode, + }; + } + + return { + createVerificationFlowHelpers, + }; +}); diff --git a/tests/background-verification-flow-module.test.js b/tests/background-verification-flow-module.test.js new file mode 100644 index 0000000..2fd9c47 --- /dev/null +++ b/tests/background-verification-flow-module.test.js @@ -0,0 +1,17 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +test('background imports verification flow module', () => { + const source = fs.readFileSync('background.js', 'utf8'); + assert.match(source, /background\/verification-flow\.js/); +}); + +test('verification flow module exposes a factory', () => { + const source = fs.readFileSync('background/verification-flow.js', 'utf8'); + const globalScope = {}; + + const api = new Function('self', `${source}; return self.MultiPageBackgroundVerificationFlow;`)(globalScope); + + assert.equal(typeof api?.createVerificationFlowHelpers, 'function'); +}); diff --git a/tests/verification-stop-propagation.test.js b/tests/verification-stop-propagation.test.js index dba7532..72797dd 100644 --- a/tests/verification-stop-propagation.test.js +++ b/tests/verification-stop-propagation.test.js @@ -1,9 +1,10 @@ const assert = require('assert'); const fs = require('fs'); -const source = fs.readFileSync('background.js', 'utf8'); +const helperSource = fs.readFileSync('background.js', 'utf8'); +const verificationFlowSource = fs.readFileSync('background/verification-flow.js', 'utf8'); -function extractFunction(name) { +function extractFunction(source, name) { const markers = [`async function ${name}(`, `function ${name}(`]; const start = markers .map(marker => source.indexOf(marker)) @@ -51,13 +52,13 @@ function extractFunction(name) { } async function testPollFreshVerificationCodeRethrowsStop() { - const bundle = [ - extractFunction('isStopError'), - extractFunction('throwIfStopped'), - extractFunction('pollFreshVerificationCode'), + const helperBundle = [ + extractFunction(helperSource, 'isStopError'), + extractFunction(helperSource, 'throwIfStopped'), ].join('\n'); -const api = new Function(` +const api = new Function('verificationFlowSource', ` +const self = {}; let stopRequested = false; const STOP_ERROR_MESSAGE = '流程已被用户停止。'; const HOTMAIL_PROVIDER = 'hotmail-api'; @@ -76,14 +77,8 @@ async function pollHotmailVerificationCode() { async function pollLuckmailVerificationCode() { throw new Error('luckmail path should not run in this test'); } -function getVerificationCodeStateKey(step) { - return step === 4 ? 'lastSignupCode' : 'lastLoginCode'; -} -function getVerificationPollPayload(step, state, overrides = {}) { - return { - filterAfterTimestamp: 123, - ...overrides, - }; +async function pollCloudflareTempEmailVerificationCode() { + throw new Error('cloudflare path should not run in this test'); } async function sendToMailContentScriptResilient() { throw new Error(STOP_ERROR_MESSAGE); @@ -95,15 +90,41 @@ async function addLog(message, level) { logs.push({ message, level }); } -${bundle} +${helperBundle} +${verificationFlowSource} + +const helpers = self.MultiPageBackgroundVerificationFlow.createVerificationFlowHelpers({ + addLog, + chrome: {}, + CLOUDFLARE_TEMP_EMAIL_PROVIDER, + completeStepFromBackground: async () => {}, + confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }), + getHotmailVerificationPollConfig, + getHotmailVerificationRequestTimestamp: () => 123, + getState: async () => ({}), + getTabId: async () => null, + HOTMAIL_PROVIDER, + isStopError, + LUCKMAIL_PROVIDER, + pollCloudflareTempEmailVerificationCode, + pollHotmailVerificationCode, + pollLuckmailVerificationCode, + sendToContentScript: async () => ({}), + sendToMailContentScriptResilient, + setState: async () => {}, + setStepStatus: async () => {}, + sleepWithStop: async () => {}, + throwIfStopped, + VERIFICATION_POLL_MAX_ROUNDS, +}); return { - pollFreshVerificationCode, + pollFreshVerificationCode: helpers.pollFreshVerificationCode, snapshot() { return { logs, resendCalls }; }, }; -`)(); +`)(verificationFlowSource); let error = null; try { @@ -119,56 +140,63 @@ return { } async function testResolveVerificationStepRethrowsStopFromFreshRequest() { - const bundle = [ - extractFunction('isStopError'), - extractFunction('resolveVerificationStep'), + const helperBundle = [ + extractFunction(helperSource, 'isStopError'), ].join('\n'); -const api = new Function(` +const api = new Function('verificationFlowSource', ` +const self = {}; const STOP_ERROR_MESSAGE = '流程已被用户停止。'; const HOTMAIL_PROVIDER = 'hotmail-api'; const LUCKMAIL_PROVIDER = 'luckmail-api'; const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email'; const logs = []; let pollCalls = 0; - -function getVerificationCodeStateKey(step) { - return step === 4 ? 'lastSignupCode' : 'lastLoginCode'; -} -function getHotmailVerificationPollConfig() { - return {}; -} -function getVerificationCodeLabel(step) { - return step === 4 ? '注册' : '登录'; -} -function isStep7RestartFromStep6Error() { - return false; -} -async function requestVerificationCodeResend() { - throw new Error(STOP_ERROR_MESSAGE); -} async function addLog(message, level) { logs.push({ message, level }); } -async function pollFreshVerificationCode() { - pollCalls += 1; - return { code: '123456', emailTimestamp: Date.now() }; -} -async function submitVerificationCode() { - throw new Error('submit should not run in this test'); -} -async function setState() {} -async function completeStepFromBackground() {} +const chrome = { tabs: { async update() {} } }; -${bundle} +${helperBundle} +${verificationFlowSource} + +const helpers = self.MultiPageBackgroundVerificationFlow.createVerificationFlowHelpers({ + addLog, + chrome, + CLOUDFLARE_TEMP_EMAIL_PROVIDER, + completeStepFromBackground: async () => {}, + confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }), + getHotmailVerificationPollConfig: () => ({}), + getHotmailVerificationRequestTimestamp: () => 123, + getState: async () => ({}), + getTabId: async () => 1, + HOTMAIL_PROVIDER, + isStopError, + LUCKMAIL_PROVIDER, + pollCloudflareTempEmailVerificationCode: async () => ({}), + pollHotmailVerificationCode: async () => ({}), + pollLuckmailVerificationCode: async () => ({}), + sendToContentScript: async () => { + throw new Error(STOP_ERROR_MESSAGE); + }, + sendToMailContentScriptResilient: async () => { + pollCalls += 1; + return {}; + }, + setState: async () => {}, + setStepStatus: async () => {}, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + VERIFICATION_POLL_MAX_ROUNDS: 5, +}); return { - resolveVerificationStep, + resolveVerificationStep: helpers.resolveVerificationStep, snapshot() { return { logs, pollCalls }; }, }; -`)(); +`)(verificationFlowSource); let error = null; try {