fix(flow): harden proxy phone and checkout recovery paths

This commit is contained in:
朴圣佑
2026-05-03 13:58:33 +08:00
committed by QLHazyCoder
parent d851cc4d36
commit 29093e08db
34 changed files with 2958 additions and 293 deletions
+58 -17
View File
@@ -75,9 +75,25 @@ async function waitUntil(predicate, options = {}) {
}
}
async function waitForDocumentComplete() {
await waitUntil(() => document.readyState === 'complete', { intervalMs: 200 });
await sleep(800);
async function waitForDocumentComplete(options = {}) {
const timeoutMs = Math.max(1000, Math.floor(Number(options.timeoutMs) || 8000));
const settleMs = Math.max(0, Math.floor(Number(options.settleMs) || 500));
try {
await waitUntil(() => {
const readyState = String(document.readyState || '').toLowerCase();
return readyState === 'complete'
|| readyState === 'interactive'
|| Boolean(document.querySelector?.('button, input, textarea, a, [role="button"]'))
|| Boolean(normalizeText(document.body?.innerText || document.body?.textContent || ''));
}, {
intervalMs: 200,
timeoutMs,
label: 'GoPay DOM',
});
} catch (_) {
// GoPay linking 页面有时长时间保持 loading;后续定位控件本身还有等待/重试。
}
await sleep(settleMs);
}
function isVisibleElement(el) {
@@ -114,6 +130,7 @@ function getActionText(el) {
return normalizeText([
el?.textContent,
el?.value,
el?.getAttribute?.('data-testid'),
el?.getAttribute?.('aria-label'),
el?.getAttribute?.('title'),
el?.getAttribute?.('placeholder'),
@@ -171,14 +188,33 @@ function isGoPayOtpPageText() {
if (isGoPayPinPageText()) {
return false;
}
if (getVisibleTextInputs().some((input) => isGoPayPinInput(input))) {
return false;
}
const text = getPageBodyText();
return /otp|one[-\s]*time|kode|verification|whatsapp|验证码|短信/i.test(text);
}
function isGoPayPinPageText() {
const text = getPageBodyText();
return /pin|password|passcode|security|sandi|6\s*digit/i.test(text)
|| /pin-web-client\.gopayapi\.com|\/auth\/pin/i.test(location.href || '');
return /pin|password|passcode|security|sandi|6\s*digit|masukkin\s+pin|masukkan\s+pin|ketik\s+6\s+digit|enter\s+pin|支付密码/i.test(text)
|| /pin-web-client\.gopayapi\.com|\/auth\/pin|\/payment\/validate-pin|linking-validate-pin/i.test(location.href || '');
}
function isGoPayPinInput(input) {
if (!input || isCountrySearchInput(input)) {
return false;
}
const text = getCombinedElementText(input);
const testId = String(input.getAttribute?.('data-testid') || '').trim();
const type = String(input.getAttribute?.('type') || input.type || '').trim().toLowerCase();
const maxLength = Number(input.getAttribute?.('maxlength') || input.maxLength || 0);
const pinPage = isGoPayPinPageText();
const strongPinHint = /password|passcode|security|sandi|支付密码|密码/i.test(text);
const ambiguousPinWidget = /^pin-input/i.test(testId)
|| /pin-input(?:-field)?|(?:^|[\s_-])pin(?:$|[\s_-])/i.test(text);
return strongPinHint
|| (pinPage && (ambiguousPinWidget || type === 'password' || maxLength === 1));
}
function detectGoPayTerminalError(text = getPageBodyText()) {
@@ -213,31 +249,33 @@ function detectGoPayTerminalError(text = getPageBodyText()) {
}
function findOtpInput() {
if (isGoPayPinPageText()) {
return null;
}
const input = findInputByPatterns([
/otp|one[-\s]*time|verification|verify|code|kode|whatsapp|wa|pin-input-field/i,
/otp|one[-\s]*time|verification|verify|code|kode|whatsapp|wa/i,
/验证码|短信|代码/i,
]);
if (input && !isCountrySearchInput(input)) {
if (input && !isCountrySearchInput(input) && !isGoPayPinInput(input)) {
return input;
}
if (isGoPayOtpPageText()) {
return getVisibleTextInputs().find((candidate) => !isCountrySearchInput(candidate)) || null;
return getVisibleTextInputs().find((candidate) => !isCountrySearchInput(candidate) && !isGoPayPinInput(candidate)) || null;
}
return null;
}
function getGoPayPinInputs() {
return getVisibleTextInputs().filter((candidate) => {
if (isCountrySearchInput(candidate)) return false;
const text = getCombinedElementText(candidate);
const maxLength = Number(candidate.getAttribute?.('maxlength') || candidate.maxLength || 0);
return /pin|password|passcode|security|sandi|pin-input/i.test(text)
|| candidate.getAttribute?.('data-testid')?.startsWith?.('pin-input')
|| (isGoPayPinPageText() && maxLength === 1);
return isGoPayPinInput(candidate);
});
}
function findPinInput() {
const pinInputs = getGoPayPinInputs();
if (pinInputs[0]) {
return pinInputs[0];
}
if (isGoPayOtpPageText()) {
return null;
}
@@ -248,8 +286,7 @@ function findPinInput() {
if (input && !isCountrySearchInput(input)) {
return input;
}
return getGoPayPinInputs()[0]
|| getVisibleControls('input[type="password"]').find((candidate) => isEnabledControl(candidate) && !isCountrySearchInput(candidate))
return getVisibleControls('input[type="password"]').find((candidate) => isEnabledControl(candidate) && !isCountrySearchInput(candidate))
|| null;
}
@@ -274,7 +311,7 @@ function findPayNowButton() {
function findContinueButton() {
return findClickableByText([
/continue|next|submit|verify|confirm|pay|authorize|allow|lanjut|berikut|kirim|bayar|konfirmasi|link/i,
/continue|next|submit|verify|confirm|pay|authorize|allow|lanjut|lanjutkan|berikut|kirim|bayar|konfirmasi|hubungkan|sambungkan|tautkan|setuju|izinkan|link/i,
/继续|下一步|提交|验证|确认|支付|授权|绑定|关联/i,
]);
}
@@ -556,9 +593,13 @@ function fillVisiblePinInputs(pin = '') {
function fillVisibleOtpInputs(code = '') {
const normalizedCode = normalizeOtp(code);
if (!normalizedCode) return false;
if (isGoPayPinPageText() || getVisibleTextInputs().some((input) => isGoPayPinInput(input))) {
return false;
}
const otpInputs = getVisibleTextInputs()
.filter((input) => {
if (isGoPayPinInput(input)) return false;
const text = getActionText(input);
const maxLength = Number(input.getAttribute?.('maxlength') || input.maxLength || 0);
return /otp|code|kode|verification|验证码|短信/i.test(text)
+20 -8
View File
@@ -791,6 +791,17 @@ function hasBillingAddressFields() {
});
}
function hasPaymentMethodSelectionMarker(el) {
if (!el) return false;
const className = typeof el.className === 'string' ? el.className : el.getAttribute?.('class') || '';
return el.checked === true
|| el.getAttribute?.('aria-checked') === 'true'
|| el.getAttribute?.('aria-selected') === 'true'
|| el.getAttribute?.('data-state') === 'checked'
|| el.getAttribute?.('data-selected') === 'true'
|| /\b(selected|checked|active)\b/i.test(className);
}
function hasSelectedPaymentMethodControl(method = PLUS_PAYMENT_METHOD_PAYPAL) {
const config = getPaymentMethodConfig(method);
const candidates = getPaymentMethodSearchCandidates(config.id);
@@ -798,15 +809,16 @@ function hasSelectedPaymentMethodControl(method = PLUS_PAYMENT_METHOD_PAYPAL) {
let current = candidate;
for (let depth = 0; current && depth < 6; depth += 1, current = current.parentElement) {
if (isDocumentLevelContainer(current)) break;
if (!config.patterns.some((pattern) => pattern.test(getCombinedSearchText(current)))) continue;
const className = typeof current.className === 'string' ? current.className : current.getAttribute?.('class') || '';
const currentMatchesPayment = config.patterns.some((pattern) => pattern.test(getCombinedSearchText(current)));
if (currentMatchesPayment && hasPaymentMethodSelectionMarker(current)) {
return true;
}
const radio = current.querySelector?.('input[type="radio"], [role="radio"]');
if (
current.checked === true
|| current.getAttribute?.('aria-checked') === 'true'
|| current.getAttribute?.('aria-selected') === 'true'
|| current.getAttribute?.('data-state') === 'checked'
|| current.getAttribute?.('data-selected') === 'true'
|| /\b(selected|checked|active)\b/i.test(className)
radio
&& config.patterns.some((pattern) => pattern.test(getCombinedSearchText(current) || getCombinedSearchText(radio)))
&& hasPaymentMethodSelectionMarker(radio)
) {
return true;
}
+98 -1
View File
@@ -280,13 +280,104 @@ function is405MethodNotAllowedPage() {
return AUTH_ROUTE_ERROR_PATTERN.test(pageText);
}
function getStep405RecoveryStateKey(step) {
return `__MULTIPAGE_STEP_${Number(step) || '?'}_405_RECOVERY_COUNT__`;
}
function getStep405StorageScope() {
if (typeof window !== 'undefined' && window) {
return window;
}
if (typeof globalThis !== 'undefined' && globalThis) {
return globalThis;
}
return {};
}
function getStep405RecoveryLimit(step) {
if (Number(step) !== 4) {
return 0;
}
return typeof STEP4_405_RECOVERY_LIMIT !== 'undefined'
? STEP4_405_RECOVERY_LIMIT
: 3;
}
function getStep405RecoveryErrorPrefix(step) {
if (Number(step) !== 4) {
return '';
}
return typeof STEP4_405_RECOVERY_ERROR_PREFIX !== 'undefined'
? STEP4_405_RECOVERY_ERROR_PREFIX
: 'STEP4_405_RECOVERY_LIMIT::';
}
function getStep405RecoveryCount(step) {
const key = getStep405RecoveryStateKey(step);
let value = '';
try {
if (typeof sessionStorage !== 'undefined' && sessionStorage?.getItem) {
value = sessionStorage.getItem(key) || '';
}
} catch {}
if (!value) {
value = getStep405StorageScope()[key];
}
return Math.max(0, Math.floor(Number(value) || 0));
}
function setStep405RecoveryCount(step, count) {
const key = getStep405RecoveryStateKey(step);
const value = String(Math.max(0, Math.floor(Number(count) || 0)));
try {
if (typeof sessionStorage !== 'undefined' && sessionStorage?.setItem) {
sessionStorage.setItem(key, value);
}
} catch {}
getStep405StorageScope()[key] = value;
}
function clearStep405RecoveryCount(step) {
const key = getStep405RecoveryStateKey(step);
try {
if (typeof sessionStorage !== 'undefined' && sessionStorage?.removeItem) {
sessionStorage.removeItem(key);
}
} catch {}
try {
delete getStep405StorageScope()[key];
} catch {}
}
function createStep405RecoveryLimitError(step, count) {
const normalizedStep = Number(step) || step || '?';
const limit = getStep405RecoveryLimit(normalizedStep) || count;
const message = `步骤 ${normalizedStep}:检测到 405 错误页面,已连续点击“重试”恢复 ${count}/${limit} 次仍未恢复,当前轮将结束并进入下一轮。URL: ${location.href}`;
return new Error(`${getStep405RecoveryErrorPrefix(normalizedStep)}${message}`);
}
async function handle405ResendError(step, remainingTimeout = 30000) {
const currentCount = getStep405RecoveryCount(step);
if (Number(step) === 4 && currentCount >= getStep405RecoveryLimit(step)) {
throw createStep405RecoveryLimitError(step, currentCount);
}
const nextCount = currentCount + 1;
setStep405RecoveryCount(step, nextCount);
const maxClickAttempts = Number(step) === 4 ? 1 : 5;
await recoverCurrentAuthRetryPage({
logLabel: `步骤 ${step}:检测到 405 错误页面,正在点击“重试”恢复`,
logLabel: Number(step) === 4
? `步骤 ${step}:检测到 405 错误页面,正在点击“重试”恢复(总计 ${nextCount}/${getStep405RecoveryLimit(step)}`
: `步骤 ${step}:检测到 405 错误页面,正在点击“重试”恢复`,
maxClickAttempts,
pathPatterns: [],
step,
timeoutMs: Math.max(1000, remainingTimeout),
});
if (is405MethodNotAllowedPage()) {
throw createStep405RecoveryLimitError(step, nextCount);
}
if (typeof clearStep405RecoveryCount === 'function') clearStep405RecoveryCount(step);
log(`步骤 ${step}:405 错误已恢复,页面已返回验证码页面。`);
}
@@ -1038,6 +1129,8 @@ const STEP5_SUBMIT_ERROR_PATTERN = /无法根据该信息创建帐户|请重试|
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|请求超时|操作超时|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 STEP4_405_RECOVERY_ERROR_PREFIX = 'STEP4_405_RECOVERY_LIMIT::';
const STEP4_405_RECOVERY_LIMIT = 3;
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;
@@ -2546,6 +2639,7 @@ async function fillVerificationCode(step, payload) {
if (step === 4) {
const postVerificationState = getStep4PostVerificationState();
if (postVerificationState?.state === 'logged_in_home') {
if (typeof clearStep405RecoveryCount === 'function') clearStep405RecoveryCount(step);
log(`步骤 ${step}:检测到页面已进入 ChatGPT 已登录态,本次验证码提交按成功处理。`, 'ok');
return {
success: true,
@@ -2556,6 +2650,7 @@ async function fillVerificationCode(step, payload) {
};
}
if (postVerificationState?.state === 'step5') {
if (typeof clearStep405RecoveryCount === 'function') clearStep405RecoveryCount(step);
log(`步骤 ${step}:检测到页面已进入下一阶段,本次验证码提交按成功处理。`, 'ok');
return { success: true, assumed: true, alreadyAdvanced: true };
}
@@ -2664,6 +2759,7 @@ async function fillVerificationCode(step, payload) {
} else if (outcome.addPhonePage) {
log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn');
} else {
if (typeof clearStep405RecoveryCount === 'function') clearStep405RecoveryCount(step);
log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}`, 'ok');
}
if (combinedSignupProfilePage && !outcome.invalidCode) {
@@ -2698,6 +2794,7 @@ async function fillVerificationCode(step, payload) {
} else if (outcome.addPhonePage) {
log(`步骤 ${step}:验证码提交后页面进入手机号页面,当前流程将停止自动授权。`, 'warn');
} else {
if (typeof clearStep405RecoveryCount === 'function') clearStep405RecoveryCount(step);
log(`步骤 ${step}:验证码已通过${outcome.assumed ? '(按成功推定)' : ''}`, 'ok');
}