fix(flow): harden proxy phone and checkout recovery paths
This commit is contained in:
+58
-17
@@ -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)
|
||||
|
||||
@@ -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
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user