feat: 增加 OAuth 流程6分钟超时处理,优化验证码请求时间窗口逻辑
This commit is contained in:
+100
-16
@@ -136,6 +136,7 @@ const HUMAN_STEP_DELAY_MIN = 700;
|
||||
const HUMAN_STEP_DELAY_MAX = 2200;
|
||||
const STEP6_MAX_ATTEMPTS = 3;
|
||||
const STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS = 8;
|
||||
const OAUTH_FLOW_TIMEOUT_MS = 6 * 60 * 1000;
|
||||
const SUB2API_STEP1_RESPONSE_TIMEOUT_MS = 90000;
|
||||
const SUB2API_STEP9_RESPONSE_TIMEOUT_MS = 120000;
|
||||
const DEFAULT_SUB2API_URL = 'https://sub2api.hisence.fun/admin/accounts';
|
||||
@@ -315,6 +316,7 @@ const DEFAULT_STATE = {
|
||||
autoRunCountdownNote: '',
|
||||
signupVerificationRequestedAt: null,
|
||||
loginVerificationRequestedAt: null,
|
||||
oauthFlowDeadlineAt: null,
|
||||
currentHotmailAccountId: null,
|
||||
preferredIcloudHost: '',
|
||||
};
|
||||
@@ -3879,6 +3881,7 @@ function getDownstreamStateResets(step) {
|
||||
lastEmailTimestamp: null,
|
||||
signupVerificationRequestedAt: null,
|
||||
loginVerificationRequestedAt: null,
|
||||
oauthFlowDeadlineAt: null,
|
||||
lastSignupCode: null,
|
||||
lastLoginCode: null,
|
||||
localhostUrl: null,
|
||||
@@ -3890,6 +3893,7 @@ function getDownstreamStateResets(step) {
|
||||
lastEmailTimestamp: null,
|
||||
signupVerificationRequestedAt: null,
|
||||
loginVerificationRequestedAt: null,
|
||||
oauthFlowDeadlineAt: null,
|
||||
lastSignupCode: null,
|
||||
lastLoginCode: null,
|
||||
localhostUrl: null,
|
||||
@@ -3900,6 +3904,7 @@ function getDownstreamStateResets(step) {
|
||||
lastEmailTimestamp: null,
|
||||
signupVerificationRequestedAt: null,
|
||||
loginVerificationRequestedAt: null,
|
||||
oauthFlowDeadlineAt: null,
|
||||
lastSignupCode: null,
|
||||
lastLoginCode: null,
|
||||
localhostUrl: null,
|
||||
@@ -3909,6 +3914,7 @@ function getDownstreamStateResets(step) {
|
||||
return {
|
||||
lastLoginCode: null,
|
||||
loginVerificationRequestedAt: null,
|
||||
oauthFlowDeadlineAt: null,
|
||||
localhostUrl: null,
|
||||
};
|
||||
}
|
||||
@@ -4569,7 +4575,10 @@ async function handleStepData(step, payload) {
|
||||
if (!isLocalhostOAuthCallbackUrl(payload.localhostUrl)) {
|
||||
throw new Error('步骤 9 返回了无效的 localhost OAuth 回调地址。');
|
||||
}
|
||||
await setState({ localhostUrl: payload.localhostUrl });
|
||||
await setState({
|
||||
localhostUrl: payload.localhostUrl,
|
||||
oauthFlowDeadlineAt: null,
|
||||
});
|
||||
broadcastDataUpdate({ localhostUrl: payload.localhostUrl });
|
||||
}
|
||||
break;
|
||||
@@ -5689,6 +5698,7 @@ const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({
|
||||
completeStepFromBackground,
|
||||
getErrorMessage,
|
||||
getLoginAuthStateLabel,
|
||||
getOAuthFlowStepTimeoutMs,
|
||||
getState,
|
||||
isStep6RecoverableResult,
|
||||
isStep6SuccessResult,
|
||||
@@ -5697,6 +5707,7 @@ const step7Executor = self.MultiPageBackgroundStep7?.createStep7Executor({
|
||||
sendToContentScriptResilient,
|
||||
shouldSkipLoginVerificationForCpaCallback,
|
||||
skipLoginVerificationStepsForCpaCallback,
|
||||
startOAuthFlowTimeoutWindow,
|
||||
STEP6_MAX_ATTEMPTS,
|
||||
throwIfStopped,
|
||||
});
|
||||
@@ -5707,6 +5718,8 @@ const step8Executor = self.MultiPageBackgroundStep8?.createStep8Executor({
|
||||
confirmCustomVerificationStepBypass: verificationFlowHelpers.confirmCustomVerificationStepBypass,
|
||||
ensureStep8VerificationPageReady,
|
||||
executeStep7: (...args) => executeStep7(...args),
|
||||
getOAuthFlowRemainingMs,
|
||||
getOAuthFlowStepTimeoutMs,
|
||||
getPanelMode,
|
||||
getMailConfig,
|
||||
getState,
|
||||
@@ -6180,6 +6193,64 @@ async function refreshOAuthUrlBeforeStep6(state) {
|
||||
return refreshResult.oauthUrl;
|
||||
}
|
||||
|
||||
function buildOAuthFlowTimeoutError(step, actionLabel = '后续授权流程') {
|
||||
return new Error(
|
||||
`步骤 ${step}:从拿到 OAuth 登录地址开始,${Math.round(OAUTH_FLOW_TIMEOUT_MS / 60000)} 分钟内未完成${actionLabel},结束当前链路,准备从步骤 7 重新开始。`
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeOAuthFlowDeadlineAt(value) {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric) || numeric <= 0) {
|
||||
return null;
|
||||
}
|
||||
return Math.floor(numeric);
|
||||
}
|
||||
|
||||
async function startOAuthFlowTimeoutWindow(options = {}) {
|
||||
const step = Number(options.step) || 7;
|
||||
const deadlineAt = Date.now() + OAUTH_FLOW_TIMEOUT_MS;
|
||||
await setState({ oauthFlowDeadlineAt: deadlineAt });
|
||||
await addLog(`步骤 ${step}:已拿到新的 OAuth 登录地址,开始 6 分钟倒计时。`, 'info');
|
||||
return deadlineAt;
|
||||
}
|
||||
|
||||
async function getOAuthFlowRemainingMs(options = {}) {
|
||||
const step = Number(options.step) || 7;
|
||||
const actionLabel = String(options.actionLabel || '后续授权流程').trim() || '后续授权流程';
|
||||
const state = options.state || await getState();
|
||||
const deadlineAt = normalizeOAuthFlowDeadlineAt(state?.oauthFlowDeadlineAt);
|
||||
if (!deadlineAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const remainingMs = deadlineAt - Date.now();
|
||||
if (remainingMs <= 0) {
|
||||
throw buildOAuthFlowTimeoutError(step, actionLabel);
|
||||
}
|
||||
|
||||
return remainingMs;
|
||||
}
|
||||
|
||||
async function getOAuthFlowStepTimeoutMs(defaultTimeoutMs, options = {}) {
|
||||
const normalizedDefault = Math.max(1000, Number(defaultTimeoutMs) || 1000);
|
||||
const reserveMs = Math.max(0, Number(options.reserveMs) || 0);
|
||||
const remainingMs = await getOAuthFlowRemainingMs(options);
|
||||
if (remainingMs === null) {
|
||||
return normalizedDefault;
|
||||
}
|
||||
|
||||
const budgetMs = remainingMs - reserveMs;
|
||||
if (budgetMs <= 0) {
|
||||
throw buildOAuthFlowTimeoutError(
|
||||
Number(options.step) || 7,
|
||||
String(options.actionLabel || '后续授权流程').trim() || '后续授权流程'
|
||||
);
|
||||
}
|
||||
|
||||
return Math.max(1000, Math.min(normalizedDefault, budgetMs));
|
||||
}
|
||||
|
||||
function isStep6SuccessResult(result) {
|
||||
return result?.step6Outcome === 'success';
|
||||
}
|
||||
@@ -6259,8 +6330,9 @@ async function getLoginAuthStateFromContent(options = {}) {
|
||||
payload: {},
|
||||
},
|
||||
{
|
||||
timeoutMs: 15000,
|
||||
retryDelayMs: 600,
|
||||
timeoutMs: options.timeoutMs ?? 15000,
|
||||
retryDelayMs: options.retryDelayMs ?? 600,
|
||||
responseTimeoutMs: options.responseTimeoutMs ?? (options.timeoutMs ?? 15000),
|
||||
logMessage,
|
||||
}
|
||||
);
|
||||
@@ -6272,8 +6344,8 @@ async function getLoginAuthStateFromContent(options = {}) {
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function ensureStep8VerificationPageReady() {
|
||||
const pageState = await getLoginAuthStateFromContent();
|
||||
async function ensureStep8VerificationPageReady(options = {}) {
|
||||
const pageState = await getLoginAuthStateFromContent(options);
|
||||
if (pageState.state === 'verification_page') {
|
||||
return pageState;
|
||||
}
|
||||
@@ -6287,6 +6359,7 @@ async function skipLoginVerificationStepsForCpaCallback() {
|
||||
await setState({
|
||||
lastLoginCode: null,
|
||||
loginVerificationRequestedAt: null,
|
||||
oauthFlowDeadlineAt: null,
|
||||
});
|
||||
await setStepStatus(7, 'skipped');
|
||||
await addLog('步骤 7:当前已选择“第七步回调”,直接跳过步骤 7、8。', 'warn');
|
||||
@@ -6438,7 +6511,7 @@ async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS)
|
||||
flow: 'auth',
|
||||
logLabel: '步骤 9:检测到认证页重试页,正在点击“重试”恢复',
|
||||
step: 8,
|
||||
timeoutMs: Math.max(5000, Math.min(12000, timeoutMs)),
|
||||
timeoutMs: Math.max(1000, Math.min(12000, timeoutMs)),
|
||||
});
|
||||
retryRecovered = true;
|
||||
await sleepWithStop(250);
|
||||
@@ -6465,9 +6538,11 @@ async function waitForStep8Ready(tabId, timeoutMs = STEP8_READY_WAIT_TIMEOUT_MS)
|
||||
throw new Error('步骤 9:长时间未进入 OAuth 同意页,无法定位“继续”按钮。');
|
||||
}
|
||||
|
||||
async function prepareStep8DebuggerClick(tabId) {
|
||||
async function prepareStep8DebuggerClick(tabId, options = {}) {
|
||||
const timeoutMs = options.timeoutMs ?? 15000;
|
||||
const responseTimeoutMs = options.responseTimeoutMs ?? timeoutMs;
|
||||
await ensureStep8SignupPageReady(tabId, {
|
||||
timeoutMs: 15000,
|
||||
timeoutMs,
|
||||
logMessage: '步骤 9:认证页内容脚本已失联,正在恢复后继续定位按钮...',
|
||||
});
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
@@ -6475,7 +6550,8 @@ async function prepareStep8DebuggerClick(tabId) {
|
||||
source: 'background',
|
||||
payload: {},
|
||||
}, {
|
||||
timeoutMs: 15000,
|
||||
timeoutMs,
|
||||
responseTimeoutMs,
|
||||
retryDelayMs: 600,
|
||||
logMessage: '步骤 9:认证页正在切换,等待 OAuth 同意页按钮重新就绪...',
|
||||
});
|
||||
@@ -6487,9 +6563,11 @@ async function prepareStep8DebuggerClick(tabId) {
|
||||
return result;
|
||||
}
|
||||
|
||||
async function triggerStep8ContentStrategy(tabId, strategy) {
|
||||
async function triggerStep8ContentStrategy(tabId, strategy, options = {}) {
|
||||
const timeoutMs = options.timeoutMs ?? 15000;
|
||||
const responseTimeoutMs = options.responseTimeoutMs ?? timeoutMs;
|
||||
await ensureStep8SignupPageReady(tabId, {
|
||||
timeoutMs: 15000,
|
||||
timeoutMs,
|
||||
logMessage: '步骤 9:认证页内容脚本已失联,正在恢复后继续点击“继续”按钮...',
|
||||
});
|
||||
const result = await sendToContentScriptResilient('signup-page', {
|
||||
@@ -6501,7 +6579,8 @@ async function triggerStep8ContentStrategy(tabId, strategy) {
|
||||
enabledTimeoutMs: 3000,
|
||||
},
|
||||
}, {
|
||||
timeoutMs: 15000,
|
||||
timeoutMs,
|
||||
responseTimeoutMs,
|
||||
retryDelayMs: 600,
|
||||
logMessage: '步骤 9:认证页正在切换,等待“继续”按钮重新就绪...',
|
||||
});
|
||||
@@ -6514,8 +6593,11 @@ async function triggerStep8ContentStrategy(tabId, strategy) {
|
||||
}
|
||||
|
||||
async function recoverAuthRetryPageOnTab(tabId, payload = {}, options = {}) {
|
||||
const readyTimeoutMs = options.readyTimeoutMs ?? 15000;
|
||||
const timeoutMs = options.timeoutMs ?? 15000;
|
||||
const responseTimeoutMs = options.responseTimeoutMs ?? timeoutMs;
|
||||
await ensureStep8SignupPageReady(tabId, {
|
||||
timeoutMs: options.readyTimeoutMs ?? 15000,
|
||||
timeoutMs: readyTimeoutMs,
|
||||
retryDelayMs: options.retryDelayMs ?? 600,
|
||||
logMessage: options.readyLogMessage || '步骤 9:认证页内容脚本已失联,正在恢复后继续处理重试页...',
|
||||
});
|
||||
@@ -6524,7 +6606,8 @@ async function recoverAuthRetryPageOnTab(tabId, payload = {}, options = {}) {
|
||||
source: 'background',
|
||||
payload,
|
||||
}, {
|
||||
timeoutMs: options.timeoutMs ?? 15000,
|
||||
timeoutMs,
|
||||
responseTimeoutMs,
|
||||
retryDelayMs: options.retryDelayMs ?? 600,
|
||||
logMessage: options.logMessage || '步骤 9:认证页正在切换,等待“重试”按钮重新就绪...',
|
||||
});
|
||||
@@ -6603,7 +6686,7 @@ async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLI
|
||||
flow: 'auth',
|
||||
logLabel: '步骤 9:点击“继续”后进入重试页,正在点击“重试”恢复',
|
||||
step: 8,
|
||||
timeoutMs: Math.max(5000, Math.min(12000, timeoutMs)),
|
||||
timeoutMs: Math.max(1000, Math.min(12000, timeoutMs)),
|
||||
});
|
||||
return {
|
||||
progressed: false,
|
||||
@@ -6616,7 +6699,7 @@ async function waitForStep8ClickEffect(tabId, baselineUrl, timeoutMs = STEP8_CLI
|
||||
if (!recovered) {
|
||||
recovered = true;
|
||||
await ensureStep8SignupPageReady(tabId, {
|
||||
timeoutMs: Math.max(3000, Math.min(8000, timeoutMs)),
|
||||
timeoutMs: Math.max(1000, Math.min(8000, timeoutMs)),
|
||||
logMessage: '步骤 9:点击后认证页正在重载,正在等待内容脚本重新就绪...',
|
||||
}).catch(() => null);
|
||||
continue;
|
||||
@@ -6662,6 +6745,7 @@ const step9Executor = self.MultiPageBackgroundStep9?.createStep9Executor({
|
||||
clickWithDebugger,
|
||||
completeStepFromBackground,
|
||||
ensureStep8SignupPageReady,
|
||||
getOAuthFlowStepTimeoutMs,
|
||||
getStep8CallbackUrlFromNavigation,
|
||||
getStep8CallbackUrlFromTabUpdate,
|
||||
getStep8EffectLabel,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:认证页正在切换,等待页面重新就绪后继续登录...',
|
||||
}
|
||||
|
||||
@@ -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
@@ -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');
|
||||
|
||||
@@ -76,3 +76,56 @@ test('step 7 retries up to configured limit and then fails', async () => {
|
||||
assert.equal(events.sendCalls, 3);
|
||||
assert.equal(events.completed, 0);
|
||||
});
|
||||
|
||||
test('step 7 starts a new oauth timeout window for each refreshed oauth url', async () => {
|
||||
const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8');
|
||||
const globalScope = {};
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope);
|
||||
|
||||
const events = {
|
||||
startedWindows: [],
|
||||
timeoutRequests: [],
|
||||
};
|
||||
|
||||
const executor = api.createStep7Executor({
|
||||
addLog: async () => {},
|
||||
completeStepFromBackground: async () => {},
|
||||
getErrorMessage: (error) => error?.message || String(error || ''),
|
||||
getLoginAuthStateLabel: (state) => state || 'unknown',
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs, options) => {
|
||||
events.timeoutRequests.push({ defaultTimeoutMs, options });
|
||||
return 5000;
|
||||
},
|
||||
getState: async () => ({ email: 'user@example.com', password: 'secret' }),
|
||||
isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable',
|
||||
isStep6SuccessResult: (result) => result?.step6Outcome === 'success',
|
||||
refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest',
|
||||
reuseOrCreateTab: async () => {},
|
||||
sendToContentScriptResilient: async (_source, _message, options) => ({
|
||||
step6Outcome: 'success',
|
||||
usedTimeoutMs: options.timeoutMs,
|
||||
}),
|
||||
shouldSkipLoginVerificationForCpaCallback: () => false,
|
||||
skipLoginVerificationStepsForCpaCallback: async () => {},
|
||||
startOAuthFlowTimeoutWindow: async (payload) => {
|
||||
events.startedWindows.push(payload);
|
||||
},
|
||||
STEP6_MAX_ATTEMPTS: 3,
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep7({ email: 'user@example.com', password: 'secret' });
|
||||
|
||||
assert.deepStrictEqual(events.startedWindows, [
|
||||
{ step: 7, oauthUrl: 'https://oauth.example/latest' },
|
||||
]);
|
||||
assert.deepStrictEqual(events.timeoutRequests, [
|
||||
{
|
||||
defaultTimeoutMs: 180000,
|
||||
options: {
|
||||
step: 7,
|
||||
actionLabel: 'OAuth 登录并进入验证码页',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -9,10 +9,13 @@ const api = new Function('self', `${source}; return self.MultiPageBackgroundStep
|
||||
test('step 8 refreshes CPA oauth via step 7 replay before submitting verification code', async () => {
|
||||
const calls = {
|
||||
ensureReady: 0,
|
||||
ensureReadyOptions: [],
|
||||
executeStep7: 0,
|
||||
sleep: [],
|
||||
resolveOptions: null,
|
||||
};
|
||||
const realDateNow = Date.now;
|
||||
Date.now = () => 123456;
|
||||
|
||||
const executor = api.createStep8Executor({
|
||||
addLog: async () => {},
|
||||
@@ -23,13 +26,16 @@ test('step 8 refreshes CPA oauth via step 7 replay before submitting verificatio
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureStep8VerificationPageReady: async () => {
|
||||
ensureStep8VerificationPageReady: async (options) => {
|
||||
calls.ensureReady += 1;
|
||||
calls.ensureReadyOptions.push(options || null);
|
||||
return { state: 'verification_page' };
|
||||
},
|
||||
executeStep7: async () => {
|
||||
calls.executeStep7 += 1;
|
||||
},
|
||||
getOAuthFlowRemainingMs: async () => 5000,
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 5000),
|
||||
getMailConfig: () => ({
|
||||
provider: 'qq',
|
||||
label: 'QQ 邮箱',
|
||||
@@ -61,17 +67,28 @@ test('step 8 refreshes CPA oauth via step 7 replay before submitting verificatio
|
||||
throwIfStopped: () => {},
|
||||
});
|
||||
|
||||
await executor.executeStep8({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
oauthUrl: 'https://oauth.example/latest',
|
||||
});
|
||||
try {
|
||||
await executor.executeStep8({
|
||||
email: 'user@example.com',
|
||||
password: 'secret',
|
||||
oauthUrl: 'https://oauth.example/latest',
|
||||
});
|
||||
} finally {
|
||||
Date.now = realDateNow;
|
||||
}
|
||||
|
||||
assert.equal(typeof calls.resolveOptions.beforeSubmit, 'function');
|
||||
assert.equal(calls.ensureReady, 2);
|
||||
assert.equal(calls.executeStep7, 1);
|
||||
assert.deepStrictEqual(calls.sleep, [1200]);
|
||||
assert.equal(calls.resolveOptions.filterAfterTimestamp, 123456);
|
||||
assert.equal(typeof calls.resolveOptions.getRemainingTimeMs, 'function');
|
||||
assert.equal(await calls.resolveOptions.getRemainingTimeMs({ actionLabel: '登录验证码流程' }), 5000);
|
||||
assert.equal(calls.resolveOptions.resendIntervalMs, 25000);
|
||||
assert.deepStrictEqual(calls.ensureReadyOptions, [
|
||||
{ timeoutMs: 5000 },
|
||||
{ timeoutMs: 5000 },
|
||||
]);
|
||||
});
|
||||
|
||||
test('step 8 disables resend interval for 2925 mailbox polling', async () => {
|
||||
@@ -88,6 +105,8 @@ test('step 8 disables resend interval for 2925 mailbox polling', async () => {
|
||||
confirmCustomVerificationStepBypass: async () => {},
|
||||
ensureStep8VerificationPageReady: async () => ({ state: 'verification_page' }),
|
||||
executeStep7: async () => {},
|
||||
getOAuthFlowRemainingMs: async () => 8000,
|
||||
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 8000),
|
||||
getMailConfig: () => ({
|
||||
provider: '2925',
|
||||
label: '2925 邮箱',
|
||||
@@ -124,4 +143,5 @@ test('step 8 disables resend interval for 2925 mailbox polling', async () => {
|
||||
|
||||
assert.equal(capturedOptions.resendIntervalMs, 0);
|
||||
assert.equal(capturedOptions.beforeSubmit, undefined);
|
||||
assert.equal(typeof capturedOptions.getRemainingTimeMs, 'function');
|
||||
});
|
||||
|
||||
@@ -109,6 +109,70 @@ test('verification flow runs beforeSubmit hook before filling the code', async (
|
||||
]);
|
||||
});
|
||||
|
||||
test('verification flow caps mail polling timeout to the remaining oauth budget', async () => {
|
||||
const mailPollCalls = [];
|
||||
|
||||
const helpers = api.createVerificationFlowHelpers({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
update: async () => {},
|
||||
},
|
||||
},
|
||||
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
|
||||
completeStepFromBackground: async () => {},
|
||||
confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
|
||||
getHotmailVerificationPollConfig: () => ({}),
|
||||
getHotmailVerificationRequestTimestamp: () => 0,
|
||||
getState: async () => ({}),
|
||||
getTabId: async () => 1,
|
||||
HOTMAIL_PROVIDER: 'hotmail-api',
|
||||
isStopError: () => false,
|
||||
LUCKMAIL_PROVIDER: 'luckmail-api',
|
||||
MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
|
||||
MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
|
||||
pollCloudflareTempEmailVerificationCode: async () => ({}),
|
||||
pollHotmailVerificationCode: async () => ({}),
|
||||
pollLuckmailVerificationCode: async () => ({}),
|
||||
sendToContentScript: async (_source, message) => {
|
||||
if (message.type === 'FILL_CODE') {
|
||||
return {};
|
||||
}
|
||||
return {};
|
||||
},
|
||||
sendToMailContentScriptResilient: async (_mail, message, options) => {
|
||||
mailPollCalls.push({
|
||||
payload: message.payload,
|
||||
options,
|
||||
});
|
||||
return { code: '654321', emailTimestamp: 123 };
|
||||
},
|
||||
setState: async () => {},
|
||||
setStepStatus: async () => {},
|
||||
sleepWithStop: async () => {},
|
||||
throwIfStopped: () => {},
|
||||
VERIFICATION_POLL_MAX_ROUNDS: 5,
|
||||
});
|
||||
|
||||
await helpers.resolveVerificationStep(
|
||||
8,
|
||||
{
|
||||
email: 'user@example.com',
|
||||
lastLoginCode: null,
|
||||
},
|
||||
{ provider: 'qq', label: 'QQ 邮箱' },
|
||||
{
|
||||
getRemainingTimeMs: async () => 5000,
|
||||
resendIntervalMs: 0,
|
||||
}
|
||||
);
|
||||
|
||||
assert.ok(mailPollCalls.length >= 1);
|
||||
assert.equal(mailPollCalls[0].options.timeoutMs, 5000);
|
||||
assert.equal(mailPollCalls[0].options.responseTimeoutMs, 5000);
|
||||
assert.equal(mailPollCalls[0].payload.maxAttempts, 2);
|
||||
});
|
||||
|
||||
test('verification flow keeps Hotmail request timestamp filtering on the first poll', async () => {
|
||||
const pollPayloads = [];
|
||||
|
||||
@@ -162,10 +226,8 @@ test('verification flow keeps Hotmail request timestamp filtering on the first p
|
||||
assert.equal(pollPayloads[0].filterAfterTimestamp, 87654);
|
||||
});
|
||||
|
||||
test('verification flow refreshes Hotmail signup filter timestamp after step 4 resend', async () => {
|
||||
test('verification flow keeps fixed filter timestamp after step 4 resend', async () => {
|
||||
const pollPayloads = [];
|
||||
const realDateNow = Date.now;
|
||||
Date.now = () => 200000;
|
||||
|
||||
let submitCount = 0;
|
||||
|
||||
@@ -213,24 +275,22 @@ test('verification flow refreshes Hotmail signup filter timestamp after step 4 r
|
||||
VERIFICATION_POLL_MAX_ROUNDS: 5,
|
||||
});
|
||||
|
||||
try {
|
||||
await helpers.resolveVerificationStep(
|
||||
4,
|
||||
{
|
||||
email: 'user@example.com',
|
||||
signupVerificationRequestedAt: 100000,
|
||||
lastSignupCode: null,
|
||||
},
|
||||
{ provider: 'hotmail-api', label: 'Hotmail' },
|
||||
{}
|
||||
);
|
||||
} finally {
|
||||
Date.now = realDateNow;
|
||||
}
|
||||
await helpers.resolveVerificationStep(
|
||||
4,
|
||||
{
|
||||
email: 'user@example.com',
|
||||
signupVerificationRequestedAt: 100000,
|
||||
lastSignupCode: null,
|
||||
},
|
||||
{ provider: 'hotmail-api', label: 'Hotmail' },
|
||||
{
|
||||
filterAfterTimestamp: 123456,
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(pollPayloads.length, 2);
|
||||
assert.equal(pollPayloads[0].filterAfterTimestamp, 85000);
|
||||
assert.equal(pollPayloads[1].filterAfterTimestamp, 185000);
|
||||
assert.equal(pollPayloads[0].filterAfterTimestamp, 123456);
|
||||
assert.equal(pollPayloads[1].filterAfterTimestamp, 123456);
|
||||
});
|
||||
|
||||
test('verification flow uses configured signup resend count for step 4', async () => {
|
||||
|
||||
+1
-1
@@ -246,7 +246,7 @@
|
||||
补充行为:
|
||||
|
||||
- `2925` provider 会拉长单轮轮询窗口,并关闭 Step 4 / 8 的自动重发间隔,减少因邮件延迟过早判负。
|
||||
- Hotmail 在 Step 4 / 8 首轮轮询时会优先使用最近一次验证码请求时间作为时间窗口;如果中途触发重发,轮询窗口也会同步前移,避免旧邮件反复命中。
|
||||
- Step 4 / 8 会在当前验证码步骤启动时固定一次邮件筛选时间窗;本步后续即使触发重新发送验证码,也继续沿用这一次启动时间作为筛选基准,不再随着重发时间前移。
|
||||
- 自动重新发送验证码次数现在使用 sidepanel 里的单一“验证码重发”配置;普通邮箱仍按 25 秒间隔节流,Hotmail / 2925 不走这个 25 秒间隔。Step 4 若启用先请求新验证码,会先消耗一次当前步骤的自动重发次数。
|
||||
- CPA 模式下,Step 8 在真正回填登录验证码前,会先刷新最新 OAuth 地址并快速重走一次 Step 7,降低验证码等待过久导致 OAuth 过期的概率。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user