feat(flow): unify sms multi-provider fallback and stabilize proxy/auth retries

This commit is contained in:
daniellee2015
2026-05-03 01:20:10 +08:00
parent 99313843c1
commit 2437e6e67d
37 changed files with 12254 additions and 1142 deletions
+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
@@ -19,7 +19,9 @@
CLOUDFLARE_TEMP_EMAIL_PROVIDER,
resolveVerificationStep,
reuseOrCreateTab,
sendToContentScript,
sendToContentScriptResilient,
isRetryableContentScriptTransportError = () => false,
shouldUseCustomRegistrationEmail,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
throwIfStopped,
@@ -80,24 +82,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;
}
}