Merge remote-tracking branch 'origin/dev' into fix/combined-signup-verification-profile

This commit is contained in:
QLHazycoder
2026-05-03 03:37:50 +08:00
38 changed files with 12436 additions and 952 deletions
+61 -1
View File
@@ -97,6 +97,26 @@
.join('');
}
function isPhoneNumberSupplyExhaustedFailure(errorLike) {
const message = String(
typeof errorLike === 'string'
? errorLike
: (errorLike?.message || errorLike || '')
).trim();
if (!message) {
return false;
}
const hasGlobalNoSupplySignal = /Step\s*9:\s*all\s+provider\s+candidates\s+failed\s+to\s+acquire\s+number|(?:HeroSMS|5sim|NexSMS)\s+no\s+numbers\s+available\s+across|no\s+numbers\s+within\s+maxPrice|no\s+free\s+phones|numbers?\s+not\s+found/i.test(message);
if (!hasGlobalNoSupplySignal) {
return false;
}
const hasRecoverableStep9RotationSignal = /phone\s+verification\s+did\s+not\s+succeed\s+after\s+\d+\s+number\s+replacements|sms_timeout_after_|route_405_retry_loop|resend_throttled|activation_not_found|order\s+not\s+found/i.test(message);
if (hasRecoverableStep9RotationSignal) {
return false;
}
return true;
}
function shouldKeepCustomMailProviderPoolEmail(state = {}) {
return String(state?.mailProvider || '').trim().toLowerCase() === 'custom'
&& Array.isArray(state?.customMailProviderPool)
@@ -482,10 +502,15 @@
const reason = getErrorMessage(err);
roundSummary.failureReasons.push(reason);
const blockedByAddPhone = typeof isAddPhoneAuthFailure === 'function' && isAddPhoneAuthFailure(err);
const blockedByPhoneSupplyExhausted = isPhoneNumberSupplyExhaustedFailure(err);
const blockedBySignupUserAlreadyExists = typeof isSignupUserAlreadyExistsFailure === 'function'
&& !keepSameEmailUntilAddPhone
&& isSignupUserAlreadyExistsFailure(err);
const canRetry = !blockedByAddPhone && !blockedBySignupUserAlreadyExists && autoRunSkipFailures && attemptRun < maxAttemptsForRound;
const canRetry = !blockedByAddPhone
&& !blockedByPhoneSupplyExhausted
&& !blockedBySignupUserAlreadyExists
&& autoRunSkipFailures
&& attemptRun < maxAttemptsForRound;
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
@@ -561,6 +586,41 @@
break;
}
if (blockedByPhoneSupplyExhausted) {
roundSummary.status = 'failed';
roundSummary.finalFailureReason = reason;
await setState({
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
});
await appendRoundRecordIfNeeded('failed', reason);
cancelPendingCommands('当前轮因接码号池不可用已终止。');
await broadcastStopToContentScripts();
if (!autoRunSkipFailures) {
await addLog(
`${targetRun}/${totalRuns} 轮触发接码号池不可用,自动重试未开启,当前自动运行将停止。`,
'warn'
);
stoppedEarly = true;
await broadcastAutoRunStatus('stopped', {
currentRun: targetRun,
totalRuns,
attemptRun,
sessionId: 0,
});
break;
}
await addLog(`${targetRun}/${totalRuns} 轮接码号池暂不可用,本轮将直接失败并跳过剩余重试。`, 'warn');
await addLog(
targetRun < totalRuns
? `${targetRun}/${totalRuns} 轮因接码号池不可用提前结束,自动流程将继续下一轮。`
: `${targetRun}/${totalRuns} 轮因接码号池不可用提前结束,已无后续轮次,本次自动运行结束。`,
'warn'
);
forceFreshTabsNextRun = true;
break;
}
if (canRetry) {
const retryIndex = attemptRun;
if (isRestartCurrentAttemptError(err)) {
+9 -2
View File
@@ -284,9 +284,16 @@
}
if (generator === 'icloud') {
const stateFetchMode = String(mergedState.icloudFetchMode || '').trim().toLowerCase();
return fetchIcloudHideMyEmail({
const icloudOptions = {
generateNew: Boolean(options.generateNew) || stateFetchMode === 'always_new',
});
};
if (mergedState.icloudHostPreference !== undefined) {
icloudOptions.hostPreference = mergedState.icloudHostPreference;
}
if (mergedState.preferredIcloudHost !== undefined) {
icloudOptions.preferredHost = mergedState.preferredIcloudHost;
}
return fetchIcloudHideMyEmail(icloudOptions);
}
if (generator === 'cloudflare') {
return fetchCloudflareEmail(mergedState, options);
+42 -15
View File
@@ -3,6 +3,7 @@ let ipProxyAuthListenerInstalled = false;
let ipProxyErrorListenerInstalled = false;
let currentIpProxyAuthEntry = null;
let ipProxyExitDetectionToken = 0;
let ipProxyProbeInFlightPromise = null;
let lastAppliedIpProxyEntrySignature = '';
let ipProxyAuthHostVariantToggle = false;
const ipProxyHostResolveCache = new Map();
@@ -573,6 +574,17 @@ function normalizeIpProxyServiceProfile(rawValue = {}) {
const raw = (rawValue && typeof rawValue === 'object' && !Array.isArray(rawValue))
? rawValue
: {};
const normalizeAutoSyncInterval = (value = '', fallback = 15) => {
const rawValue = String(value ?? '').trim();
if (!rawValue) {
return Math.max(1, Math.min(1440, Number(fallback) || 15));
}
const numeric = Number.parseInt(rawValue, 10);
if (!Number.isFinite(numeric)) {
return Math.max(1, Math.min(1440, Number(fallback) || 15));
}
return Math.max(1, Math.min(1440, numeric));
};
return {
mode: normalizeIpProxyMode(raw.mode),
apiUrl: String(raw.apiUrl || '').trim(),
@@ -580,6 +592,8 @@ function normalizeIpProxyServiceProfile(rawValue = {}) {
accountSessionPrefix: normalizeIpProxyAccountSessionPrefix(raw.accountSessionPrefix || ''),
accountLifeMinutes: normalizeIpProxyAccountLifeMinutes(raw.accountLifeMinutes || ''),
poolTargetCount: normalizeIpProxyPoolTargetCount(raw.poolTargetCount || '', 20),
autoSyncEnabled: Boolean(raw.autoSyncEnabled),
autoSyncIntervalMinutes: normalizeAutoSyncInterval(raw.autoSyncIntervalMinutes, 15),
host: String(raw.host || '').trim(),
port: String(normalizeIpProxyPort(raw.port || '') || ''),
protocol: normalizeIpProxyProtocol(raw.protocol),
@@ -597,6 +611,8 @@ function buildIpProxyServiceProfileFromState(state = {}) {
accountSessionPrefix: state?.ipProxyAccountSessionPrefix,
accountLifeMinutes: state?.ipProxyAccountLifeMinutes,
poolTargetCount: state?.ipProxyPoolTargetCount,
autoSyncEnabled: state?.ipProxyAutoSyncEnabled,
autoSyncIntervalMinutes: state?.ipProxyAutoSyncIntervalMinutes,
host: state?.ipProxyHost,
port: state?.ipProxyPort,
protocol: state?.ipProxyProtocol,
@@ -1595,20 +1611,14 @@ function applyExitRegionExpectation(status = {}, expectedRegion = '') {
const authHint = authSummary ? ` 诊断:probe:${authSummary}` : '';
const mismatchMessage = `代理出口地区与预期不一致:期望 ${expected},实际 ${detected || 'unknown'}${sourceHint}${usernameHint}${missingAuthChallengeHint}${authHint}`.trim();
if (normalizedProvider === '711proxy') {
if (missingAuthChallenge) {
return {
...status,
applied: false,
reason: 'connectivity_failed',
error: mismatchMessage,
warning: '',
};
}
const warningPrefix = missingAuthChallenge
? '地区校验未通过且未触发代理鉴权挑战,疑似匿名链路;先保留代理接管并给出强告警:'
: '地区校验未通过,但已保留代理接管:';
return {
...status,
applied: true,
reason: 'applied_with_warning',
warning: `地区校验未通过,但已保留代理接管:${mismatchMessage}`,
warning: `${warningPrefix}${mismatchMessage}`,
error: '',
};
}
@@ -3172,9 +3182,11 @@ async function refreshIpProxyPool(options = {}) {
forceAuthRebind: true,
suppressAuthRebind: false,
resetNetworkState: true,
skipExitProbe: false,
skipExitProbe: Boolean(options?.skipExitProbe),
}
: {
skipExitProbe: Boolean(options?.skipExitProbe),
}
: undefined
);
}
@@ -3249,9 +3261,11 @@ async function switchIpProxy(direction = 'next', options = {}) {
forceAuthRebind: true,
suppressAuthRebind: false,
resetNetworkState: true,
skipExitProbe: false,
skipExitProbe: Boolean(options?.skipExitProbe),
}
: {
skipExitProbe: Boolean(options?.skipExitProbe),
}
: undefined
);
}
const summary = buildProxyPoolSummary(pool, nextIndex);
@@ -3299,7 +3313,7 @@ async function changeIpProxyExit(options = {}) {
forceAuthRebind: true,
suppressAuthRebind: false,
resetNetworkState: true,
skipExitProbe: false,
skipExitProbe: Boolean(options?.skipExitProbe),
});
const runtime = getIpProxyRuntimeSnapshot(state, mode, provider);
@@ -3405,6 +3419,10 @@ async function tryRecoverApiProxyByRotation(options = {}) {
}
async function probeIpProxyExit(options = {}) {
if (ipProxyProbeInFlightPromise) {
return ipProxyProbeInFlightPromise;
}
const probePromise = (async () => {
const state = options.state || await getState();
if (!state?.ipProxyEnabled) {
const status = {
@@ -3599,6 +3617,15 @@ async function probeIpProxyExit(options = {}) {
}
await updateIpProxyRuntimeStatus(normalizedFinalStatus);
return { proxyRouting: normalizedFinalStatus };
})();
ipProxyProbeInFlightPromise = probePromise;
try {
return await probePromise;
} finally {
if (ipProxyProbeInFlightPromise === probePromise) {
ipProxyProbeInFlightPromise = null;
}
}
}
async function ensureIpProxySettingsAppliedFromCurrentState(options = {}) {
+1 -1
View File
@@ -58,7 +58,7 @@
function isVerificationMailPollingError(error) {
const message = getErrorMessage(error);
return /未在 .*邮箱中找到新的匹配邮件|未在 Hotmail 收件箱中找到新的匹配验证码|邮箱轮询结束,但未获取到验证码|无法获取新的(?:注册|登录)验证码|页面未能重新就绪|页面通信异常|did not respond in \d+s/i.test(message);
return /未在 .*邮箱中找到新的匹配邮件|未在 Hotmail 收件箱中找到新的匹配验证码|邮箱轮询结束,但未获取到验证码|无法获取新的(?:注册|登录)验证码|页面未能重新就绪|页面通信异常|did not respond in \d+s|405\s+method\s+not\s+allowed|route\s+error.*405|did\s+not\s+provide\s+an?\s+[`'"]?action|post\s+request\s+to\s+["']?\/(?:email|phone)-verification/i.test(message);
}
function isAddPhoneAuthFailure(error) {
+34 -1
View File
@@ -61,6 +61,9 @@
isStopError,
isTabAlive,
launchAutoRunTimerPlan,
ensureIpProxyAutoSyncAlarm,
clearIpProxyAutoSyncAlarm,
runIpProxyAutoSync,
listIcloudAliases,
listLuckmailPurchasesForManagement,
refreshIpProxyPool,
@@ -720,6 +723,12 @@
oauthFlowDeadlineSourceUrl: null,
} : {}),
};
if (Object.prototype.hasOwnProperty.call(updates, 'icloudHostPreference')) {
const nextHostPreference = String(updates.icloudHostPreference || '').trim().toLowerCase();
stateUpdates.preferredIcloudHost = nextHostPreference === 'icloud.com' || nextHostPreference === 'icloud.com.cn'
? nextHostPreference
: '';
}
if (stepModeChanged && typeof getStepIdsForState === 'function') {
const nextStateForSteps = { ...currentState, ...stateUpdates };
stateUpdates.stepStatuses = Object.fromEntries(
@@ -729,6 +738,19 @@
}
await setState(stateUpdates);
const mergedState = await getState();
const hasIpProxyAutoSyncSettingChanged = (
Object.prototype.hasOwnProperty.call(updates, 'ipProxyAutoSyncEnabled')
|| Object.prototype.hasOwnProperty.call(updates, 'ipProxyAutoSyncIntervalMinutes')
);
if (hasIpProxyAutoSyncSettingChanged) {
if (Boolean(mergedState?.ipProxyAutoSyncEnabled)) {
if (typeof ensureIpProxyAutoSyncAlarm === 'function') {
await ensureIpProxyAutoSyncAlarm(mergedState);
}
} else if (typeof clearIpProxyAutoSyncAlarm === 'function') {
await clearIpProxyAutoSyncAlarm();
}
}
const hasIpProxyUpdates = Object.keys(updates).some((key) => key.startsWith('ipProxy'));
const hasIpProxyEnabledUpdate = Object.prototype.hasOwnProperty.call(updates, 'ipProxyEnabled');
const previousIpProxyEnabled = Boolean(currentState?.ipProxyEnabled);
@@ -782,6 +804,14 @@
return { ok: true, state: await getState(), proxyRouting };
}
case 'RUN_IP_PROXY_AUTO_SYNC_NOW': {
if (typeof runIpProxyAutoSync !== 'function') {
throw new Error('IP 代理自动同步能力尚未接入。');
}
const result = await runIpProxyAutoSync('manual');
return { ok: true, ...result };
}
case 'REFRESH_IP_PROXY_POOL': {
if (typeof refreshIpProxyPool !== 'function') {
throw new Error('IP 代理池能力尚未接入。');
@@ -789,6 +819,7 @@
const result = await refreshIpProxyPool({
maxItems: message.payload?.maxItems,
mode: message.payload?.mode,
skipExitProbe: message.payload?.skipExitProbe,
});
return { ok: true, ...result };
}
@@ -801,6 +832,7 @@
maxItems: message.payload?.maxItems,
mode: message.payload?.mode,
forceRefresh: message.payload?.forceRefresh,
skipExitProbe: message.payload?.skipExitProbe,
});
return { ok: true, ...result };
}
@@ -811,6 +843,7 @@
}
const result = await changeIpProxyExit({
mode: message.payload?.mode,
skipExitProbe: message.payload?.skipExitProbe,
});
return { ok: true, ...result };
}
@@ -837,7 +870,7 @@
);
const timeoutMs = Number(message.payload?.timeoutMs) > 0
? Number(message.payload.timeoutMs)
: (is711AccountMode ? (shouldPreRebindBeforeProbe ? 8000 : 6000) : undefined);
: (is711AccountMode ? (shouldPreRebindBeforeProbe ? 15000 : 12000) : undefined);
// 手动“检测出口”前先轻量应用当前配置,避免读取到旧代理链路状态。
if (probeState?.ipProxyEnabled && typeof applyIpProxySettingsFromState === 'function') {
File diff suppressed because it is too large Load Diff
+18 -1
View File
@@ -316,6 +316,8 @@
let mailPollingAttempt = 1;
let lastMailPollingError = null;
let stickyLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
let retryWithoutStep7Streak = 0;
const maxRetryWithoutStep7Streak = 3;
while (true) {
try {
@@ -331,6 +333,7 @@
if (Number(result?.lastResendAt) > 0) {
stickyLastResendAt = Math.max(stickyLastResendAt, Number(result.lastResendAt) || 0);
}
retryWithoutStep7Streak = 0;
return;
} catch (err) {
const visibleStep = getVisibleStep(currentState, 8);
@@ -361,8 +364,21 @@
mailPollingAttempt += 1;
if (retryWithoutStep7) {
retryWithoutStep7Streak += 1;
if (retryWithoutStep7Streak > maxRetryWithoutStep7Streak) {
await addLog(
`步骤 ${visibleStep}:邮箱通信异常在当前链路已连续重试 ${retryWithoutStep7Streak} 次,改为回到步骤 ${authLoginStep} 重新发起授权链路,避免空轮询循环。`,
'warn'
);
await rerunStep7ForStep8Recovery({
logMessage: `步骤 ${visibleStep}:邮箱通信异常持续未恢复,正在回到步骤 ${authLoginStep} 重新发起登录流程...`,
});
currentState = await getState();
retryWithoutStep7Streak = 0;
continue;
}
await addLog(
`步骤 ${visibleStep}:认证页仍保持在验证码页,将在当前链路直接重试(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}),不回到步骤 ${authLoginStep}`,
`步骤 ${visibleStep}:认证页仍保持在验证码页,将在当前链路直接重试(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}),不回到步骤 ${authLoginStep}(连续同链路重试 ${retryWithoutStep7Streak}/${maxRetryWithoutStep7Streak}`,
'warn'
);
const latestState = await getState();
@@ -390,6 +406,7 @@
}
continue;
}
retryWithoutStep7Streak = 0;
await addLog(
isStep8RestartStep7Error(currentError)
? `步骤 ${visibleStep}:检测到认证页进入重试/超时报错状态,准备从步骤 ${authLoginStep} 重新开始(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}...`
+67 -16
View File
@@ -21,7 +21,9 @@
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
resolveVerificationStep,
reuseOrCreateTab,
sendToContentScript,
sendToContentScriptResilient,
isRetryableContentScriptTransportError = () => false,
shouldUseCustomRegistrationEmail,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
throwIfStopped,
@@ -97,24 +99,73 @@
throwIfStopped();
await addLog('步骤 4:正在确认注册验证码页面是否就绪,必要时自动恢复密码页超时报错...');
const prepareResult = await sendToContentScriptResilient(
'signup-page',
{
type: 'PREPARE_SIGNUP_VERIFICATION',
step: 4,
source: 'background',
payload: {
password: state.password || state.customPassword || '',
prepareSource: 'step4_execute',
prepareLogLabel: '步骤 4 执行',
},
const prepareRequest = {
type: 'PREPARE_SIGNUP_VERIFICATION',
step: 4,
source: 'background',
payload: {
password: state.password || state.customPassword || '',
prepareSource: 'step4_execute',
prepareLogLabel: '步骤 4 执行',
},
{
timeoutMs: 30000,
retryDelayMs: 700,
logMessage: '步骤 4:认证页正在切换,等待页面重新就绪后继续检测...',
};
const prepareTimeoutMs = 30000;
const prepareResponseTimeoutMs = 30000;
const prepareStartAt = Date.now();
let prepareResult = null;
while (Date.now() - prepareStartAt < prepareTimeoutMs) {
throwIfStopped();
try {
prepareResult = typeof sendToContentScript === 'function'
? await sendToContentScript('signup-page', prepareRequest, {
responseTimeoutMs: prepareResponseTimeoutMs,
})
: await sendToContentScriptResilient('signup-page', prepareRequest, {
timeoutMs: Math.max(1000, prepareTimeoutMs - (Date.now() - prepareStartAt)),
responseTimeoutMs: prepareResponseTimeoutMs,
retryDelayMs: 700,
logMessage: '步骤 4:认证页正在切换,等待页面重新就绪后继续检测...',
});
break;
} catch (error) {
if (!isRetryableContentScriptTransportError(error)) {
throw error;
}
const remainingMs = Math.max(0, prepareTimeoutMs - (Date.now() - prepareStartAt));
if (remainingMs <= 0) {
throw error;
}
const recoverResult = await sendToContentScriptResilient('signup-page', {
type: 'RECOVER_AUTH_RETRY_PAGE',
step: 4,
source: 'background',
payload: {
flow: 'signup',
step: 4,
timeoutMs: Math.min(12000, remainingMs),
maxClickAttempts: 2,
logLabel: '步骤 4:检测到注册认证重试页,正在点击“重试”恢复',
},
}, {
timeoutMs: Math.min(12000, remainingMs),
responseTimeoutMs: Math.min(12000, remainingMs),
retryDelayMs: 700,
logMessage: '步骤 4:认证页正在切换,等待页面重新就绪后继续检测...',
});
if (recoverResult?.error) {
throw new Error(recoverResult.error);
}
}
);
}
if (!prepareResult) {
throw new Error('步骤 4:等待注册验证码页面就绪超时,请刷新认证页后重试。');
}
if (prepareResult && prepareResult.error) {
throw new Error(prepareResult.error);
+49 -8
View File
@@ -135,6 +135,26 @@
}
}
function isSub2ApiTransientExchangeError(error) {
const message = normalizeString(error?.message || error);
if (!message) {
return false;
}
const tokenExchangeFailure = /auth\.openai\.com\/oauth\/token/i.test(message);
const transientNetworkSignal = /unexpected\s+eof|eof|connection\s+refused|i\/o\s+timeout|context\s+deadline\s+exceeded|connection\s+reset|broken\s+pipe|failed\s+to\s+fetch|temporarily\s+unavailable|timeout/i.test(message);
const transientExchangeUserSignal = /token_exchange_user_error|invalid\s+request\.\s+please\s+try\s+again\s+later/i.test(message);
if (transientExchangeUserSignal) {
return true;
}
return tokenExchangeFailure && transientNetworkSignal;
}
async function sleep(ms = 0) {
const timeout = Math.max(0, Number(ms) || 0);
if (!timeout) return;
await new Promise((resolve) => setTimeout(resolve, timeout));
}
async function fetchCodex2ApiJson(origin, path, options = {}) {
const controller = new AbortController();
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 30000));
@@ -286,6 +306,7 @@
}
async function executeSub2ApiStep10(state) {
const visibleStep = resolvePlatformVerifyStep(state);
if (state.localhostUrl && !isLocalhostOAuthCallbackUrl(state.localhostUrl)) {
throw new Error('步骤 9 捕获到的 localhost OAuth 回调地址无效,请重新执行步骤 9。');
}
@@ -327,8 +348,8 @@
injectSource: 'sub2api-panel',
});
await addLog('步骤 10:正在向 SUB2API 提交回调并创建账号...');
const result = await sendToContentScript('sub2api-panel', {
await addLog(`步骤 ${visibleStep}:正在向 SUB2API 提交回调并创建账号...`);
const requestMessage = {
type: 'EXECUTE_STEP',
step: 10,
source: 'background',
@@ -345,12 +366,32 @@
sub2apiGroupId: state.sub2apiGroupId,
sub2apiDraftName: state.sub2apiDraftName,
},
}, {
responseTimeoutMs: SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
});
if (result?.error) {
throw new Error(result.error);
};
const maxExchangeAttempts = 3;
let lastError = null;
for (let attempt = 1; attempt <= maxExchangeAttempts; attempt += 1) {
try {
const result = await sendToContentScript('sub2api-panel', requestMessage, {
responseTimeoutMs: SUB2API_STEP9_RESPONSE_TIMEOUT_MS,
});
if (result?.error) {
throw new Error(result.error);
}
return;
} catch (error) {
lastError = error;
if (!isSub2ApiTransientExchangeError(error) || attempt >= maxExchangeAttempts) {
throw error;
}
await addLog(
`步骤 ${visibleStep}:SUB2API 回调交换出现临时网络波动(${error.message}),正在重试 ${attempt + 1}/${maxExchangeAttempts}...`,
'warn'
);
await sleep(1200 * attempt);
}
}
if (lastError) {
throw lastError;
}
}
+115 -8
View File
@@ -1,6 +1,9 @@
(function attachBackgroundVerificationFlow(root, factory) {
root.MultiPageBackgroundVerificationFlow = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundVerificationFlowModule() {
const ICLOUD_MAIL_POLL_RESPONSE_TIMEOUT_CAP_MS = 18000;
const ICLOUD_MAIL_POLL_TOTAL_TIMEOUT_CAP_MS = 22000;
function createVerificationFlowHelpers(deps = {}) {
const {
addLog,
@@ -46,6 +49,33 @@
return step === 4 ? '注册' : '登录';
}
function capIcloudMailPollingTimeouts(mail, timedPoll) {
const defaultResponseTimeoutMs = Math.max(1000, Number(timedPoll?.responseTimeoutMs) || 30000);
const defaultTimeoutMs = Math.max(defaultResponseTimeoutMs, Number(timedPoll?.timeoutMs) || defaultResponseTimeoutMs);
if (mail?.source !== 'icloud-mail') {
return {
responseTimeoutMs: defaultResponseTimeoutMs,
timeoutMs: defaultTimeoutMs,
capped: false,
};
}
const cappedResponseTimeoutMs = Math.max(
5000,
Math.min(defaultResponseTimeoutMs, ICLOUD_MAIL_POLL_RESPONSE_TIMEOUT_CAP_MS)
);
const cappedTimeoutMs = Math.max(
cappedResponseTimeoutMs,
Math.min(defaultTimeoutMs, ICLOUD_MAIL_POLL_TOTAL_TIMEOUT_CAP_MS)
);
return {
responseTimeoutMs: cappedResponseTimeoutMs,
timeoutMs: cappedTimeoutMs,
capped: cappedResponseTimeoutMs < defaultResponseTimeoutMs || cappedTimeoutMs < defaultTimeoutMs,
};
}
function isLikelyLoggedInChatgptHomeUrl(rawUrl) {
const url = String(rawUrl || '').trim();
if (!url) return false;
@@ -375,6 +405,10 @@
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(
@@ -386,7 +420,10 @@
const defaultResponseTimeoutMs = Math.max(45000, nextPayload.maxAttempts * intervalMs + 25000);
const responseTimeoutMs = disableTimeBudgetCap || remainingMs === null
? defaultResponseTimeoutMs
: Math.max(1000, Math.min(defaultResponseTimeoutMs, remainingMs));
: Math.max(
minPollingResponseTimeoutMs,
Math.min(defaultResponseTimeoutMs, remainingMs)
);
return {
payload: nextPayload,
@@ -583,9 +620,27 @@
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;
@@ -619,6 +674,13 @@
pollOverrides,
`轮询${getVerificationCodeLabel(step)}验证码邮箱`
);
const timeoutWindow = capIcloudMailPollingTimeouts(mail, timedPoll);
if (timeoutWindow.capped) {
await addLog(
`步骤 ${step}:iCloud 邮箱轮询已启用快速超时保护(${Math.ceil(timeoutWindow.timeoutMs / 1000)} 秒),避免页面无响应导致长时间卡住。`,
'info'
);
}
const result = await sendToMailContentScriptResilient(
mail,
{
@@ -628,9 +690,9 @@
payload: timedPoll.payload,
},
{
timeoutMs: timedPoll.timeoutMs,
timeoutMs: timeoutWindow.timeoutMs,
maxRecoveryAttempts: 2,
responseTimeoutMs: timedPoll.responseTimeoutMs,
responseTimeoutMs: timeoutWindow.responseTimeoutMs,
}
);
@@ -646,6 +708,8 @@
throw new Error(`步骤 ${step}:再次收到了相同的${getVerificationCodeLabel(step)}验证码:${result.code}`);
}
transportErrorStreak = 0;
return {
...result,
lastResendAt,
@@ -661,16 +725,52 @@
}
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;
if (remainingBeforeResendMs > 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(remainingBeforeResendMs / 1000)} 秒,继续刷新邮箱(第 ${round}/${maxRounds} 轮)...`,
`步骤 ${step}:距离下次重新发送验证码还差 ${Math.ceil(effectiveCooldownMs / 1000)} 秒,继续刷新邮箱(第 ${round}/${maxRounds} 轮)...`,
'info'
);
const configuredIntervalMs = Math.max(
@@ -680,7 +780,7 @@
|| 3000
);
const cooldownSleepMs = Math.min(
remainingBeforeResendMs,
effectiveCooldownMs,
Math.max(1000, Math.min(configuredIntervalMs, 3000))
);
await sleepWithStop(cooldownSleepMs);
@@ -840,6 +940,13 @@
pollOverrides,
`轮询${getVerificationCodeLabel(step)}验证码邮箱`
);
const timeoutWindow = capIcloudMailPollingTimeouts(mail, timedPoll);
if (timeoutWindow.capped) {
await addLog(
`步骤 ${step}:iCloud 邮箱轮询已启用快速超时保护(${Math.ceil(timeoutWindow.timeoutMs / 1000)} 秒),避免页面无响应导致长时间卡住。`,
'info'
);
}
const result = await sendToMailContentScriptResilient(
mail,
{
@@ -849,9 +956,9 @@
payload: timedPoll.payload,
},
{
timeoutMs: timedPoll.timeoutMs,
timeoutMs: timeoutWindow.timeoutMs,
maxRecoveryAttempts: 2,
responseTimeoutMs: timedPoll.responseTimeoutMs,
responseTimeoutMs: timeoutWindow.responseTimeoutMs,
}
);