refactor flows into canonical runtime architecture

This commit is contained in:
QLHazyCoder
2026-05-21 07:21:34 +08:00
parent e2485d2e64
commit a7b35ee11a
167 changed files with 9034 additions and 3032 deletions
-245
View File
@@ -1,245 +0,0 @@
(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,
performOperationWithDelay: injectedPerformOperationWithDelay,
routeErrorPattern = null,
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;
const routeErrorMatched = routeErrorPattern instanceof RegExp
? routeErrorPattern.test(text)
: 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 && !fetchFailedMatched && !maxCheckAttemptsBlocked && !userAlreadyExistsBlocked) {
return null;
}
return {
path: pathname,
url: location.href,
retryButton,
retryEnabled: isActionEnabled(retryButton),
titleMatched,
detailMatched,
routeErrorMatched,
fetchFailedMatched,
maxCheckAttemptsBlocked,
userAlreadyExistsBlocked,
};
}
async function waitForRetryPageRecoveryAfterClick(options = {}) {
const {
pathPatterns = [],
pollIntervalMs = 250,
settleAfterClickMs = 3000,
} = options;
const startedAt = Date.now();
while (Date.now() - startedAt < settleAfterClickMs) {
if (typeof throwIfStopped === 'function') {
throwIfStopped();
}
const retryState = getAuthTimeoutErrorPageState({ pathPatterns });
if (!retryState) {
return {
recovered: true,
elapsedMs: Date.now() - startedAt,
};
}
await sleep(pollIntervalMs);
}
return {
recovered: false,
elapsedMs: Date.now() - startedAt,
};
}
async function recoverAuthRetryPage(options = {}) {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
const performOperationWithDelay = injectedPerformOperationWithDelay
|| rootScope.CodexOperationDelay?.performOperationWithDelay
|| (async (_metadata, operation) => operation());
const {
logLabel = '',
maxClickAttempts = 5,
pathPatterns = [],
pollIntervalMs = 250,
step = null,
timeoutMs = 12000,
waitAfterClickMs = 3000,
} = options;
const maxIdlePolls = timeoutMs > 0
? Math.max(1, Math.ceil(timeoutMs / Math.max(1, pollIntervalMs)))
: Number.POSITIVE_INFINITY;
let clickCount = 0;
let idlePollCount = 0;
while (clickCount < maxClickAttempts) {
if (typeof throwIfStopped === 'function') {
throwIfStopped();
}
const retryState = getAuthTimeoutErrorPageState({ pathPatterns });
if (!retryState) {
return {
recovered: clickCount > 0,
clickCount,
url: location.href,
};
}
if (retryState.maxCheckAttemptsBlocked) {
throw new Error(
'CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器'
);
}
if (retryState.userAlreadyExistsBlocked) {
throw new Error(
'SIGNUP_USER_ALREADY_EXISTS::步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。'
);
}
if (retryState.retryButton && retryState.retryEnabled) {
idlePollCount = 0;
clickCount += 1;
if (typeof log === 'function') {
const prefix = logLabel || `步骤 ${step || '?'}:检测到重试页,正在点击“重试”恢复`;
log(`${prefix}(第 ${clickCount} 次)...`, 'warn');
}
if (typeof humanPause === 'function') {
await humanPause(300, 800);
}
await performOperationWithDelay({ stepKey: options.stepKey || '', kind: 'click', label: 'auth-retry-click' }, async () => {
simulateClick(retryState.retryButton);
});
const recoveryResult = await waitForRetryPageRecoveryAfterClick({
pathPatterns,
pollIntervalMs,
settleAfterClickMs: waitAfterClickMs,
});
if (recoveryResult.recovered) {
return {
recovered: true,
clickCount,
url: location.href,
};
}
continue;
}
idlePollCount += 1;
if (idlePollCount >= maxIdlePolls) {
throw new Error(
`${logLabel || `步骤 ${step || '?'}:重试页恢复`}超时:重试按钮长时间不可点击。URL: ${location.href}`
);
}
await sleep(pollIntervalMs);
}
const finalRetryState = getAuthTimeoutErrorPageState({ pathPatterns });
if (!finalRetryState) {
return {
recovered: clickCount > 0,
clickCount,
url: location.href,
};
}
if (finalRetryState.maxCheckAttemptsBlocked) {
throw new Error(
'CF_SECURITY_BLOCKED::您已触发Cloudflare 安全防护系统,已完全停止流程,请不要短时间内多次进行重新发送验证码,连续刷新、反复点击重试会加重风控;请先关闭页面等待 15-30 分钟,让系统的临时限制自动解除。或者更换浏览器'
);
}
if (finalRetryState.userAlreadyExistsBlocked) {
throw new Error(
'SIGNUP_USER_ALREADY_EXISTS::步骤 4:检测到 user_already_exists,说明当前用户已存在,当前轮将直接停止。'
);
}
throw new Error(
`${logLabel || `步骤 ${step || '?'}:重试页恢复`}失败:已连续点击“重试” ${maxClickAttempts} 次,页面仍未恢复。URL: ${location.href}`
);
}
return {
getAuthRetryButton,
getAuthTimeoutErrorPageState,
recoverAuthRetryPage,
};
}
return {
createAuthPageRecovery,
};
});
-828
View File
@@ -1,828 +0,0 @@
// 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),
};
}
-358
View File
@@ -1,358 +0,0 @@
console.log('[MultiPage:kiro-desktop-authorize-page] Content script loaded on', location.href);
const KIRO_DESKTOP_AUTHORIZE_LISTENER_SENTINEL = 'data-multipage-kiro-desktop-authorize-page-listener';
const KIRO_DESKTOP_ALLOW_TEXT_PATTERN = /allow access|authorize|continue|allow|允许访问|授权|继续/i;
const KIRO_DESKTOP_LOADING_TEXT_PATTERN = /loading|redirecting|please wait|加载中|跳转中|请稍候/i;
const KIRO_DESKTOP_CLOUDFRONT_403_TEXT_PATTERN = /403 error|the request could not be satisfied|generated by cloudfront/i;
const KIRO_DESKTOP_AWS_REQUEST_ERROR_TEXT_PATTERN = /sorry,\s*there was an error processing your request\.?\s*please try again\.?|抱歉,处理您的请求时出错。请重试。?/i;
const KIRO_DESKTOP_EMAIL_INPUT_SELECTOR = [
'input[placeholder="username@example.com"]',
'input[type="email"]',
'input[name="email"]',
'input[autocomplete="username"]',
].join(', ');
const KIRO_DESKTOP_PASSWORD_INPUT_SELECTOR = [
'input[type="password"]',
'input[name="password"]',
'input[id*="password" i]',
'input[autocomplete="current-password"]',
].join(', ');
const KIRO_DESKTOP_OTP_INPUT_SELECTOR = [
'input[autocomplete="one-time-code"]',
'input[inputmode="numeric"]',
'input[placeholder*="6-digit" i]',
'input[placeholder*="6 位" i]',
'input[name*="otp" i]',
'input[name*="code" i]',
].join(', ');
function isVisibleDesktopElement(element) {
if (!element) return false;
const style = window.getComputedStyle(element);
const rect = element.getBoundingClientRect();
return style.display !== 'none'
&& style.visibility !== 'hidden'
&& rect.width > 0
&& rect.height > 0;
}
function collectVisibleDesktopElements(selector) {
return Array.from(document.querySelectorAll(selector))
.filter((element) => isVisibleDesktopElement(element));
}
function findFirstVisibleDesktopElement(selector) {
return collectVisibleDesktopElements(selector)[0] || null;
}
function getDesktopPageText() {
return String(document.body?.textContent || '')
.replace(/\s+/g, ' ')
.trim();
}
function getDesktopActionText(element) {
return [
element?.textContent,
element?.value,
element?.getAttribute?.('aria-label'),
element?.getAttribute?.('title'),
element?.getAttribute?.('data-testid'),
]
.filter(Boolean)
.join(' ')
.replace(/\s+/g, ' ')
.trim();
}
function findDesktopActionButton(options = {}) {
const {
preferredSelectors = [],
textPattern = null,
formOwner = null,
} = options;
for (const selector of preferredSelectors) {
const preferred = findFirstVisibleDesktopElement(selector);
if (preferred && !preferred.disabled && preferred.getAttribute('aria-disabled') !== 'true') {
return preferred;
}
}
const candidates = collectVisibleDesktopElements('button, [role="button"], input[type="submit"], input[type="button"]')
.filter((element) => !element.disabled && element.getAttribute('aria-disabled') !== 'true');
const prioritized = formOwner
? candidates.filter((element) => (element.form || element.closest?.('form') || null) === formOwner)
: [];
const pool = prioritized.length ? prioritized : candidates;
if (textPattern instanceof RegExp) {
return pool.find((element) => textPattern.test(getDesktopActionText(element))) || null;
}
return pool[0] || null;
}
function isKiroDesktopAuthHost(hostname = '') {
const normalized = String(hostname || '').trim().toLowerCase();
return normalized === 'view.awsapps.com'
|| normalized === 'login.awsapps.com'
|| normalized === 'profile.aws.amazon.com'
|| normalized === 'profile.aws'
|| normalized.endsWith('.profile.aws.amazon.com')
|| normalized.endsWith('.profile.aws')
|| normalized === 'signin.aws.amazon.com'
|| normalized === 'signin.aws'
|| normalized.endsWith('.signin.aws.amazon.com')
|| normalized.endsWith('.signin.aws');
}
function detectDesktopFatalState(pageText = '', currentUrl = '', pageTitle = '') {
const combinedText = `${pageTitle} ${pageText}`.replace(/\s+/g, ' ').trim();
const isKiroAuthHost = isKiroDesktopAuthHost(location.hostname || '');
if (isKiroAuthHost && KIRO_DESKTOP_AWS_REQUEST_ERROR_TEXT_PATTERN.test(combinedText)) {
return {
state: 'proxy_error_page',
url: currentUrl,
fatalMessage: 'Kiro 桌面授权页出现 AWS 请求异常,通常是当前代理 IP 或出口区域异常,请先切换代理后再重试。',
};
}
if (KIRO_DESKTOP_CLOUDFRONT_403_TEXT_PATTERN.test(combinedText)) {
return {
state: 'cloudfront_403_page',
url: currentUrl,
fatalMessage: 'Kiro 桌面授权页返回 403CloudFront 拒绝请求),通常是当前代理 IP 或区域触发了 AWS 风控,请更换代理后重试。',
};
}
return null;
}
function detectKiroDesktopAuthorizeState() {
const currentUrl = location.href;
const pageText = getDesktopPageText();
const fatalState = detectDesktopFatalState(pageText, currentUrl, document.title || '');
if (fatalState) {
return fatalState;
}
if (/^https?:\/\/(127\.0\.0\.1|localhost):/i.test(currentUrl)) {
const parsed = new URL(currentUrl);
if (parsed.searchParams.get('error')) {
return {
state: 'callback_error',
url: currentUrl,
error: parsed.searchParams.get('error_description') || parsed.searchParams.get('error') || 'unknown_error',
};
}
return {
state: 'callback_page',
url: currentUrl,
code: parsed.searchParams.get('code') || '',
stateValue: parsed.searchParams.get('state') || '',
};
}
const otpInput = findFirstVisibleDesktopElement(KIRO_DESKTOP_OTP_INPUT_SELECTOR);
if (otpInput) {
return {
state: 'otp_page',
url: currentUrl,
otpInput,
continueButton: findDesktopActionButton({
preferredSelectors: ['button[data-testid="email-verification-verify-button"]'],
textPattern: KIRO_DESKTOP_ALLOW_TEXT_PATTERN,
formOwner: otpInput.form || otpInput.closest?.('form') || null,
}),
};
}
const emailInput = findFirstVisibleDesktopElement(KIRO_DESKTOP_EMAIL_INPUT_SELECTOR);
if (emailInput) {
return {
state: 'relogin_email',
url: currentUrl,
emailInput,
continueButton: findDesktopActionButton({
preferredSelectors: ['button[data-testid="test-primary-button"]'],
textPattern: KIRO_DESKTOP_ALLOW_TEXT_PATTERN,
formOwner: emailInput.form || emailInput.closest?.('form') || null,
}),
};
}
const passwordInput = findFirstVisibleDesktopElement(KIRO_DESKTOP_PASSWORD_INPUT_SELECTOR);
if (passwordInput) {
return {
state: 'relogin_password',
url: currentUrl,
passwordInput,
continueButton: findDesktopActionButton({
preferredSelectors: ['button[data-testid="test-primary-button"]'],
textPattern: KIRO_DESKTOP_ALLOW_TEXT_PATTERN,
formOwner: passwordInput.form || passwordInput.closest?.('form') || null,
}),
};
}
const consentButton = findDesktopActionButton({
preferredSelectors: [
'button[data-testid="confirm-button"]',
'button[data-testid="allow-access-button"]',
],
textPattern: KIRO_DESKTOP_ALLOW_TEXT_PATTERN,
});
if (consentButton) {
return {
state: 'consent_page',
url: currentUrl,
actionButton: consentButton,
actionText: getDesktopActionText(consentButton),
};
}
if (KIRO_DESKTOP_LOADING_TEXT_PATTERN.test(pageText)) {
return {
state: 'redirecting',
url: currentUrl,
};
}
return {
state: 'loading',
url: currentUrl,
};
}
async function getCurrentDesktopAuthorizeState() {
const detected = detectKiroDesktopAuthorizeState();
if (detected.state === 'cloudfront_403_page' || detected.state === 'proxy_error_page') {
throw new Error(detected.fatalMessage || 'Kiro 桌面授权页出现代理异常。');
}
if (detected.state === 'callback_error') {
throw new Error(`Kiro 桌面授权回调失败:${detected.error || 'unknown_error'}`);
}
return detected;
}
async function submitDesktopEmail(payload = {}) {
const email = String(payload?.email || '').trim();
if (!email) {
throw new Error('缺少桌面授权邮箱,无法继续。');
}
const currentState = await getCurrentDesktopAuthorizeState();
if (currentState.state !== 'relogin_email' || !currentState.emailInput || !currentState.continueButton) {
throw new Error(`当前桌面授权页不是邮箱重登状态:${currentState.state}`);
}
fillInput(currentState.emailInput, email);
await sleep(150);
simulateClick(currentState.continueButton);
return {
submitted: true,
state: 'email_submitted',
url: location.href,
};
}
async function submitDesktopPassword(payload = {}) {
const password = String(payload?.password || '');
if (!password) {
throw new Error('缺少桌面授权密码,无法继续。');
}
const currentState = await getCurrentDesktopAuthorizeState();
if (currentState.state !== 'relogin_password' || !currentState.passwordInput || !currentState.continueButton) {
throw new Error(`当前桌面授权页不是密码重登状态:${currentState.state}`);
}
fillInput(currentState.passwordInput, password);
await sleep(150);
simulateClick(currentState.continueButton);
return {
submitted: true,
state: 'password_submitted',
url: location.href,
};
}
async function submitDesktopOtp(payload = {}) {
const code = String(payload?.code || '').trim();
if (!code) {
throw new Error('缺少桌面授权验证码,无法继续。');
}
const currentState = await getCurrentDesktopAuthorizeState();
if (currentState.state !== 'otp_page' || !currentState.otpInput || !currentState.continueButton) {
throw new Error(`当前桌面授权页不是验证码状态:${currentState.state}`);
}
fillInput(currentState.otpInput, code);
await sleep(150);
simulateClick(currentState.continueButton);
return {
submitted: true,
state: 'otp_submitted',
url: location.href,
};
}
async function confirmDesktopConsent() {
const currentState = await getCurrentDesktopAuthorizeState();
if (currentState.state !== 'consent_page' || !currentState.actionButton) {
throw new Error(`当前桌面授权页不是授权确认状态:${currentState.state}`);
}
simulateClick(currentState.actionButton);
return {
submitted: true,
state: 'consent_submitted',
url: location.href,
actionText: currentState.actionText || '',
};
}
async function handleKiroDesktopAuthorizeCommand(message) {
switch (message.type) {
case 'GET_KIRO_DESKTOP_AUTHORIZE_STATE':
return getCurrentDesktopAuthorizeState();
case 'EXECUTE_KIRO_DESKTOP_AUTHORIZE_ACTION': {
const action = String(message.payload?.action || '').trim();
if (action === 'submit-email') {
return submitDesktopEmail(message.payload || {});
}
if (action === 'submit-password') {
return submitDesktopPassword(message.payload || {});
}
if (action === 'submit-otp') {
return submitDesktopOtp(message.payload || {});
}
if (action === 'confirm-consent') {
return confirmDesktopConsent();
}
throw new Error(`desktop-authorize-page.js 不处理动作:${action}`);
}
default:
return null;
}
}
if (document.documentElement.getAttribute(KIRO_DESKTOP_AUTHORIZE_LISTENER_SENTINEL) !== '1') {
document.documentElement.setAttribute(KIRO_DESKTOP_AUTHORIZE_LISTENER_SENTINEL, '1');
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (
message.type === 'GET_KIRO_DESKTOP_AUTHORIZE_STATE'
|| message.type === 'EXECUTE_KIRO_DESKTOP_AUTHORIZE_ACTION'
) {
resetStopState();
handleKiroDesktopAuthorizeCommand(message)
.then((result) => {
sendResponse({ ok: true, ...(result || {}) });
})
.catch((error) => {
if (isStopError(error)) {
sendResponse({ stopped: true, error: error.message });
return;
}
sendResponse({ error: error?.message || String(error || '未知错误') });
});
return true;
}
return false;
});
}
File diff suppressed because it is too large Load Diff
-886
View File
@@ -1,886 +0,0 @@
// content/paypal-flow.js — PayPal login and approval helper.
console.log('[MultiPage:paypal-flow] Content script loaded on', location.href);
const PAYPAL_FLOW_LISTENER_SENTINEL = 'data-multipage-paypal-flow-listener';
const PAYPAL_HOSTED_DEFAULT_PHONE = '1234567890';
const PAYPAL_HOSTED_STAGE_OUTSIDE = 'outside_paypal';
const PAYPAL_HOSTED_STAGE_LOGIN = 'pay_login';
const PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT = 'guest_checkout';
const PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT = 'create_account';
const PAYPAL_HOSTED_STAGE_REVIEW = 'review_consent';
const PAYPAL_HOSTED_STAGE_APPROVAL = 'approval';
const PAYPAL_HOSTED_STAGE_UNKNOWN = 'unknown';
const PAYPAL_HOSTED_STEP_KEYS = {
[PAYPAL_HOSTED_STAGE_LOGIN]: 'paypal-hosted-email',
[PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT]: 'paypal-hosted-card',
[PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT]: 'paypal-hosted-create-account',
[PAYPAL_HOSTED_STAGE_REVIEW]: 'paypal-hosted-review',
};
if (document.documentElement.getAttribute(PAYPAL_FLOW_LISTENER_SENTINEL) !== '1') {
document.documentElement.setAttribute(PAYPAL_FLOW_LISTENER_SENTINEL, '1');
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (
message.type === 'PAYPAL_GET_STATE'
|| message.type === 'PAYPAL_SUBMIT_LOGIN'
|| message.type === 'PAYPAL_DISMISS_PROMPTS'
|| message.type === 'PAYPAL_CLICK_APPROVE'
|| message.type === 'PAYPAL_HOSTED_GET_STATE'
|| message.type === 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP'
) {
resetStopState();
handlePayPalCommand(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:paypal-flow] 消息监听已存在,跳过重复注册');
}
async function performPayPalOperationWithDelay(metadata, operation) {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
const gate = rootScope?.CodexOperationDelay?.performOperationWithDelay;
return typeof gate === 'function' ? gate(metadata, operation) : operation();
}
async function handlePayPalCommand(message) {
switch (message.type) {
case 'PAYPAL_GET_STATE':
return inspectPayPalState();
case 'PAYPAL_SUBMIT_LOGIN':
return submitPayPalLogin(message.payload || {});
case 'PAYPAL_DISMISS_PROMPTS':
return dismissPayPalPrompts();
case 'PAYPAL_CLICK_APPROVE':
return clickPayPalApprove();
case 'PAYPAL_HOSTED_GET_STATE':
return inspectPayPalHostedState();
case 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP':
return runPayPalHostedCheckoutStep(message.payload || {});
default:
throw new Error(`paypal-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 || 'PayPal page timed out waiting for target state.');
}
await sleep(intervalMs);
}
}
async function waitForDocumentComplete() {
await waitUntil(() => document.readyState === 'complete', { intervalMs: 200 });
await sleep(1000);
}
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?.('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 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) => {
const text = getActionText(el);
return normalizedPatterns.some((pattern) => pattern.test(text));
}) || null;
}
function findInputByPatterns(patterns) {
const inputs = getVisibleControls('input')
.filter((input) => {
const type = String(input.getAttribute('type') || input.type || '').trim().toLowerCase();
return isEnabledControl(input) && !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type);
});
return inputs.find((input) => {
const text = getActionText(input);
return patterns.some((pattern) => pattern.test(text));
}) || null;
}
function findEmailInput() {
const isPasswordCandidate = (input) => {
const type = String(input?.getAttribute?.('type') || input?.type || '').trim().toLowerCase();
const metadataText = normalizeText([
input?.textContent,
input?.getAttribute?.('aria-label'),
input?.getAttribute?.('title'),
input?.getAttribute?.('placeholder'),
input?.getAttribute?.('name'),
input?.id,
].filter(Boolean).join(' '));
return type === 'password' || /password|pass|密码/i.test(metadataText);
};
const inputs = getVisibleControls('input')
.filter((input) => {
const type = String(input.getAttribute('type') || input.type || '').trim().toLowerCase();
return isEnabledControl(input)
&& !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type)
&& !isPasswordCandidate(input);
});
return inputs.find((input) => [
/email|login|user|账号|邮箱/i,
].some((pattern) => pattern.test(getActionText(input))))
|| getVisibleControls('input[type="email"]').find((input) => isVisibleElement(input) && !isPasswordCandidate(input))
|| null;
}
function findPasswordInput() {
const inputs = getVisibleControls('input')
.filter((input) => {
const type = String(input.getAttribute('type') || input.type || '').trim().toLowerCase();
return isEnabledControl(input) && !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type);
});
return inputs.find((input) => {
const type = String(input.getAttribute('type') || input.type || '').trim().toLowerCase();
const metadataText = normalizeText([
input?.textContent,
input?.getAttribute?.('aria-label'),
input?.getAttribute?.('title'),
input?.getAttribute?.('placeholder'),
input?.getAttribute?.('name'),
input?.id,
].filter(Boolean).join(' '));
return type === 'password' || /password|pass|密码/i.test(metadataText);
}) || getVisibleControls('input[type="password"]').find(isVisibleElement) || null;
}
function findLoginNextButton() {
return findClickableByText([
/next|continue|login|log\s*in|sign\s*in/i,
/下一步|继续|登录|登入/i,
]);
}
function findEmailNextButton() {
return findClickableByText([
/next|btn\s*next|btnnext/i,
/下一页|下一步/i,
]);
}
function findPasswordLoginButton() {
const button = findClickableByText([
/login|log\s*in|sign\s*in/i,
/登录|登入/i,
]);
return button && button !== findEmailNextButton() ? button : null;
}
function findApproveButton() {
return findClickableByText([
/同意并继续|同意|继续|授权|确认并继续/i,
/agree\s*(?:and)?\s*continue|continue|accept|authorize|agree|pay\s*now/i,
]);
}
function getPayPalPathname() {
return String(location?.pathname || '').trim();
}
function isHostedLoginPage() {
return getPayPalPathname() === '/pay' || Boolean(document.getElementById('email'));
}
function isHostedGuestCheckoutPage() {
if (document.getElementById('cardNumber') || document.getElementById('billingLine1')) {
return true;
}
const pageText = normalizeText(document.body?.innerText || document.body?.textContent || '');
if (/create\s*(?:paypal\s*)?account|agree\s*(?:&|and)?\s*create|创建.*(?:账户|账号)/i.test(pageText)
&& findHostedCreateAccountButton()) {
return false;
}
return /\/checkoutweb\//i.test(getPayPalPathname())
&& Boolean(document.getElementById('phone') || document.getElementById('email'));
}
function isHostedReviewPage() {
return /\/webapps\/hermes/i.test(getPayPalPathname());
}
function findHostedCreateAccountButton() {
return document.getElementById('createAccount')
|| document.getElementById('createAccountButton')
|| document.querySelector('button[data-testid="createAccountButton"]')
|| document.querySelector('button[data-testid="create-account-button"]')
|| findClickableByText([
/agree\s*(?:&|and)?\s*create\s*(?:paypal\s*)?account/i,
/create\s*(?:paypal\s*)?account/i,
/同意.*创建|创建.*账户|创建.*账号/i,
]);
}
function isHostedCreateAccountPage() {
if (isHostedLoginPage()) {
return false;
}
if (document.getElementById('cardNumber') || document.getElementById('billingLine1')) {
return false;
}
const button = findHostedCreateAccountButton();
if (!button || !isVisibleElement(button) || !isEnabledControl(button)) {
return false;
}
const pageText = normalizeText(document.body?.innerText || document.body?.textContent || '');
return /create\s*(?:paypal\s*)?account|agree\s*(?:&|and)?\s*create|创建.*(?:账户|账号)/i.test(pageText)
|| /create/i.test(getActionText(button));
}
function findHostedReviewConsentButton() {
const direct = document.getElementById('consentButton')
|| document.querySelector('button[data-testid="consentButton"]');
if (direct && isVisibleElement(direct) && isEnabledControl(direct)) {
return direct;
}
return findClickableByText([
/agree\s*(?:and)?\s*continue|accept|continue/i,
/同意并继续|同意|继续/i,
]);
}
function detectPayPalHostedStage() {
if (!/paypal\./i.test(String(location?.host || ''))) {
return PAYPAL_HOSTED_STAGE_OUTSIDE;
}
if (isHostedGuestCheckoutPage()) {
return PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT;
}
if (isHostedReviewPage() && findHostedReviewConsentButton()) {
return PAYPAL_HOSTED_STAGE_REVIEW;
}
if (isHostedCreateAccountPage()) {
return PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT;
}
if (isHostedLoginPage()) {
return PAYPAL_HOSTED_STAGE_LOGIN;
}
return findApproveButton() ? PAYPAL_HOSTED_STAGE_APPROVAL : PAYPAL_HOSTED_STAGE_UNKNOWN;
}
function fillHostedInputById(id, value) {
const input = document.getElementById(String(id || '').trim());
if (!input || !isVisibleElement(input) || !isEnabledControl(input)) {
return false;
}
fillInput(input, String(value || ''));
return true;
}
function selectHostedOptionByIdText(id, value) {
const select = document.getElementById(String(id || '').trim());
const expected = normalizeText(value).toLowerCase();
if (!select || !expected) {
return false;
}
const option = Array.from(select.options || []).find((item) => {
const optionText = normalizeText(item.textContent || item.label || '').toLowerCase();
const optionValue = normalizeText(item.value || '').toLowerCase();
return optionText.includes(expected) || optionValue.includes(expected);
});
if (!option) {
return false;
}
select.value = option.value;
select.dispatchEvent(new Event('input', { bubbles: true }));
select.dispatchEvent(new Event('change', { bubbles: true }));
return true;
}
function findHostedSubmitButton() {
return document.querySelector('button[data-testid="submit-button"]')
|| document.querySelector('button[data-testid="hosted-payment-submit-button"]')
|| document.querySelector('button[data-atomic-wait-intent="Submit_Email"]')
|| document.querySelector('button.SubmitButton--complete')
|| findEmailNextButton()
|| findLoginNextButton()
|| findClickableByText([
/pay|continue|next|agree|subscribe/i,
/支付|继续|下一步|同意|订阅/i,
]);
}
function getHostedStepKey(stage = '', fallback = 'plus-checkout-create') {
return PAYPAL_HOSTED_STEP_KEYS[stage] || fallback;
}
async function clickHostedSubmitButton(options = {}) {
const stepKey = String(options.stepKey || getHostedStepKey(options.stage)).trim();
const label = String(options.label || 'hosted-paypal-submit').trim();
const maxAttempts = Math.max(1, Math.floor(Number(options.maxAttempts) || 3));
let lastButtonText = '';
let lastDisabled = false;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
const button = await waitUntil(() => {
const candidate = findHostedSubmitButton();
return candidate && isVisibleElement(candidate) ? candidate : null;
}, {
intervalMs: 500,
timeoutMs: 15000,
timeoutMessage: 'PayPal hosted checkout 未找到可点击的继续/提交按钮。',
});
lastButtonText = getActionText(button);
lastDisabled = !isEnabledControl(button);
if (lastDisabled) {
if (attempt >= maxAttempts) {
throw new Error('PayPal hosted checkout 继续/提交按钮长时间不可用。');
}
await sleep(1000);
continue;
}
await performPayPalOperationWithDelay({ stepKey, kind: 'click', label }, async () => {
simulateClick(button);
});
await sleep(1000);
return {
clicked: true,
buttonText: lastButtonText,
attempt,
};
}
return {
clicked: false,
buttonText: lastButtonText,
disabled: lastDisabled,
};
}
async function clickHostedEmailNextButton() {
const button = await waitUntil(() => {
const candidate = findEmailNextButton();
return candidate && isVisibleElement(candidate) && isEnabledControl(candidate) ? candidate : null;
}, {
intervalMs: 500,
timeoutMs: 15000,
timeoutMessage: 'PayPal hosted checkout 未找到邮箱页“下一页”按钮。',
});
const buttonText = getActionText(button);
await performPayPalOperationWithDelay({
stepKey: getHostedStepKey(PAYPAL_HOSTED_STAGE_LOGIN),
kind: 'click',
label: 'hosted-paypal-email-next',
}, async () => {
simulateClick(button);
});
return {
clicked: true,
buttonText,
};
}
function normalizeHostedPhoneDigits(value = '') {
return String(value || '').replace(/\D/g, '');
}
function verifyHostedPhoneBeforeSubmit(expectedPhone = '') {
const phoneInput = document.getElementById('phone');
if (!phoneInput || !isVisibleElement(phoneInput)) {
throw new Error('PayPal hosted checkout 未找到电话输入框。');
}
const expectedDigits = normalizeHostedPhoneDigits(expectedPhone || PAYPAL_HOSTED_DEFAULT_PHONE);
const renderedDigits = normalizeHostedPhoneDigits(phoneInput.value || '');
if (!expectedDigits) {
throw new Error('PayPal hosted checkout 电话配置为空。');
}
const comparableRenderedDigits = renderedDigits.length > expectedDigits.length
? renderedDigits.slice(-expectedDigits.length)
: renderedDigits;
if (comparableRenderedDigits !== expectedDigits) {
throw new Error(`PayPal hosted checkout 电话不一致:配置 ${expectedDigits},页面 ${renderedDigits || '(空)'}`);
}
return {
payloadPhoneDigits: expectedDigits,
renderedPhoneDigits: renderedDigits,
phoneMatched: true,
};
}
async function clickHostedCreateAccount(payload = {}) {
await waitForDocumentComplete();
const button = await waitUntil(() => {
const candidate = findHostedCreateAccountButton();
return candidate && isVisibleElement(candidate) && isEnabledControl(candidate) ? candidate : null;
}, {
intervalMs: 500,
timeoutMs: 30000,
timeoutMessage: 'PayPal hosted checkout 未找到创建账号确认按钮。',
});
await performPayPalOperationWithDelay({
stepKey: getHostedStepKey(PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT),
kind: 'click',
label: 'hosted-paypal-create-account',
}, async () => {
simulateClick(button);
});
return {
stage: PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT,
clicked: true,
submitted: true,
buttonText: getActionText(button),
};
}
function buildHostedRandomEmail() {
const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';
let value = '';
for (let index = 0; index < 16; index += 1) {
value += alphabet[Math.floor(Math.random() * alphabet.length)];
}
return `${value}@gmail.com`;
}
function buildHostedRandomPassword() {
const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^';
let value = 'Aa1!';
while (value.length < 14) {
value += alphabet[Math.floor(Math.random() * alphabet.length)];
}
return value;
}
function buildHostedVisaCard() {
const digits = [4, 1, 4, 7];
while (digits.length < 15) {
digits.push(Math.floor(Math.random() * 10));
}
const reversed = digits.slice().reverse();
let sum = 0;
for (let index = 0; index < reversed.length; index += 1) {
let digit = reversed[index];
if (index % 2 === 0) {
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
}
digits.push((10 - (sum % 10)) % 10);
const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0');
const year = (new Date().getFullYear() % 100) + 3;
return {
number: digits.join(''),
expiry: `${month} / ${year}`,
cvv: String(Math.floor(100 + Math.random() * 900)),
};
}
async function submitHostedLogin(payload = {}) {
await waitForDocumentComplete();
const email = normalizeText(payload.email || buildHostedRandomEmail());
const emailInput = document.getElementById('email') || findEmailInput();
if (!emailInput) {
throw new Error('PayPal hosted checkout 未找到邮箱输入框。');
}
refillPayPalEmailInput(emailInput, email);
const clickResult = await clickHostedEmailNextButton();
return {
stage: PAYPAL_HOSTED_STAGE_LOGIN,
submitted: true,
generatedEmail: email,
clicked: Boolean(clickResult.clicked),
};
}
async function fillHostedGuestCheckout(payload = {}) {
await waitForDocumentComplete();
const countrySelect = document.getElementById('country');
if (countrySelect && String(countrySelect.value || '').trim().toUpperCase() !== 'US') {
countrySelect.value = 'US';
countrySelect.dispatchEvent(new Event('change', { bubbles: true }));
await sleep(1000);
}
const generatedCard = buildHostedVisaCard();
const address = payload.address && typeof payload.address === 'object' ? payload.address : {};
const values = {
email: normalizeText(payload.email || buildHostedRandomEmail()),
phone: normalizeText(payload.phone || PAYPAL_HOSTED_DEFAULT_PHONE),
cardNumber: String(payload.cardNumber || generatedCard.number).replace(/\s+/g, ''),
cardExpiry: normalizeText(payload.cardExpiry || generatedCard.expiry),
cardCvv: normalizeText(payload.cardCvv || generatedCard.cvv),
password: String(payload.password || buildHostedRandomPassword()),
firstName: normalizeText(payload.firstName || 'James'),
lastName: normalizeText(payload.lastName || 'Smith'),
};
fillHostedInputById('email', values.email);
fillHostedInputById('phone', values.phone);
fillHostedInputById('cardNumber', values.cardNumber);
fillHostedInputById('cardExpiry', values.cardExpiry);
fillHostedInputById('cardCvv', values.cardCvv);
fillHostedInputById('password', values.password);
fillHostedInputById('firstName', values.firstName);
fillHostedInputById('lastName', values.lastName);
fillHostedInputById('billingLine1', address.street || address.address1 || '');
fillHostedInputById('billingCity', address.city || '');
fillHostedInputById('billingPostalCode', address.zip || address.postalCode || '');
selectHostedOptionByIdText('billingState', address.state || address.region || '');
const phoneCheck = verifyHostedPhoneBeforeSubmit(values.phone);
const clickResult = await clickHostedSubmitButton({
stage: PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT,
label: 'hosted-paypal-card-submit',
maxAttempts: 4,
});
return {
stage: PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT,
submitted: true,
payloadPhone: values.phone,
...phoneCheck,
};
}
async function clickHostedReviewConsent() {
await waitForDocumentComplete();
const button = await waitUntil(() => {
const candidate = findHostedReviewConsentButton();
return candidate && isVisibleElement(candidate) && isEnabledControl(candidate) ? candidate : null;
}, {
intervalMs: 500,
timeoutMs: 30000,
timeoutMessage: 'PayPal hosted checkout 未找到账单确认按钮。',
});
await performPayPalOperationWithDelay({
stepKey: getHostedStepKey(PAYPAL_HOSTED_STAGE_REVIEW),
kind: 'click',
label: 'hosted-paypal-review-consent',
}, async () => {
simulateClick(button);
});
return {
stage: PAYPAL_HOSTED_STAGE_REVIEW,
submitted: true,
};
}
async function runPayPalHostedCheckoutStep(payload = {}) {
const stage = detectPayPalHostedStage();
const expectedStage = String(payload.expectedStage || '').trim();
if (expectedStage && stage !== expectedStage) {
return {
stage,
expectedStage,
submitted: false,
skipped: true,
approveReady: Boolean(findApproveButton()),
};
}
if (stage === PAYPAL_HOSTED_STAGE_LOGIN) {
return submitHostedLogin(payload);
}
if (stage === PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT) {
return fillHostedGuestCheckout(payload);
}
if (stage === PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT) {
return clickHostedCreateAccount(payload);
}
if (stage === PAYPAL_HOSTED_STAGE_REVIEW) {
return clickHostedReviewConsent();
}
return {
stage,
submitted: false,
approveReady: Boolean(findApproveButton()),
};
}
function inspectPayPalHostedState() {
const stage = detectPayPalHostedStage();
const createAccountButton = findHostedCreateAccountButton();
return {
url: location.href,
readyState: document.readyState,
hostedStage: stage,
hasGuestCardFields: Boolean(document.getElementById('cardNumber')),
hasHostedEmailInput: Boolean(document.getElementById('email') || findEmailInput()),
createAccountReady: Boolean(createAccountButton && isVisibleElement(createAccountButton) && isEnabledControl(createAccountButton)),
reviewConsentReady: Boolean(findHostedReviewConsentButton()),
approveReady: Boolean(findApproveButton()),
bodyTextPreview: normalizeText(document.body?.innerText || '').slice(0, 240),
};
}
function findPasskeyPromptButtons() {
const promptPatterns = [
/passkey|通行密钥|安全密钥|下次登录|faster|save/i,
];
const bodyText = normalizeText(document.body?.innerText || '');
const likelyPrompt = promptPatterns.some((pattern) => pattern.test(bodyText));
if (!likelyPrompt) {
return [];
}
const cancelOrClose = getVisibleControls('button, a, [role="button"]')
.filter((el) => {
const text = getActionText(el);
return /取消|稍后|不保存|不用|关闭|cancel|not now|maybe later|skip|close|x/i.test(text)
|| el.getAttribute?.('aria-label')?.match(/close|关闭/i);
});
const iconCloseButtons = getVisibleControls('button, [role="button"]')
.filter((el) => {
const text = getActionText(el);
const rect = el.getBoundingClientRect();
return (/^×$|^x$/i.test(text) || /close|关闭/i.test(text))
&& rect.width <= 64
&& rect.height <= 64;
});
return [...cancelOrClose, ...iconCloseButtons];
}
function hasPasskeyPrompt() {
return findPasskeyPromptButtons().length > 0;
}
function getPayPalLoginPhase(emailInput, passwordInput) {
const emailNextButton = findEmailNextButton();
const passwordLoginButton = findPasswordLoginButton();
if (emailInput && emailNextButton && isEnabledControl(emailNextButton) && (!passwordInput || !passwordLoginButton)) {
return 'email';
}
if (emailInput && passwordInput) return 'login_combined';
if (passwordInput) return 'password';
if (emailInput) return 'email';
return '';
}
function refillPayPalEmailInput(emailInput, email) {
if (!emailInput) return;
if (typeof emailInput.focus === 'function') {
emailInput.focus();
}
fillInput(emailInput, '');
fillInput(emailInput, email);
if (typeof emailInput.blur === 'function') {
emailInput.blur();
}
}
async function submitPayPalLogin(payload = {}) {
const delayOperation = typeof performPayPalOperationWithDelay === 'function'
? performPayPalOperationWithDelay
: 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 email = normalizeText(payload.email || '');
const password = String(payload.password || '');
if (!password) {
throw new Error('PayPal 密码为空,请先在侧边栏配置。');
}
let passwordInput = findPasswordInput();
const emailInput = findEmailInput();
const emailNextButton = findEmailNextButton();
if (emailInput && emailNextButton && isEnabledControl(emailNextButton) && (!passwordInput || !findPasswordLoginButton())) {
await delayOperation({ stepKey: 'paypal-approve', kind: 'submit', label: 'paypal-email' }, async () => {
refillPayPalEmailInput(emailInput, email);
simulateClick(emailNextButton);
});
return {
submitted: false,
phase: 'email_submitted',
awaiting: 'password_page',
};
}
if (!passwordInput && emailInput && email) {
await delayOperation({ stepKey: 'paypal-approve', kind: 'submit', label: 'paypal-email' }, async () => {
refillPayPalEmailInput(emailInput, email);
const nextButton = await waitUntil(() => {
const button = findEmailNextButton() || findLoginNextButton();
return button && isEnabledControl(button) ? button : null;
}, {
intervalMs: 250,
timeoutMs: 8000,
timeoutMessage: 'PayPal email page did not expose a clickable next/continue button.',
});
simulateClick(nextButton);
});
return {
submitted: false,
phase: 'email_submitted',
awaiting: 'password_page',
};
} else if (!passwordInput && emailInput && !email) {
throw new Error('PayPal 账号为空,请先在侧边栏配置。');
} else if (emailInput && email) {
await delayOperation({ stepKey: 'paypal-approve', kind: 'fill', label: 'paypal-email' }, async () => {
refillPayPalEmailInput(emailInput, email);
});
}
passwordInput = passwordInput || await waitUntil(() => findPasswordInput(), {
intervalMs: 250,
timeoutMs: 8000,
timeoutMessage: 'PayPal password page did not expose a password input.',
});
await delayOperation({ stepKey: 'paypal-approve', kind: 'submit', label: 'paypal-password' }, async () => {
fillInput(passwordInput, password);
await sleep(1000);
const loginButton = await waitUntil(() => {
const button = findClickableByText([
/login|log\s*in|sign\s*in|continue/i,
/登录|登入|继续/i,
]);
return button && isEnabledControl(button) ? button : null;
}, {
intervalMs: 250,
timeoutMs: 8000,
timeoutMessage: 'PayPal password page did not expose a clickable login/continue button.',
});
simulateClick(loginButton);
});
return {
submitted: true,
phase: 'password_submitted',
awaiting: 'redirect_or_approval',
};
}
async function dismissPayPalPrompts() {
const delayOperation = typeof performPayPalOperationWithDelay === 'function'
? performPayPalOperationWithDelay
: 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 buttons = findPasskeyPromptButtons();
let clicked = 0;
for (const button of buttons) {
if (!isVisibleElement(button) || !isEnabledControl(button)) {
continue;
}
await delayOperation({ stepKey: 'paypal-approve', kind: 'click', label: 'paypal-dismiss-prompt' }, async () => {
simulateClick(button);
});
clicked += 1;
await sleep(500);
}
return {
clicked,
hasPromptAfterClick: hasPasskeyPrompt(),
};
}
async function clickPayPalApprove() {
const delayOperation = typeof performPayPalOperationWithDelay === 'function'
? performPayPalOperationWithDelay
: 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();
await dismissPayPalPrompts().catch(() => ({ clicked: 0 }));
const button = findApproveButton();
if (!button || !isEnabledControl(button)) {
return {
clicked: false,
state: inspectPayPalState(),
};
}
await delayOperation({ stepKey: 'paypal-approve', kind: 'click', label: 'paypal-approve' }, async () => {
simulateClick(button);
});
return {
clicked: true,
buttonText: getActionText(button),
};
}
function inspectPayPalState() {
const emailInput = findEmailInput();
const passwordInput = findPasswordInput();
const approveButton = findApproveButton();
const loginPhase = getPayPalLoginPhase(emailInput, passwordInput);
return {
url: location.href,
readyState: document.readyState,
needsLogin: Boolean(loginPhase),
loginPhase,
hasEmailInput: Boolean(emailInput),
hasPasswordInput: Boolean(passwordInput),
approveReady: Boolean(approveButton && isEnabledControl(approveButton)),
approveButtonText: approveButton ? getActionText(approveButton) : '',
hasPasskeyPrompt: hasPasskeyPrompt(),
bodyTextPreview: normalizeText(document.body?.innerText || '').slice(0, 240),
};
}
File diff suppressed because it is too large Load Diff
-237
View File
@@ -1,237 +0,0 @@
(function attachPhoneCountryUtils(root, factory) {
root.MultiPagePhoneCountryUtils = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createPhoneCountryUtils() {
const KNOWN_DIAL_CODES = Object.freeze([
'1246', '1264', '1268', '1284', '1340', '1345', '1441', '1473', '1649', '1664', '1670', '1671', '1684',
'1721', '1758', '1767', '1784', '1809', '1829', '1849', '1868', '1869', '1876',
'971', '962', '886', '880', '856', '855', '852', '853', '673', '672', '670', '599', '598', '597', '596',
'595', '594', '593', '592', '591', '590', '509', '508', '507', '506', '505', '504', '503', '502', '501',
'423', '421', '420', '389', '387', '386', '385', '383', '382', '381', '380', '379', '378', '377', '376',
'375', '374', '373', '372', '371', '370', '359', '358', '357', '356', '355', '354', '353', '352', '351',
'350', '299', '298', '297', '291', '290', '269', '268', '267', '266', '265', '264', '263', '262', '261',
'260', '258', '257', '256', '255', '254', '253', '252', '251', '250', '249', '248', '247', '246', '245',
'244', '243', '242', '241', '240', '239', '238', '237', '236', '235', '234', '233', '232', '231', '230',
'229', '228', '227', '226', '225', '224', '223', '222', '221', '220', '218', '216', '213', '212', '211',
'98', '95', '94', '93', '92', '91', '90', '89', '88', '86', '84', '82', '81', '66', '65', '64', '63',
'62', '61', '60', '58', '57', '56', '55', '54', '53', '52', '51', '49', '48', '47', '46', '45', '44',
'43', '41', '40', '39', '36', '34', '33', '32', '31', '30', '27', '20', '7', '1',
]);
function normalizePhoneDigits(value) {
let digits = String(value || '').replace(/\D+/g, '');
if (digits.startsWith('00')) {
digits = digits.slice(2);
}
return digits;
}
function extractDialCodeFromText(value) {
const match = String(value || '').match(/\(\+\s*(\d{1,4})\s*\)|\+\s*\(\s*(\d{1,4})\s*\)|\+\s*(\d{1,4})\b/);
return String(match?.[1] || match?.[2] || match?.[3] || '').trim();
}
function normalizeCountryLabel(value) {
return String(value || '')
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/&/g, ' and ')
.replace(/[^\p{L}\p{N}\s]/gu, ' ')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}
function normalizeCountryOptionValue(value) {
return String(value || '').trim().toUpperCase();
}
function getCountryLabelAliases(value) {
const aliases = new Set();
const addAlias = (alias) => {
const normalized = normalizeCountryLabel(alias);
if (normalized) {
aliases.add(normalized);
}
};
const raw = String(value || '').trim();
addAlias(raw);
const normalized = normalizeCountryLabel(raw);
const compact = normalized.replace(/\s+/g, '');
if (
/(?:^|\s)(?:gb|uk)(?:\s|$)/i.test(raw)
|| /england|united\s*kingdom|great\s*britain|\bbritain\b/i.test(raw)
|| /英国|英格兰|大不列颠/.test(raw)
|| ['gb', 'uk', 'england', 'unitedkingdom', 'greatbritain', 'britain'].includes(compact)
) {
[
'GB',
'UK',
'United Kingdom',
'Great Britain',
'Britain',
'England',
'英国',
'英格兰',
'大不列颠',
].forEach(addAlias);
}
return Array.from(aliases);
}
function getRegionDisplayName(regionCode, locale) {
const normalizedRegionCode = normalizeCountryOptionValue(regionCode);
const normalizedLocale = String(locale || '').trim();
if (!/^[A-Z]{2}$/.test(normalizedRegionCode) || !normalizedLocale || typeof Intl?.DisplayNames !== 'function') {
return '';
}
try {
return String(
new Intl.DisplayNames([normalizedLocale], { type: 'region' }).of(normalizedRegionCode) || ''
).trim();
} catch {
return '';
}
}
function getOptionLabel(option) {
return String(option?.textContent || option?.label || '')
.replace(/\s+/g, ' ')
.trim();
}
function getOptionMatchLabels(option, options = {}) {
const labels = new Set();
const pushLabel = (value) => {
const label = String(value || '').replace(/\s+/g, ' ').trim();
if (label) {
labels.add(label);
}
};
const getLabel = typeof options.getOptionLabel === 'function'
? options.getOptionLabel
: getOptionLabel;
pushLabel(getLabel(option));
const regionCode = normalizeCountryOptionValue(option?.value);
if (/^[A-Z]{2}$/.test(regionCode)) {
pushLabel(regionCode);
pushLabel(getRegionDisplayName(regionCode, 'en'));
const pageLocale = String(
options.pageLocale
|| options.document?.documentElement?.lang
|| options.document?.documentElement?.getAttribute?.('lang')
|| options.navigator?.language
|| ''
).trim();
if (pageLocale && !/^en(?:[-_]|$)/i.test(pageLocale)) {
pushLabel(getRegionDisplayName(regionCode, pageLocale));
}
}
return Array.from(labels);
}
function resolveDialCodeFromPhoneNumber(phoneNumber = '', texts = []) {
const digits = normalizePhoneDigits(phoneNumber);
if (!digits) {
return '';
}
const textDialCodes = texts
.map((text) => normalizePhoneDigits(extractDialCodeFromText(text)))
.filter((dialCode) => dialCode && digits.startsWith(dialCode) && digits.length > dialCode.length)
.sort((left, right) => right.length - left.length);
if (textDialCodes[0]) {
return textDialCodes[0];
}
return KNOWN_DIAL_CODES.find((code) => digits.startsWith(code) && digits.length > code.length) || '';
}
function findOptionByCountryLabel(options, countryLabel, config = {}) {
const source = Array.from(options || []);
const normalizedTargets = getCountryLabelAliases(countryLabel);
if (source.length === 0 || normalizedTargets.length === 0) {
return null;
}
return source.find((option) => (
getOptionMatchLabels(option, config).some((label) => normalizedTargets.includes(normalizeCountryLabel(label)))
))
|| source.find((option) => {
const normalizedLabels = getOptionMatchLabels(option, config)
.map((label) => normalizeCountryLabel(label))
.filter(Boolean);
return normalizedLabels.some((optionLabel) => normalizedTargets.some((normalizedTarget) => (
optionLabel.length > 2
&& normalizedTarget.length > 2
&& (optionLabel.includes(normalizedTarget) || normalizedTarget.includes(optionLabel))
)));
})
|| null;
}
function findOptionByPhoneNumber(options, phoneNumber, config = {}) {
const source = Array.from(options || []);
const digits = normalizePhoneDigits(phoneNumber);
if (source.length === 0 || !digits) {
return null;
}
const getLabel = typeof config.getOptionLabel === 'function'
? config.getOptionLabel
: getOptionLabel;
let bestMatch = null;
let bestDialCodeLength = 0;
for (const option of source) {
const dialCode = normalizePhoneDigits(extractDialCodeFromText(getLabel(option)));
if (!dialCode || !digits.startsWith(dialCode) || dialCode.length <= bestDialCodeLength) {
continue;
}
bestMatch = option;
bestDialCodeLength = dialCode.length;
}
return bestMatch;
}
function findElementByDialCode(elements, phoneNumber, config = {}) {
const source = Array.from(elements || []);
const digits = normalizePhoneDigits(phoneNumber);
if (source.length === 0 || !digits) {
return null;
}
const getText = typeof config.getText === 'function' ? config.getText : getOptionLabel;
let bestMatch = null;
let bestDialCodeLength = 0;
for (const element of source) {
const dialCode = normalizePhoneDigits(extractDialCodeFromText(getText(element)));
if (!dialCode || !digits.startsWith(dialCode) || dialCode.length <= bestDialCodeLength) {
continue;
}
bestMatch = element;
bestDialCodeLength = dialCode.length;
}
return bestMatch;
}
return {
extractDialCodeFromText,
findElementByDialCode,
findOptionByCountryLabel,
findOptionByPhoneNumber,
getCountryLabelAliases,
getOptionLabel,
getOptionMatchLabels,
getRegionDisplayName,
normalizeCountryLabel,
normalizeCountryOptionValue,
normalizePhoneDigits,
resolveDialCodeFromPhoneNumber,
};
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-686
View File
@@ -1,686 +0,0 @@
// content/sub2api-panel.js — 页内脚本:SUB2API 后台(OAuth 生成与回调提交)
console.log('[MultiPage:sub2api-panel] Content script loaded on', location.href);
const SUB2API_PANEL_LISTENER_SENTINEL = 'data-multipage-sub2api-panel-listener';
const SUB2API_DEFAULT_GROUP_NAME = 'codex';
const SUB2API_DEFAULT_PROXY_NAME = '';
const SUB2API_DEFAULT_REDIRECT_URI = 'http://localhost:1455/auth/callback';
const SUB2API_DEFAULT_CONCURRENCY = 10;
const SUB2API_DEFAULT_PRIORITY = 1;
const SUB2API_DEFAULT_RATE_MULTIPLIER = 1;
if (document.documentElement.getAttribute(SUB2API_PANEL_LISTENER_SENTINEL) !== '1') {
document.documentElement.setAttribute(SUB2API_PANEL_LISTENER_SENTINEL, '1');
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'EXECUTE_NODE' || message.type === 'REQUEST_OAUTH_URL') {
resetStopState();
const handler = message.type === 'REQUEST_OAUTH_URL'
? requestOAuthUrl(message.payload)
: handleNode(message.nodeId || message.payload?.nodeId, message.payload);
handler.then((result) => {
sendResponse({ ok: true, ...(result || {}) });
}).catch((err) => {
if (isStopError(err)) {
if (message.payload?.visibleStep || message.step) {
log('已被用户停止。', 'warn', { step: message.payload?.visibleStep || message.step });
}
sendResponse({ stopped: true, error: err.message });
return;
}
if (message.nodeId || message.payload?.nodeId) {
reportError(message.nodeId || message.payload?.nodeId, err.message);
}
sendResponse({ error: err.message });
});
return true;
}
});
} else {
console.log('[MultiPage:sub2api-panel] 消息监听已存在,跳过重复注册');
}
function getSub2ApiOrigin(payload = {}) {
const rawUrl = payload.sub2apiUrl || location.href;
try {
return new URL(rawUrl).origin;
} catch {
return location.origin;
}
}
function normalizeRedirectUri() {
const input = SUB2API_DEFAULT_REDIRECT_URI;
const withProtocol = /^https?:\/\//i.test(input) ? input : `http://${input}`;
const parsed = new URL(withProtocol);
if (!parsed.pathname || parsed.pathname === '/') {
parsed.pathname = '/auth/callback';
}
if (parsed.pathname !== '/auth/callback') {
throw new Error('SUB2API 回调地址必须是 /auth/callback,例如 http://localhost:1455/auth/callback');
}
return parsed.toString();
}
async function handleStep(step, payload = {}) {
switch (step) {
case 1:
return step1_generateOpenAiAuthUrl(payload);
case 10:
case 12:
case 13:
return step9_submitOpenAiCallback({ ...(payload || {}), visibleStep: step });
default:
throw new Error(`sub2api-panel.js 不处理步骤 ${step}`);
}
}
async function handleNode(nodeId, payload = {}) {
const normalizedNodeId = String(nodeId || '').trim();
switch (normalizedNodeId) {
case 'platform-verify':
return step9_submitOpenAiCallback(payload);
default:
throw new Error(`sub2api-panel.js 不处理节点 ${normalizedNodeId}`);
}
}
async function requestOAuthUrl(payload = {}) {
return step1_generateOpenAiAuthUrl(payload, { report: false });
}
async function requestJson(origin, path, options = {}) {
throwIfStopped();
const {
method = 'GET',
token = '',
body = undefined,
} = options;
const response = await fetch(`${origin}${path}`, {
method,
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: body === undefined ? undefined : JSON.stringify(body),
});
const text = await response.text();
let json = null;
try {
json = text ? JSON.parse(text) : null;
} catch {
json = null;
}
if (json && typeof json === 'object' && 'code' in json) {
if (json.code === 0) {
return json.data;
}
throw new Error(json.message || json.detail || `请求失败(${path}`);
}
if (!response.ok) {
throw new Error((json && (json.message || json.detail)) || `请求失败(HTTP ${response.status}):${path}`);
}
return json;
}
function storeAuthSession(loginData) {
if (!loginData?.access_token) {
throw new Error('SUB2API 登录返回缺少 access_token。');
}
localStorage.setItem('auth_token', loginData.access_token);
if (loginData.refresh_token) {
localStorage.setItem('refresh_token', loginData.refresh_token);
} else {
localStorage.removeItem('refresh_token');
}
if (loginData.expires_in) {
localStorage.setItem('token_expires_at', String(Date.now() + Number(loginData.expires_in) * 1000));
}
if (loginData.user) {
localStorage.setItem('auth_user', JSON.stringify(loginData.user));
}
sessionStorage.removeItem('auth_expired');
}
async function loginSub2Api(payload = {}) {
const email = (payload.sub2apiEmail || '').trim();
const password = payload.sub2apiPassword || '';
const origin = getSub2ApiOrigin(payload);
if (!email) {
throw new Error('缺少 SUB2API 登录邮箱,请先在侧边栏填写。');
}
if (!password) {
throw new Error('缺少 SUB2API 登录密码,请先在侧边栏填写。');
}
log('步骤:正在登录 SUB2API 后台...');
const loginData = await requestJson(origin, '/api/v1/auth/login', {
method: 'POST',
body: {
email,
password,
},
});
storeAuthSession(loginData);
return {
origin,
token: loginData.access_token,
user: loginData.user || null,
};
}
async function getGroupByName(origin, token, groupName) {
const targetName = (groupName || SUB2API_DEFAULT_GROUP_NAME).trim() || SUB2API_DEFAULT_GROUP_NAME;
const groups = await requestJson(origin, '/api/v1/admin/groups/all', {
method: 'GET',
token,
});
const normalized = targetName.toLowerCase();
const group = (groups || []).find((item) => {
const itemName = String(item?.name || '').trim().toLowerCase();
if (!itemName) return false;
if (itemName !== normalized) return false;
return !item.platform || item.platform === 'openai';
});
if (!group) {
throw new Error(`SUB2API 中未找到名为“${targetName}”的 openai 分组。`);
}
return group;
}
function normalizeSub2ApiGroupNames(value) {
const source = Array.isArray(value)
? value
: String(value || '').split(/[\r\n,;]+/);
const seen = new Set();
const names = [];
for (const item of source) {
const name = String(item || '').trim();
const key = name.toLowerCase();
if (!name || seen.has(key)) continue;
seen.add(key);
names.push(name);
}
return names.length ? names : [SUB2API_DEFAULT_GROUP_NAME];
}
async function getGroupsByNames(origin, token, groupNames) {
const targetNames = normalizeSub2ApiGroupNames(groupNames);
const groups = await requestJson(origin, '/api/v1/admin/groups/all', {
method: 'GET',
token,
});
const matched = [];
const missing = [];
for (const targetName of targetNames) {
const normalized = targetName.toLowerCase();
const group = (groups || []).find((item) => {
const itemName = String(item?.name || '').trim().toLowerCase();
if (!itemName) return false;
if (itemName !== normalized) return false;
return !item.platform || item.platform === 'openai';
});
if (group) {
matched.push(group);
} else {
missing.push(targetName);
}
}
if (missing.length) {
throw new Error(`SUB2API 中未找到以下 openai 分组:${missing.join('、')}`);
}
return matched;
}
function normalizeSub2ApiProxyPreference(value) {
return String(value || '').trim();
}
function resolveSub2ApiProxyPreference(payload = {}, backgroundState = {}) {
if (payload.sub2apiDefaultProxyName !== undefined) {
return normalizeSub2ApiProxyPreference(payload.sub2apiDefaultProxyName);
}
if (backgroundState.sub2apiDefaultProxyName !== undefined) {
return normalizeSub2ApiProxyPreference(backgroundState.sub2apiDefaultProxyName);
}
return SUB2API_DEFAULT_PROXY_NAME;
}
function resolveSub2ApiAccountPriority(payload = {}, backgroundState = {}) {
const candidate = payload.sub2apiAccountPriority !== undefined
? payload.sub2apiAccountPriority
: backgroundState.sub2apiAccountPriority;
const rawValue = String(candidate ?? '').trim();
if (!rawValue) {
return SUB2API_DEFAULT_PRIORITY;
}
const numeric = Number(rawValue);
if (!Number.isSafeInteger(numeric) || numeric < 1) {
throw new Error('SUB2API 账号优先级必须是大于等于 1 的整数。');
}
return numeric;
}
function normalizeProxyId(value) {
if (value === undefined || value === null || value === '') {
return null;
}
const normalized = Number(value);
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
return null;
}
return normalized;
}
function buildProxyDisplayName(proxy = {}) {
const id = normalizeProxyId(proxy.id);
const name = String(proxy.name || '').trim();
const protocol = String(proxy.protocol || '').trim();
const host = String(proxy.host || '').trim();
const port = proxy.port === undefined || proxy.port === null ? '' : String(proxy.port).trim();
const address = protocol && host && port ? `${protocol}://${host}:${port}` : '';
const parts = [
name || '(未命名代理)',
id ? `#${id}` : '',
address,
].filter(Boolean);
return parts.join(' ');
}
function buildProxySearchText(proxy = {}) {
return [
proxy.id,
proxy.name,
proxy.protocol,
proxy.host,
proxy.port,
buildProxyDisplayName(proxy),
]
.filter((value) => value !== undefined && value !== null && value !== '')
.map((value) => String(value).trim().toLowerCase())
.filter(Boolean)
.join(' ');
}
function isActiveProxy(proxy = {}) {
const status = String(proxy.status || '').trim().toLowerCase();
return !status || status === 'active';
}
function findSub2ApiProxy(proxies = [], preference = '') {
const activeProxies = (Array.isArray(proxies) ? proxies : [])
.filter(isActiveProxy)
.filter((proxy) => normalizeProxyId(proxy.id));
const normalizedPreference = normalizeSub2ApiProxyPreference(preference).toLowerCase();
const preferredId = normalizeProxyId(normalizedPreference);
if (preferredId) {
const matchedById = activeProxies.find((proxy) => normalizeProxyId(proxy.id) === preferredId);
return {
proxy: matchedById || null,
reason: matchedById ? 'id' : 'missing-id',
candidates: activeProxies,
};
}
if (normalizedPreference) {
const exactMatches = activeProxies.filter((proxy) => {
const name = String(proxy.name || '').trim().toLowerCase();
return name === normalizedPreference;
});
if (exactMatches.length === 1) {
return { proxy: exactMatches[0], reason: 'name', candidates: activeProxies };
}
if (exactMatches.length > 1) {
return { proxy: null, reason: 'ambiguous-name', candidates: exactMatches };
}
const fuzzyMatches = activeProxies.filter((proxy) => buildProxySearchText(proxy).includes(normalizedPreference));
if (fuzzyMatches.length === 1) {
return { proxy: fuzzyMatches[0], reason: 'fuzzy', candidates: activeProxies };
}
if (fuzzyMatches.length > 1) {
return { proxy: null, reason: 'ambiguous-fuzzy', candidates: fuzzyMatches };
}
return { proxy: null, reason: 'missing-name', candidates: activeProxies };
}
if (activeProxies.length === 1) {
return { proxy: activeProxies[0], reason: 'single-active', candidates: activeProxies };
}
return {
proxy: null,
reason: activeProxies.length ? 'no-preference' : 'none-active',
candidates: activeProxies,
};
}
async function resolveSub2ApiProxy(origin, token, preference = '') {
const proxies = await requestJson(origin, '/api/v1/admin/proxies/all?with_count=true', {
method: 'GET',
token,
});
if (!Array.isArray(proxies)) {
throw new Error('SUB2API 代理列表返回格式异常,无法自动选择代理。');
}
const { proxy, reason, candidates } = findSub2ApiProxy(proxies, preference);
if (proxy) {
return proxy;
}
const configured = normalizeSub2ApiProxyPreference(preference) || '(未配置)';
const available = (candidates || [])
.slice(0, 8)
.map(buildProxyDisplayName)
.join('') || '无可用代理';
if (reason === 'ambiguous-name' || reason === 'ambiguous-fuzzy') {
throw new Error(`SUB2API 默认代理“${configured}”匹配到多个代理,请改填代理 ID。候选:${available}`);
}
if (reason === 'missing-id') {
throw new Error(`SUB2API 默认代理 ID “${configured}”不存在或未启用。可用代理:${available}`);
}
if (reason === 'missing-name') {
throw new Error(`SUB2API 默认代理“${configured}”不存在或未启用。可用代理:${available}`);
}
if (reason === 'no-preference') {
throw new Error(`SUB2API 存在多个可用代理,请在侧边栏填写默认代理名称或 ID;留空则不使用代理。可用代理:${available}`);
}
throw new Error('SUB2API 没有可用代理;请检查默认代理配置,或将其留空以禁用代理。');
}
function buildDraftAccountName(groupName) {
const prefix = (groupName || SUB2API_DEFAULT_GROUP_NAME)
.trim()
.replace(/[^\w\u4e00-\u9fa5-]+/g, '-')
.replace(/^-+|-+$/g, '') || SUB2API_DEFAULT_GROUP_NAME;
const stamp = new Date().toISOString().replace(/\D/g, '').slice(2, 14);
const random = Math.floor(Math.random() * 9000 + 1000);
return `${prefix}-${stamp}-${random}`;
}
function extractStateFromAuthUrl(authUrl) {
try {
return new URL(authUrl).searchParams.get('state') || '';
} catch {
return '';
}
}
function parseLocalhostCallback(rawUrl, visibleStep = 10) {
let parsed;
try {
parsed = new URL(rawUrl);
} catch {
throw new Error('提供的回调 URL 不是合法链接。');
}
if (!['http:', 'https:'].includes(parsed.protocol)) {
throw new Error('回调 URL 协议不正确。');
}
if (!['localhost', '127.0.0.1'].includes(parsed.hostname)) {
throw new Error(`步骤 ${visibleStep} 只接受 localhost / 127.0.0.1 回调地址。`);
}
if (parsed.pathname !== '/auth/callback') {
throw new Error('回调 URL 路径必须是 /auth/callback。');
}
const code = (parsed.searchParams.get('code') || '').trim();
const state = (parsed.searchParams.get('state') || '').trim();
if (!code || !state) {
throw new Error('回调 URL 中缺少 code 或 state。');
}
return {
url: parsed.toString(),
code,
state,
};
}
function buildOpenAiCredentials(exchangeData) {
const credentials = {};
const allowedKeys = [
'access_token',
'refresh_token',
'id_token',
'expires_at',
'email',
'chatgpt_account_id',
'chatgpt_user_id',
'organization_id',
'plan_type',
'client_id',
];
for (const key of allowedKeys) {
if (exchangeData?.[key] !== undefined && exchangeData?.[key] !== null && exchangeData?.[key] !== '') {
credentials[key] = exchangeData[key];
}
}
if (!credentials.access_token) {
throw new Error('SUB2API 交换授权码后未返回 access_token。');
}
return credentials;
}
function buildOpenAiExtra(exchangeData) {
const extra = {};
const allowedKeys = ['email', 'name', 'privacy_mode'];
for (const key of allowedKeys) {
if (exchangeData?.[key] !== undefined && exchangeData?.[key] !== null && exchangeData?.[key] !== '') {
extra[key] = exchangeData[key];
}
}
return Object.keys(extra).length ? extra : undefined;
}
async function getBackgroundState() {
try {
return await chrome.runtime.sendMessage({ type: 'GET_STATE', source: 'sub2api-panel' });
} catch {
return {};
}
}
function openAccountsPageSoon(origin) {
const accountsUrl = `${origin}/admin/accounts`;
if (location.href === accountsUrl || location.pathname.startsWith('/admin/accounts')) {
return;
}
setTimeout(() => {
try {
location.replace(accountsUrl);
} catch { }
}, 500);
}
async function step1_generateOpenAiAuthUrl(payload = {}, options = {}) {
const { report = true } = options;
const logStep = Number.isInteger(payload?.logStep) ? payload.logStep : 1;
const redirectUri = normalizeRedirectUri();
const groupNames = normalizeSub2ApiGroupNames(payload.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME);
const groupName = groupNames[0] || SUB2API_DEFAULT_GROUP_NAME;
const { origin, token } = await loginSub2Api(payload);
const groups = await getGroupsByNames(origin, token, groupNames);
const group = groups[0];
const proxyPreference = resolveSub2ApiProxyPreference(payload);
const proxy = proxyPreference ? await resolveSub2ApiProxy(origin, token, proxyPreference) : null;
const proxyId = normalizeProxyId(proxy?.id);
const draftName = buildDraftAccountName(group.name || groupName);
const groupLabel = groups.map((item) => `${item.name}#${item.id}`).join('、');
log(`步骤 ${logStep}:已登录 SUB2API,使用分组 ${groupLabel}`);
if (proxy) {
log(`步骤 ${logStep}:已选择 SUB2API 默认代理 ${buildProxyDisplayName(proxy)}`);
} else {
log(`步骤 ${logStep}:未配置 SUB2API 默认代理,本次将不使用代理。`);
}
log(`步骤 ${logStep}:正在向 SUB2API 生成 OpenAI Auth 链接,回调地址为 ${redirectUri}`);
const authRequestBody = {
redirect_uri: redirectUri,
};
if (proxyId) {
authRequestBody.proxy_id = proxyId;
}
const authData = await requestJson(origin, '/api/v1/admin/openai/generate-auth-url', {
method: 'POST',
token,
body: authRequestBody,
});
const oauthUrl = String(authData?.auth_url || '').trim();
const sessionId = String(authData?.session_id || '').trim();
const oauthState = String(authData?.state || extractStateFromAuthUrl(oauthUrl)).trim();
if (!oauthUrl || !sessionId) {
throw new Error('SUB2API 未返回完整的 auth_url / session_id。');
}
log(`步骤 ${logStep}:已获取 SUB2API OAuth 链接:${oauthUrl.slice(0, 96)}...`, 'ok');
const result = {
oauthUrl,
sub2apiSessionId: sessionId,
sub2apiOAuthState: oauthState,
sub2apiGroupId: group.id,
sub2apiGroupIds: groups.map((item) => item.id),
sub2apiDraftName: draftName,
sub2apiProxyId: proxyId,
};
if (report) {
reportComplete(1, result);
}
openAccountsPageSoon(origin);
return result;
}
async function step9_submitOpenAiCallback(payload = {}) {
const visibleStep = Number(payload?.visibleStep) || 10;
const callback = parseLocalhostCallback(payload.localhostUrl || '', visibleStep);
const backgroundState = await getBackgroundState();
const flowEmail = String(backgroundState.email || '').trim();
const sessionId = String(payload.sub2apiSessionId || backgroundState.sub2apiSessionId || '').trim();
const expectedState = String(payload.sub2apiOAuthState || backgroundState.sub2apiOAuthState || '').trim();
const { origin, token } = await loginSub2Api(payload);
const proxyPreference = resolveSub2ApiProxyPreference(payload, backgroundState);
const preferredProxyId = normalizeProxyId(payload.sub2apiProxyId || backgroundState.sub2apiProxyId);
const proxySelector = preferredProxyId || proxyPreference;
const proxy = proxySelector ? await resolveSub2ApiProxy(origin, token, proxySelector) : null;
const proxyId = normalizeProxyId(proxy?.id);
const accountPriority = resolveSub2ApiAccountPriority(payload, backgroundState);
const storedGroupIds = Array.isArray(payload.sub2apiGroupIds)
? payload.sub2apiGroupIds
: (Array.isArray(backgroundState.sub2apiGroupIds) ? backgroundState.sub2apiGroupIds : []);
const groupIdsFromState = storedGroupIds
.map((id) => Number(id))
.filter((id) => Number.isFinite(id) && id > 0);
const groups = groupIdsFromState.length
? groupIdsFromState.map((id) => ({ id }))
: (payload.sub2apiGroupId
? [{ id: payload.sub2apiGroupId, name: payload.sub2apiGroupName || backgroundState.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME }]
: await getGroupsByNames(origin, token, payload.sub2apiGroupName || backgroundState.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME));
if (!sessionId) {
throw new Error('缺少 SUB2API session_id,请重新执行步骤 1。');
}
if (expectedState && expectedState !== callback.state) {
throw new Error('本次 localhost 回调中的 state 与步骤 1 生成的 state 不一致,请重新执行步骤 1。');
}
log('正在向 SUB2API 交换 OpenAI 授权码...', 'info', { step: visibleStep, stepKey: 'platform-verify' });
if (proxy) {
log(`使用 SUB2API 默认代理 ${buildProxyDisplayName(proxy)}`, 'info', { step: visibleStep, stepKey: 'platform-verify' });
} else {
log('未配置 SUB2API 默认代理,本次将不使用代理。', 'info', { step: visibleStep, stepKey: 'platform-verify' });
}
const exchangeRequestBody = {
session_id: sessionId,
code: callback.code,
state: callback.state,
};
if (proxyId) {
exchangeRequestBody.proxy_id = proxyId;
}
const exchangeData = await requestJson(origin, '/api/v1/admin/openai/exchange-code', {
method: 'POST',
token,
body: exchangeRequestBody,
});
const credentials = buildOpenAiCredentials(exchangeData);
const extra = buildOpenAiExtra(exchangeData);
const resolvedEmail = String(exchangeData?.email || credentials?.email || '').trim();
const groupIds = groups
.map((group) => Number(group.id))
.filter((id) => Number.isFinite(id) && id > 0);
if (!groupIds.length) {
throw new Error('SUB2API 返回的目标分组 ID 无效。');
}
const accountName = resolvedEmail
|| flowEmail
|| String(payload.sub2apiDraftName || backgroundState.sub2apiDraftName || '').trim()
|| buildDraftAccountName(payload.sub2apiGroupName || backgroundState.sub2apiGroupName || SUB2API_DEFAULT_GROUP_NAME);
const createPayload = {
name: accountName,
notes: '',
platform: 'openai',
type: 'oauth',
credentials,
concurrency: SUB2API_DEFAULT_CONCURRENCY,
priority: accountPriority,
rate_multiplier: SUB2API_DEFAULT_RATE_MULTIPLIER,
group_ids: groupIds,
auto_pause_on_expired: true,
};
if (proxyId) {
createPayload.proxy_id = proxyId;
}
if (extra) {
createPayload.extra = extra;
}
log(`授权码交换成功,正在创建 SUB2API 账号(名称:${accountName}...`, 'info', { step: visibleStep, stepKey: 'platform-verify' });
const createdAccount = await requestJson(origin, '/api/v1/admin/accounts', {
method: 'POST',
token,
body: createPayload,
});
const verifiedStatus = `SUB2API 已创建账号 #${createdAccount?.id || 'unknown'}`;
log(verifiedStatus, 'ok', { step: visibleStep, stepKey: 'platform-verify' });
reportComplete('platform-verify', {
localhostUrl: callback.url,
verifiedStatus,
visibleStep,
});
openAccountsPageSoon(origin);
}
reportReady();
-1092
View File
File diff suppressed because it is too large Load Diff
-144
View File
@@ -1,144 +0,0 @@
// content/whatsapp-flow.js — WhatsApp Web code reader for GoPay.
console.log('[MultiPage:whatsapp-flow] Content script loaded on', location.href);
const WHATSAPP_FLOW_LISTENER_SENTINEL = 'data-multipage-whatsapp-flow-listener';
if (document.documentElement.getAttribute(WHATSAPP_FLOW_LISTENER_SENTINEL) !== '1') {
document.documentElement.setAttribute(WHATSAPP_FLOW_LISTENER_SENTINEL, '1');
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (
message.type === 'WHATSAPP_GET_STATE'
|| message.type === 'WHATSAPP_FIND_CODE'
) {
resetStopState();
handleWhatsAppCommand(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:whatsapp-flow] 消息监听已存在,跳过重复注册');
}
async function handleWhatsAppCommand(message) {
switch (message.type) {
case 'WHATSAPP_GET_STATE':
return inspectWhatsAppState();
case 'WHATSAPP_FIND_CODE':
return findWhatsAppCode(message.payload || {});
default:
throw new Error(`whatsapp-flow.js 不处理消息:${message.type}`);
}
}
function normalizeText(text = '') {
return String(text || '').replace(/\s+/g, ' ').trim();
}
function getBodyText() {
return normalizeText(document.body?.innerText || document.body?.textContent || '');
}
function extractVerificationCode(text = '') {
const normalized = normalizeText(text);
const preferredPatterns = [
/(?:gopay|gojek|openai|chatgpt|kode|code|otp|verification|verifikasi|whatsapp|验证码|驗證碼|代码|代碼)[^\d]{0,40}(\d{4,8})/i,
/(\d{4,8})[^\d]{0,40}(?:gopay|gojek|openai|chatgpt|kode|code|otp|verification|verifikasi|验证码|驗證碼|代码|代碼)/i,
];
for (const pattern of preferredPatterns) {
const match = normalized.match(pattern);
if (match?.[1]) {
return match[1];
}
}
const genericMatch = normalized.match(/(?<!\d)(\d{4,8})(?!\d)/);
return genericMatch?.[1] || '';
}
function getMessageCandidates() {
const selectors = [
'[data-testid*="msg" i]',
'[data-pre-plain-text]',
'.message-in',
'[role="row"]',
'div',
'span',
];
const seen = new Set();
const messages = [];
for (const selector of selectors) {
for (const el of Array.from(document.querySelectorAll(selector))) {
const text = normalizeText(el.innerText || el.textContent || '');
if (!text || text.length < 4 || seen.has(text)) continue;
seen.add(text);
messages.push(text);
}
}
return messages.slice(-80);
}
async function waitUntil(predicate, options = {}) {
const intervalMs = Math.max(50, Math.floor(Number(options.intervalMs) || 1000));
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) {
return null;
}
await sleep(intervalMs);
}
}
async function findWhatsAppCode(payload = {}) {
const timeoutMs = Math.max(0, Math.floor(Number(payload.timeoutMs) || 0));
const result = await waitUntil(() => {
const messages = getMessageCandidates();
for (let index = messages.length - 1; index >= 0; index -= 1) {
const text = messages[index];
const code = extractVerificationCode(text);
if (code) {
return {
code,
messageText: text.slice(0, 500),
};
}
}
const code = extractVerificationCode(getBodyText());
return code ? { code, messageText: getBodyText().slice(0, 500) } : null;
}, {
intervalMs: 1000,
timeoutMs,
});
return result || {
code: '',
messageText: '',
};
}
function inspectWhatsAppState() {
const bodyText = getBodyText();
const code = extractVerificationCode(bodyText);
return {
url: location.href,
readyState: document.readyState,
loggedIn: !/use whatsapp on your computer|link a device|scan|qr|使用 WhatsApp|扫码|掃碼/i.test(bodyText),
code,
textPreview: bodyText.slice(0, 500),
messagePreview: getMessageCandidates().slice(-8),
};
}