Files
FlowPilot/background/verification-flow.js
T
2026-05-21 07:21:34 +08:00

1462 lines
56 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function attachBackgroundVerificationFlow(root, factory) {
root.MultiPageBackgroundVerificationFlow = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundVerificationFlowModule() {
const ICLOUD_MAIL_POLL_MIN_ATTEMPTS = 5;
const ICLOUD_MAIL_POLL_TIMEOUT_MARGIN_MS = 25000;
function createVerificationFlowHelpers(deps = {}) {
const {
addLog: rawAddLog = async () => {},
buildVerificationPollPayload: externalBuildVerificationPollPayload = null,
chrome,
closeConflictingTabsForSource,
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
CLOUD_MAIL_PROVIDER = 'cloudmail',
completeNodeFromBackground,
confirmCustomVerificationStepBypassRequest,
getNodeIdByStepForState,
getHotmailVerificationPollConfig,
getHotmailVerificationRequestTimestamp,
handleMail2925LimitReachedError,
getState,
getTabId,
HOTMAIL_PROVIDER,
isMail2925LimitReachedError,
isStopError,
LUCKMAIL_PROVIDER,
YYDS_MAIL_PROVIDER = 'yyds-mail',
MAIL_2925_VERIFICATION_INTERVAL_MS,
MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
pollCloudflareTempEmailVerificationCode,
pollCloudMailVerificationCode,
pollHotmailVerificationCode,
pollLuckmailVerificationCode,
pollYydsMailVerificationCode,
sendToContentScript,
sendToContentScriptResilient,
sendToMailContentScriptResilient,
setNodeStatus,
setState,
sleepWithStop,
throwIfStopped,
VERIFICATION_POLL_MAX_ROUNDS,
} = deps;
let activeVerificationLogStep = null;
function normalizeLogStep(value) {
const step = Math.floor(Number(value) || 0);
return step > 0 ? step : null;
}
function normalizeVerificationLogMessage(message) {
return String(message || '')
.replace(/^步骤\s*\d+\s*[:]\s*/, '')
.replace(/^Step\s+\d+\s*[:]\s*/i, '')
.trim();
}
function addLog(message, level = 'info', options = {}) {
const normalizedOptions = options && typeof options === 'object' ? { ...options } : {};
const step = normalizeLogStep(normalizedOptions.step || normalizedOptions.visibleStep)
|| normalizeLogStep(activeVerificationLogStep);
if (step) {
normalizedOptions.step = step;
if (!normalizedOptions.stepKey) {
normalizedOptions.stepKey = step === 4 ? 'fetch-signup-code' : 'fetch-login-code';
}
}
delete normalizedOptions.visibleStep;
return rawAddLog(normalizeVerificationLogMessage(message), level, normalizedOptions);
}
async function getNodeIdForStep(step) {
const state = typeof getState === 'function' ? await getState() : {};
return typeof getNodeIdByStepForState === 'function'
? String(getNodeIdByStepForState(step, state) || '').trim()
: '';
}
const isRetryableVerificationTransportError = typeof deps.isRetryableContentScriptTransportError === 'function'
? deps.isRetryableContentScriptTransportError
: ((error) => /back\/forward cache|message channel is closed|Receiving end does not exist|port closed before a response was received|A listener indicated an asynchronous response|内容脚本\s+\d+(?:\.\d+)?\s*秒内未响应|did not respond in \d+s/i.test(
String(typeof error === 'string' ? error : error?.message || '')
));
function getVerificationCodeStateKey(step) {
return step === 4 ? 'lastSignupCode' : 'lastLoginCode';
}
function getVerificationCodeLabel(step) {
return step === 4 ? '注册' : '登录';
}
function isIcloudMail(mail) {
return mail?.source === 'icloud-mail' || mail?.provider === 'icloud';
}
function normalizeIcloudMailPollPayload(mail, payload = {}) {
if (!isIcloudMail(mail)) {
return payload;
}
const currentAttempts = Math.max(1, Math.floor(Number(payload?.maxAttempts) || 1));
if (currentAttempts >= ICLOUD_MAIL_POLL_MIN_ATTEMPTS) {
return payload;
}
return {
...payload,
maxAttempts: ICLOUD_MAIL_POLL_MIN_ATTEMPTS,
};
}
function getMailPollingResponseTimeoutMs(payload = {}) {
const maxAttempts = Math.max(1, Math.floor(Number(payload?.maxAttempts) || 1));
const intervalMs = Math.max(1, Number(payload?.intervalMs) || 3000);
return Math.max(45000, maxAttempts * intervalMs + ICLOUD_MAIL_POLL_TIMEOUT_MARGIN_MS);
}
function resolveMailPollingTimeouts(mail, timedPoll) {
const payload = normalizeIcloudMailPollPayload(mail, timedPoll?.payload || {});
const defaultResponseTimeoutMs = Math.max(1000, Number(timedPoll?.responseTimeoutMs) || 30000);
const defaultTimeoutMs = Math.max(defaultResponseTimeoutMs, Number(timedPoll?.timeoutMs) || defaultResponseTimeoutMs);
if (!isIcloudMail(mail)) {
return {
payload,
responseTimeoutMs: defaultResponseTimeoutMs,
timeoutMs: defaultTimeoutMs,
};
}
const derivedResponseTimeoutMs = Math.max(
defaultResponseTimeoutMs,
getMailPollingResponseTimeoutMs(payload)
);
const derivedTimeoutMs = Math.max(defaultTimeoutMs, derivedResponseTimeoutMs);
return {
payload,
responseTimeoutMs: derivedResponseTimeoutMs,
timeoutMs: derivedTimeoutMs,
};
}
function isLikelyLoggedInChatgptHomeUrl(rawUrl) {
const url = String(rawUrl || '').trim();
if (!url) return false;
try {
const parsed = new URL(url);
const host = String(parsed.hostname || '').toLowerCase();
if (!['chatgpt.com', 'www.chatgpt.com'].includes(host)) {
return false;
}
const path = String(parsed.pathname || '');
if (/^\/(?:auth\/|create-account\/|email-verification|log-in|add-phone)(?:[/?#]|$)/i.test(path)) {
return false;
}
return true;
} catch {
return false;
}
}
function isSignupProfilePageUrl(rawUrl) {
const url = String(rawUrl || '').trim();
if (!url) return false;
try {
const parsed = new URL(url);
const host = String(parsed.hostname || '').toLowerCase();
if (!['auth.openai.com', 'auth0.openai.com', 'accounts.openai.com'].includes(host)) {
return false;
}
return /\/(?:create-account\/profile|u\/signup\/profile|signup\/profile|about-you)(?:[/?#]|$)/i.test(String(parsed.pathname || ''));
} catch {
return false;
}
}
async function detectStep4PostSubmitFallback(tabId, options = {}) {
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 8000);
const pollIntervalMs = Math.max(100, Number(options.pollIntervalMs) || 250);
const startedAt = Date.now();
let lastUrl = '';
while (Date.now() - startedAt < timeoutMs) {
throwIfStopped();
try {
const tab = await chrome.tabs.get(tabId);
const currentUrl = String(tab?.url || '').trim();
if (currentUrl) {
lastUrl = currentUrl;
}
if (isLikelyLoggedInChatgptHomeUrl(currentUrl)) {
return {
success: true,
reason: 'chatgpt_home',
skipProfileStep: true,
url: currentUrl,
};
}
if (isSignupProfilePageUrl(currentUrl)) {
return {
success: true,
reason: 'signup_profile',
skipProfileStep: false,
url: currentUrl,
};
}
} catch {
// Keep polling until timeout; tab may be mid-navigation.
}
await sleepWithStop(pollIntervalMs);
}
return {
success: false,
reason: 'unknown',
skipProfileStep: false,
url: lastUrl,
};
}
async function detectStep8PostSubmitFallback(options = {}) {
const timeoutMs = Math.max(1000, Number(options.timeoutMs) || 9000);
const pollIntervalMs = Math.max(100, Number(options.pollIntervalMs) || 300);
const step = Number(options.step) || 8;
const startedAt = Date.now();
let lastSnapshot = null;
while (Date.now() - startedAt < timeoutMs) {
throwIfStopped();
try {
const request = {
type: 'GET_LOGIN_AUTH_STATE',
source: 'background',
payload: {},
};
const requestTimeoutMs = Math.max(1200, Math.min(5000, timeoutMs));
const result = typeof sendToContentScriptResilient === 'function'
? await sendToContentScriptResilient(
'openai-auth',
request,
{
timeoutMs: requestTimeoutMs,
responseTimeoutMs: requestTimeoutMs,
retryDelayMs: 400,
logMessage: `步骤 ${step}:验证码提交后页面正在切换,等待页面恢复并确认授权状态...`,
}
)
: await sendToContentScript('openai-auth', request, {
responseTimeoutMs: requestTimeoutMs,
});
if (result?.error) {
throw new Error(result.error);
}
const authState = String(result?.state || '').trim();
const authUrl = String(result?.url || '').trim();
const verificationErrorText = String(result?.verificationErrorText || '').trim();
lastSnapshot = {
state: authState || 'unknown',
url: authUrl,
};
if (authState === 'verification_page' && verificationErrorText) {
return {
success: false,
reason: 'invalid_code',
invalidCode: true,
errorText: verificationErrorText,
url: authUrl,
};
}
if (authState === 'oauth_consent_page') {
return {
success: true,
reason: 'oauth_consent_page',
addPhonePage: false,
url: authUrl,
};
}
if (authState === 'add_phone_page' || authState === 'phone_verification_page') {
return {
success: true,
reason: 'add_phone_page',
addPhonePage: true,
url: authUrl || 'https://auth.openai.com/add-phone',
};
}
if (authState === 'login_timeout_error_page') {
return {
success: false,
reason: 'login_timeout_error_page',
restartStep7: true,
url: authUrl,
};
}
} catch (_) {
// Ignore transient inspect failures and keep polling.
}
await sleepWithStop(pollIntervalMs);
}
return {
success: false,
reason: 'unknown',
snapshot: lastSnapshot,
};
}
function getVerificationResendStateKey() {
return 'verificationResendCount';
}
function normalizeVerificationResendCount(value, fallback = 0) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return Math.max(0, Math.floor(Number(fallback) || 0));
}
return Math.min(20, Math.max(0, Math.floor(numeric)));
}
function getVerificationRequestedAtStateKey(step) {
if (Number(step) === 4) return 'signupVerificationRequestedAt';
if (Number(step) === 8) return 'loginVerificationRequestedAt';
return '';
}
function resolveInitialVerificationRequestedAt(step, state = {}, fallback = 0) {
const stateKey = getVerificationRequestedAtStateKey(step);
const candidateValues = [
fallback,
stateKey ? state?.[stateKey] : 0,
];
for (const value of candidateValues) {
const numeric = Number(value);
if (Number.isFinite(numeric) && numeric > 0) {
return Math.floor(numeric);
}
}
return 0;
}
function getLegacyVerificationResendCountDefault(step, options = {}) {
const requestFreshCodeFirst = Boolean(options.requestFreshCodeFirst);
const legacyMaxRounds = Math.max(1, Math.floor(Number(VERIFICATION_POLL_MAX_ROUNDS) || 1));
if (step === 4 && requestFreshCodeFirst) {
return legacyMaxRounds;
}
return Math.max(0, legacyMaxRounds - 1);
}
function getConfiguredVerificationResendCount(step, state, options = {}) {
const stateKey = getVerificationResendStateKey(step);
const configuredValue = state?.[stateKey] !== undefined
? state[stateKey]
: (state?.signupVerificationResendCount ?? state?.loginVerificationResendCount);
return normalizeVerificationResendCount(
configuredValue,
getLegacyVerificationResendCountDefault(step, options)
);
}
function resolveMaxResendRequests(pollOverrides = {}) {
if (pollOverrides.maxResendRequests !== undefined) {
return normalizeVerificationResendCount(pollOverrides.maxResendRequests, 0);
}
const legacyMaxRounds = Number(pollOverrides.maxRounds);
if (Number.isFinite(legacyMaxRounds)) {
return Math.max(0, Math.floor(legacyMaxRounds) - 1);
}
return Math.max(0, Math.floor(Number(VERIFICATION_POLL_MAX_ROUNDS) || 1) - 1);
}
function getCompletionStep(step, options = {}) {
const completionStep = Number(options.completionStep);
return Number.isFinite(completionStep) && completionStep > 0 ? completionStep : step;
}
async function confirmCustomVerificationStepBypass(step, options = {}) {
const completionStep = getCompletionStep(step, options);
const promptStep = getCompletionStep(step, { completionStep: options.promptStep ?? completionStep });
const verificationLabel = getVerificationCodeLabel(step);
await addLog(`步骤 ${completionStep}:当前为自定义邮箱模式,请手动在页面中输入${verificationLabel}验证码并进入下一页面。`, 'warn');
let response = null;
try {
response = await confirmCustomVerificationStepBypassRequest(promptStep);
} catch {
throw new Error(`步骤 ${completionStep}:无法打开确认弹窗,请先保持侧边栏打开后重试。`);
}
if (response?.error) {
throw new Error(response.error);
}
if (step === 8 && response?.addPhoneDetected) {
throw new Error(`步骤 ${completionStep}:验证码提交后页面进入手机号页面,当前流程无法继续自动授权。 URL: https://auth.openai.com/add-phone`);
}
if (!response?.confirmed) {
throw new Error(`步骤 ${completionStep}:已取消手动${verificationLabel}验证码确认。`);
}
await setState({
lastEmailTimestamp: null,
signupVerificationRequestedAt: null,
loginVerificationRequestedAt: null,
});
const completionNodeId = await getNodeIdForStep(completionStep);
if (!completionNodeId) {
throw new Error(`步骤 ${completionStep} 未映射到验证码节点。`);
}
await setNodeStatus(completionNodeId, 'skipped');
await addLog(`步骤 ${completionStep}:已确认手动完成${verificationLabel}验证码输入,当前步骤已跳过。`, 'warn');
}
function getVerificationPollPayload(step, state, overrides = {}) {
if (typeof externalBuildVerificationPollPayload === 'function') {
return externalBuildVerificationPollPayload(step, state, overrides);
}
const normalizedStep = Number(step) === 4 ? 4 : 8;
const is2925Provider = state?.mailProvider === '2925';
const mail2925MatchTargetEmail = is2925Provider
&& String(state?.mail2925Mode || '').trim().toLowerCase() === 'receive';
return {
flowId: String(state?.activeFlowId || '').trim(),
step: normalizedStep,
filterAfterTimestamp: is2925Provider ? 0 : getHotmailVerificationRequestTimestamp(normalizedStep, state),
senderFilters: [],
subjectFilters: [],
requiredKeywords: [],
codePatterns: [],
targetEmail: normalizedStep === 4
? state.email
: (String(state?.step8VerificationTargetEmail || '').trim() || state.email),
targetEmailHints: [],
mail2925MatchTargetEmail,
maxAttempts: is2925Provider ? MAIL_2925_VERIFICATION_MAX_ATTEMPTS : 5,
intervalMs: is2925Provider ? MAIL_2925_VERIFICATION_INTERVAL_MS : 3000,
...overrides,
};
}
async function getRemainingTimeBudgetMs(step, options = {}, actionLabel = '') {
const resolver = typeof options.getRemainingTimeMs === 'function'
? options.getRemainingTimeMs
: null;
if (!resolver) {
return null;
}
const value = await resolver({ step, actionLabel });
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return null;
}
return Math.max(0, Math.floor(numeric));
}
async function getResponseTimeoutMsForStep(step, options = {}, fallbackMs = 30000, actionLabel = '') {
const remainingMs = await getRemainingTimeBudgetMs(step, options, actionLabel);
const fallbackTimeoutMs = Math.max(1000, Number(fallbackMs) || 1000);
const minResponseTimeoutMs = Math.min(
fallbackTimeoutMs,
Math.max(1000, Number(options.minResponseTimeoutMs) || 1000)
);
if (remainingMs === null) {
return Math.max(minResponseTimeoutMs, fallbackTimeoutMs);
}
return Math.max(minResponseTimeoutMs, Math.min(fallbackTimeoutMs, remainingMs));
}
async function applyMailPollingTimeBudget(step, payload, options = {}, actionLabel = '') {
const nextPayload = { ...payload };
const intervalMs = Math.max(1, Number(nextPayload.intervalMs) || 3000);
const baseMaxAttempts = Math.max(1, Number(nextPayload.maxAttempts) || 1);
const disableTimeBudgetCap = Boolean(options.disableTimeBudgetCap);
const remainingMs = await getRemainingTimeBudgetMs(step, options, actionLabel);
const minPollingResponseTimeoutMs = Math.max(
3000,
Number(options.minPollingResponseTimeoutMs) || 5000
);
if (!disableTimeBudgetCap && remainingMs !== null) {
nextPayload.maxAttempts = Math.max(
1,
Math.min(baseMaxAttempts, Math.floor(Math.max(0, remainingMs - 1000) / intervalMs) + 1)
);
}
const defaultResponseTimeoutMs = Math.max(45000, nextPayload.maxAttempts * intervalMs + 25000);
const responseTimeoutMs = disableTimeBudgetCap || remainingMs === null
? defaultResponseTimeoutMs
: Math.max(
minPollingResponseTimeoutMs,
Math.min(defaultResponseTimeoutMs, remainingMs)
);
return {
payload: nextPayload,
responseTimeoutMs,
timeoutMs: responseTimeoutMs,
};
}
async function requestVerificationCodeResend(step, options = {}) {
throwIfStopped();
const signupTabId = await getTabId('openai-auth');
if (!signupTabId) {
throw new Error('认证页面标签页已关闭,无法重新请求验证码。');
}
throwIfStopped();
await chrome.tabs.update(signupTabId, { active: true });
throwIfStopped();
const result = await sendToContentScript('openai-auth', {
type: 'RESEND_VERIFICATION_CODE',
step,
source: 'background',
payload: {},
}, {
responseTimeoutMs: await getResponseTimeoutMsForStep(
step,
options,
30000,
`重新发送${getVerificationCodeLabel(step)}验证码`
),
});
if (result && result.error) {
throw new Error(result.error);
}
await addLog(`步骤 ${step}:已请求新的${getVerificationCodeLabel(step)}验证码。`, 'warn');
const requestedAt = Date.now();
if (step === 4) {
await setState({ signupVerificationRequestedAt: requestedAt });
}
if (step === 8) {
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;
}
function shouldPreclear2925Mailbox(step, mail, options = {}) {
if (mail?.provider !== '2925' || (step !== 4 && step !== 8)) {
return false;
}
return !(Number(options.filterAfterTimestamp) > 0);
}
async function clear2925MailboxBeforePolling(step, mail, options = {}) {
if (!shouldPreclear2925Mailbox(step, mail, options)) {
return;
}
throwIfStopped();
await addLog(`步骤 ${step}:开始刷新 2925 邮箱前先清空全部邮件,避免读取旧验证码邮件。`, 'warn');
try {
const responseTimeoutMs = await getResponseTimeoutMsForStep(
step,
options,
15000,
'清空 2925 邮箱历史邮件'
);
const result = await sendToMailContentScriptResilient(
mail,
{
type: 'DELETE_ALL_EMAILS',
step,
source: 'background',
payload: {},
},
{
timeoutMs: responseTimeoutMs,
responseTimeoutMs,
maxRecoveryAttempts: 2,
logStep: activeVerificationLogStep,
logStepKey: step === 4 ? 'fetch-signup-code' : 'fetch-login-code',
}
);
if (result?.error) {
throw new Error(result.error);
}
if (result?.deleted === false) {
await addLog(`步骤 ${step}:未能确认 2925 邮箱已清空,将继续刷新等待新邮件。`, 'warn');
return;
}
await addLog(`步骤 ${step}:2925 邮箱已预先清空,开始刷新等待新邮件。`, 'info');
} catch (err) {
if (isStopError(err)) {
throw err;
}
await addLog(`步骤 ${step}:预清空 2925 邮箱失败,将继续刷新等待新邮件:${err.message}`, 'warn');
}
}
async function closeIcloudMailboxTabAfterSuccess(step, mail) {
if (mail?.source !== 'icloud-mail') {
return;
}
const tabId = typeof getTabId === 'function'
? await getTabId(mail.source)
: null;
if (Number.isInteger(tabId)) {
await chrome.tabs.remove(tabId).catch(() => {});
await addLog(`步骤 ${step}:已关闭 iCloud 邮箱标签页,避免长期累积。`, 'info');
return;
}
if (typeof closeConflictingTabsForSource === 'function' && mail.url) {
await closeConflictingTabsForSource(mail.source, mail.url).catch(() => {});
}
}
function triggerPostSuccessMailboxCleanup(step, mail) {
if (mail?.provider !== '2925' && mail?.source !== 'icloud-mail') {
return;
}
Promise.resolve().then(async () => {
try {
if (mail?.source === 'icloud-mail') {
await closeIcloudMailboxTabAfterSuccess(step, mail);
return;
}
await sendToMailContentScriptResilient(
mail,
{
type: 'DELETE_ALL_EMAILS',
step,
source: 'background',
payload: {},
},
{
timeoutMs: 10000,
responseTimeoutMs: 5000,
maxRecoveryAttempts: 1,
logStep: activeVerificationLogStep,
logStepKey: step === 4 ? 'fetch-signup-code' : 'fetch-login-code',
}
);
} catch (_) {
// Best-effort cleanup only.
}
});
}
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,
maxResendRequests: _ignoredMaxResendRequests,
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 maxResendRequests = resolveMaxResendRequests(pollOverrides);
const totalRounds = maxResendRequests + 1;
const maxRounds = totalRounds;
const resendIntervalMs = Math.max(0, Number(pollOverrides.resendIntervalMs) || 0);
let lastResendAt = Number(pollOverrides.lastResendAt) || 0;
let usedResendRequests = 0;
let pollOnlyNoResendRounds = 0;
let transportErrorStreak = 0;
const maxTransportErrorStreak = mail?.source === 'icloud-mail' ? 6 : 4;
const maxIcloudNoResendRounds = mail?.source === 'icloud-mail' ? 4 : 0;
const hasExistingResendTimestamp = Number(lastResendAt) > 0;
const initialRoundNoResendWindowMs = resendIntervalMs > 0
? Math.max(10000, Math.min(45000, resendIntervalMs))
: 0;
const initialRoundNoResendUntil = hasExistingResendTimestamp
? 0
: (initialRoundNoResendWindowMs > 0 ? (Date.now() + initialRoundNoResendWindowMs) : 0);
for (let round = 1; round <= totalRounds; round++) {
throwIfStopped();
if (round === 1 && initialRoundNoResendUntil > 0) {
const waitSeconds = Math.max(1, Math.ceil((initialRoundNoResendUntil - Date.now()) / 1000));
await addLog(
`步骤 ${step}:首次进入验证码轮询,先等待 ${waitSeconds} 秒观察新邮件,避免过早重复重发。`,
'info'
);
}
if (round > 1) {
lastResendAt = await requestVerificationCodeResend(step, pollOverrides);
usedResendRequests += 1;
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 timedPoll = await applyMailPollingTimeBudget(
step,
payload,
pollOverrides,
`轮询${getVerificationCodeLabel(step)}验证码邮箱`
);
const timeoutWindow = resolveMailPollingTimeouts(mail, timedPoll);
const result = await sendToMailContentScriptResilient(
mail,
{
type: 'POLL_EMAIL',
step,
source: 'background',
payload: timeoutWindow.payload,
},
{
timeoutMs: timeoutWindow.timeoutMs,
maxRecoveryAttempts: 2,
responseTimeoutMs: timeoutWindow.responseTimeoutMs,
logStep: activeVerificationLogStep,
logStepKey: step === 4 ? 'fetch-signup-code' : 'fetch-login-code',
}
);
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}`);
}
transportErrorStreak = 0;
return {
...result,
lastResendAt,
remainingResendRequests: Math.max(0, maxResendRequests - usedResendRequests),
};
} catch (err) {
if (isStopError(err)) {
throw err;
}
if (mail?.provider === '2925' && typeof isMail2925LimitReachedError === 'function' && isMail2925LimitReachedError(err)) {
if (typeof handleMail2925LimitReachedError === 'function') {
throw await handleMail2925LimitReachedError(step, err);
}
throw err;
}
const isTransportError = isRetryableVerificationTransportError(err);
if (isTransportError) {
transportErrorStreak += 1;
lastError = err;
await addLog(`步骤 ${step}${err.message}`, 'warn');
if (transportErrorStreak >= maxTransportErrorStreak) {
throw new Error(
`步骤 ${step}${mail?.label || '邮箱'}页面通信异常连续 ${transportErrorStreak} 次,已停止当前轮询以避免重复重发验证码。最后错误:${err.message}`
);
}
const fallbackIntervalMs = Math.max(
800,
Math.min(
3000,
Number(payloadOverrides.intervalMs)
|| Number(pollOverrides.intervalMs)
|| 2000
)
);
await sleepWithStop(fallbackIntervalMs);
continue;
}
transportErrorStreak = 0;
lastError = err;
await addLog(`步骤 ${step}${err.message}`, 'warn');
}
if (mail?.source === 'icloud-mail' && maxIcloudNoResendRounds > 0) {
pollOnlyNoResendRounds += 1;
if (pollOnlyNoResendRounds >= maxIcloudNoResendRounds) {
throw new Error(
`步骤 ${step}iCloud 邮箱连续 ${pollOnlyNoResendRounds} 轮轮询均未拿到验证码且未触发重发,已停止当前链路以避免空轮询循环,请刷新邮箱页后重试。`
);
}
}
const remainingBeforeResendMs = lastResendAt > 0
? Math.max(0, resendIntervalMs - (Date.now() - lastResendAt))
: 0;
const initialCooldownMs = (round === 1 && initialRoundNoResendUntil > 0)
? Math.max(0, initialRoundNoResendUntil - Date.now())
: 0;
const effectiveCooldownMs = Math.max(remainingBeforeResendMs, initialCooldownMs);
if (effectiveCooldownMs > 0) {
await addLog(
`步骤 ${step}:距离下次重新发送验证码还差 ${Math.ceil(effectiveCooldownMs / 1000)} 秒,继续刷新邮箱(第 ${round}/${maxRounds} 轮)...`,
'info'
);
const configuredIntervalMs = Math.max(
1,
Number(payloadOverrides.intervalMs)
|| Number(pollOverrides.intervalMs)
|| 3000
);
const cooldownSleepMs = Math.min(
effectiveCooldownMs,
Math.max(1000, Math.min(configuredIntervalMs, 3000))
);
await sleepWithStop(cooldownSleepMs);
continue;
}
if (round < maxRounds) {
await addLog(`步骤 ${step}:已到 25 秒重发间隔,准备重新发送验证码(第 ${round + 1}/${maxRounds} 轮)...`, 'warn');
}
break;
}
}
throw lastError || new Error(`步骤 ${step}:无法获取新的${getVerificationCodeLabel(step)}验证码。`);
}
function shouldRequestLuckmailResendBeforeRetry(error) {
const message = String(error?.message || error || '');
if (!message) {
return true;
}
return !/没有可用 token|token 对应邮箱与当前邮箱不一致/i.test(message);
}
async function pollLuckmailVerificationCodeWithResend(step, state, pollOverrides = {}) {
const {
onResendRequestedAt,
maxRounds: _ignoredMaxRounds,
maxResendRequests: _ignoredMaxResendRequests,
initialPollMaxAttempts: _ignoredInitialPollMaxAttempts,
pollAttemptPlan: _ignoredPollAttemptPlan,
...cleanPollOverrides
} = pollOverrides;
const basePayload = {
...getVerificationPollPayload(step, state),
...cleanPollOverrides,
};
const maxAttempts = Math.max(1, Number(basePayload.maxAttempts) || 1);
const intervalMs = Math.max(15000, Number(basePayload.intervalMs) || 15000);
let lastError = null;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
throwIfStopped();
try {
return await pollLuckmailVerificationCode(step, state, {
...basePayload,
maxAttempts: 1,
intervalMs,
});
} catch (err) {
if (isStopError(err)) {
throw err;
}
lastError = err;
const canRetry = attempt < maxAttempts;
if (!canRetry) {
break;
}
if (shouldRequestLuckmailResendBeforeRetry(err)) {
try {
await requestVerificationCodeResend(step, pollOverrides);
} catch (resendError) {
if (isStopError(resendError)) {
throw resendError;
}
await addLog(`步骤 ${step}:LuckMail 点击重新发送验证码失败:${resendError.message},仍将在 ${Math.ceil(intervalMs / 1000)} 秒后继续轮询 /code 接口。`, 'warn');
}
}
await addLog(`步骤 ${step}LuckMail 暂未获取到新的${getVerificationCodeLabel(step)}验证码,等待 ${Math.ceil(intervalMs / 1000)} 秒后继续轮询 /code 接口(${attempt + 1}/${maxAttempts}...`, 'warn');
await sleepWithStop(intervalMs);
}
}
throw lastError || new Error(`步骤 ${step}:无法获取新的${getVerificationCodeLabel(step)}验证码。`);
}
async function pollFreshVerificationCode(step, state, mail, pollOverrides = {}) {
const {
onResendRequestedAt,
maxRounds: _ignoredMaxRounds,
maxResendRequests: _ignoredMaxResendRequests,
initialPollMaxAttempts: _ignoredInitialPollMaxAttempts,
pollAttemptPlan: _ignoredPollAttemptPlan,
...cleanPollOverrides
} = pollOverrides;
if (mail.provider === HOTMAIL_PROVIDER) {
const hotmailPollConfig = getHotmailVerificationPollConfig(step);
const timedPoll = await applyMailPollingTimeBudget(step, {
...getVerificationPollPayload(step, state),
...hotmailPollConfig,
...cleanPollOverrides,
}, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`);
return pollHotmailVerificationCode(step, state, timedPoll.payload);
}
if (mail.provider === LUCKMAIL_PROVIDER) {
const timedPoll = await applyMailPollingTimeBudget(step, {
...getVerificationPollPayload(step, state),
...cleanPollOverrides,
}, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`);
return pollLuckmailVerificationCodeWithResend(step, state, {
...cleanPollOverrides,
...timedPoll.payload,
onResendRequestedAt,
});
}
if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
const timedPoll = await applyMailPollingTimeBudget(step, {
...getVerificationPollPayload(step, state),
...cleanPollOverrides,
}, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`);
return pollCloudflareTempEmailVerificationCode(step, state, timedPoll.payload);
}
if (mail.provider === CLOUD_MAIL_PROVIDER) {
const timedPoll = await applyMailPollingTimeBudget(step, {
...getVerificationPollPayload(step, state),
...cleanPollOverrides,
}, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`);
return pollCloudMailVerificationCode(step, state, timedPoll.payload);
}
if (mail.provider === YYDS_MAIL_PROVIDER) {
const timedPoll = await applyMailPollingTimeBudget(step, {
...getVerificationPollPayload(step, state),
...cleanPollOverrides,
}, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`);
return pollYydsMailVerificationCode(step, state, timedPoll.payload);
}
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 maxResendRequests = resolveMaxResendRequests(pollOverrides);
const maxRounds = maxResendRequests + 1;
const initialPollMaxAttempts = Math.max(0, Math.floor(Number(pollOverrides.initialPollMaxAttempts) || 0));
const configuredPollAttemptPlan = Array.isArray(pollOverrides.pollAttemptPlan)
? pollOverrides.pollAttemptPlan
.map((value) => Math.floor(Number(value) || 0))
.filter((value) => value > 0)
: [];
const pollAttemptPlan = rejectedCodes.size > 0 ? [] : configuredPollAttemptPlan;
let usedResendRequests = 0;
for (let round = 1; round <= maxRounds; round++) {
throwIfStopped();
if (round > 1) {
const requestedAt = await requestVerificationCodeResend(step, pollOverrides);
usedResendRequests += 1;
if (typeof onResendRequestedAt === 'function') {
const nextFilterAfterTimestamp = await onResendRequestedAt(requestedAt);
if (nextFilterAfterTimestamp !== undefined) {
filterAfterTimestamp = nextFilterAfterTimestamp;
}
}
}
const payload = getVerificationPollPayload(step, state, {
...cleanPollOverrides,
filterAfterTimestamp,
excludeCodes: [...rejectedCodes],
});
const plannedPollMaxAttempts = pollAttemptPlan[round - 1] || 0;
if (plannedPollMaxAttempts > 0) {
payload.maxAttempts = plannedPollMaxAttempts;
} else if (round === 1 && initialPollMaxAttempts > 0) {
payload.maxAttempts = initialPollMaxAttempts;
}
try {
const timedPoll = await applyMailPollingTimeBudget(
step,
payload,
pollOverrides,
`轮询${getVerificationCodeLabel(step)}验证码邮箱`
);
const timeoutWindow = resolveMailPollingTimeouts(mail, timedPoll);
const result = await sendToMailContentScriptResilient(
mail,
{
type: 'POLL_EMAIL',
step,
source: 'background',
payload: timeoutWindow.payload,
},
{
timeoutMs: timeoutWindow.timeoutMs,
maxRecoveryAttempts: 2,
responseTimeoutMs: timeoutWindow.responseTimeoutMs,
logStep: activeVerificationLogStep,
logStepKey: step === 4 ? 'fetch-signup-code' : 'fetch-login-code',
}
);
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,
remainingResendRequests: Math.max(0, maxResendRequests - usedResendRequests),
};
} catch (err) {
if (isStopError(err)) {
throw err;
}
if (mail?.provider === '2925' && typeof isMail2925LimitReachedError === 'function' && isMail2925LimitReachedError(err)) {
if (typeof handleMail2925LimitReachedError === 'function') {
throw await handleMail2925LimitReachedError(step, 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, options = {}) {
const completionStep = getCompletionStep(step, options);
const authLoginStep = completionStep >= 11 ? 10 : 7;
const signupTabId = await getTabId('openai-auth');
if (!signupTabId) {
throw new Error('认证页面标签页已关闭,无法填写验证码。');
}
await chrome.tabs.update(signupTabId, { active: true });
const completionNodeId = await getNodeIdForStep(completionStep);
const baseResponseTimeoutMs = await getResponseTimeoutMsForStep(
step,
step === 8
? {
...options,
minResponseTimeoutMs: Math.max(15000, Number(options.minResponseTimeoutMs) || 0),
}
: options,
step === 7 ? 45000 : 30000,
`填写${getVerificationCodeLabel(step)}验证码`
);
const message = {
type: 'FILL_CODE',
step,
source: 'background',
payload: {
code,
...(completionNodeId ? { nodeId: completionNodeId } : {}),
...(step === 4 && options.signupProfile ? { signupProfile: options.signupProfile } : {}),
},
};
let result;
const shouldAvoidReplaySubmit = step === 8;
if (typeof sendToContentScriptResilient === 'function' && !shouldAvoidReplaySubmit) {
try {
result = await sendToContentScriptResilient('openai-auth', message, {
timeoutMs: Math.max(baseResponseTimeoutMs + 15000, 30000),
retryDelayMs: 700,
responseTimeoutMs: baseResponseTimeoutMs,
logMessage: '认证页正在切换,等待页面重新就绪后继续确认验证码提交结果...',
logStep: completionStep,
logStepKey: step === 4 ? 'fetch-signup-code' : 'fetch-login-code',
});
} catch (err) {
if (step === 4 && isRetryableVerificationTransportError(err)) {
const fallback = await detectStep4PostSubmitFallback(signupTabId, {
timeoutMs: 9000,
pollIntervalMs: 300,
});
if (fallback.success) {
const fallbackLabel = fallback.reason === 'chatgpt_home'
? 'ChatGPT 已登录首页'
: '注册资料页';
await addLog(`步骤 4:验证码提交后页面已切换到${fallbackLabel},按提交成功继续。`, 'warn');
return {
success: true,
assumed: true,
transportRecovered: true,
skipProfileStep: Boolean(fallback.skipProfileStep),
url: fallback.url,
};
}
}
if (step === 8 && isRetryableVerificationTransportError(err)) {
const fallback = await detectStep8PostSubmitFallback({
step,
timeoutMs: 9000,
pollIntervalMs: 300,
});
if (fallback.success) {
if (fallback.addPhonePage) {
await addLog('验证码提交后通信中断,但页面已进入手机号验证页,按提交成功继续。', 'warn', {
step: completionStep,
stepKey: 'fetch-login-code',
});
} else {
await addLog('验证码提交后通信中断,但页面已进入 OAuth 授权页,按提交成功继续。', 'warn', {
step: completionStep,
stepKey: 'fetch-login-code',
});
}
return {
success: true,
assumed: true,
transportRecovered: true,
addPhonePage: Boolean(fallback.addPhonePage),
url: fallback.url || '',
};
}
if (fallback.restartStep7) {
const urlPart = fallback.url ? ` URL: ${fallback.url}` : '';
throw new Error(`STEP8_RESTART_STEP7::步骤 ${completionStep}:验证码提交后认证页进入登录超时报错页,请回到步骤 ${authLoginStep} 重新开始。${urlPart}`.trim());
}
}
throw err;
}
} else if (shouldAvoidReplaySubmit) {
try {
result = await sendToContentScript('openai-auth', message, {
responseTimeoutMs: baseResponseTimeoutMs,
});
} catch (err) {
if (isRetryableVerificationTransportError(err)) {
await addLog('认证页正在切换,等待页面重新就绪后继续确认验证码提交结果...', 'warn', {
step: completionStep,
stepKey: 'fetch-login-code',
});
const fallback = await detectStep8PostSubmitFallback({
step,
timeoutMs: 9000,
pollIntervalMs: 300,
});
if (fallback.invalidCode) {
return {
invalidCode: true,
errorText: fallback.errorText || '验证码被拒绝。',
url: fallback.url || '',
};
}
if (fallback.success) {
if (fallback.addPhonePage) {
await addLog('验证码提交后通信中断,但页面已进入手机号验证页,按提交成功继续。', 'warn', {
step: completionStep,
stepKey: 'fetch-login-code',
});
} else {
await addLog('验证码提交后通信中断,但页面已进入 OAuth 授权页,按提交成功继续。', 'warn', {
step: completionStep,
stepKey: 'fetch-login-code',
});
}
return {
success: true,
assumed: true,
transportRecovered: true,
addPhonePage: Boolean(fallback.addPhonePage),
url: fallback.url || '',
};
}
if (fallback.restartStep7) {
const urlPart = fallback.url ? ` URL: ${fallback.url}` : '';
throw new Error(`STEP8_RESTART_STEP7::步骤 ${completionStep}:验证码提交后认证页进入登录超时报错页,请回到步骤 ${authLoginStep} 重新开始。${urlPart}`.trim());
}
}
throw err;
}
} else {
result = await sendToContentScript('openai-auth', message, {
responseTimeoutMs: baseResponseTimeoutMs,
});
}
if (result && result.error) {
throw new Error(result.error);
}
return result || {};
}
async function resolveVerificationStep(step, state, mail, options = {}) {
const completionStep = getCompletionStep(step, options);
activeVerificationLogStep = completionStep;
const completionNodeId = await getNodeIdForStep(completionStep);
const stateKey = getVerificationCodeStateKey(step);
const rejectedCodes = new Set();
const hotmailPollConfig = mail.provider === HOTMAIL_PROVIDER
? getHotmailVerificationPollConfig(step)
: null;
const beforeSubmit = typeof options.beforeSubmit === 'function'
? options.beforeSubmit
: 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);
let remainingAutomaticResendCount = options.maxResendRequests !== undefined
? normalizeVerificationResendCount(
options.maxResendRequests,
getLegacyVerificationResendCountDefault(step, { requestFreshCodeFirst })
)
: getConfiguredVerificationResendCount(step, state, { requestFreshCodeFirst });
const maxSubmitAttempts = mail.provider === LUCKMAIL_PROVIDER ? 3 : 15;
const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0);
const externalOnResendRequestedAt = typeof options.onResendRequestedAt === 'function'
? options.onResendRequestedAt
: null;
let lastResendAt = resolveInitialVerificationRequestedAt(
step,
state,
Number(options.lastResendAt) || 0
);
const updateFilterAfterTimestampForVerificationStep = async (requestedAt) => {
if (externalOnResendRequestedAt) {
try {
await externalOnResendRequestedAt(requestedAt);
} catch (_) {
// Keep resend flow best-effort; state sync callback failures should not break verification.
}
}
return nextFilterAfterTimestamp;
};
await clear2925MailboxBeforePolling(step, mail, options);
if (requestFreshCodeFirst) {
if (remainingAutomaticResendCount <= 0) {
await addLog(`步骤 ${step}:当前自动重新发送验证码次数为 0,将直接使用当前时间窗口轮询邮箱。`, 'info');
} else {
try {
lastResendAt = await requestVerificationCodeResend(step, options);
remainingAutomaticResendCount -= 1;
await updateFilterAfterTimestampForVerificationStep(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) {
const remainingMs = await getRemainingTimeBudgetMs(
step,
options,
`等待${getVerificationCodeLabel(step)}验证码邮件到达`
);
const delayMs = remainingMs === null
? initialDelayMs
: Math.min(initialDelayMs, Math.max(0, remainingMs));
await addLog(`步骤 ${step}:等待 ${Math.round(initialDelayMs / 1000)} 秒,让 Hotmail 验证码邮件先到达...`, 'info');
await sleepWithStop(delayMs);
}
}
for (let attempt = 1; attempt <= maxSubmitAttempts; attempt++) {
const pollOptions = {
excludeCodes: [...rejectedCodes],
disableTimeBudgetCap: Boolean(options.disableTimeBudgetCap),
getRemainingTimeMs: options.getRemainingTimeMs,
maxResendRequests: remainingAutomaticResendCount,
initialPollMaxAttempts: mail.provider === '2925' && rejectedCodes.size > 0
? undefined
: options.initialPollMaxAttempts,
pollAttemptPlan: mail.provider === '2925' && rejectedCodes.size > 0
? undefined
: options.pollAttemptPlan,
resendIntervalMs,
lastResendAt,
onResendRequestedAt: updateFilterAfterTimestampForVerificationStep,
};
if (nextFilterAfterTimestamp !== null && nextFilterAfterTimestamp !== undefined) {
pollOptions.filterAfterTimestamp = nextFilterAfterTimestamp;
}
const result = await pollFreshVerificationCode(step, state, mail, pollOptions);
lastResendAt = Number(result?.lastResendAt) || lastResendAt;
remainingAutomaticResendCount = normalizeVerificationResendCount(
result?.remainingResendRequests,
remainingAutomaticResendCount
);
throwIfStopped();
await addLog(`步骤 ${step}:已获取${getVerificationCodeLabel(step)}验证码:${result.code}`);
if (beforeSubmit) {
await beforeSubmit(result, {
attempt,
rejectedCodes: new Set(rejectedCodes),
filterAfterTimestamp: nextFilterAfterTimestamp ?? undefined,
lastResendAt,
});
}
throwIfStopped();
const submitResult = await submitVerificationCode(step, result.code, options);
if (submitResult.invalidCode) {
rejectedCodes.add(result.code);
await addLog(`步骤 ${step}:验证码被页面拒绝:${submitResult.errorText || result.code}`, 'warn');
if (attempt >= maxSubmitAttempts) {
throw new Error(`步骤 ${step}:验证码连续失败,已达到 ${maxSubmitAttempts} 次重试上限。`);
}
if (mail.provider === LUCKMAIL_PROVIDER) {
await addLog(`步骤 ${step}:LuckMail 验证码提交失败,等待 15 秒后重新轮询 /code 接口(${attempt + 1}/${maxSubmitAttempts}...`, 'warn');
await sleepWithStop(15000);
continue;
}
if (remainingAutomaticResendCount <= 0) {
await addLog(`步骤 ${step}:已达到自动重新发送验证码次数上限,将排除已拒绝验证码并继续轮询新邮件。`, 'warn');
continue;
}
lastResendAt = await requestVerificationCodeResend(step, options);
remainingAutomaticResendCount -= 1;
await updateFilterAfterTimestampForVerificationStep(lastResendAt);
await addLog(`步骤 ${step}:提交失败后已请求新验证码(${attempt + 1}/${maxSubmitAttempts}...`, 'warn');
continue;
}
await setState({
lastEmailTimestamp: result.emailTimestamp,
[stateKey]: result.code,
});
if (!completionNodeId) {
throw new Error(`步骤 ${completionStep} 未映射到验证码节点。`);
}
await completeNodeFromBackground(completionNodeId, {
emailTimestamp: result.emailTimestamp,
code: result.code,
phoneVerificationRequired: Boolean(submitResult.addPhonePage),
...(step === 4 && submitResult?.skipProfileStep ? { skipProfileStep: true } : {}),
...(step === 4 && submitResult?.skipProfileStepReason
? { skipProfileStepReason: submitResult.skipProfileStepReason }
: {}),
});
triggerPostSuccessMailboxCleanup(step, mail);
return {
phoneVerificationRequired: Boolean(submitResult.addPhonePage),
url: submitResult.url || '',
};
}
}
return {
confirmCustomVerificationStepBypass,
getVerificationCodeLabel,
getVerificationCodeStateKey,
getVerificationPollPayload,
pollFreshVerificationCode,
pollFreshVerificationCodeWithResendInterval,
requestVerificationCodeResend,
resolveVerificationStep,
submitVerificationCode,
};
}
return {
createVerificationFlowHelpers,
};
});