feat:重试页面检测和重试点击的逻辑抽取为公共逻辑

This commit is contained in:
QLHazyCoder
2026-04-17 15:16:52 +08:00
parent 1a8cbc143b
commit b387ebfa91
15 changed files with 865 additions and 35 deletions
+141
View File
@@ -0,0 +1,141 @@
(function authPageRecoveryModule(root, factory) {
if (typeof module !== 'undefined' && module.exports) {
module.exports = factory();
return;
}
root.MultiPageAuthPageRecovery = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createAuthPageRecoveryModule() {
function createAuthPageRecovery(deps = {}) {
const {
detailPattern = null,
getActionText,
getPageTextSnapshot,
humanPause,
isActionEnabled,
isVisibleElement,
log,
simulateClick,
sleep,
throwIfStopped,
titlePattern = null,
} = deps;
function matchesPathPatterns(pathname, pathPatterns = []) {
if (!Array.isArray(pathPatterns) || !pathPatterns.length) {
return true;
}
return pathPatterns.some((pattern) => pattern instanceof RegExp && pattern.test(pathname));
}
function getAuthRetryButton(options = {}) {
const { allowDisabled = false } = options;
const direct = document.querySelector('button[data-dd-action-name="Try again"]');
if (direct && isVisibleElement(direct) && (allowDisabled || isActionEnabled(direct))) {
return direct;
}
const candidates = document.querySelectorAll('button, [role="button"]');
return Array.from(candidates).find((element) => {
if (!isVisibleElement(element) || (!allowDisabled && !isActionEnabled(element))) {
return false;
}
const text = typeof getActionText === 'function' ? getActionText(element) : '';
return /重试|try\s+again/i.test(text);
}) || null;
}
function getAuthTimeoutErrorPageState(options = {}) {
const { pathPatterns = [] } = options;
const pathname = location.pathname || '';
if (!matchesPathPatterns(pathname, pathPatterns)) {
return null;
}
const retryButton = getAuthRetryButton({ allowDisabled: true });
if (!retryButton) {
return null;
}
const text = typeof getPageTextSnapshot === 'function' ? getPageTextSnapshot() : '';
const title = typeof document !== 'undefined' ? String(document.title || '') : '';
const titleMatched = titlePattern instanceof RegExp
? titlePattern.test(text) || titlePattern.test(title)
: false;
const detailMatched = detailPattern instanceof RegExp
? detailPattern.test(text)
: false;
if (!titleMatched && !detailMatched) {
return null;
}
return {
path: pathname,
url: location.href,
retryButton,
retryEnabled: isActionEnabled(retryButton),
titleMatched,
detailMatched,
};
}
async function recoverAuthRetryPage(options = {}) {
const {
logLabel = '',
pathPatterns = [],
pollIntervalMs = 250,
step = null,
timeoutMs = 12000,
waitAfterClickMs = 1200,
} = options;
const start = Date.now();
let clickCount = 0;
while (Date.now() - start < timeoutMs) {
if (typeof throwIfStopped === 'function') {
throwIfStopped();
}
const retryState = getAuthTimeoutErrorPageState({ pathPatterns });
if (!retryState) {
return {
recovered: clickCount > 0,
clickCount,
url: location.href,
};
}
if (retryState.retryButton && retryState.retryEnabled) {
clickCount += 1;
if (typeof log === 'function') {
const prefix = logLabel || `步骤 ${step || '?'}:检测到重试页,正在点击“重试”恢复`;
log(`${prefix}(第 ${clickCount} 次)...`, 'warn');
}
if (typeof humanPause === 'function') {
await humanPause(300, 800);
}
simulateClick(retryState.retryButton);
await sleep(waitAfterClickMs);
continue;
}
await sleep(pollIntervalMs);
}
throw new Error(
`${logLabel || `步骤 ${step || '?'}:重试页恢复`}超时。URL: ${location.href}`
);
}
return {
getAuthRetryButton,
getAuthTimeoutErrorPageState,
recoverAuthRetryPage,
};
}
return {
createAuthPageRecovery,
};
});
+158 -30
View File
@@ -19,6 +19,7 @@ if (document.documentElement.getAttribute(SIGNUP_PAGE_LISTENER_SENTINEL) !== '1'
|| message.type === 'STEP8_TRIGGER_CONTINUE'
|| message.type === 'GET_LOGIN_AUTH_STATE'
|| message.type === 'PREPARE_SIGNUP_VERIFICATION'
|| message.type === 'RECOVER_AUTH_RETRY_PAGE'
|| message.type === 'RESEND_VERIFICATION_CODE'
|| message.type === 'ENSURE_SIGNUP_ENTRY_READY'
|| message.type === 'ENSURE_SIGNUP_PASSWORD_PAGE_READY'
@@ -71,6 +72,8 @@ async function handleCommand(message) {
return serializeLoginAuthState(inspectLoginAuthState());
case 'PREPARE_SIGNUP_VERIFICATION':
return await prepareSignupVerificationFlow(message.payload);
case 'RECOVER_AUTH_RETRY_PAGE':
return await recoverCurrentAuthRetryPage(message.payload);
case 'RESEND_VERIFICATION_CODE':
return await resendVerificationCode(message.step);
case 'ENSURE_SIGNUP_ENTRY_READY':
@@ -626,6 +629,20 @@ const AUTH_TIMEOUT_ERROR_TITLE_PATTERN = /糟糕,出错了|something\s+went\s+
const AUTH_TIMEOUT_ERROR_DETAIL_PATTERN = /operation\s+timed\s+out|timed\s+out|请求超时|操作超时/i;
const SIGNUP_EMAIL_EXISTS_PATTERN = /与此电子邮件地址相关联的帐户已存在|account\s+associated\s+with\s+this\s+email\s+address\s+already\s+exists|email\s+address.*already\s+exists/i;
const authPageRecovery = self.MultiPageAuthPageRecovery?.createAuthPageRecovery?.({
detailPattern: AUTH_TIMEOUT_ERROR_DETAIL_PATTERN,
getActionText,
getPageTextSnapshot,
humanPause,
isActionEnabled,
isVisibleElement,
log,
simulateClick,
sleep,
throwIfStopped,
titlePattern: AUTH_TIMEOUT_ERROR_TITLE_PATTERN,
}) || null;
function getVerificationErrorText() {
const messages = [];
const selectors = [
@@ -863,6 +880,10 @@ function getSignupPasswordSubmitButton({ allowDisabled = false } = {}) {
}
function getAuthRetryButton({ allowDisabled = false } = {}) {
if (authPageRecovery?.getAuthRetryButton) {
return authPageRecovery.getAuthRetryButton({ allowDisabled });
}
const direct = document.querySelector('button[data-dd-action-name="Try again"]');
if (direct && isVisibleElement(direct) && (allowDisabled || isActionEnabled(direct))) {
return direct;
@@ -877,6 +898,10 @@ function getAuthRetryButton({ allowDisabled = false } = {}) {
}
function getAuthTimeoutErrorPageState(options = {}) {
if (authPageRecovery?.getAuthTimeoutErrorPageState) {
return authPageRecovery.getAuthTimeoutErrorPageState(options);
}
const { pathPatterns = [] } = options;
const path = location.pathname || '';
if (pathPatterns.length && !pathPatterns.some((pattern) => pattern.test(path))) {
@@ -907,6 +932,70 @@ function getAuthTimeoutErrorPageState(options = {}) {
};
}
function getAuthRetryPathPatternsForFlow(flow = 'auth') {
switch (flow) {
case 'signup_password':
return [/\/create-account\/password(?:[/?#]|$)/i];
case 'login':
return [/\/log-in(?:[/?#]|$)/i];
default:
return [];
}
}
function getCurrentAuthRetryPageState(flow = 'auth') {
return getAuthTimeoutErrorPageState({
pathPatterns: getAuthRetryPathPatternsForFlow(flow),
});
}
async function recoverCurrentAuthRetryPage(payload = {}) {
const {
flow = 'auth',
logLabel = '',
step = null,
timeoutMs = 12000,
waitAfterClickMs = 1200,
} = payload;
const pathPatterns = getAuthRetryPathPatternsForFlow(flow);
if (authPageRecovery?.recoverAuthRetryPage) {
return authPageRecovery.recoverAuthRetryPage({
logLabel,
pathPatterns,
step,
timeoutMs,
waitAfterClickMs,
});
}
const start = Date.now();
let clickCount = 0;
while (Date.now() - start < timeoutMs) {
throwIfStopped();
const retryState = getCurrentAuthRetryPageState(flow);
if (!retryState) {
return {
recovered: clickCount > 0,
clickCount,
url: location.href,
};
}
if (retryState.retryButton && retryState.retryEnabled) {
clickCount += 1;
log(`${logLabel || `步骤 ${step || '?'}:检测到重试页,正在点击“重试”恢复`}(第 ${clickCount} 次)...`, 'warn');
await humanPause(300, 800);
simulateClick(retryState.retryButton);
await sleep(waitAfterClickMs);
continue;
}
await sleep(250);
}
throw new Error(`${logLabel || `步骤 ${step || '?'}:重试页恢复`}超时。URL: ${location.href}`);
}
function getSignupPasswordTimeoutErrorPageState() {
return getAuthTimeoutErrorPageState({
pathPatterns: [/\/create-account\/password(?:[/?#]|$)/i],
@@ -1121,6 +1210,29 @@ function createStep6RecoverableResult(reason, snapshot, options = {}) {
};
}
async function createStep6LoginTimeoutRecoverableResult(reason, snapshot, message) {
const resolvedSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState());
if (resolvedSnapshot?.state === 'login_timeout_error_page') {
try {
const recoveryResult = await recoverCurrentAuthRetryPage({
flow: 'login',
logLabel: '步骤 6:检测到登录超时报错,正在点击“重试”恢复当前页面',
step: 6,
timeoutMs: 12000,
});
if (recoveryResult?.recovered) {
log('步骤 6:登录超时报错页已点击“重试”,准备重新执行当前步骤。', 'warn');
}
} catch (error) {
log(`步骤 6:登录超时报错页自动点击“重试”失败:${error.message}`, 'warn');
}
}
return createStep6RecoverableResult(reason, resolvedSnapshot, {
message,
});
}
function normalizeStep6Snapshot(snapshot) {
if (snapshot?.state !== 'oauth_consent_page') {
return snapshot;
@@ -1260,15 +1372,12 @@ async function prepareSignupVerificationFlow(payload = {}, timeout = 30000) {
recoveryRound += 1;
if (snapshot.state === 'error') {
if (snapshot.retryButton && isActionEnabled(snapshot.retryButton)) {
log(`步骤 4:检测到密码页超时报错,正在点击“重试”(第 ${recoveryRound}/${maxRecoveryRounds} 次)...`, 'warn');
await humanPause(350, 900);
simulateClick(snapshot.retryButton);
await sleep(1200);
continue;
}
log(`步骤 4:检测到异常页,但“重试”按钮暂不可用,准备继续等待(${recoveryRound}/${maxRecoveryRounds}...`, 'warn');
await recoverCurrentAuthRetryPage({
flow: 'signup_password',
logLabel: `步骤 4:检测到密码页超时报错,正在点击“重试”恢复(第 ${recoveryRound}/${maxRecoveryRounds} 次)`,
step: 4,
timeoutMs: 12000,
});
continue;
}
@@ -1459,9 +1568,11 @@ async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 120
if (snapshot.state === 'login_timeout_error_page') {
return {
action: 'recoverable',
result: createStep6RecoverableResult('login_timeout_error_page', snapshot, {
message: '提交邮箱后进入登录超时报错页。',
}),
result: await createStep6LoginTimeoutRecoverableResult(
'login_timeout_error_page',
snapshot,
'提交邮箱后进入登录超时报错页。'
),
};
}
@@ -1492,9 +1603,11 @@ async function waitForStep6EmailSubmitTransition(emailSubmittedAt, timeout = 120
if (snapshot.state === 'login_timeout_error_page') {
return {
action: 'recoverable',
result: createStep6RecoverableResult('login_timeout_error_page', snapshot, {
message: '提交邮箱后进入登录超时报错页。',
}),
result: await createStep6LoginTimeoutRecoverableResult(
'login_timeout_error_page',
snapshot,
'提交邮箱后进入登录超时报错页。'
),
};
}
if (snapshot.state === 'oauth_consent_page') {
@@ -1533,9 +1646,11 @@ async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout
if (snapshot.state === 'login_timeout_error_page') {
return {
action: 'recoverable',
result: createStep6RecoverableResult('login_timeout_error_page', snapshot, {
message: '提交密码后进入登录超时报错页。',
}),
result: await createStep6LoginTimeoutRecoverableResult(
'login_timeout_error_page',
snapshot,
'提交密码后进入登录超时报错页。'
),
};
}
@@ -1563,9 +1678,11 @@ async function waitForStep6PasswordSubmitTransition(passwordSubmittedAt, timeout
if (snapshot.state === 'login_timeout_error_page') {
return {
action: 'recoverable',
result: createStep6RecoverableResult('login_timeout_error_page', snapshot, {
message: '提交密码后进入登录超时报错页。',
}),
result: await createStep6LoginTimeoutRecoverableResult(
'login_timeout_error_page',
snapshot,
'提交密码后进入登录超时报错页。'
),
};
}
if (snapshot.state === 'oauth_consent_page') {
@@ -1602,9 +1719,11 @@ async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeou
}
if (snapshot.state === 'login_timeout_error_page') {
return createStep6RecoverableResult('login_timeout_error_page', snapshot, {
message: '切换到一次性验证码登录后进入登录超时报错页。',
});
return await createStep6LoginTimeoutRecoverableResult(
'login_timeout_error_page',
snapshot,
'切换到一次性验证码登录后进入登录超时报错页。'
);
}
if (snapshot.state === 'oauth_consent_page') {
@@ -1626,9 +1745,11 @@ async function waitForStep6SwitchTransition(loginVerificationRequestedAt, timeou
});
}
if (snapshot.state === 'login_timeout_error_page') {
return createStep6RecoverableResult('login_timeout_error_page', snapshot, {
message: '切换到一次性验证码登录后进入登录超时报错页。',
});
return await createStep6LoginTimeoutRecoverableResult(
'login_timeout_error_page',
snapshot,
'切换到一次性验证码登录后进入登录超时报错页。'
);
}
if (snapshot.state === 'oauth_consent_page') {
throw new Error(`切换到一次性验证码登录后页面直接进入 OAuth 授权页,未经过登录验证码页。URL: ${snapshot.url}`);
@@ -1757,9 +1878,11 @@ async function step6_login(payload) {
if (snapshot.state === 'login_timeout_error_page') {
log('步骤 6:检测到登录超时报错,准备重新执行步骤 6。', 'warn');
return createStep6RecoverableResult('login_timeout_error_page', snapshot, {
message: '当前页面处于登录超时报错页。',
});
return await createStep6LoginTimeoutRecoverableResult(
'login_timeout_error_page',
snapshot,
'当前页面处于登录超时报错页。'
);
}
if (snapshot.state === 'email_page') {
@@ -1797,12 +1920,17 @@ async function step8_findAndClick() {
function getStep8State() {
const continueBtn = getPrimaryContinueButton();
const retryState = getCurrentAuthRetryPageState('auth');
const state = {
url: location.href,
consentPage: isOAuthConsentPage(),
consentReady: isStep8Ready(),
verificationPage: isVerificationPageStillVisible(),
addPhonePage: isAddPhonePageReady(),
retryPage: Boolean(retryState),
retryEnabled: Boolean(retryState?.retryEnabled),
retryTitleMatched: Boolean(retryState?.titleMatched),
retryDetailMatched: Boolean(retryState?.detailMatched),
buttonFound: Boolean(continueBtn),
buttonEnabled: isButtonEnabled(continueBtn),
buttonText: continueBtn ? getActionText(continueBtn) : '',