Files
2026-05-21 07:21:34 +08:00

829 lines
30 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// flows/openai/content/gopay-flow.js — GoPay authorization helper.
console.log('[MultiPage:gopay-flow] Content script loaded on', location.href);
const GOPAY_FLOW_LISTENER_SENTINEL = 'data-multipage-gopay-flow-listener';
if (document.documentElement.getAttribute(GOPAY_FLOW_LISTENER_SENTINEL) !== '1') {
document.documentElement.setAttribute(GOPAY_FLOW_LISTENER_SENTINEL, '1');
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (
message.type === 'GOPAY_GET_STATE'
|| message.type === 'GOPAY_SUBMIT_PHONE'
|| message.type === 'GOPAY_SUBMIT_OTP'
|| message.type === 'GOPAY_SUBMIT_PIN'
|| message.type === 'GOPAY_CLICK_CONTINUE'
|| message.type === 'GOPAY_GET_CONTINUE_TARGET'
|| message.type === 'GOPAY_CLICK_PAY_NOW'
|| message.type === 'GOPAY_GET_PAY_NOW_TARGET'
) {
resetStopState();
handleGoPayCommand(message).then((result) => {
sendResponse({ ok: true, ...(result || {}) });
}).catch((err) => {
if (isStopError(err)) {
sendResponse({ stopped: true, error: err.message });
return;
}
sendResponse({ error: err.message });
});
return true;
}
});
} else {
console.log('[MultiPage:gopay-flow] 消息监听已存在,跳过重复注册');
}
async function performGoPayOperationWithDelay(metadata, operation) {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
}
async function handleGoPayCommand(message) {
switch (message.type) {
case 'GOPAY_GET_STATE':
return inspectGoPayState();
case 'GOPAY_SUBMIT_PHONE':
return submitGoPayPhone(message.payload || {});
case 'GOPAY_SUBMIT_OTP':
return submitGoPayOtp(message.payload || {});
case 'GOPAY_SUBMIT_PIN':
return submitGoPayPin(message.payload || {});
case 'GOPAY_CLICK_CONTINUE':
return clickGoPayContinue();
case 'GOPAY_GET_CONTINUE_TARGET':
return getGoPayContinueTarget();
case 'GOPAY_CLICK_PAY_NOW':
return clickGoPayPayNow();
case 'GOPAY_GET_PAY_NOW_TARGET':
return getGoPayPayNowTarget();
default:
throw new Error(`gopay-flow.js 不处理消息:${message.type}`);
}
}
async function waitUntil(predicate, options = {}) {
const intervalMs = Math.max(50, Math.floor(Number(options.intervalMs) || 250));
const timeoutMs = Math.max(0, Math.floor(Number(options.timeoutMs) || 0));
const startedAt = Date.now();
while (true) {
throwIfStopped();
const value = await predicate();
if (value) {
return value;
}
if (timeoutMs > 0 && Date.now() - startedAt >= timeoutMs) {
throw new Error(options.timeoutMessage || `${options.label || 'GoPay 页面状态'}等待超时`);
}
await sleep(intervalMs);
}
}
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) {
if (!el) return false;
let node = el;
while (node && node.nodeType === 1) {
if (node.hidden || node.getAttribute?.('aria-hidden') === 'true' || node.getAttribute?.('inert') !== null) {
return false;
}
const nodeStyle = window.getComputedStyle(node);
if (
nodeStyle.display === 'none'
|| nodeStyle.visibility === 'hidden'
|| nodeStyle.visibility === 'collapse'
|| Number(nodeStyle.opacity) === 0
) {
return false;
}
node = node.parentElement;
}
const style = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& Number(rect.width) > 0
&& Number(rect.height) > 0;
}
function normalizeText(text = '') {
return String(text || '').replace(/\s+/g, ' ').trim();
}
function getActionText(el) {
return normalizeText([
el?.textContent,
el?.value,
el?.getAttribute?.('data-testid'),
el?.getAttribute?.('aria-label'),
el?.getAttribute?.('title'),
el?.getAttribute?.('placeholder'),
el?.getAttribute?.('name'),
el?.id,
].filter(Boolean).join(' '));
}
function getVisibleControls(selector) {
return Array.from(document.querySelectorAll(selector)).filter(isVisibleElement);
}
function isEnabledControl(el) {
return Boolean(el)
&& !el.disabled
&& el.getAttribute?.('aria-disabled') !== 'true';
}
function getVisibleTextInputs() {
return getVisibleControls('input, textarea')
.filter((input) => {
const type = String(input.getAttribute('type') || input.type || '').trim().toLowerCase();
return isEnabledControl(input) && !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type);
});
}
function findInputByPatterns(patterns) {
return getVisibleTextInputs().find((input) => {
const text = getActionText(input);
return patterns.some((pattern) => pattern.test(text));
}) || null;
}
function findPhoneInput() {
const input = findInputByPatterns([
/gopay|go\s*pay|phone|mobile|whatsapp|wa|nomor|ponsel|telepon|hp/i,
/手机|手机号|电话号码|电话/i,
]);
if (input && !isCountrySearchInput(input)) {
return input;
}
return getVisibleControls('input[type="tel"]').find((candidate) => isEnabledControl(candidate) && !isCountrySearchInput(candidate)) || null;
}
function isCountrySearchInput(input) {
const text = getActionText(input);
return /country|country\s*name|country\s*code|国家|地区|区号/i.test(text);
}
function getPageBodyText() {
return normalizeText(document.body?.innerText || document.body?.textContent || '');
}
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|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()) {
const normalizedText = normalizeText(text);
if (!normalizedText) return null;
if (/waktunya\s+habis|ulang(?:i)?\s+prosesnya\s+dari\s+awal|time(?:'s|\s+is)?\s+(?:out|expired)|session\s+expired|expired|kedaluwarsa/i.test(normalizedText)) {
return {
code: 'expired',
message: 'GoPay 支付会话已超时,需要重新创建 Plus Checkout。',
rawText: normalizedText.slice(0, 240),
};
}
if (/technical\s+error|don[']t\s+worry|try\s+again|terjadi\s+kesalahan|error\s+teknis/i.test(normalizedText)) {
return {
code: 'technical-error',
message: 'GoPay 页面显示技术错误,需要重新发起支付授权。',
rawText: normalizedText.slice(0, 240),
};
}
if (/payment\s+failed|pembayaran\s+gagal|transaksi\s+gagal|ditolak|declined|failed/i.test(normalizedText)) {
return {
code: 'failed',
message: 'GoPay 页面显示支付失败,需要重新发起支付授权。',
rawText: normalizedText.slice(0, 240),
};
}
return null;
}
function findOtpInput() {
if (isGoPayPinPageText()) {
return null;
}
const input = findInputByPatterns([
/otp|one[-\s]*time|verification|verify|code|kode|whatsapp|wa/i,
/验证码|短信|代码/i,
]);
if (input && !isCountrySearchInput(input) && !isGoPayPinInput(input)) {
return input;
}
if (isGoPayOtpPageText()) {
return getVisibleTextInputs().find((candidate) => !isCountrySearchInput(candidate) && !isGoPayPinInput(candidate)) || null;
}
return null;
}
function getGoPayPinInputs() {
return getVisibleTextInputs().filter((candidate) => {
return isGoPayPinInput(candidate);
});
}
function findPinInput() {
const pinInputs = getGoPayPinInputs();
if (pinInputs[0]) {
return pinInputs[0];
}
if (isGoPayOtpPageText()) {
return null;
}
const input = findInputByPatterns([
/pin|password|passcode|security|sandi|pin-input/i,
/密码|支付密码/i,
]);
if (input && !isCountrySearchInput(input)) {
return input;
}
return getVisibleControls('input[type="password"]').find((candidate) => isEnabledControl(candidate) && !isCountrySearchInput(candidate))
|| null;
}
function findClickableByText(patterns) {
const normalizedPatterns = (Array.isArray(patterns) ? patterns : [patterns]).filter(Boolean);
const candidates = getVisibleControls('button, a, [role="button"], input[type="button"], input[type="submit"]');
return candidates.find((el) => {
if (!isEnabledControl(el)) return false;
const text = getActionText(el);
return normalizedPatterns.some((pattern) => pattern.test(text));
}) || null;
}
function findPayNowButton() {
return findClickableByText([
/^\s*pay\s+now\s*$/i,
/^\s*bayar(?:\s+sekarang)?(?:\s*rp[\s\S]*)?\s*$/i,
/^\s*支付\s*$/i,
/^\s*立即支付\s*$/i,
]);
}
function findContinueButton() {
return findClickableByText([
/continue|next|submit|verify|confirm|pay|authorize|allow|lanjut|lanjutkan|berikut|kirim|bayar|konfirmasi|hubungkan|sambungkan|tautkan|setuju|izinkan|link/i,
/继续|下一步|提交|验证|确认|支付|授权|绑定|关联/i,
]);
}
function describeElement(el) {
if (!el) return '';
const rect = el.getBoundingClientRect?.();
const parts = [String(el.tagName || '').toUpperCase()];
if (el.id) parts.push(`#${el.id}`);
const className = typeof el.className === 'string' ? el.className : el.getAttribute?.('class');
if (className) parts.push(`.${String(className).trim().replace(/\s+/g, '.')}`);
const text = getActionText(el) || normalizeText(el.innerText || el.textContent || '');
if (text) parts.push(`"${text.slice(0, 60)}"`);
if (rect) parts.push(`@${Math.round(rect.left)},${Math.round(rect.top)} ${Math.round(rect.width)}x${Math.round(rect.height)}`);
return parts.filter(Boolean).join(' ');
}
function dispatchPointerMouseSequence(target) {
const rect = target.getBoundingClientRect?.();
const clientX = rect ? Math.round(rect.left + rect.width / 2) : 0;
const clientY = rect ? Math.round(rect.top + rect.height / 2) : 0;
const eventInit = {
bubbles: true,
cancelable: true,
composed: true,
view: window,
detail: 1,
button: 0,
buttons: 1,
clientX,
clientY,
screenX: window.screenX + clientX,
screenY: window.screenY + clientY,
};
if (typeof PointerEvent === 'function') {
target.dispatchEvent(new PointerEvent('pointerover', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
target.dispatchEvent(new PointerEvent('pointerenter', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true, bubbles: false }));
target.dispatchEvent(new PointerEvent('pointermove', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
target.dispatchEvent(new PointerEvent('pointerdown', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
}
target.dispatchEvent(new MouseEvent('mouseover', eventInit));
target.dispatchEvent(new MouseEvent('mouseenter', { ...eventInit, bubbles: false }));
target.dispatchEvent(new MouseEvent('mousemove', eventInit));
target.dispatchEvent(new MouseEvent('mousedown', eventInit));
if (typeof PointerEvent === 'function') {
target.dispatchEvent(new PointerEvent('pointerup', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true, buttons: 0 }));
}
target.dispatchEvent(new MouseEvent('mouseup', { ...eventInit, buttons: 0 }));
target.dispatchEvent(new MouseEvent('click', { ...eventInit, buttons: 0 }));
}
async function humanClickElement(el, options = {}) {
if (!el) {
throw new Error('GoPay 页面未找到可点击元素。');
}
el.scrollIntoView?.({ block: 'center', inline: 'center' });
await sleep(Math.max(0, Number(options.beforeMs) || 120));
try {
el.focus?.({ preventScroll: true });
} catch (_) {
try { el.focus?.(); } catch (__) {}
}
dispatchPointerMouseSequence(el);
await sleep(Math.max(0, Number(options.afterDispatchMs) || 120));
if (typeof el.click === 'function') {
el.click();
}
await sleep(Math.max(0, Number(options.afterMs) || 1000));
}
async function clickContinueIfPresent(options = {}) {
const button = findContinueButton();
if (!button) {
return { clicked: false, target: '' };
}
await humanClickElement(button, options);
return { clicked: true, target: describeElement(button) };
}
function normalizePhoneNumber(value = '') {
return String(value || '').trim().replace(/[^\d+]/g, '');
}
function normalizeGoPayCountryCode(value = '') {
const normalized = String(value || '').trim().replace(/[^\d+]/g, '');
const digits = normalized.replace(/\D/g, '');
return digits ? `+${digits}` : '+86';
}
function getCountryCodeDigits(value = '') {
return normalizeGoPayCountryCode(value).replace(/\D/g, '');
}
function normalizeGoPayNationalPhone(value = '', countryCode = '+86') {
const countryDigits = getCountryCodeDigits(countryCode);
let digits = normalizePhoneNumber(value).replace(/\D/g, '');
if (countryDigits && digits.startsWith(countryDigits)) {
digits = digits.slice(countryDigits.length);
}
return digits;
}
function getCombinedElementText(el) {
return normalizeText([
getActionText(el),
el?.innerText,
el?.textContent,
typeof el?.className === 'string' ? el.className : el?.getAttribute?.('class'),
].filter(Boolean).join(' '));
}
function robustClick(el) {
if (!el) return;
el.scrollIntoView?.({ block: 'center', inline: 'center' });
try {
el.focus?.();
} catch (_) {}
const eventInit = { bubbles: true, cancelable: true, view: window };
if (typeof PointerEvent === 'function') {
el.dispatchEvent(new PointerEvent('pointerdown', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
}
el.dispatchEvent(new MouseEvent('mousedown', eventInit));
if (typeof PointerEvent === 'function') {
el.dispatchEvent(new PointerEvent('pointerup', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
}
el.dispatchEvent(new MouseEvent('mouseup', eventInit));
el.dispatchEvent(new MouseEvent('click', eventInit));
if (typeof el.click === 'function') {
el.click();
}
}
function readSelectedCountryCodeText() {
const candidates = getVisibleControls('.phone-code, .phone-code-wrapper, [class*="phone-code"], button, [role="button"], [tabindex]');
for (const candidate of candidates) {
const match = getCombinedElementText(candidate).match(/\+\d{1,4}/);
if (match) return normalizeGoPayCountryCode(match[0]);
}
const bodyMatch = normalizeText(document.body?.innerText || document.body?.textContent || '').match(/Phone number:\s*(\+\d{1,4})/i);
return bodyMatch ? normalizeGoPayCountryCode(bodyMatch[1]) : '';
}
function findCountryCodeToggle() {
const preferred = getVisibleControls('.phone-code-wrapper, [class*="phone-code-wrapper"], .phone-code, [class*="phone-code"]')
.find((el) => /\+\d{1,4}/.test(getCombinedElementText(el)));
if (preferred) return preferred;
return getVisibleControls('button, [role="button"], [tabindex], div, span')
.find((el) => /\+\d{1,4}/.test(getCombinedElementText(el)) && /phone|code|country|\+\d{1,4}/i.test(getCombinedElementText(el))) || null;
}
function findCountryCodeOption(countryCode = '+86') {
const normalized = normalizeGoPayCountryCode(countryCode);
const digits = getCountryCodeDigits(normalized);
const countryAliases = {
'1': [/United States|USA|Canada|美国|加拿大/i],
'60': [/Malaysia|马来西亚/i],
'62': [/Indonesia|印尼|印度尼西亚/i],
'63': [/Philippines|菲律宾/i],
'65': [/Singapore|新加坡/i],
'66': [/Thailand|泰国/i],
'84': [/Vietnam|越南/i],
'86': [/China|中国|Mainland/i],
'91': [/India|印度/i],
'852': [/Hong Kong|香港/i],
'853': [/Macau|Macao|澳门/i],
'886': [/Taiwan|台湾/i],
}[digits] || [];
const controls = getVisibleControls('li.country-item, .country-item, [class*="country-item"], [role="option"], li, button, [role="button"], a, [tabindex], div, span')
.map((el) => el.closest?.('.country-item') || el)
.filter((el, index, list) => el && list.indexOf(el) === index)
.filter((el) => {
const rect = el.getBoundingClientRect();
const text = getCombinedElementText(el);
const className = typeof el.className === 'string' ? el.className : el.getAttribute?.('class') || '';
return text.includes(normalized)
&& rect.width > 20
&& rect.height > 8
&& rect.width < Math.max(480, window.innerWidth * 0.8)
&& rect.height < Math.max(120, window.innerHeight * 0.35)
&& !/phone-number-input-wrapper|gopay-tokenization-content|asphalt-theme|search-country|country-list/i.test(className);
});
const matchesAlias = (el) => {
const text = getCombinedElementText(el);
return countryAliases.some((pattern) => pattern.test(text));
};
return controls.find((el) => /country-item/i.test(typeof el.className === 'string' ? el.className : el.getAttribute?.('class') || '') && matchesAlias(el))
|| controls.find((el) => String(el.tagName || '').toUpperCase() === 'LI' && matchesAlias(el))
|| controls.find((el) => el.getAttribute?.('role') === 'option' && matchesAlias(el))
|| controls.find(matchesAlias)
|| null;
}
async function ensureGoPayCountryCode(countryCode = '+86') {
const normalized = normalizeGoPayCountryCode(countryCode);
const selected = readSelectedCountryCodeText();
if (selected === normalized) {
return { changed: false, countryCode: normalized, selected };
}
const toggle = findCountryCodeToggle();
if (!toggle) {
throw new Error(`GoPay 页面未找到国家区号切换控件,当前识别区号:${selected || '未知'},目标区号:${normalized}`);
}
robustClick(toggle);
await sleep(500);
const countryDropdown = document.querySelector('.search-country');
if (countryDropdown && window.getComputedStyle(countryDropdown).display === 'none') {
countryDropdown.style.display = 'block';
await sleep(100);
}
const countrySearchInput = getVisibleTextInputs().find(isCountrySearchInput);
if (countrySearchInput) {
fillInput(countrySearchInput, normalized);
await sleep(300);
}
const option = await waitUntil(() => findCountryCodeOption(normalized), {
label: `GoPay 国家区号 ${normalized}`,
intervalMs: 250,
timeoutMs: 8000,
});
robustClick(option);
await sleep(500);
const nextSelected = readSelectedCountryCodeText();
if (nextSelected === normalized && countryDropdown) {
countryDropdown.style.display = 'none';
}
if (nextSelected !== normalized) {
throw new Error(`GoPay 国家区号切换失败:目标 ${normalized},当前 ${nextSelected || '未知'}`);
}
return {
changed: true,
countryCode: normalized,
selected: nextSelected,
};
}
function normalizeOtp(value = '') {
return String(value || '').trim().replace(/[^\d]/g, '');
}
function fillDigitInputs(inputs = [], code = '') {
const normalizedCode = normalizeOtp(code);
if (!normalizedCode || !inputs.length) return false;
normalizedCode.split('').forEach((digit, index) => {
const input = inputs[index];
if (!input) return;
try {
input.focus?.();
} catch (_) {}
input.dispatchEvent(new KeyboardEvent('keydown', { key: digit, code: `Digit${digit}`, bubbles: true, cancelable: true }));
fillInput(input, digit);
input.dispatchEvent(new KeyboardEvent('keyup', { key: digit, code: `Digit${digit}`, bubbles: true, cancelable: true }));
});
return true;
}
function fillVisiblePinInputs(pin = '') {
const normalizedPin = normalizeOtp(pin);
if (!normalizedPin) return false;
const pinInputs = getGoPayPinInputs();
const digitInputs = pinInputs.filter((input) => {
const maxLength = Number(input.getAttribute?.('maxlength') || input.maxLength || 0);
return maxLength > 0 && maxLength <= 1;
});
if (digitInputs.length >= Math.min(4, normalizedPin.length)) {
return fillDigitInputs(digitInputs, normalizedPin);
}
const input = findPinInput() || pinInputs[0];
if (!input) return false;
fillInput(input, normalizedPin);
return true;
}
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)
|| (maxLength > 0 && maxLength <= 1)
|| (maxLength > 1 && maxLength <= 8);
});
const digitInputs = otpInputs.filter((input) => {
const maxLength = Number(input.getAttribute?.('maxlength') || input.maxLength || 0);
return maxLength > 0 && maxLength <= 1;
});
if (digitInputs.length >= Math.min(4, normalizedCode.length)) {
return fillDigitInputs(digitInputs, normalizedCode);
}
const input = findOtpInput() || otpInputs[0];
if (!input) return false;
fillInput(input, normalizedCode);
return true;
}
async function submitGoPayPhone(payload = {}) {
const delayOperation = typeof performGoPayOperationWithDelay === 'function'
? performGoPayOperationWithDelay
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
await waitForDocumentComplete();
const countryCode = normalizeGoPayCountryCode(payload.countryCode || payload.gopayCountryCode || '+86');
const phone = normalizeGoPayNationalPhone(payload.phone || payload.gopayPhone || '', countryCode);
if (!phone) {
throw new Error('GoPay 手机号为空,请先在侧边栏配置。');
}
const input = await waitUntil(() => findPhoneInput(), {
label: 'GoPay 手机号输入框',
intervalMs: 250,
timeoutMs: 15000,
});
const { countryResult, clickResult } = await delayOperation({ stepKey: 'gopay-approve', kind: 'submit', label: 'submit-phone' }, async () => {
const nextCountryResult = await ensureGoPayCountryCode(countryCode);
fillInput(input, phone);
const nextClickResult = await clickContinueIfPresent();
return {
countryResult: nextCountryResult,
clickResult: nextClickResult,
};
});
return {
phoneSubmitted: true,
countryCode,
countryChanged: Boolean(countryResult.changed),
clicked: Boolean(clickResult.clicked),
clickTarget: clickResult.target || '',
phase: 'phone_submitted',
};
}
async function submitGoPayOtp(payload = {}) {
const delayOperation = typeof performGoPayOperationWithDelay === 'function'
? performGoPayOperationWithDelay
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
await waitForDocumentComplete();
const code = normalizeOtp(payload.code || payload.otp || '');
if (!code) {
throw new Error('GoPay WhatsApp 验证码为空。');
}
const { filled, clickResult } = await delayOperation({ stepKey: 'gopay-approve', kind: 'submit', label: 'submit-otp' }, async () => {
const filledOtp = await waitUntil(() => fillVisibleOtpInputs(code), {
label: 'GoPay 验证码输入框',
intervalMs: 250,
timeoutMs: 15000,
});
const continueResult = await clickContinueIfPresent();
return { filled: filledOtp, clickResult: continueResult };
});
return {
otpSubmitted: Boolean(filled),
clicked: Boolean(clickResult.clicked),
clickTarget: clickResult.target || '',
phase: 'otp_submitted',
};
}
async function submitGoPayPin(payload = {}) {
const delayOperation = typeof performGoPayOperationWithDelay === 'function'
? performGoPayOperationWithDelay
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
await waitForDocumentComplete();
const pin = normalizeOtp(payload.pin || payload.gopayPin || '');
if (!pin) {
throw new Error('GoPay PIN 为空,请先在侧边栏配置。');
}
const { filled, clickResult } = await delayOperation({ stepKey: 'gopay-approve', kind: 'submit', label: 'submit-pin' }, async () => {
const filledPin = await waitUntil(() => fillVisiblePinInputs(pin), {
label: 'GoPay PIN 输入框',
intervalMs: 250,
timeoutMs: 15000,
});
const continueResult = await clickContinueIfPresent();
return { filled: filledPin, clickResult: continueResult };
});
return {
pinSubmitted: Boolean(filled),
clicked: Boolean(clickResult.clicked),
clickTarget: clickResult.target || '',
phase: 'pin_submitted',
};
}
function getElementClickRect(el) {
if (!el) return null;
const rect = el.getBoundingClientRect?.();
if (!rect || !Number.isFinite(rect.left) || !Number.isFinite(rect.top)) {
return null;
}
return {
left: rect.left,
top: rect.top,
width: rect.width,
height: rect.height,
centerX: rect.left + rect.width / 2,
centerY: rect.top + rect.height / 2,
};
}
function getGoPayContinueTarget() {
const button = findContinueButton();
return {
found: Boolean(button),
target: describeElement(button),
rect: getElementClickRect(button),
};
}
function getGoPayPayNowTarget() {
const button = findPayNowButton();
return {
found: Boolean(button),
target: describeElement(button),
rect: getElementClickRect(button),
};
}
async function clickGoPayContinue() {
const delayOperation = typeof performGoPayOperationWithDelay === 'function'
? performGoPayOperationWithDelay
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
await waitForDocumentComplete();
const button = findContinueButton();
if (!button) {
return { clicked: false, clickTarget: '' };
}
const clickResult = await delayOperation({ stepKey: 'gopay-approve', kind: 'click', label: 'click-continue' }, async () => {
await humanClickElement(button, { afterMs: 1200 });
return { clicked: true, target: describeElement(button) };
});
return { clicked: Boolean(clickResult.clicked), clickTarget: clickResult.target || '' };
}
async function clickGoPayPayNow() {
const delayOperation = typeof performGoPayOperationWithDelay === 'function'
? performGoPayOperationWithDelay
: async (metadata, operation) => {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
};
await waitForDocumentComplete();
const button = findPayNowButton();
if (!button) {
return { clicked: false, clickTarget: '' };
}
await delayOperation({ stepKey: 'gopay-approve', kind: 'click', label: 'click-pay-now' }, async () => {
await humanClickElement(button, { afterMs: 1500 });
});
return { clicked: true, clickTarget: describeElement(button) };
}
function inspectGoPayState() {
const bodyText = normalizeText(document.body?.innerText || document.body?.textContent || '');
const phoneInput = findPhoneInput();
const otpInput = findOtpInput();
const pinInput = findPinInput();
const payNowButton = findPayNowButton();
const continueButton = findContinueButton();
const terminalError = detectGoPayTerminalError(bodyText);
const successTextMatched = /success|successful|completed|selesai|berhasil|approved|authorized|支付成功|绑定成功|已授权/i.test(bodyText);
const completed = !phoneInput && !otpInput && !pinInput && successTextMatched;
const selectedCountryCode = readSelectedCountryCodeText();
return {
url: location.href,
readyState: document.readyState,
selectedCountryCode,
hasPhoneInput: Boolean(phoneInput),
hasOtpInput: Boolean(otpInput),
hasPinInput: Boolean(pinInput),
hasPayNowButton: Boolean(payNowButton),
hasContinueButton: Boolean(continueButton),
hasTerminalError: Boolean(terminalError),
terminalError,
completed,
textPreview: bodyText.slice(0, 500),
inputHints: getVisibleTextInputs().map((input) => getActionText(input).slice(0, 120)).filter(Boolean).slice(0, 12),
};
}