feat: add verification flow helpers and integrate into background script
- Introduced a new module `verification-flow.js` containing verification flow helpers. - Updated `background.js` to import and utilize the new verification flow helpers. - Refactored existing verification-related functions to leverage the new helpers for improved code organization and maintainability. - Added tests for the verification flow module to ensure proper integration and functionality.
This commit is contained in:
+41
-426
@@ -4,6 +4,7 @@ importScripts(
|
|||||||
'background/panel-bridge.js',
|
'background/panel-bridge.js',
|
||||||
'background/generated-email-helpers.js',
|
'background/generated-email-helpers.js',
|
||||||
'background/signup-flow-helpers.js',
|
'background/signup-flow-helpers.js',
|
||||||
|
'background/verification-flow.js',
|
||||||
'background/steps/registry.js',
|
'background/steps/registry.js',
|
||||||
'background/steps/open-chatgpt.js',
|
'background/steps/open-chatgpt.js',
|
||||||
'background/steps/submit-signup-email.js',
|
'background/steps/submit-signup-email.js',
|
||||||
@@ -6405,6 +6406,33 @@ const signupFlowHelpers = self.MultiPageSignupFlowHelpers?.createSignupFlowHelpe
|
|||||||
SIGNUP_PAGE_INJECT_FILES,
|
SIGNUP_PAGE_INJECT_FILES,
|
||||||
waitForTabUrlMatch,
|
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({
|
const step1Executor = self.MultiPageBackgroundStep1?.createStep1Executor({
|
||||||
addLog,
|
addLog,
|
||||||
completeStepFromBackground,
|
completeStepFromBackground,
|
||||||
@@ -6439,14 +6467,14 @@ const step4Executor = self.MultiPageBackgroundStep4?.createStep4Executor({
|
|||||||
addLog,
|
addLog,
|
||||||
chrome,
|
chrome,
|
||||||
completeStepFromBackground,
|
completeStepFromBackground,
|
||||||
confirmCustomVerificationStepBypass,
|
confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass,
|
||||||
getMailConfig,
|
getMailConfig,
|
||||||
getTabId,
|
getTabId,
|
||||||
HOTMAIL_PROVIDER,
|
HOTMAIL_PROVIDER,
|
||||||
isTabAlive,
|
isTabAlive,
|
||||||
LUCKMAIL_PROVIDER,
|
LUCKMAIL_PROVIDER,
|
||||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||||
resolveVerificationStep,
|
resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep,
|
||||||
reuseOrCreateTab,
|
reuseOrCreateTab,
|
||||||
sendToContentScriptResilient,
|
sendToContentScriptResilient,
|
||||||
shouldUseCustomRegistrationEmail,
|
shouldUseCustomRegistrationEmail,
|
||||||
@@ -6483,7 +6511,7 @@ const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({
|
|||||||
addLog,
|
addLog,
|
||||||
chrome,
|
chrome,
|
||||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||||
confirmCustomVerificationStepBypass,
|
confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass,
|
||||||
ensureStep7VerificationPageReady,
|
ensureStep7VerificationPageReady,
|
||||||
executeStep6: (...args) => executeStep6(...args),
|
executeStep6: (...args) => executeStep6(...args),
|
||||||
getMailConfig,
|
getMailConfig,
|
||||||
@@ -6493,7 +6521,7 @@ const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({
|
|||||||
isTabAlive,
|
isTabAlive,
|
||||||
isVerificationMailPollingError,
|
isVerificationMailPollingError,
|
||||||
LUCKMAIL_PROVIDER,
|
LUCKMAIL_PROVIDER,
|
||||||
resolveVerificationStep,
|
resolveVerificationStep: verificationFlowHelpers.resolveVerificationStep,
|
||||||
reuseOrCreateTab,
|
reuseOrCreateTab,
|
||||||
setState,
|
setState,
|
||||||
setStepStatus,
|
setStepStatus,
|
||||||
@@ -6677,452 +6705,39 @@ function normalizeInbucketOrigin(rawValue) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getVerificationCodeStateKey(step) {
|
function getVerificationCodeStateKey(step) {
|
||||||
return step === 4 ? 'lastSignupCode' : 'lastLoginCode';
|
return verificationFlowHelpers.getVerificationCodeStateKey(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVerificationCodeLabel(step) {
|
function getVerificationCodeLabel(step) {
|
||||||
return step === 4 ? '注册' : '登录';
|
return verificationFlowHelpers.getVerificationCodeLabel(step);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmCustomVerificationStepBypass(step) {
|
async function confirmCustomVerificationStepBypass(step) {
|
||||||
const verificationLabel = getVerificationCodeLabel(step);
|
return verificationFlowHelpers.confirmCustomVerificationStepBypass(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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVerificationPollPayload(step, state, overrides = {}) {
|
function getVerificationPollPayload(step, state, overrides = {}) {
|
||||||
if (step === 4) {
|
return verificationFlowHelpers.getVerificationPollPayload(step, state, overrides);
|
||||||
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) {
|
async function requestVerificationCodeResend(step) {
|
||||||
throwIfStopped();
|
return verificationFlowHelpers.requestVerificationCodeResend(step);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) {
|
async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) {
|
||||||
const { onResendRequestedAt, ...cleanPollOverrides } = pollOverrides;
|
return verificationFlowHelpers.pollFreshVerificationCode(step, state, mail, 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 pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides = {}) {
|
async function pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides = {}) {
|
||||||
const stateKey = getVerificationCodeStateKey(step);
|
return verificationFlowHelpers.pollFreshVerificationCodeWithResendInterval(step, state, mail, pollOverrides);
|
||||||
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 submitVerificationCode(step, code) {
|
async function submitVerificationCode(step, code) {
|
||||||
const signupTabId = await getTabId('signup-page');
|
return verificationFlowHelpers.submitVerificationCode(step, code);
|
||||||
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 = {}) {
|
async function resolveVerificationStep(step, state, mail, options = {}) {
|
||||||
const stateKey = getVerificationCodeStateKey(step);
|
return verificationFlowHelpers.resolveVerificationStep(step, state, mail, options);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeStep4(state) {
|
async function executeStep4(state) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
const fs = require('fs');
|
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 markers = [`async function ${name}(`, `function ${name}(`];
|
||||||
const start = markers
|
const start = markers
|
||||||
.map(marker => source.indexOf(marker))
|
.map(marker => source.indexOf(marker))
|
||||||
@@ -51,13 +52,13 @@ function extractFunction(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function testPollFreshVerificationCodeRethrowsStop() {
|
async function testPollFreshVerificationCodeRethrowsStop() {
|
||||||
const bundle = [
|
const helperBundle = [
|
||||||
extractFunction('isStopError'),
|
extractFunction(helperSource, 'isStopError'),
|
||||||
extractFunction('throwIfStopped'),
|
extractFunction(helperSource, 'throwIfStopped'),
|
||||||
extractFunction('pollFreshVerificationCode'),
|
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
const api = new Function(`
|
const api = new Function('verificationFlowSource', `
|
||||||
|
const self = {};
|
||||||
let stopRequested = false;
|
let stopRequested = false;
|
||||||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||||
const HOTMAIL_PROVIDER = 'hotmail-api';
|
const HOTMAIL_PROVIDER = 'hotmail-api';
|
||||||
@@ -76,14 +77,8 @@ async function pollHotmailVerificationCode() {
|
|||||||
async function pollLuckmailVerificationCode() {
|
async function pollLuckmailVerificationCode() {
|
||||||
throw new Error('luckmail path should not run in this test');
|
throw new Error('luckmail path should not run in this test');
|
||||||
}
|
}
|
||||||
function getVerificationCodeStateKey(step) {
|
async function pollCloudflareTempEmailVerificationCode() {
|
||||||
return step === 4 ? 'lastSignupCode' : 'lastLoginCode';
|
throw new Error('cloudflare path should not run in this test');
|
||||||
}
|
|
||||||
function getVerificationPollPayload(step, state, overrides = {}) {
|
|
||||||
return {
|
|
||||||
filterAfterTimestamp: 123,
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
async function sendToMailContentScriptResilient() {
|
async function sendToMailContentScriptResilient() {
|
||||||
throw new Error(STOP_ERROR_MESSAGE);
|
throw new Error(STOP_ERROR_MESSAGE);
|
||||||
@@ -95,15 +90,41 @@ async function addLog(message, level) {
|
|||||||
logs.push({ 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 {
|
return {
|
||||||
pollFreshVerificationCode,
|
pollFreshVerificationCode: helpers.pollFreshVerificationCode,
|
||||||
snapshot() {
|
snapshot() {
|
||||||
return { logs, resendCalls };
|
return { logs, resendCalls };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
`)();
|
`)(verificationFlowSource);
|
||||||
|
|
||||||
let error = null;
|
let error = null;
|
||||||
try {
|
try {
|
||||||
@@ -119,56 +140,63 @@ return {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function testResolveVerificationStepRethrowsStopFromFreshRequest() {
|
async function testResolveVerificationStepRethrowsStopFromFreshRequest() {
|
||||||
const bundle = [
|
const helperBundle = [
|
||||||
extractFunction('isStopError'),
|
extractFunction(helperSource, 'isStopError'),
|
||||||
extractFunction('resolveVerificationStep'),
|
|
||||||
].join('\n');
|
].join('\n');
|
||||||
|
|
||||||
const api = new Function(`
|
const api = new Function('verificationFlowSource', `
|
||||||
|
const self = {};
|
||||||
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
|
||||||
const HOTMAIL_PROVIDER = 'hotmail-api';
|
const HOTMAIL_PROVIDER = 'hotmail-api';
|
||||||
const LUCKMAIL_PROVIDER = 'luckmail-api';
|
const LUCKMAIL_PROVIDER = 'luckmail-api';
|
||||||
const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
|
const CLOUDFLARE_TEMP_EMAIL_PROVIDER = 'cloudflare-temp-email';
|
||||||
const logs = [];
|
const logs = [];
|
||||||
let pollCalls = 0;
|
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) {
|
async function addLog(message, level) {
|
||||||
logs.push({ message, level });
|
logs.push({ message, level });
|
||||||
}
|
}
|
||||||
async function pollFreshVerificationCode() {
|
const chrome = { tabs: { async update() {} } };
|
||||||
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() {}
|
|
||||||
|
|
||||||
${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 {
|
return {
|
||||||
resolveVerificationStep,
|
resolveVerificationStep: helpers.resolveVerificationStep,
|
||||||
snapshot() {
|
snapshot() {
|
||||||
return { logs, pollCalls };
|
return { logs, pollCalls };
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
`)();
|
`)(verificationFlowSource);
|
||||||
|
|
||||||
let error = null;
|
let error = null;
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user