Merge remote-tracking branch 'origin/dev' into fix/plus-payment-log-text
This commit is contained in:
@@ -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)) {
|
||||
|
||||
@@ -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
@@ -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 = {}) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -61,6 +61,9 @@
|
||||
isStopError,
|
||||
isTabAlive,
|
||||
launchAutoRunTimerPlan,
|
||||
ensureIpProxyAutoSyncAlarm,
|
||||
clearIpProxyAutoSyncAlarm,
|
||||
runIpProxyAutoSync,
|
||||
listIcloudAliases,
|
||||
listLuckmailPurchasesForManagement,
|
||||
refreshIpProxyPool,
|
||||
@@ -328,7 +331,11 @@
|
||||
const step5Status = latestState.stepStatuses?.[5];
|
||||
if (step5Status !== 'running' && step5Status !== 'completed' && step5Status !== 'manual_completed') {
|
||||
await setStepStatus(5, 'skipped');
|
||||
await addLog('步骤 4:检测到账号已直接进入已登录态,已自动跳过步骤 5。', 'warn');
|
||||
if (payload.skipProfileStepReason === 'combined_verification_profile') {
|
||||
await addLog('步骤 4:当前验证码页已内嵌完成注册资料提交,已自动跳过步骤 5。', 'warn');
|
||||
} else {
|
||||
await addLog('步骤 4:检测到账号已直接进入已登录态,已自动跳过步骤 5。', 'warn');
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -716,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(
|
||||
@@ -725,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);
|
||||
@@ -778,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 代理池能力尚未接入。');
|
||||
@@ -785,6 +819,7 @@
|
||||
const result = await refreshIpProxyPool({
|
||||
maxItems: message.payload?.maxItems,
|
||||
mode: message.payload?.mode,
|
||||
skipExitProbe: message.payload?.skipExitProbe,
|
||||
});
|
||||
return { ok: true, ...result };
|
||||
}
|
||||
@@ -797,6 +832,7 @@
|
||||
maxItems: message.payload?.maxItems,
|
||||
mode: message.payload?.mode,
|
||||
forceRefresh: message.payload?.forceRefresh,
|
||||
skipExitProbe: message.payload?.skipExitProbe,
|
||||
});
|
||||
return { ok: true, ...result };
|
||||
}
|
||||
@@ -807,6 +843,7 @@
|
||||
}
|
||||
const result = await changeIpProxyExit({
|
||||
mode: message.payload?.mode,
|
||||
skipExitProbe: message.payload?.skipExitProbe,
|
||||
});
|
||||
return { ok: true, ...result };
|
||||
}
|
||||
@@ -833,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') {
|
||||
|
||||
+3022
-200
File diff suppressed because it is too large
Load Diff
@@ -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})...`
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
chrome,
|
||||
completeStepFromBackground,
|
||||
confirmCustomVerificationStepBypass,
|
||||
generateRandomBirthday,
|
||||
generateRandomName,
|
||||
ensureMail2925MailboxSession,
|
||||
ensureIcloudMailSession,
|
||||
getMailConfig,
|
||||
@@ -19,12 +21,29 @@
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
resolveVerificationStep,
|
||||
reuseOrCreateTab,
|
||||
sendToContentScript,
|
||||
sendToContentScriptResilient,
|
||||
isRetryableContentScriptTransportError = () => false,
|
||||
shouldUseCustomRegistrationEmail,
|
||||
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
|
||||
throwIfStopped,
|
||||
} = deps;
|
||||
|
||||
function buildSignupProfileForVerificationStep() {
|
||||
const name = typeof generateRandomName === 'function' ? generateRandomName() : null;
|
||||
const birthday = typeof generateRandomBirthday === 'function' ? generateRandomBirthday() : null;
|
||||
if (!name?.firstName || !name?.lastName || !birthday) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
firstName: name.firstName,
|
||||
lastName: name.lastName,
|
||||
year: birthday.year,
|
||||
month: birthday.month,
|
||||
day: birthday.day,
|
||||
};
|
||||
}
|
||||
|
||||
function getExpectedMail2925MailboxEmail(state = {}) {
|
||||
if (Boolean(state?.mail2925UseAccountPool)) {
|
||||
const currentAccountId = String(state?.currentMail2925AccountId || '').trim();
|
||||
@@ -80,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);
|
||||
@@ -152,12 +220,14 @@
|
||||
LUCKMAIL_PROVIDER,
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
|
||||
].includes(mail.provider);
|
||||
const signupProfile = buildSignupProfileForVerificationStep();
|
||||
|
||||
await resolveVerificationStep(4, state, mail, {
|
||||
filterAfterTimestamp: verificationFilterAfterTimestamp,
|
||||
sessionKey: verificationSessionKey,
|
||||
disableTimeBudgetCap: mail.provider === '2925',
|
||||
requestFreshCodeFirst: shouldRequestFreshCodeFirst,
|
||||
signupProfile,
|
||||
resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER
|
||||
? 15000
|
||||
: ((mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -909,7 +1016,10 @@
|
||||
type: 'FILL_CODE',
|
||||
step,
|
||||
source: 'background',
|
||||
payload: { code },
|
||||
payload: {
|
||||
code,
|
||||
...(step === 4 && options.signupProfile ? { signupProfile: options.signupProfile } : {}),
|
||||
},
|
||||
};
|
||||
let result;
|
||||
if (typeof sendToContentScriptResilient === 'function') {
|
||||
@@ -1144,6 +1254,9 @@
|
||||
code: result.code,
|
||||
phoneVerificationRequired: Boolean(submitResult.addPhonePage),
|
||||
...(step === 4 && submitResult?.skipProfileStep ? { skipProfileStep: true } : {}),
|
||||
...(step === 4 && submitResult?.skipProfileStepReason
|
||||
? { skipProfileStepReason: submitResult.skipProfileStepReason }
|
||||
: {}),
|
||||
});
|
||||
triggerPostSuccessMailboxCleanup(step, mail);
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user