1441 lines
55 KiB
JavaScript
1441 lines
55 KiB
JavaScript
(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,
|
||
MAIL_2925_VERIFICATION_INTERVAL_MS,
|
||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS,
|
||
pollCloudflareTempEmailVerificationCode,
|
||
pollCloudMailVerificationCode,
|
||
pollHotmailVerificationCode,
|
||
pollLuckmailVerificationCode,
|
||
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(
|
||
'signup-page',
|
||
request,
|
||
{
|
||
timeoutMs: requestTimeoutMs,
|
||
responseTimeoutMs: requestTimeoutMs,
|
||
retryDelayMs: 400,
|
||
logMessage: `步骤 ${step}:验证码提交后页面正在切换,等待页面恢复并确认授权状态...`,
|
||
}
|
||
)
|
||
: await sendToContentScript('signup-page', 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);
|
||
if (remainingMs === null) {
|
||
return Math.max(1000, Number(fallbackMs) || 1000);
|
||
}
|
||
|
||
return Math.max(1000, Math.min(Math.max(1000, Number(fallbackMs) || 1000), 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('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: {},
|
||
}, {
|
||
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 (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('signup-page');
|
||
if (!signupTabId) {
|
||
throw new Error('认证页面标签页已关闭,无法填写验证码。');
|
||
}
|
||
|
||
await chrome.tabs.update(signupTabId, { active: true });
|
||
const baseResponseTimeoutMs = await getResponseTimeoutMsForStep(
|
||
step,
|
||
options,
|
||
step === 7 ? 45000 : 30000,
|
||
`填写${getVerificationCodeLabel(step)}验证码`
|
||
);
|
||
const message = {
|
||
type: 'FILL_CODE',
|
||
step,
|
||
source: 'background',
|
||
payload: {
|
||
code,
|
||
...(step === 4 && options.signupProfile ? { signupProfile: options.signupProfile } : {}),
|
||
},
|
||
};
|
||
let result;
|
||
const shouldAvoidReplaySubmit = step === 8;
|
||
if (typeof sendToContentScriptResilient === 'function' && !shouldAvoidReplaySubmit) {
|
||
try {
|
||
result = await sendToContentScriptResilient('signup-page', 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('signup-page', 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('signup-page', 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 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,
|
||
});
|
||
|
||
const completionNodeId = await getNodeIdForStep(completionStep);
|
||
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,
|
||
};
|
||
});
|