feat(flow): unify sms multi-provider fallback and stabilize proxy/auth retries
This commit is contained in:
@@ -71,8 +71,9 @@
|
||||
: false;
|
||||
const maxCheckAttemptsBlocked = /max_check_attempts/i.test(text);
|
||||
const userAlreadyExistsBlocked = /user_already_exists/i.test(text);
|
||||
const fetchFailedMatched = /failed\s+to\s+fetch|network\s+error|fetch\s+failed/i.test(text);
|
||||
|
||||
if (!titleMatched && !detailMatched && !routeErrorMatched && !maxCheckAttemptsBlocked && !userAlreadyExistsBlocked) {
|
||||
if (!titleMatched && !detailMatched && !routeErrorMatched && !fetchFailedMatched && !maxCheckAttemptsBlocked && !userAlreadyExistsBlocked) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -84,6 +85,7 @@
|
||||
titleMatched,
|
||||
detailMatched,
|
||||
routeErrorMatched,
|
||||
fetchFailedMatched,
|
||||
maxCheckAttemptsBlocked,
|
||||
userAlreadyExistsBlocked,
|
||||
};
|
||||
|
||||
+78
-5
@@ -1,5 +1,6 @@
|
||||
const ICLOUD_MAIL_PREFIX = '[MultiPage:icloud-mail]';
|
||||
const isTopFrame = window === window.top;
|
||||
const ICLOUD_POLL_SESSION_CACHE = new Map();
|
||||
|
||||
console.log(ICLOUD_MAIL_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
|
||||
|
||||
@@ -12,7 +13,10 @@ function isMailApplicationFrame() {
|
||||
|
||||
if (isTopFrame) {
|
||||
console.log(ICLOUD_MAIL_PREFIX, 'Top frame detected; waiting for mail iframe.');
|
||||
} else {
|
||||
}
|
||||
|
||||
const shouldHandlePollEmailInCurrentFrame = !isTopFrame || isMailApplicationFrame();
|
||||
if (shouldHandlePollEmailInCurrentFrame) {
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === 'POLL_EMAIL') {
|
||||
if (!isMailApplicationFrame()) {
|
||||
@@ -221,19 +225,76 @@ if (isTopFrame) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePollSessionKey(payload = {}) {
|
||||
const raw = String(payload?.sessionKey || '').trim();
|
||||
if (raw) {
|
||||
return raw;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function getOrCreatePollSessionBaseline(sessionKey, currentItems = []) {
|
||||
if (!sessionKey) {
|
||||
return {
|
||||
signatures: new Set(currentItems.map(buildItemSignature)),
|
||||
fallbackCarry: 0,
|
||||
fromCache: false,
|
||||
};
|
||||
}
|
||||
|
||||
const cached = ICLOUD_POLL_SESSION_CACHE.get(sessionKey);
|
||||
if (cached && cached.signatures instanceof Set) {
|
||||
return {
|
||||
signatures: new Set(cached.signatures),
|
||||
fallbackCarry: Math.max(0, Number(cached.fallbackCarry) || 0),
|
||||
fromCache: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
signatures: new Set(currentItems.map(buildItemSignature)),
|
||||
fallbackCarry: 0,
|
||||
fromCache: false,
|
||||
};
|
||||
}
|
||||
|
||||
function persistPollSessionBaseline(sessionKey, signatures, fallbackCarry = 0) {
|
||||
if (!sessionKey) {
|
||||
return;
|
||||
}
|
||||
ICLOUD_POLL_SESSION_CACHE.set(sessionKey, {
|
||||
signatures: new Set(signatures || []),
|
||||
fallbackCarry: Math.max(0, Number(fallbackCarry) || 0),
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
if (ICLOUD_POLL_SESSION_CACHE.size > 12) {
|
||||
const oldest = Array.from(ICLOUD_POLL_SESSION_CACHE.entries())
|
||||
.sort((left, right) => Number(left?.[1]?.updatedAt || 0) - Number(right?.[1]?.updatedAt || 0))
|
||||
.slice(0, ICLOUD_POLL_SESSION_CACHE.size - 12);
|
||||
oldest.forEach(([key]) => ICLOUD_POLL_SESSION_CACHE.delete(key));
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePollEmail(step, payload) {
|
||||
const { senderFilters, subjectFilters, maxAttempts, intervalMs, excludeCodes = [] } = payload;
|
||||
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
|
||||
const FALLBACK_AFTER = 3;
|
||||
const pollSessionKey = normalizePollSessionKey(payload);
|
||||
const normalizedSenderFilters = senderFilters.map((filter) => String(filter || '').toLowerCase()).filter(Boolean);
|
||||
const normalizedSubjectFilters = subjectFilters.map((filter) => String(filter || '').toLowerCase()).filter(Boolean);
|
||||
|
||||
log(`步骤 ${step}:开始轮询 iCloud 邮箱(最多 ${maxAttempts} 次)`);
|
||||
await waitForElement('.content-container', 10000);
|
||||
await sleep(1500);
|
||||
|
||||
const existingSignatures = new Set(collectThreadItems().map(buildItemSignature));
|
||||
log(`步骤 ${step}:已记录当前 ${existingSignatures.size} 封旧邮件快照`);
|
||||
const currentItems = collectThreadItems();
|
||||
const sessionBaseline = getOrCreatePollSessionBaseline(pollSessionKey, currentItems);
|
||||
const existingSignatures = sessionBaseline.signatures;
|
||||
let fallbackCarry = sessionBaseline.fallbackCarry;
|
||||
if (sessionBaseline.fromCache) {
|
||||
log(`步骤 ${step}:已复用当前会话旧邮件快照(${existingSignatures.size} 封)。`);
|
||||
} else {
|
||||
log(`步骤 ${step}:已记录当前 ${existingSignatures.size} 封旧邮件快照`);
|
||||
}
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
log(`步骤 ${step}:正在轮询 iCloud 邮箱,第 ${attempt}/${maxAttempts} 次`);
|
||||
@@ -244,7 +305,7 @@ if (isTopFrame) {
|
||||
}
|
||||
|
||||
const items = collectThreadItems();
|
||||
const useFallback = attempt > FALLBACK_AFTER;
|
||||
const useFallback = (fallbackCarry + attempt) > FALLBACK_AFTER;
|
||||
|
||||
for (const item of items) {
|
||||
const signature = buildItemSignature(item);
|
||||
@@ -287,6 +348,11 @@ if (isTopFrame) {
|
||||
|
||||
const source = useFallback && existingSignatures.has(signature) ? '回退匹配邮件' : '新邮件';
|
||||
log(`步骤 ${step}:已找到验证码:${code}(来源:${source})`, 'ok');
|
||||
persistPollSessionBaseline(
|
||||
pollSessionKey,
|
||||
new Set(collectThreadItems().map(buildItemSignature)),
|
||||
0
|
||||
);
|
||||
return {
|
||||
ok: true,
|
||||
code,
|
||||
@@ -304,6 +370,13 @@ if (isTopFrame) {
|
||||
}
|
||||
}
|
||||
|
||||
fallbackCarry += maxAttempts;
|
||||
persistPollSessionBaseline(
|
||||
pollSessionKey,
|
||||
new Set(collectThreadItems().map(buildItemSignature)),
|
||||
fallbackCarry
|
||||
);
|
||||
|
||||
throw new Error(
|
||||
`${Math.round((maxAttempts * intervalMs) / 1000)} 秒后仍未在 iCloud 邮箱中找到新的匹配邮件。请手动检查收件箱。`
|
||||
);
|
||||
|
||||
+226
-48
@@ -19,7 +19,15 @@
|
||||
waitForElement,
|
||||
} = deps;
|
||||
const PHONE_RESEND_THROTTLED_ERROR_PREFIX = 'PHONE_RESEND_THROTTLED::';
|
||||
const PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX = 'PHONE_ROUTE_405_RECOVERY_FAILED::';
|
||||
const PHONE_ROUTE_405_RECOVERY_COOLDOWN_MS = 6000;
|
||||
const PHONE_RESEND_ROUTE_405_MAX_RECOVERIES = 2;
|
||||
const PHONE_RESEND_ROUTE_405_MAX_RECOVERY_TOTAL_MS = 12000;
|
||||
const PHONE_RESEND_THROTTLED_PATTERN = /tried\s+to\s+resend\s+too\s+many\s+times|please\s+try\s+again\s+later|too\s+many\s+resend|resend\s+too\s+many|发送.*过于频繁|稍后再试|重试次数过多/i;
|
||||
const PHONE_ROUTE_405_PATTERN = /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+["']?\/phone-verification/i;
|
||||
const PHONE_ROUTE_405_MAX_RECOVERY_CLICKS = 3;
|
||||
let lastPhoneRoute405RecoveryFailedAt = 0;
|
||||
let activePhoneResendPromise = null;
|
||||
|
||||
function dispatchInputEvents(element) {
|
||||
if (!element) return;
|
||||
@@ -35,6 +43,10 @@
|
||||
return digits;
|
||||
}
|
||||
|
||||
function isExplicitInternationalPhoneInput(value) {
|
||||
return /^\s*(?:\+|00)\s*\d/.test(String(value || '').trim());
|
||||
}
|
||||
|
||||
function normalizeCountryLabel(value) {
|
||||
return String(value || '')
|
||||
.normalize('NFKD')
|
||||
@@ -151,23 +163,31 @@
|
||||
function toNationalPhoneNumber(value, dialCode) {
|
||||
const digits = normalizePhoneDigits(value);
|
||||
const normalizedDialCode = normalizePhoneDigits(dialCode);
|
||||
const isExplicitInternational = isExplicitInternationalPhoneInput(value);
|
||||
if (!digits) {
|
||||
return '';
|
||||
}
|
||||
if (normalizedDialCode && digits.startsWith(normalizedDialCode) && digits.length > normalizedDialCode.length) {
|
||||
return digits.slice(normalizedDialCode.length);
|
||||
}
|
||||
if (isExplicitInternational) {
|
||||
return digits;
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
|
||||
function toE164PhoneNumber(value, dialCode) {
|
||||
const digits = normalizePhoneDigits(value);
|
||||
const normalizedDialCode = normalizePhoneDigits(dialCode);
|
||||
const isExplicitInternational = isExplicitInternationalPhoneInput(value);
|
||||
if (!digits) {
|
||||
return '';
|
||||
}
|
||||
if (isExplicitInternational) {
|
||||
return `+${digits}`;
|
||||
}
|
||||
if (!normalizedDialCode) {
|
||||
return digits.startsWith('+') ? digits : `+${digits}`;
|
||||
return `+${digits}`;
|
||||
}
|
||||
if (digits.startsWith(normalizedDialCode)) {
|
||||
return `+${digits}`;
|
||||
@@ -234,34 +254,71 @@
|
||||
.map((label) => normalizeCountryLabel(label))
|
||||
.filter(Boolean);
|
||||
return normalizedLabels.some((optionLabel) => (
|
||||
optionLabel.includes(normalizedTarget) || normalizedTarget.includes(optionLabel)
|
||||
optionLabel.length > 2
|
||||
&& normalizedTarget.length > 2
|
||||
&& (optionLabel.includes(normalizedTarget) || normalizedTarget.includes(optionLabel))
|
||||
));
|
||||
})
|
||||
|| null;
|
||||
}
|
||||
|
||||
async function ensureCountrySelected(countryLabel) {
|
||||
function findCountryOptionByPhoneNumber(phoneNumber) {
|
||||
const select = getCountrySelect();
|
||||
if (!select) {
|
||||
return null;
|
||||
}
|
||||
const digits = normalizePhoneDigits(phoneNumber);
|
||||
if (!digits) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let bestMatch = null;
|
||||
let bestDialCodeLength = 0;
|
||||
for (const option of Array.from(select.options || [])) {
|
||||
const dialCode = normalizePhoneDigits(extractDialCodeFromText(getOptionLabel(option)));
|
||||
if (!dialCode || !digits.startsWith(dialCode)) {
|
||||
continue;
|
||||
}
|
||||
if (dialCode.length > bestDialCodeLength) {
|
||||
bestMatch = option;
|
||||
bestDialCodeLength = dialCode.length;
|
||||
}
|
||||
}
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
async function trySelectCountryOption(select, targetOption) {
|
||||
if (!select || !targetOption) {
|
||||
return false;
|
||||
}
|
||||
const selectedOption = getSelectedCountryOption();
|
||||
if (selectedOption && isSameCountryOption(selectedOption, targetOption)) {
|
||||
return true;
|
||||
}
|
||||
select.value = String(targetOption.value || '');
|
||||
dispatchInputEvents(select);
|
||||
await sleep(250);
|
||||
const nextSelectedOption = getSelectedCountryOption();
|
||||
return Boolean(nextSelectedOption && isSameCountryOption(nextSelectedOption, targetOption));
|
||||
}
|
||||
|
||||
async function ensureCountrySelected(countryLabel, phoneNumber = '') {
|
||||
const select = getCountrySelect();
|
||||
if (!select) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const targetOption = findCountryOptionByLabel(countryLabel);
|
||||
if (!targetOption) {
|
||||
throw new Error(`Add-phone page is missing the country option for "${countryLabel}".`);
|
||||
}
|
||||
|
||||
const selectedOption = getSelectedCountryOption();
|
||||
if (selectedOption && isSameCountryOption(selectedOption, targetOption)) {
|
||||
const byLabel = findCountryOptionByLabel(countryLabel);
|
||||
if (await trySelectCountryOption(select, byLabel)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
select.value = String(targetOption.value || '');
|
||||
dispatchInputEvents(select);
|
||||
await sleep(250);
|
||||
const byPhoneNumber = findCountryOptionByPhoneNumber(phoneNumber);
|
||||
if (await trySelectCountryOption(select, byPhoneNumber)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const nextSelectedOption = getSelectedCountryOption();
|
||||
return Boolean(nextSelectedOption && isSameCountryOption(nextSelectedOption, targetOption));
|
||||
return Boolean(getSelectedCountryOption());
|
||||
}
|
||||
|
||||
function getAddPhoneSubmitButton() {
|
||||
@@ -398,6 +455,81 @@
|
||||
return '';
|
||||
}
|
||||
|
||||
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"], input[type="submit"], input[type="button"]');
|
||||
return Array.from(candidates).find((element) => {
|
||||
if (!isVisibleElement(element) || (!allowDisabled && !isActionEnabled(element))) {
|
||||
return false;
|
||||
}
|
||||
const text = String(getActionText?.(element) || '').trim();
|
||||
return /重试|try\s+again/i.test(text);
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function is405MethodNotAllowedPage() {
|
||||
const path = String(location?.pathname || '');
|
||||
if (!/\/phone-verification(?:[/?#]|$)/i.test(path) && !/\/add-phone(?:[/?#]|$)/i.test(path)) {
|
||||
return false;
|
||||
}
|
||||
const text = String(getPageTextSnapshot?.() || '').replace(/\s+/g, ' ').trim();
|
||||
const title = String(document?.title || '');
|
||||
const matched = PHONE_ROUTE_405_PATTERN.test(text) || PHONE_ROUTE_405_PATTERN.test(title);
|
||||
if (!matched) {
|
||||
return false;
|
||||
}
|
||||
return Boolean(getAuthRetryButton({ allowDisabled: true }));
|
||||
}
|
||||
|
||||
async function recoverPhoneRoute405(timeout = 12000, options = {}) {
|
||||
const now = Date.now();
|
||||
if (
|
||||
lastPhoneRoute405RecoveryFailedAt > 0
|
||||
&& now - lastPhoneRoute405RecoveryFailedAt < PHONE_ROUTE_405_RECOVERY_COOLDOWN_MS
|
||||
) {
|
||||
throw new Error(
|
||||
`${PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX}Phone verification route is still in 405 recovery cooldown (${Math.ceil((PHONE_ROUTE_405_RECOVERY_COOLDOWN_MS - (now - lastPhoneRoute405RecoveryFailedAt)) / 1000)}s left). URL: ${location.href}`
|
||||
);
|
||||
}
|
||||
|
||||
const startedAt = Date.now();
|
||||
let clicked = 0;
|
||||
const maxRetryClicks = Math.max(
|
||||
1,
|
||||
Math.floor(Number(options?.maxRetryClicks) || PHONE_ROUTE_405_MAX_RECOVERY_CLICKS)
|
||||
);
|
||||
while (Date.now() - startedAt < timeout) {
|
||||
throwIfStopped();
|
||||
if (!is405MethodNotAllowedPage()) {
|
||||
return;
|
||||
}
|
||||
const retryButton = getAuthRetryButton({ allowDisabled: true });
|
||||
if (retryButton && isActionEnabled(retryButton)) {
|
||||
if (clicked >= maxRetryClicks) {
|
||||
lastPhoneRoute405RecoveryFailedAt = Date.now();
|
||||
throw new Error(
|
||||
`${PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX}Phone verification route stayed on 405 after ${clicked} retry click(s). URL: ${location.href}`
|
||||
);
|
||||
}
|
||||
clicked += 1;
|
||||
await humanPause(200, 500);
|
||||
simulateClick(retryButton);
|
||||
await sleep(1000);
|
||||
continue;
|
||||
}
|
||||
await sleep(250);
|
||||
}
|
||||
lastPhoneRoute405RecoveryFailedAt = Date.now();
|
||||
throw new Error(
|
||||
`${PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX}Phone verification route 405 recovery timed out after ${clicked} retry click(s). URL: ${location.href}`
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForAddPhoneReady(timeout = 20000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
@@ -414,6 +546,10 @@
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
if (is405MethodNotAllowedPage()) {
|
||||
await recoverPhoneRoute405(Math.min(12000, Math.max(1000, timeout - (Date.now() - start))));
|
||||
continue;
|
||||
}
|
||||
if (isPhoneVerificationPageReady()) {
|
||||
return {
|
||||
phoneVerificationPage: true,
|
||||
@@ -448,18 +584,15 @@
|
||||
|
||||
async function submitPhoneNumber(payload = {}) {
|
||||
const countryLabel = String(payload.countryLabel || '').trim();
|
||||
if (!countryLabel) {
|
||||
throw new Error('Missing country label for add-phone submission.');
|
||||
}
|
||||
|
||||
const isExplicitInternational = isExplicitInternationalPhoneInput(payload.phoneNumber);
|
||||
await waitForAddPhoneReady();
|
||||
const countrySelected = await ensureCountrySelected(countryLabel);
|
||||
const countrySelected = await ensureCountrySelected(countryLabel, payload.phoneNumber);
|
||||
if (!countrySelected) {
|
||||
throw new Error(`Failed to select "${countryLabel}" on the add-phone page.`);
|
||||
throw new Error(`Failed to select "${countryLabel || 'target country'}" on the add-phone page.`);
|
||||
}
|
||||
|
||||
const dialCode = getDisplayedDialCode();
|
||||
if (!dialCode) {
|
||||
if (!dialCode && !isExplicitInternational) {
|
||||
throw new Error(`Could not determine the dial code for "${countryLabel}" on the add-phone page.`);
|
||||
}
|
||||
|
||||
@@ -498,6 +631,10 @@
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
if (is405MethodNotAllowedPage()) {
|
||||
await recoverPhoneRoute405(Math.min(12000, Math.max(1000, timeout - (Date.now() - start))));
|
||||
continue;
|
||||
}
|
||||
|
||||
const errorText = getVerificationErrorText();
|
||||
if (errorText) {
|
||||
@@ -565,40 +702,81 @@
|
||||
fillInput(codeInput, code);
|
||||
await sleep(250);
|
||||
simulateClick(submitButton);
|
||||
if (is405MethodNotAllowedPage()) {
|
||||
await recoverPhoneRoute405(12000);
|
||||
}
|
||||
return waitForPhoneVerificationOutcome();
|
||||
}
|
||||
|
||||
async function resendPhoneVerificationCode(timeout = 45000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
const throttledText = getPhoneResendThrottleText();
|
||||
if (throttledText) {
|
||||
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${throttledText}`);
|
||||
}
|
||||
const resendButton = getPhoneVerificationResendButton({ allowDisabled: true });
|
||||
if (resendButton && isActionEnabled(resendButton)) {
|
||||
await humanPause(250, 700);
|
||||
simulateClick(resendButton);
|
||||
await sleep(1000);
|
||||
const afterClickThrottleText = getPhoneResendThrottleText();
|
||||
if (afterClickThrottleText) {
|
||||
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${afterClickThrottleText}`);
|
||||
if (activePhoneResendPromise) {
|
||||
return activePhoneResendPromise;
|
||||
}
|
||||
|
||||
activePhoneResendPromise = (async () => {
|
||||
const start = Date.now();
|
||||
const route405RecoveryStart = Date.now();
|
||||
let route405RecoveryCount = 0;
|
||||
const recoverRoute405WithinResend = async () => {
|
||||
route405RecoveryCount += 1;
|
||||
if (route405RecoveryCount > PHONE_RESEND_ROUTE_405_MAX_RECOVERIES) {
|
||||
throw new Error(
|
||||
`${PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX}Phone verification resend stayed on route-405 page after ${PHONE_RESEND_ROUTE_405_MAX_RECOVERIES} recovery round(s). URL: ${location.href}`
|
||||
);
|
||||
}
|
||||
return {
|
||||
resent: true,
|
||||
url: location.href,
|
||||
};
|
||||
const recoveryBudgetLeft = PHONE_RESEND_ROUTE_405_MAX_RECOVERY_TOTAL_MS - (Date.now() - route405RecoveryStart);
|
||||
if (recoveryBudgetLeft <= 0) {
|
||||
throw new Error(
|
||||
`${PHONE_ROUTE_405_RECOVERY_FAILED_ERROR_PREFIX}Phone verification resend exceeded route-405 recovery budget (${PHONE_RESEND_ROUTE_405_MAX_RECOVERY_TOTAL_MS}ms). URL: ${location.href}`
|
||||
);
|
||||
}
|
||||
const remainingTimeout = Math.max(1000, timeout - (Date.now() - start));
|
||||
const recoveryTimeout = Math.max(1000, Math.min(12000, recoveryBudgetLeft, remainingTimeout));
|
||||
await recoverPhoneRoute405(recoveryTimeout);
|
||||
};
|
||||
|
||||
while (Date.now() - start < timeout) {
|
||||
throwIfStopped();
|
||||
if (is405MethodNotAllowedPage()) {
|
||||
await recoverRoute405WithinResend();
|
||||
continue;
|
||||
}
|
||||
const throttledText = getPhoneResendThrottleText();
|
||||
if (throttledText) {
|
||||
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${throttledText}`);
|
||||
}
|
||||
const resendButton = getPhoneVerificationResendButton({ allowDisabled: true });
|
||||
if (resendButton && isActionEnabled(resendButton)) {
|
||||
await humanPause(250, 700);
|
||||
simulateClick(resendButton);
|
||||
await sleep(1000);
|
||||
if (is405MethodNotAllowedPage()) {
|
||||
await recoverRoute405WithinResend();
|
||||
continue;
|
||||
}
|
||||
const afterClickThrottleText = getPhoneResendThrottleText();
|
||||
if (afterClickThrottleText) {
|
||||
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${afterClickThrottleText}`);
|
||||
}
|
||||
return {
|
||||
resent: true,
|
||||
url: location.href,
|
||||
};
|
||||
}
|
||||
await sleep(250);
|
||||
}
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
const timeoutThrottleText = getPhoneResendThrottleText();
|
||||
if (timeoutThrottleText) {
|
||||
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${timeoutThrottleText}`);
|
||||
}
|
||||
const timeoutThrottleText = getPhoneResendThrottleText();
|
||||
if (timeoutThrottleText) {
|
||||
throw new Error(`${PHONE_RESEND_THROTTLED_ERROR_PREFIX}${timeoutThrottleText}`);
|
||||
}
|
||||
|
||||
throw new Error('Timed out waiting for the phone verification resend button.');
|
||||
throw new Error('Timed out waiting for the phone verification resend button.');
|
||||
})().finally(() => {
|
||||
activePhoneResendPromise = null;
|
||||
});
|
||||
|
||||
return activePhoneResendPromise;
|
||||
}
|
||||
|
||||
async function returnToAddPhone(timeout = 20000) {
|
||||
|
||||
@@ -1036,8 +1036,8 @@ const CONTINUE_ACTION_PATTERN = /继续|continue/i;
|
||||
const ADD_PHONE_PAGE_PATTERN = /add[\s-]*phone|添加手机号|手机号码|手机号|phone\s+number|telephone/i;
|
||||
const STEP5_SUBMIT_ERROR_PATTERN = /无法根据该信息创建帐户|请重试|unable\s+to\s+create\s+(?:your\s+)?account|couldn'?t\s+create\s+(?:your\s+)?account|something\s+went\s+wrong|invalid\s+(?:birthday|birth|date)|生日|出生日期/i;
|
||||
const AUTH_TIMEOUT_ERROR_TITLE_PATTERN = /糟糕,出错了|something\s+went\s+wrong|oops/i;
|
||||
const AUTH_TIMEOUT_ERROR_DETAIL_PATTERN = /operation\s+timed\s+out|timed\s+out|请求超时|操作超时/i;
|
||||
const AUTH_ROUTE_ERROR_PATTERN = /405\s+method\s+not\s+allowed|route\s+error.*405/i;
|
||||
const AUTH_TIMEOUT_ERROR_DETAIL_PATTERN = /operation\s+timed\s+out|timed\s+out|请求超时|操作超时|failed\s+to\s+fetch|network\s+error|fetch\s+failed/i;
|
||||
const AUTH_ROUTE_ERROR_PATTERN = /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-verification/i;
|
||||
const SIGNUP_USER_ALREADY_EXISTS_ERROR_PREFIX = 'SIGNUP_USER_ALREADY_EXISTS::';
|
||||
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;
|
||||
|
||||
@@ -1536,10 +1536,11 @@ function getAuthTimeoutErrorPageState(options = {}) {
|
||||
|| AUTH_TIMEOUT_ERROR_TITLE_PATTERN.test(document.title || '');
|
||||
const detailMatched = AUTH_TIMEOUT_ERROR_DETAIL_PATTERN.test(text);
|
||||
const routeErrorMatched = AUTH_ROUTE_ERROR_PATTERN.test(text);
|
||||
const fetchFailedMatched = /failed\s+to\s+fetch|network\s+error|fetch\s+failed/i.test(text);
|
||||
const maxCheckAttemptsBlocked = /max_check_attempts/i.test(text);
|
||||
const userAlreadyExistsBlocked = /user_already_exists/i.test(text);
|
||||
|
||||
if (!titleMatched && !detailMatched && !routeErrorMatched && !maxCheckAttemptsBlocked && !userAlreadyExistsBlocked) {
|
||||
if (!titleMatched && !detailMatched && !routeErrorMatched && !fetchFailedMatched && !maxCheckAttemptsBlocked && !userAlreadyExistsBlocked) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1551,6 +1552,7 @@ function getAuthTimeoutErrorPageState(options = {}) {
|
||||
titleMatched,
|
||||
detailMatched,
|
||||
routeErrorMatched,
|
||||
fetchFailedMatched,
|
||||
maxCheckAttemptsBlocked,
|
||||
userAlreadyExistsBlocked,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user