feat: 增加 OAuth 流程6分钟超时处理,优化验证码请求时间窗口逻辑

This commit is contained in:
QLHazyCoder
2026-04-17 19:12:06 +08:00
parent a827ede6f4
commit 4add7477d1
11 changed files with 500 additions and 92 deletions
+63 -7
View File
@@ -9,6 +9,7 @@
clickWithDebugger,
completeStepFromBackground,
ensureStep8SignupPageReady,
getOAuthFlowStepTimeoutMs,
getStep8CallbackUrlFromNavigation,
getStep8CallbackUrlFromTabUpdate,
getStep8EffectLabel,
@@ -39,6 +40,13 @@
await addLog('步骤 9:正在监听 localhost 回调地址...');
const callbackTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(120000, {
step: 9,
actionLabel: 'OAuth localhost 回调',
})
: 120000;
return new Promise((resolve, reject) => {
let resolved = false;
let signupTabId = null;
@@ -74,7 +82,7 @@
const timeout = setTimeout(() => {
rejectStep9(new Error('120 秒内未捕获到 localhost 回调跳转,步骤 9 的点击可能被拦截了。'));
}, 120000);
}, callbackTimeoutMs);
setStep8PendingReject((error) => {
rejectStep9(error);
@@ -114,13 +122,26 @@
chrome.webNavigation.onCommitted.addListener(deps.getWebNavCommittedListener());
chrome.tabs.onUpdated.addListener(deps.getStep8TabUpdatedListener());
await ensureStep8SignupPageReady(signupTabId, {
timeoutMs: 15000,
timeoutMs: typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(15000, {
step: 9,
actionLabel: '等待 OAuth 同意页内容脚本就绪',
})
: 15000,
logMessage: '步骤 9:认证页内容脚本尚未就绪,正在等待页面恢复...',
});
for (let round = 1; round <= STEP8_MAX_ROUNDS && !resolved; round++) {
throwIfStep8SettledOrStopped(resolved);
const pageState = await waitForStep8Ready(signupTabId, STEP8_READY_WAIT_TIMEOUT_MS);
const pageState = await waitForStep8Ready(
signupTabId,
typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(STEP8_READY_WAIT_TIMEOUT_MS, {
step: 9,
actionLabel: '等待 OAuth 同意页出现',
})
: STEP8_READY_WAIT_TIMEOUT_MS
);
if (!pageState?.consentReady) {
await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS);
continue;
@@ -131,18 +152,45 @@
await addLog(`步骤 9:第 ${round}/${STEP8_MAX_ROUNDS} 轮尝试点击“继续”(${strategy.label}...`);
if (strategy.mode === 'debugger') {
const clickTarget = await prepareStep8DebuggerClick(signupTabId);
const clickActionTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(15000, {
step: 9,
actionLabel: '定位 OAuth 同意页继续按钮',
})
: 15000;
const clickTarget = await prepareStep8DebuggerClick(signupTabId, {
timeoutMs: clickActionTimeoutMs,
responseTimeoutMs: clickActionTimeoutMs,
});
throwIfStep8SettledOrStopped(resolved);
await clickWithDebugger(signupTabId, clickTarget?.rect);
} else {
await triggerStep8ContentStrategy(signupTabId, strategy.strategy);
const clickActionTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(15000, {
step: 9,
actionLabel: '点击 OAuth 同意页继续按钮',
})
: 15000;
await triggerStep8ContentStrategy(signupTabId, strategy.strategy, {
timeoutMs: clickActionTimeoutMs,
responseTimeoutMs: clickActionTimeoutMs,
});
}
if (resolved) {
return;
}
const effect = await waitForStep8ClickEffect(signupTabId, pageState.url);
const effect = await waitForStep8ClickEffect(
signupTabId,
pageState.url,
typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(15000, {
step: 9,
actionLabel: '等待 OAuth 同意页点击生效',
})
: 15000
);
if (resolved) {
return;
}
@@ -163,7 +211,15 @@
}
await addLog(`步骤 9${strategy.label} 本轮点击后页面无反应,正在刷新认证页后重试(下一轮 ${round + 1}/${STEP8_MAX_ROUNDS}...`, 'warn');
await reloadStep8ConsentPage(signupTabId);
await reloadStep8ConsentPage(
signupTabId,
typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(30000, {
step: 9,
actionLabel: '刷新 OAuth 同意页',
})
: 30000
);
await sleepWithStop(STEP8_CLICK_RETRY_DELAY_MS);
}
} catch (err) {
+33 -3
View File
@@ -9,6 +9,8 @@
confirmCustomVerificationStepBypass,
ensureStep8VerificationPageReady,
executeStep7,
getOAuthFlowRemainingMs,
getOAuthFlowStepTimeoutMs,
getPanelMode,
getMailConfig,
getState,
@@ -29,6 +31,28 @@
throwIfStopped,
} = deps;
async function getStep8ReadyTimeoutMs(actionLabel) {
if (typeof getOAuthFlowStepTimeoutMs !== 'function') {
return 15000;
}
return getOAuthFlowStepTimeoutMs(15000, {
step: 8,
actionLabel,
});
}
function getStep8RemainingTimeResolver() {
if (typeof getOAuthFlowRemainingMs !== 'function') {
return undefined;
}
return async (details = {}) => getOAuthFlowRemainingMs({
step: 8,
actionLabel: details.actionLabel || '登录验证码流程',
});
}
async function runStep8Attempt(state) {
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
@@ -45,7 +69,9 @@
}
throwIfStopped();
await ensureStep8VerificationPageReady();
await ensureStep8VerificationPageReady({
timeoutMs: await getStep8ReadyTimeoutMs('确认登录验证码页已就绪'),
});
await addLog('步骤 8:登录验证码页面已就绪,开始获取验证码。', 'info');
if (shouldUseCustomRegistrationEmail(state)) {
@@ -82,7 +108,8 @@
let step7ReplayCompleted = false;
await resolveVerificationStep(8, state, mail, {
filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : Math.max(0, stepStartedAt - 60000),
filterAfterTimestamp: stepStartedAt,
getRemainingTimeMs: getStep8RemainingTimeResolver(),
requestFreshCodeFirst: false,
resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
? 0
@@ -98,7 +125,9 @@
logMessage: '步骤 8:正在重新获取最新 CPA OAuth 链接,并快速重走步骤 7...',
postStepDelayMs: 1200,
});
await ensureStep8VerificationPageReady();
await ensureStep8VerificationPageReady({
timeoutMs: await getStep8ReadyTimeoutMs('重放步骤 7 后重新确认登录验证码页'),
});
await addLog('步骤 8:登录验证码页面已重新就绪,开始回填刚才获取到的验证码。', 'info');
} : undefined,
});
@@ -122,6 +151,7 @@
await setState({
lastLoginCode: null,
loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null,
});
await setStepStatus(8, 'skipped');
await addLog('步骤 8:当前已选择“第七步回调”,本轮无需获取登录验证码。', 'warn');
+1 -1
View File
@@ -87,7 +87,7 @@
}
await resolveVerificationStep(4, state, mail, {
filterAfterTimestamp: mail.provider === HOTMAIL_PROVIDER ? undefined : stepStartedAt,
filterAfterTimestamp: stepStartedAt,
requestFreshCodeFirst: mail.provider === HOTMAIL_PROVIDER ? false : true,
resendIntervalMs: (mail.provider === HOTMAIL_PROVIDER || mail.provider === '2925')
? 0
+13 -1
View File
@@ -7,6 +7,7 @@
completeStepFromBackground,
getErrorMessage,
getLoginAuthStateLabel,
getOAuthFlowStepTimeoutMs,
getState,
isStep6RecoverableResult,
isStep6SuccessResult,
@@ -15,6 +16,7 @@
sendToContentScriptResilient,
shouldSkipLoginVerificationForCpaCallback,
skipLoginVerificationStepsForCpaCallback,
startOAuthFlowTimeoutWindow,
STEP6_MAX_ATTEMPTS,
throwIfStopped,
} = deps;
@@ -38,6 +40,15 @@
const currentState = attempt === 1 ? state : await getState();
const password = currentState.password || currentState.customPassword || '';
const oauthUrl = await refreshOAuthUrlBeforeStep6(currentState);
if (typeof startOAuthFlowTimeoutWindow === 'function') {
await startOAuthFlowTimeoutWindow({ step: 7, oauthUrl });
}
const loginTimeoutMs = typeof getOAuthFlowStepTimeoutMs === 'function'
? await getOAuthFlowStepTimeoutMs(180000, {
step: 7,
actionLabel: 'OAuth 登录并进入验证码页',
})
: 180000;
if (attempt === 1) {
await addLog('步骤 7:正在打开最新 OAuth 链接并登录...');
@@ -59,7 +70,8 @@
},
},
{
timeoutMs: 180000,
timeoutMs: loginTimeoutMs,
responseTimeoutMs: loginTimeoutMs,
retryDelayMs: 700,
logMessage: '步骤 7:认证页正在切换,等待页面重新就绪后继续登录...',
}
+21 -4
View File
@@ -547,7 +547,12 @@
}
async function sendToContentScriptResilient(source, message, options = {}) {
const { timeoutMs = 30000, retryDelayMs = 600, logMessage = '' } = options;
const {
timeoutMs = 30000,
retryDelayMs = 600,
logMessage = '',
responseTimeoutMs,
} = options;
const start = Date.now();
let lastError = null;
let logged = false;
@@ -558,7 +563,11 @@
attempt += 1;
try {
return await sendToContentScript(source, message);
return await sendToContentScript(
source,
message,
responseTimeoutMs !== undefined ? { responseTimeoutMs } : {}
);
} catch (err) {
const retryable = isRetryableContentScriptTransportError(err);
if (!retryable) {
@@ -579,7 +588,11 @@
}
async function sendToMailContentScriptResilient(mail, message, options = {}) {
const { timeoutMs = 45000, maxRecoveryAttempts = 2 } = options;
const {
timeoutMs = 45000,
maxRecoveryAttempts = 2,
responseTimeoutMs,
} = options;
const start = Date.now();
let lastError = null;
let recoveries = 0;
@@ -589,7 +602,11 @@
throwIfStopped();
try {
return await sendToContentScript(mail.source, message);
return await sendToContentScript(
mail.source,
message,
responseTimeoutMs !== undefined ? { responseTimeoutMs } : {}
);
} catch (err) {
if (!isRetryableContentScriptTransportError(err)) {
throw err;
+110 -34
View File
@@ -134,7 +134,58 @@
};
}
async function requestVerificationCodeResend(step) {
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 remainingMs = await getRemainingTimeBudgetMs(step, options, actionLabel);
if (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 = remainingMs === null
? defaultResponseTimeoutMs
: Math.max(1000, Math.min(defaultResponseTimeoutMs, remainingMs));
return {
payload: nextPayload,
responseTimeoutMs,
timeoutMs: responseTimeoutMs,
};
}
async function requestVerificationCodeResend(step, options = {}) {
throwIfStopped();
const signupTabId = await getTabId('signup-page');
if (!signupTabId) {
@@ -150,6 +201,13 @@
step,
source: 'background',
payload: {},
}, {
responseTimeoutMs: await getResponseTimeoutMsForStep(
step,
options,
30000,
`重新发送${getVerificationCodeLabel(step)}验证码`
),
});
if (result && result.error) {
@@ -211,7 +269,7 @@
for (let round = 1; round <= totalRounds; round++) {
throwIfStopped();
if (round > 1) {
lastResendAt = await requestVerificationCodeResend(step);
lastResendAt = await requestVerificationCodeResend(step, pollOverrides);
usedResendRequests += 1;
if (onResendRequestedAt) {
const nextFilterAfterTimestamp = await onResendRequestedAt(lastResendAt);
@@ -237,17 +295,24 @@
}
try {
const timedPoll = await applyMailPollingTimeBudget(
step,
payload,
pollOverrides,
`轮询${getVerificationCodeLabel(step)}验证码邮箱`
);
const result = await sendToMailContentScriptResilient(
mail,
{
type: 'POLL_EMAIL',
step,
source: 'background',
payload,
payload: timedPoll.payload,
},
{
timeoutMs: 45000,
timeoutMs: timedPoll.timeoutMs,
maxRecoveryAttempts: 2,
responseTimeoutMs: timedPoll.responseTimeoutMs,
}
);
@@ -307,23 +372,26 @@
if (mail.provider === HOTMAIL_PROVIDER) {
const hotmailPollConfig = getHotmailVerificationPollConfig(step);
return pollHotmailVerificationCode(step, state, {
const timedPoll = await applyMailPollingTimeBudget(step, {
...getVerificationPollPayload(step, state),
...hotmailPollConfig,
...cleanPollOverrides,
});
}, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`);
return pollHotmailVerificationCode(step, state, timedPoll.payload);
}
if (mail.provider === LUCKMAIL_PROVIDER) {
return pollLuckmailVerificationCode(step, state, {
const timedPoll = await applyMailPollingTimeBudget(step, {
...getVerificationPollPayload(step, state),
...cleanPollOverrides,
});
}, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`);
return pollLuckmailVerificationCode(step, state, timedPoll.payload);
}
if (mail.provider === CLOUDFLARE_TEMP_EMAIL_PROVIDER) {
return pollCloudflareTempEmailVerificationCode(step, state, {
const timedPoll = await applyMailPollingTimeBudget(step, {
...getVerificationPollPayload(step, state),
...cleanPollOverrides,
});
}, cleanPollOverrides, `轮询${getVerificationCodeLabel(step)}验证码邮箱`);
return pollCloudflareTempEmailVerificationCode(step, state, timedPoll.payload);
}
if (Number(pollOverrides.resendIntervalMs) > 0) {
@@ -348,7 +416,7 @@
for (let round = 1; round <= maxRounds; round++) {
throwIfStopped();
if (round > 1) {
const requestedAt = await requestVerificationCodeResend(step);
const requestedAt = await requestVerificationCodeResend(step, pollOverrides);
usedResendRequests += 1;
if (typeof onResendRequestedAt === 'function') {
const nextFilterAfterTimestamp = await onResendRequestedAt(requestedAt);
@@ -365,17 +433,24 @@
});
try {
const timedPoll = await applyMailPollingTimeBudget(
step,
payload,
pollOverrides,
`轮询${getVerificationCodeLabel(step)}验证码邮箱`
);
const result = await sendToMailContentScriptResilient(
mail,
{
type: 'POLL_EMAIL',
step,
source: 'background',
payload,
payload: timedPoll.payload,
},
{
timeoutMs: 45000,
timeoutMs: timedPoll.timeoutMs,
maxRecoveryAttempts: 2,
responseTimeoutMs: timedPoll.responseTimeoutMs,
}
);
@@ -410,7 +485,7 @@
throw lastError || new Error(`步骤 ${step}:无法获取新的${getVerificationCodeLabel(step)}验证码。`);
}
async function submitVerificationCode(step, code) {
async function submitVerificationCode(step, code, options = {}) {
const signupTabId = await getTabId('signup-page');
if (!signupTabId) {
throw new Error('认证页面标签页已关闭,无法填写验证码。');
@@ -422,6 +497,13 @@
step,
source: 'background',
payload: { code },
}, {
responseTimeoutMs: await getResponseTimeoutMsForStep(
step,
options,
step === 7 ? 45000 : 30000,
`填写${getVerificationCodeLabel(step)}验证码`
),
});
if (result && result.error) {
@@ -459,22 +541,7 @@
const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0);
let lastResendAt = Number(options.lastResendAt) || 0;
const updateFilterAfterTimestampForVerificationStep = async (requestedAt) => {
if ((step !== 4 && step !== 8) || !requestedAt) {
return nextFilterAfterTimestamp;
}
if (mail.provider === HOTMAIL_PROVIDER) {
nextFilterAfterTimestamp = getHotmailVerificationRequestTimestamp(step, {
...state,
...(step === 4
? { signupVerificationRequestedAt: requestedAt }
: { loginVerificationRequestedAt: requestedAt }),
});
} else {
nextFilterAfterTimestamp = Math.max(0, Number(requestedAt) - 60000);
}
const updateFilterAfterTimestampForVerificationStep = async (_requestedAt) => {
return nextFilterAfterTimestamp;
};
@@ -483,7 +550,7 @@
await addLog(`步骤 ${step}:当前自动重新发送验证码次数为 0,将直接使用当前时间窗口轮询邮箱。`, 'info');
} else {
try {
lastResendAt = await requestVerificationCodeResend(step);
lastResendAt = await requestVerificationCodeResend(step, options);
remainingAutomaticResendCount -= 1;
await updateFilterAfterTimestampForVerificationStep(lastResendAt);
await addLog(`步骤 ${step}:已先请求一封新的${getVerificationCodeLabel(step)}验证码,再开始轮询邮箱。`, 'warn');
@@ -499,14 +566,23 @@
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(initialDelayMs);
await sleepWithStop(delayMs);
}
}
for (let attempt = 1; attempt <= maxSubmitAttempts; attempt++) {
const pollOptions = {
excludeCodes: [...rejectedCodes],
getRemainingTimeMs: options.getRemainingTimeMs,
maxResendRequests: remainingAutomaticResendCount,
resendIntervalMs,
lastResendAt,
@@ -533,7 +609,7 @@
});
}
throwIfStopped();
const submitResult = await submitVerificationCode(step, result.code);
const submitResult = await submitVerificationCode(step, result.code, options);
if (submitResult.invalidCode) {
rejectedCodes.add(result.code);
@@ -559,7 +635,7 @@
continue;
}
lastResendAt = await requestVerificationCodeResend(step);
lastResendAt = await requestVerificationCodeResend(step, options);
remainingAutomaticResendCount -= 1;
await updateFilterAfterTimestampForVerificationStep(lastResendAt);
await addLog(`步骤 ${step}:提交失败后已请求新验证码(${attempt + 1}/${maxSubmitAttempts}...`, 'warn');