import GuJumpgate snapshot from GitHub

This commit is contained in:
2026-05-20 20:33:51 +08:00
commit f8903dcd14
335 changed files with 179790 additions and 0 deletions
+65
View File
@@ -0,0 +1,65 @@
(function activationUtilsModule(root, factory) {
if (typeof module !== 'undefined' && module.exports) {
module.exports = factory();
return;
}
root.MultiPageActivationUtils = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createActivationUtils() {
function normalizeTagName(tagName) {
return String(tagName || '').trim().toLowerCase();
}
function normalizeType(type) {
return String(type || '').trim().toLowerCase();
}
function normalizePathname(pathname) {
return String(pathname || '').trim().toLowerCase();
}
function getActivationStrategy(target = {}) {
const tagName = normalizeTagName(target.tagName);
const type = normalizeType(target.type);
const pathname = normalizePathname(target.pathname);
const hasForm = Boolean(target.hasForm);
const isEmailVerificationRoute = /\/email-verification(?:[/?#]|$)/i.test(pathname);
const isSubmitButton = hasForm
&& (
(tagName === 'button' && (!type || type === 'submit'))
|| (tagName === 'input' && type === 'submit')
);
if (isSubmitButton && isEmailVerificationRoute) {
return { method: 'requestSubmit' };
}
return { method: 'click' };
}
function isRecoverableStep9AuthFailure(statusText) {
const text = String(statusText || '').replace(/\s+/g, ' ').trim();
if (!text) {
return false;
}
if (/oauth flow is not pending/i.test(text)) {
return true;
}
if (/请更新\s*cli\s*proxy\s*api\s*或检查连接/i.test(text)) {
return true;
}
if (/bad request|state code error|failed to exchange authorization code for tokens|failed to save authentication tokens|unknown or expired state|invalid state|state is required|code or error is required|invalid redirect_url|provider does not match state|failed to persist oauth callback|timeout waiting for oauth callback|oauth flow timed out/i.test(text)) {
return true;
}
return /(?:认证失败|回调\s*url\s*提交失败|回调url提交失败|提交回调失败)\s*[:]?\s*/i.test(text);
}
return {
getActivationStrategy,
isRecoverableStep9AuthFailure,
};
});
+245
View File
@@ -0,0 +1,245 @@
(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,
};
});
+206
View File
@@ -0,0 +1,206 @@
// content/duck-mail.js - Content script for DuckDuckGo Email Protection autofill settings
console.log('[MultiPage:duck-mail] Content script loaded on', location.href);
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type !== 'FETCH_DUCK_EMAIL') return;
resetStopState();
fetchDuckEmail(message.payload).then((result) => {
sendResponse(result);
}).catch((err) => {
if (isStopError(err)) {
log('Duck 邮箱:已被用户停止。', 'warn');
sendResponse({ stopped: true, error: err.message });
return;
}
sendResponse({ error: err.message });
});
return true;
});
async function fetchDuckEmail(payload = {}) {
const {
generateNew = true,
baselineEmail = '',
} = payload;
log(`Duck 邮箱:正在${generateNew ? '生成' : '读取'}私有地址...`);
await waitForElement(
'input.AutofillSettingsPanel__PrivateDuckAddressValue, button.AutofillSettingsPanel__GeneratorButton',
15000
);
const GENERATE_BUTTON_PATTERN = /generate\s+private\s+duck\s+address|new\s+private\s+duck\s+address|generate\s+new|new\s+address|生成.*duck.*地址|生成.*私有.*地址|生成.*地址|新.*地址/i;
const DUCK_EMAIL_PATTERN = /([a-z0-9._%+-]+@duck\.com)/i;
const ADDRESS_VALUE_SELECTORS = [
'input.AutofillSettingsPanel__PrivateDuckAddressValue',
'input[class*="PrivateDuckAddressValue"]',
'input[data-testid*="PrivateDuckAddressValue"]',
'input[value*="@duck.com" i]',
];
const normalizeDuckEmail = (value) => {
const match = String(value || '').trim().match(DUCK_EMAIL_PATTERN);
return match ? match[1].toLowerCase() : '';
};
const getAddressInputs = () => {
const seen = new Set();
return ADDRESS_VALUE_SELECTORS
.flatMap((selector) => Array.from(document.querySelectorAll(selector) || []))
.filter((element) => {
if (!element || seen.has(element)) {
return false;
}
seen.add(element);
return true;
});
};
const getGeneratorButton = () => {
const direct = document.querySelector('button.AutofillSettingsPanel__GeneratorButton');
if (direct) return direct;
const selectors = [
'button[data-testid*="Generator"]',
'button[class*="Generator"]',
'button[aria-label*="duck" i]',
'button[title*="duck" i]',
'[role="button"]',
'button',
];
const candidates = selectors.flatMap((selector) => Array.from(document.querySelectorAll(selector) || []));
return candidates.find((btn) => {
const text = [
btn.textContent,
btn.getAttribute?.('aria-label'),
btn.getAttribute?.('title'),
]
.filter(Boolean)
.join(' ')
.replace(/\s+/g, ' ')
.trim();
return GENERATE_BUTTON_PATTERN.test(text);
}) || null;
};
const readEmail = () => {
for (const input of getAddressInputs()) {
const candidates = [
input?.value,
input?.getAttribute?.('value'),
input?.textContent,
input?.innerText,
input?.getAttribute?.('aria-label'),
input?.getAttribute?.('title'),
];
for (const candidate of candidates) {
const email = normalizeDuckEmail(candidate);
if (email) {
return email;
}
}
}
return '';
};
const waitForVisibleEmail = async (attemptLimit = 12) => {
for (let i = 0; i < attemptLimit; i++) {
const visibleEmail = readEmail();
if (visibleEmail) {
return visibleEmail;
}
await sleep(150);
}
return '';
};
const waitForEmailValue = async (previousValues = []) => {
const blockedValues = new Set(
(Array.isArray(previousValues) ? previousValues : [previousValues])
.map((value) => normalizeDuckEmail(value))
.filter(Boolean)
);
let stableCandidate = '';
let stableCount = 0;
for (let i = 0; i < 100; i++) {
const nextValue = readEmail();
if (nextValue && !blockedValues.has(nextValue)) {
if (nextValue === stableCandidate) {
stableCount += 1;
} else {
stableCandidate = nextValue;
stableCount = 1;
}
if (stableCount >= 2) {
return nextValue;
}
} else {
stableCandidate = '';
stableCount = 0;
}
await sleep(150);
}
throw new Error('等待 Duck 地址变化超时。');
};
const knownBaselineEmail = normalizeDuckEmail(baselineEmail);
let currentEmail = readEmail();
if (!currentEmail) {
currentEmail = await waitForVisibleEmail(generateNew ? (knownBaselineEmail ? 12 : 30) : 20);
}
if (currentEmail && !generateNew) {
log(`Duck 邮箱:已发现现有地址 ${currentEmail}`);
return { email: currentEmail, generated: false };
}
const generatorButton = getGeneratorButton();
if (!generatorButton) {
if (generateNew) {
throw new Error('未找到“生成新 Duck 地址”按钮(可能是页面结构、文案或登录状态发生变化)。');
}
if (currentEmail) {
log(`Duck 邮箱:未找到生成按钮,复用现有地址 ${currentEmail}`, 'warn');
return { email: currentEmail, generated: false };
}
throw new Error('未找到 Duck 私有地址生成按钮。');
}
const comparisonEmails = [currentEmail, knownBaselineEmail].filter(Boolean);
if (!currentEmail && knownBaselineEmail) {
log(`Duck 邮箱:当前地址尚未显示,改用已知基线 ${knownBaselineEmail} 作为对比基线。`, 'warn');
}
for (let attempt = 1; attempt <= 2; attempt++) {
await humanPause(500, 1300);
await window.CodexOperationDelay.performOperationWithDelay(
{
stepKey: 'fetch-signup-code',
kind: 'click',
label: 'duck-generate-address',
},
async () => {
if (typeof simulateClick === 'function') {
simulateClick(generatorButton);
} else {
generatorButton.click();
}
}
);
log(`Duck 邮箱:已点击“生成 Duck 私有地址”按钮(${attempt}/2`);
try {
const nextEmail = await waitForEmailValue(comparisonEmails);
log(`Duck 邮箱:地址已就绪 ${nextEmail}`, 'ok');
return { email: nextEmail, generated: true };
} catch (err) {
if (attempt >= 2) {
throw err;
}
log('Duck 邮箱:首次生成后地址未变化,准备再试一次...', 'warn');
await sleep(800);
}
}
throw new Error('Duck 地址生成失败。');
}
+727
View File
@@ -0,0 +1,727 @@
// content/gmail-mail.js — Content script for Gmail polling (steps 4, 7)
// Injected dynamically on: mail.google.com
const GMAIL_PREFIX = '[MultiPage:gmail-mail]';
const GMAIL_SEEN_CODES_KEY = 'seenGmailCodes';
const GMAIL_FALLBACK_AFTER = 3;
const isTopFrame = window === window.top;
console.log(GMAIL_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
if (!isTopFrame) {
console.log(GMAIL_PREFIX, 'Skipping child frame');
} else {
let seenCodes = new Set();
async function loadSeenCodes() {
try {
const data = await chrome.storage.session.get(GMAIL_SEEN_CODES_KEY);
if (Array.isArray(data[GMAIL_SEEN_CODES_KEY])) {
seenCodes = new Set(data[GMAIL_SEEN_CODES_KEY]);
console.log(GMAIL_PREFIX, `Loaded ${seenCodes.size} previously seen codes`);
}
} catch (err) {
console.warn(GMAIL_PREFIX, 'Session storage unavailable, using in-memory seen codes:', err?.message || err);
}
}
async function persistSeenCodes() {
try {
await chrome.storage.session.set({ [GMAIL_SEEN_CODES_KEY]: [...seenCodes] });
} catch (err) {
console.warn(GMAIL_PREFIX, 'Could not persist seen codes, continuing in-memory only:', err?.message || err);
}
}
loadSeenCodes();
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'POLL_EMAIL') {
resetStopState();
handlePollEmail(message.step, message.payload).then((result) => {
sendResponse(result);
}).catch((err) => {
if (isStopError(err)) {
log(`步骤 ${message.step}:已被用户停止。`, 'warn');
sendResponse({ stopped: true, error: err.message });
return;
}
log(`步骤 ${message.step}Gmail 轮询失败:${err.message}`, 'warn');
sendResponse({ error: err.message });
});
return true;
}
});
function normalizeText(value) {
return String(value || '').replace(/\s+/g, ' ').trim();
}
function isDisplayed(element) {
if (!element) return false;
const style = window.getComputedStyle(element);
return style.display !== 'none' && style.visibility !== 'hidden';
}
function isVisibleElement(element) {
if (!isDisplayed(element)) return false;
const rect = element.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
}
function normalizeMinuteTimestamp(timestamp) {
if (!Number.isFinite(timestamp) || timestamp <= 0) return 0;
const date = new Date(timestamp);
date.setSeconds(0, 0);
return date.getTime();
}
function getTargetEmailMatchState(text, targetEmail) {
const normalizedTarget = String(targetEmail || '').trim().toLowerCase();
if (!normalizedTarget) {
return { matches: true, hasExplicitEmail: false };
}
const normalizedText = String(text || '').toLowerCase();
if (normalizedText.includes(normalizedTarget)) {
return { matches: true, hasExplicitEmail: true };
}
const atIndex = normalizedTarget.indexOf('@');
if (atIndex > 0) {
const encodedTarget = `${normalizedTarget.slice(0, atIndex)}=${normalizedTarget.slice(atIndex + 1)}`;
if (normalizedText.includes(encodedTarget)) {
return { matches: true, hasExplicitEmail: true };
}
}
return { matches: false, hasExplicitEmail: false };
}
function normalizeRulePatternList(patterns = []) {
return Array.isArray(patterns) ? patterns : [];
}
function extractCodeByRulePatterns(text, patterns = []) {
const normalizedText = String(text || '');
for (const pattern of normalizeRulePatternList(patterns)) {
try {
const source = String(pattern?.source || '').trim();
if (!source) {
continue;
}
const flags = String(pattern?.flags || '').replace(/[^dgimsuvy]/g, '');
const match = normalizedText.match(new RegExp(source, flags));
if (!match) {
continue;
}
for (let index = 1; index < match.length; index += 1) {
const candidate = String(match[index] || '').trim();
if (candidate) {
return candidate;
}
}
if (String(match[0] || '').trim()) {
return String(match[0] || '').trim();
}
} catch (_) {
// Ignore invalid runtime rule patterns and continue with other candidates.
}
}
return null;
}
const MONTH_INDEX_MAP = {
jan: 0,
feb: 1,
mar: 2,
apr: 3,
may: 4,
jun: 5,
jul: 6,
aug: 7,
sep: 8,
oct: 9,
nov: 10,
dec: 11,
};
function parseGmailTimestampText(rawText) {
const text = normalizeText(rawText);
if (!text) return null;
const parsedNative = Date.parse(text);
if (Number.isFinite(parsedNative)) {
return parsedNative;
}
let match = text.match(/(\d{4})[年/-](\d{1,2})[月/-](\d{1,2})日?\s+(\d{1,2}):(\d{2})(?:\s*([AP]M))?/i);
if (match) {
const [, year, month, day, hourText, minute, meridiem] = match;
let hour = Number(hourText);
if (/pm/i.test(meridiem) && hour < 12) hour += 12;
if (/am/i.test(meridiem) && hour === 12) hour = 0;
return new Date(Number(year), Number(month) - 1, Number(day), hour, Number(minute), 0, 0).getTime();
}
match = text.match(/\b([A-Za-z]{3,9})\s+(\d{1,2}),\s*(\d{4}),?\s*(\d{1,2}):(\d{2})\s*([AP]M)\b/i);
if (match) {
const [, monthText, day, year, hourText, minute, meridiem] = match;
const month = MONTH_INDEX_MAP[monthText.slice(0, 3).toLowerCase()];
if (month !== undefined) {
let hour = Number(hourText);
if (/pm/i.test(meridiem) && hour < 12) hour += 12;
if (/am/i.test(meridiem) && hour === 12) hour = 0;
return new Date(Number(year), month, Number(day), hour, Number(minute), 0, 0).getTime();
}
}
match = text.match(/今天\s*(\d{1,2}):(\d{2})/);
if (match) {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), now.getDate(), Number(match[1]), Number(match[2]), 0, 0).getTime();
}
match = text.match(/昨天\s*(\d{1,2}):(\d{2})/);
if (match) {
const now = new Date();
now.setDate(now.getDate() - 1);
return new Date(now.getFullYear(), now.getMonth(), now.getDate(), Number(match[1]), Number(match[2]), 0, 0).getTime();
}
match = text.match(/^(\d{1,2}):(\d{2})$/);
if (match) {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), now.getDate(), Number(match[1]), Number(match[2]), 0, 0).getTime();
}
return null;
}
function extractVerificationCode(text, options = {}) {
const normalized = String(text || '');
const matchedByRule = extractCodeByRulePatterns(normalized, options?.codePatterns);
if (matchedByRule) {
return matchedByRule;
}
const cnMatch = normalized.match(/(?:验证码|代码)[^0-9]{0,16}(\d{6})/i);
if (cnMatch) return cnMatch[1];
const enMatch = normalized.match(/(?:verification\s+code|temporary\s+verification\s+code|log-?in\s+code|enter\s+this\s+code|code(?:\s+is)?)[^0-9]{0,24}(\d{6})/i);
if (enMatch) return enMatch[1];
const plainMatch = normalized.match(/\b(\d{6})\b/);
if (plainMatch) return plainMatch[1];
return null;
}
function findInboxLink() {
const selectors = [
'a[href*="#inbox"]',
'a[aria-label*="收件箱"]',
'a[aria-label*="Inbox"]',
];
for (const selector of selectors) {
const candidates = Array.from(document.querySelectorAll(selector));
const visible = candidates.find(isVisibleElement);
if (visible) return visible;
if (candidates[0]) return candidates[0];
}
return Array.from(document.querySelectorAll('a, [role="link"]')).find((element) => {
const text = normalizeText(
element.getAttribute('aria-label')
|| element.getAttribute('title')
|| element.textContent
);
return /收件箱|Inbox/i.test(text);
}) || null;
}
const GMAIL_CATEGORY_LABELS = {
primary: [/^primary$/i, /^inbox$/i, /^主要$/],
updates: [/^updates$/i, /^更新$/],
promotions: [/^promotions$/i, /^推广$/],
social: [/^social$/i, /^社交$/],
};
function getCategoryKeyFromText(text) {
const normalizedText = normalizeText(text);
if (!normalizedText) return '';
for (const [key, patterns] of Object.entries(GMAIL_CATEGORY_LABELS)) {
if (patterns.some((pattern) => pattern.test(normalizedText))) {
return key;
}
}
return '';
}
function getCategoryTabLabel(tab) {
const text = normalizeText(
tab?.getAttribute?.('aria-label')
|| tab?.getAttribute?.('data-tooltip')
|| tab?.getAttribute?.('title')
|| tab?.textContent
|| ''
);
return text;
}
function collectCategoryTabs() {
const tabs = Array.from(document.querySelectorAll('[role="tab"], [data-tooltip-align][role="link"]'));
const categoryTabs = [];
const seenKeys = new Set();
tabs.forEach((tab) => {
if (!isVisibleElement(tab)) return;
const label = getCategoryTabLabel(tab);
const key = getCategoryKeyFromText(label);
if (!key || seenKeys.has(key)) return;
seenKeys.add(key);
categoryTabs.push({
key,
label,
selected: tab.getAttribute('aria-selected') === 'true' || /\bTO\b/.test(tab.className || ''),
tab,
});
});
return categoryTabs;
}
function getCategoryScanOrder() {
const categoryTabs = collectCategoryTabs();
if (!categoryTabs.length) {
return [{ key: 'primary', label: 'Primary', selected: true, tab: null }];
}
const ordered = ['updates', 'primary']
.map((key) => categoryTabs.find((item) => item.key === key))
.filter(Boolean);
return ordered.length
? ordered
: [{ key: 'primary', label: 'Primary', selected: true, tab: null }];
}
async function activateCategoryTab(step, categoryKey) {
const categoryTabs = collectCategoryTabs();
const target = categoryTabs.find((item) => item.key === categoryKey);
if (!target?.tab) {
return { key: categoryKey, label: categoryKey, switched: false };
}
if (target.selected) {
return { key: target.key, label: target.label, switched: false };
}
simulateClick(target.tab);
for (let i = 0; i < 20; i++) {
await sleep(200);
const refreshed = collectCategoryTabs().find((item) => item.key === categoryKey);
if (refreshed?.selected) {
await sleep(500);
log(`步骤 ${step}:已切换到 Gmail 分类 ${refreshed.label}`);
return { key: refreshed.key, label: refreshed.label, switched: true };
}
}
await sleep(600);
log(`步骤 ${step}:已尝试切换到 Gmail 分类 ${target.label}`, 'info');
return { key: target.key, label: target.label, switched: true };
}
function findRefreshButton() {
const selectors = [
'div[role="button"][data-tooltip="刷新"]',
'div[role="button"][aria-label="刷新"]',
'div[role="button"][data-tooltip*="刷新"]',
'div[role="button"][aria-label*="刷新"]',
'div[role="button"][data-tooltip="Refresh"]',
'div[role="button"][aria-label="Refresh"]',
'div[role="button"][data-tooltip*="Refresh"]',
'div[role="button"][aria-label*="Refresh"]',
'div[act="20"][role="button"]',
'div.asf.T-I-J3.J-J5-Ji',
];
for (const selector of selectors) {
const matched = document.querySelector(selector);
const button = matched?.closest?.('[role="button"]') || matched;
if (button && isVisibleElement(button)) {
return button;
}
}
return Array.from(document.querySelectorAll('div[role="button"], button')).find((element) => {
const text = normalizeText(
element.getAttribute('aria-label')
|| element.getAttribute('data-tooltip')
|| element.getAttribute('title')
|| element.textContent
);
return /刷新|Refresh/i.test(text);
}) || null;
}
function collectThreadRows() {
const candidates = [
...document.querySelectorAll('tr.zA'),
...document.querySelectorAll('tr[role="row"]'),
];
const rows = [];
const seenRows = new Set();
candidates.forEach((row) => {
if (!row || seenRows.has(row)) return;
seenRows.add(row);
if (!isDisplayed(row)) return;
const text = normalizeText(row.textContent || row.innerText || '');
if (!text) return;
if (
row.matches('tr.zA')
|| row.querySelector('.bog, .y6, .y2, .afn, [data-thread-id], [data-legacy-thread-id], [data-legacy-last-message-id]')
|| /verify|verification|code|验证码|log-?in/i.test(text)
) {
rows.push(row);
}
});
return rows;
}
function getRowPreviewText(row) {
const sender = normalizeText(
row.querySelector('.zF, .yP, span[email], [email]')?.textContent
|| row.querySelector('[email]')?.getAttribute?.('email')
|| ''
);
const subject = normalizeText(
row.querySelector('.bog [data-thread-id], .bog [data-legacy-thread-id], .bog, .y6, .bqe')?.textContent
|| ''
);
const digest = normalizeText(
row.querySelector('.y2, .afn, .a4W, .bog + .y2')?.textContent
|| ''
);
const timeText = normalizeText(
row.querySelector('td.xW span')?.getAttribute?.('title')
|| row.querySelector('td.xW span, td.xW time')?.getAttribute?.('title')
|| row.querySelector('td.xW span, td.xW time')?.textContent
|| ''
);
const fullText = normalizeText(row.textContent || row.innerText || '');
return {
sender,
subject,
digest,
timeText,
fullText,
combinedText: normalizeText([sender, subject, digest, timeText, fullText].filter(Boolean).join(' ')),
};
}
function getRowTimestamp(row) {
const timeCell = row.querySelector('td.xW span, td.xW time, td.xW [title]');
const candidates = [
timeCell?.getAttribute?.('title'),
timeCell?.getAttribute?.('aria-label'),
timeCell?.textContent,
].filter(Boolean);
for (const candidate of candidates) {
const parsed = parseGmailTimestampText(candidate);
if (parsed) return parsed;
}
return null;
}
function getRowFingerprint(row, index = 0) {
const marker = row.querySelector('[data-thread-id], [data-legacy-thread-id], [data-legacy-last-message-id]');
const stableId = row.getAttribute('data-thread-id')
|| row.getAttribute('data-legacy-thread-id')
|| row.getAttribute('data-legacy-last-message-id')
|| marker?.getAttribute?.('data-thread-id')
|| marker?.getAttribute?.('data-legacy-thread-id')
|| marker?.getAttribute?.('data-legacy-last-message-id')
|| row.getAttribute('id')
|| `row-${index}`;
const preview = getRowPreviewText(row);
return `${stableId}::${preview.subject}::${preview.timeText}`.slice(0, 300);
}
function getCurrentMailIds(rows = []) {
const ids = new Set();
const sourceRows = rows.length ? rows : collectThreadRows();
sourceRows.forEach((row, index) => {
ids.add(getRowFingerprint(row, index));
});
return ids;
}
function rowMatchesFilters(preview, senderFilters, subjectFilters) {
const senderText = normalizeText(preview.sender).toLowerCase();
const subjectText = normalizeText(preview.subject).toLowerCase();
const combinedText = normalizeText(preview.combinedText).toLowerCase();
const senderMatch = senderFilters.some((filter) => {
const value = String(filter || '').toLowerCase();
return value && (senderText.includes(value) || combinedText.includes(value));
});
const subjectMatch = subjectFilters.some((filter) => {
const value = String(filter || '').toLowerCase();
return value && (subjectText.includes(value) || combinedText.includes(value));
});
return senderMatch || subjectMatch;
}
async function ensureInboxReady(step) {
if (!/#inbox/i.test(location.href)) {
const inboxLink = findInboxLink();
if (inboxLink) {
simulateClick(inboxLink);
await sleep(800);
log(`步骤 ${step}:已切回 Gmail 收件箱。`);
} else {
location.hash = '#inbox';
await sleep(800);
}
}
for (let i = 0; i < 20; i++) {
const rows = collectThreadRows();
if (rows.length > 0) {
return rows;
}
await sleep(400);
}
return [];
}
async function refreshInbox(step) {
const refreshButton = findRefreshButton();
if (refreshButton) {
simulateClick(refreshButton);
log(`步骤 ${step}:已点击 Gmail 刷新。`);
await sleep(1500);
return;
}
const inboxLink = findInboxLink();
if (inboxLink) {
simulateClick(inboxLink);
log(`步骤 ${step}:未找到刷新按钮,已重新进入收件箱。`);
await sleep(1200);
return;
}
location.reload();
log(`步骤 ${step}:未找到刷新按钮,已直接刷新页面。`);
await sleep(2500);
}
async function returnToInbox() {
if (/#inbox/i.test(location.href) && collectThreadRows().length > 0) {
return;
}
const inboxLink = findInboxLink();
if (inboxLink) {
simulateClick(inboxLink);
} else {
location.hash = '#inbox';
}
for (let i = 0; i < 20; i++) {
if (collectThreadRows().length > 0) {
return;
}
await sleep(250);
}
}
async function openRowAndGetMessageText(row) {
simulateClick(row);
for (let i = 0; i < 20; i++) {
const messageContainer = document.querySelector('div[role="main"] .a3s, div[role="main"] [data-message-id], h2[data-thread-perm-id]');
if (messageContainer || !/#inbox/i.test(location.href)) {
break;
}
await sleep(250);
}
await sleep(900);
const main = document.querySelector('div[role="main"]');
const text = normalizeText(main?.innerText || document.body?.innerText || document.body?.textContent || '');
await returnToInbox();
return text;
}
async function handlePollEmail(step, payload) {
const {
codePatterns = [],
senderFilters = [],
subjectFilters = [],
maxAttempts = 5,
intervalMs = 3000,
filterAfterTimestamp = 0,
excludeCodes = [],
targetEmail = '',
} = payload || {};
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0);
log(`步骤 ${step}:开始轮询 Gmail(最多 ${maxAttempts} 次)`);
if (filterAfterMinute) {
log(`步骤 ${step}:仅尝试 ${new Date(filterAfterMinute).toLocaleString('zh-CN', { hour12: false })} 及之后时间的邮件。`);
}
let initialRows = await ensureInboxReady(step);
if (!initialRows.length) {
await refreshInbox(step);
initialRows = await ensureInboxReady(step);
}
if (!initialRows.length) {
throw new Error('Gmail 收件箱列表未加载完成,请确认当前已打开 Gmail 收件箱。');
}
const categoryOrder = getCategoryScanOrder();
const existingMailIdsByCategory = new Map();
for (const category of categoryOrder) {
const activeCategory = await activateCategoryTab(step, category.key);
const rows = collectThreadRows();
existingMailIdsByCategory.set(activeCategory.key, getCurrentMailIds(rows));
log(`步骤 ${step}:已记录 Gmail 分类 ${activeCategory.label}${rows.length} 封旧邮件快照`);
}
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
log(`步骤 ${step}:正在轮询 Gmail,第 ${attempt}/${maxAttempts}`);
if (attempt > 1) {
await refreshInbox(step);
}
const useFallback = attempt > GMAIL_FALLBACK_AFTER;
for (const category of categoryOrder) {
const activeCategory = await activateCategoryTab(step, category.key);
const rows = collectThreadRows();
const existingMailIds = existingMailIdsByCategory.get(activeCategory.key) || new Set();
for (let index = 0; index < rows.length; index++) {
const row = rows[index];
const rowId = getRowFingerprint(row, index);
const rowTimestamp = getRowTimestamp(row);
const rowMinute = normalizeMinuteTimestamp(rowTimestamp || 0);
const passesTimeFilter = !filterAfterMinute || (rowMinute && rowMinute >= filterAfterMinute);
const shouldBypassOldSnapshot = Boolean(filterAfterMinute && passesTimeFilter && rowMinute > 0);
if (!passesTimeFilter) {
continue;
}
if (!useFallback && !shouldBypassOldSnapshot && existingMailIds.has(rowId)) {
continue;
}
const preview = getRowPreviewText(row);
if (!rowMatchesFilters(preview, senderFilters, subjectFilters)) {
continue;
}
const previewTargetState = getTargetEmailMatchState(preview.combinedText, targetEmail);
const previewCode = extractVerificationCode(preview.combinedText, {
codePatterns,
});
if (previewCode) {
if (excludedCodeSet.has(previewCode)) {
log(`步骤 ${step}:跳过排除的验证码:${previewCode}`, 'info');
continue;
}
if (seenCodes.has(previewCode)) {
log(`步骤 ${step}:跳过已处理过的验证码:${previewCode}`, 'info');
continue;
}
seenCodes.add(previewCode);
persistSeenCodes();
const source = useFallback && existingMailIds.has(rowId) ? '回退匹配邮件' : '新邮件';
const timeLabel = rowTimestamp ? `,时间:${new Date(rowTimestamp).toLocaleString('zh-CN', { hour12: false })}` : '';
const targetLabel = previewTargetState.matches ? ',目标邮箱命中' : '';
log(`步骤 ${step}:已在 Gmail ${activeCategory.label} 分类找到验证码:${previewCode}(来源:${source}${timeLabel}${targetLabel}`, 'ok');
return {
ok: true,
code: previewCode,
emailTimestamp: Date.now(),
mailId: rowId,
};
}
const openedText = await openRowAndGetMessageText(row);
const openedTargetState = getTargetEmailMatchState(openedText, targetEmail);
const bodyCode = extractVerificationCode(openedText, {
codePatterns,
});
if (!bodyCode) {
continue;
}
if (excludedCodeSet.has(bodyCode)) {
log(`步骤 ${step}:跳过排除的验证码:${bodyCode}`, 'info');
continue;
}
if (seenCodes.has(bodyCode)) {
log(`步骤 ${step}:跳过已处理过的验证码:${bodyCode}`, 'info');
continue;
}
seenCodes.add(bodyCode);
persistSeenCodes();
const source = useFallback && existingMailIds.has(rowId) ? '回退匹配邮件正文' : '新邮件正文';
const timeLabel = rowTimestamp ? `,时间:${new Date(rowTimestamp).toLocaleString('zh-CN', { hour12: false })}` : '';
const targetLabel = openedTargetState.matches ? ',目标邮箱命中' : '';
log(`步骤 ${step}:已在 Gmail ${activeCategory.label} 分类正文中找到验证码:${bodyCode}(来源:${source}${timeLabel}${targetLabel}`, 'ok');
return {
ok: true,
code: bodyCode,
emailTimestamp: Date.now(),
mailId: rowId,
};
}
}
if (attempt === GMAIL_FALLBACK_AFTER + 1) {
log(`步骤 ${step}:连续 ${GMAIL_FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn');
}
if (attempt < maxAttempts) {
await sleep(intervalMs);
}
}
throw new Error(
`${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 Gmail 中找到匹配邮件。请手动检查 Gmail 收件箱。`
);
}
}
+828
View File
@@ -0,0 +1,828 @@
// 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),
};
}
+431
View File
@@ -0,0 +1,431 @@
const ICLOUD_MAIL_PREFIX = '[MultiPage:icloud-mail]';
const isTopFrame = window === window.top;
const ICLOUD_POLL_SESSION_CACHE = new Map();
console.log(ICLOUD_MAIL_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
function isMailApplicationFrame() {
if (/\/applications\/mail2\//.test(location.pathname)) {
return true;
}
return Boolean(document.querySelector('.content-container, .mail-message-defaults, .thread-participants'));
}
if (isTopFrame) {
console.log(ICLOUD_MAIL_PREFIX, 'Top frame detected; waiting for mail iframe.');
}
const shouldHandlePollEmailInCurrentFrame = !isTopFrame || isMailApplicationFrame();
if (shouldHandlePollEmailInCurrentFrame) {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'POLL_EMAIL') {
if (!isMailApplicationFrame()) {
sendResponse({ ok: false, reason: 'wrong-frame' });
return;
}
resetStopState();
handlePollEmail(message.step, message.payload).then((result) => {
sendResponse(result);
}).catch((err) => {
if (isStopError(err)) {
log(`步骤 ${message.step}:已被用户停止。`, 'warn');
sendResponse({ stopped: true, error: err.message });
return;
}
log(`步骤 ${message.step}iCloud 邮箱轮询失败:${err.message}`, 'warn');
sendResponse({ error: err.message });
});
return true;
}
});
function normalizeText(value) {
return String(value || '').replace(/\s+/g, ' ').trim();
}
function normalizeRulePatternList(patterns = []) {
return Array.isArray(patterns) ? patterns : [];
}
function extractCodeByRulePatterns(text, patterns = []) {
const normalizedText = String(text || '');
for (const pattern of normalizeRulePatternList(patterns)) {
try {
const source = String(pattern?.source || '').trim();
if (!source) {
continue;
}
const flags = String(pattern?.flags || '').replace(/[^dgimsuvy]/g, '');
const match = normalizedText.match(new RegExp(source, flags));
if (!match) {
continue;
}
for (let index = 1; index < match.length; index += 1) {
const candidate = String(match[index] || '').trim();
if (candidate) {
return candidate;
}
}
if (String(match[0] || '').trim()) {
return String(match[0] || '').trim();
}
} catch (_) {
// Ignore invalid runtime rule patterns and continue with other candidates.
}
}
return null;
}
function isVisibleElement(node) {
return Boolean(node instanceof HTMLElement)
&& (Boolean(node.offsetParent) || getComputedStyle(node).position === 'fixed');
}
function collectThreadItems() {
return Array.from(document.querySelectorAll('.content-container')).filter((item) => {
if (!isVisibleElement(item)) return false;
return item.querySelector('.thread-participants')
&& item.querySelector('.thread-subject')
&& item.querySelector('.thread-preview');
});
}
function getThreadItemMetadata(item) {
const sender = normalizeText(item.querySelector('.thread-participants')?.textContent || '');
const subject = normalizeText(item.querySelector('.thread-subject')?.textContent || '');
const preview = normalizeText(item.querySelector('.thread-preview')?.textContent || '');
const timestamp = normalizeText(item.querySelector('.thread-timestamp')?.textContent || '');
return {
sender,
subject,
preview,
timestamp,
combinedText: normalizeText([sender, subject, preview, timestamp].filter(Boolean).join(' ')),
};
}
function buildItemSignature(item) {
const meta = getThreadItemMetadata(item);
return normalizeText([
item.getAttribute('aria-label') || '',
meta.sender,
meta.subject,
meta.preview,
meta.timestamp,
].join('::')).slice(0, 240);
}
function extractVerificationCode(text, options = {}) {
const matchedByRule = extractCodeByRulePatterns(text, options?.codePatterns);
if (matchedByRule) return matchedByRule;
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s:]*(\d{6})/);
if (matchCn) return matchCn[1];
const matchLoginCode = text.match(/(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
if (matchLoginCode) return matchLoginCode[1];
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
if (matchEn) return matchEn[1] || matchEn[2];
const match6 = text.match(/\b(\d{6})\b/);
if (match6) return match6[1];
return null;
}
function readOpenedMailHeader() {
const headerRoot = document.querySelector('.ic-efwqa7');
if (!headerRoot) {
return { sender: '', recipients: '', timestamp: '' };
}
const contactValues = Array.from(headerRoot.querySelectorAll('.contact-token .ic-x1z554'))
.map((node) => normalizeText(node.textContent))
.filter(Boolean);
const sender = contactValues[0] || '';
const recipients = contactValues.slice(1).join(' ');
const timestamp = normalizeText(headerRoot.querySelector('.ic-rffsj8')?.textContent || '');
return { sender, recipients, timestamp };
}
function getOpenedMailBodyRoot() {
return document.querySelector('.mail-message-defaults, .pane.thread-detail-pane');
}
function readOpenedMailBody() {
const bodyRoot = getOpenedMailBodyRoot();
return normalizeText(bodyRoot?.innerText || bodyRoot?.textContent || '');
}
function getThreadListItemRoot(item) {
return item?.closest?.('.thread-list-item, [role="treeitem"]') || null;
}
function isThreadItemSelected(item, expectedSignature = '') {
const expected = normalizeText(expectedSignature);
const candidates = collectThreadItems();
const matchedItem = expected
? candidates.find((candidate) => buildItemSignature(candidate) === expected)
: item;
const root = getThreadListItemRoot(matchedItem || item);
if (!root) {
return false;
}
if (root.getAttribute('aria-selected') === 'true') {
return true;
}
const className = String(root.className || '').toLowerCase();
return /\b(selected|current|active)\b/.test(className);
}
function openedMailMatchesExpectedContent(expectedMeta = {}, header = null, bodyText = '') {
const expectedSender = normalizeText(expectedMeta.sender || '').toLowerCase();
const expectedSubject = normalizeText(expectedMeta.subject || '').toLowerCase();
const combined = normalizeText([
header?.sender || '',
header?.recipients || '',
header?.timestamp || '',
bodyText || '',
].join(' ')).toLowerCase();
if (expectedSender && combined.includes(expectedSender)) {
return true;
}
if (expectedSubject && combined.includes(expectedSubject)) {
return true;
}
return false;
}
async function waitForOpenedMailContent(item, expectedMeta = {}, timeout = 10000) {
const expectedSignature = buildItemSignature(item);
const start = Date.now();
while (Date.now() - start < timeout) {
throwIfStopped();
const headerRoot = document.querySelector('.ic-efwqa7');
const bodyRoot = getOpenedMailBodyRoot();
const selected = isThreadItemSelected(item, expectedSignature);
if (selected && (headerRoot || bodyRoot)) {
const header = readOpenedMailHeader();
const bodyText = normalizeText(bodyRoot?.innerText || bodyRoot?.textContent || '');
if (openedMailMatchesExpectedContent(expectedMeta, header, bodyText)) {
return { headerRoot, bodyRoot };
}
}
await sleep(100);
}
throw new Error('打开邮件后未找到详情区域,请确认邮件内容已加载。');
}
async function openMailItemAndRead(item) {
const expectedMeta = getThreadItemMetadata(item);
simulateClick(item);
const { bodyRoot } = await waitForOpenedMailContent(item, expectedMeta, 10000);
await sleep(300);
const header = readOpenedMailHeader();
const bodyText = normalizeText(
bodyRoot?.innerText || bodyRoot?.textContent || readOpenedMailBody()
);
return {
...header,
bodyText,
combinedText: normalizeText([header.sender, header.recipients, header.timestamp, bodyText].filter(Boolean).join(' ')),
};
}
async function refreshInbox() {
const refreshPatterns = [/刷新/i, /refresh/i, /重新载入/i];
const candidates = document.querySelectorAll('button, [role="button"], a');
for (const node of candidates) {
const text = normalizeText(node.innerText || node.textContent || '');
const label = normalizeText(node.getAttribute('aria-label') || node.getAttribute('title') || '');
if (refreshPatterns.some((pattern) => pattern.test(text) || pattern.test(label))) {
simulateClick(node);
await sleep(1000);
return;
}
}
const inboxPatterns = [/收件箱/, /inbox/i];
for (const node of candidates) {
const text = normalizeText(node.innerText || node.textContent || '');
const label = normalizeText(node.getAttribute('aria-label') || node.getAttribute('title') || '');
if (inboxPatterns.some((pattern) => pattern.test(text) || pattern.test(label))) {
simulateClick(node);
await sleep(1000);
return;
}
}
}
function normalizePollSessionKey(payload = {}) {
const raw = String(payload?.sessionKey || '').trim();
if (raw) {
return raw;
}
return '';
}
function getOrCreatePollSessionBaseline(sessionKey, currentItems = []) {
if (!sessionKey) {
return {
signatures: new Set(currentItems.map(buildItemSignature)),
fallbackCarry: 0,
fromCache: false,
};
}
const cached = ICLOUD_POLL_SESSION_CACHE.get(sessionKey);
if (cached && cached.signatures instanceof Set) {
return {
signatures: new Set(cached.signatures),
fallbackCarry: Math.max(0, Number(cached.fallbackCarry) || 0),
fromCache: true,
};
}
return {
signatures: new Set(currentItems.map(buildItemSignature)),
fallbackCarry: 0,
fromCache: false,
};
}
function persistPollSessionBaseline(sessionKey, signatures, fallbackCarry = 0) {
if (!sessionKey) {
return;
}
ICLOUD_POLL_SESSION_CACHE.set(sessionKey, {
signatures: new Set(signatures || []),
fallbackCarry: Math.max(0, Number(fallbackCarry) || 0),
updatedAt: Date.now(),
});
if (ICLOUD_POLL_SESSION_CACHE.size > 12) {
const oldest = Array.from(ICLOUD_POLL_SESSION_CACHE.entries())
.sort((left, right) => Number(left?.[1]?.updatedAt || 0) - Number(right?.[1]?.updatedAt || 0))
.slice(0, ICLOUD_POLL_SESSION_CACHE.size - 12);
oldest.forEach(([key]) => ICLOUD_POLL_SESSION_CACHE.delete(key));
}
}
async function handlePollEmail(step, payload) {
const {
codePatterns = [],
senderFilters,
subjectFilters,
maxAttempts,
intervalMs,
excludeCodes = [],
} = payload;
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
const FALLBACK_AFTER = 3;
const pollSessionKey = normalizePollSessionKey(payload);
const normalizedSenderFilters = senderFilters.map((filter) => String(filter || '').toLowerCase()).filter(Boolean);
const normalizedSubjectFilters = subjectFilters.map((filter) => String(filter || '').toLowerCase()).filter(Boolean);
log(`步骤 ${step}:开始轮询 iCloud 邮箱(最多 ${maxAttempts} 次)`);
await waitForElement('.content-container', 10000);
await sleep(1500);
const currentItems = collectThreadItems();
const sessionBaseline = getOrCreatePollSessionBaseline(pollSessionKey, currentItems);
const existingSignatures = sessionBaseline.signatures;
let fallbackCarry = sessionBaseline.fallbackCarry;
if (sessionBaseline.fromCache) {
log(`步骤 ${step}:已复用当前会话旧邮件快照(${existingSignatures.size} 封)。`);
} else {
log(`步骤 ${step}:已记录当前 ${existingSignatures.size} 封旧邮件快照`);
}
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
log(`步骤 ${step}:正在轮询 iCloud 邮箱,第 ${attempt}/${maxAttempts}`);
if (attempt > 1) {
await refreshInbox();
await sleep(1200);
}
const items = collectThreadItems();
const useFallback = (fallbackCarry + attempt) > FALLBACK_AFTER;
for (const [index, item] of items.entries()) {
const signature = buildItemSignature(item);
const allowInitialStep8VisibleCode = Number(step) === 8
&& attempt === 1
&& index === 0
&& !sessionBaseline.fromCache;
if (!useFallback && existingSignatures.has(signature) && !allowInitialStep8VisibleCode) {
continue;
}
const meta = getThreadItemMetadata(item);
const lowerSender = meta.sender.toLowerCase();
const lowerSubject = normalizeText([meta.subject, meta.preview].join(' ')).toLowerCase();
const senderMatch = normalizedSenderFilters.some((filter) => lowerSender.includes(filter));
const subjectMatch = normalizedSubjectFilters.some((filter) => lowerSubject.includes(filter));
if (!senderMatch && !subjectMatch) {
continue;
}
let code = extractVerificationCode(meta.combinedText, { codePatterns });
let opened = null;
if (!code) {
opened = await openMailItemAndRead(item);
const openedSender = opened.sender.toLowerCase();
const openedBody = opened.bodyText.toLowerCase();
const openedSenderMatch = normalizedSenderFilters.some((filter) => openedSender.includes(filter));
const openedSubjectMatch = normalizedSubjectFilters.some((filter) => openedBody.includes(filter));
if (!openedSenderMatch && !openedSubjectMatch && !senderMatch && !subjectMatch) {
continue;
}
code = extractVerificationCode(opened.combinedText, { codePatterns });
}
if (!code) {
continue;
}
if (excludedCodeSet.has(code)) {
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
continue;
}
const source = useFallback && existingSignatures.has(signature) ? '回退匹配邮件' : '新邮件';
log(`步骤 ${step}:已找到验证码:${code}(来源:${source}`, 'ok');
persistPollSessionBaseline(
pollSessionKey,
new Set(collectThreadItems().map(buildItemSignature)),
0
);
return {
ok: true,
code,
emailTimestamp: Date.now(),
preview: (opened?.combinedText || meta.combinedText).slice(0, 160),
};
}
if (attempt === FALLBACK_AFTER + 1) {
log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn');
}
if (attempt < maxAttempts) {
await sleep(intervalMs);
}
}
fallbackCarry += maxAttempts;
persistPollSessionBaseline(
pollSessionKey,
new Set(collectThreadItems().map(buildItemSignature)),
fallbackCarry
);
throw new Error(
`${Math.round((maxAttempts * intervalMs) / 1000)} 秒后仍未在 iCloud 邮箱中找到新的匹配邮件。请手动检查收件箱。`
);
}
}
+321
View File
@@ -0,0 +1,321 @@
// content/inbucket-mail.js — Content script for Inbucket polling (steps 4, 7)
// Injected dynamically on the configured Inbucket host
//
// Supported page:
// - /m/<mailbox>/
const INBUCKET_PREFIX = '[MultiPage:inbucket-mail]';
const isTopFrame = window === window.top;
const SEEN_MAIL_IDS_KEY = 'seenInbucketMailIds';
console.log(INBUCKET_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
if (!isTopFrame) {
console.log(INBUCKET_PREFIX, 'Skipping child frame');
} else {
let seenMailIds = new Set();
async function loadSeenMailIds() {
try {
const data = await chrome.storage.session.get(SEEN_MAIL_IDS_KEY);
if (Array.isArray(data[SEEN_MAIL_IDS_KEY])) {
seenMailIds = new Set(data[SEEN_MAIL_IDS_KEY]);
console.log(INBUCKET_PREFIX, `Loaded ${seenMailIds.size} previously seen mail ids`);
}
} catch (err) {
console.warn(INBUCKET_PREFIX, 'Session storage unavailable, using in-memory seen mail ids:', err?.message || err);
}
}
async function persistSeenMailIds() {
try {
await chrome.storage.session.set({ [SEEN_MAIL_IDS_KEY]: [...seenMailIds] });
} catch (err) {
console.warn(INBUCKET_PREFIX, 'Could not persist seen mail ids, continuing in-memory only:', err?.message || err);
}
}
loadSeenMailIds();
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'POLL_EMAIL') {
resetStopState();
handlePollEmail(message.step, message.payload).then(result => {
sendResponse(result);
}).catch(err => {
if (isStopError(err)) {
log(`步骤 ${message.step}:已被用户停止。`, 'warn');
sendResponse({ stopped: true, error: err.message });
return;
}
log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn');
sendResponse({ error: err.message });
});
return true;
}
});
function normalizeText(value) {
return (value || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
function normalizeRulePatternList(patterns = []) {
return Array.isArray(patterns) ? patterns : [];
}
function extractCodeByRulePatterns(text, patterns = []) {
const normalizedText = String(text || '');
for (const pattern of normalizeRulePatternList(patterns)) {
try {
const source = String(pattern?.source || '').trim();
if (!source) {
continue;
}
const flags = String(pattern?.flags || '').replace(/[^dgimsuvy]/g, '');
const match = normalizedText.match(new RegExp(source, flags));
if (!match) {
continue;
}
for (let index = 1; index < match.length; index += 1) {
const candidate = String(match[index] || '').trim();
if (candidate) {
return candidate;
}
}
if (String(match[0] || '').trim()) {
return String(match[0] || '').trim();
}
} catch (_) {
// Ignore invalid runtime rule patterns and continue with other candidates.
}
}
return null;
}
function matchesKeywordHints(text, keywords = []) {
const normalizedText = normalizeText(text);
const normalizedKeywords = Array.isArray(keywords) ? keywords : [];
return normalizedKeywords.some((keyword) => normalizedText.includes(normalizeText(keyword)));
}
function extractVerificationCode(text, options = {}) {
const matchedByRule = extractCodeByRulePatterns(text, options?.codePatterns);
if (matchedByRule) {
return matchedByRule;
}
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s:]*(\d{6})/);
if (matchCn) return matchCn[1];
const matchLoginCode = text.match(/(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
if (matchLoginCode) return matchLoginCode[1];
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
if (matchEn) return matchEn[1] || matchEn[2];
const match6 = text.match(/\b(\d{6})\b/);
if (match6) return match6[1];
return null;
}
function rowMatchesFilters(mail, senderFilters, subjectFilters, targetEmail, options = {}) {
const sender = normalizeText(mail.sender);
const subject = normalizeText(mail.subject);
const mailbox = normalizeText(mail.mailbox);
const combined = normalizeText(mail.combinedText);
const targetLocal = normalizeText((targetEmail || '').split('@')[0]);
const senderMatch = senderFilters.some(f => sender.includes(f.toLowerCase()) || combined.includes(f.toLowerCase()));
const subjectMatch = subjectFilters.some(f => subject.includes(f.toLowerCase()) || combined.includes(f.toLowerCase()));
const mailboxMatch = Boolean(targetLocal) && mailbox.includes(targetLocal);
const forwardedDuck = /duckduckgo|forward(?:ed)?\s*by/i.test(mail.combinedText);
const code = extractVerificationCode(mail.combinedText, {
codePatterns: options?.codePatterns,
});
const keywordMatch = options?.requiredKeywords?.length
? matchesKeywordHints(combined, options.requiredKeywords)
: /verify|verification|confirm|log-?in|验证码|代码/.test(combined);
if (mailboxMatch) return { matched: true, mailboxMatch, code };
if (senderMatch || subjectMatch) return { matched: true, mailboxMatch: false, code };
if (code && (forwardedDuck || keywordMatch)) return { matched: true, mailboxMatch: false, code };
return { matched: false, mailboxMatch: false, code };
}
function findMailboxEntries() {
return document.querySelectorAll('.message-list-entry');
}
function getMailboxEntryId(entry, index = 0) {
const explicitId = entry.getAttribute('data-id') || entry.dataset?.id || '';
if (explicitId) return explicitId;
const subject = entry.querySelector('.subject')?.textContent?.trim() || '';
const sender = entry.querySelector('.from')?.textContent?.trim() || '';
const dateText = entry.querySelector('.date')?.textContent?.trim() || '';
return `mailbox:${index}:${normalizeText(subject)}|${normalizeText(sender)}|${normalizeText(dateText)}`;
}
function parseMailboxEntry(entry, index = 0) {
const subject = entry.querySelector('.subject')?.textContent?.trim() || '';
const sender = entry.querySelector('.from')?.textContent?.trim() || '';
const dateText = entry.querySelector('.date')?.textContent?.trim() || '';
const combinedText = [subject, sender, dateText].filter(Boolean).join(' ');
return {
entry,
dateText,
sender,
mailbox: '',
subject,
unread: entry.classList.contains('unseen'),
combinedText,
mailId: getMailboxEntryId(entry, index),
};
}
function getCurrentMailboxIds() {
const ids = new Set();
Array.from(findMailboxEntries()).forEach((entry, index) => {
ids.add(getMailboxEntryId(entry, index));
});
return ids;
}
async function refreshMailbox() {
const refreshButton = document.querySelector('button[alt="Refresh Mailbox"]');
if (!refreshButton) return;
simulateClick(refreshButton);
await sleep(800);
}
async function openMailboxEntry(entry) {
simulateClick(entry);
for (let i = 0; i < 20; i++) {
if (entry.classList.contains('selected') || document.querySelector('.message-header, .message-body, .button-bar')) {
return;
}
await sleep(150);
}
}
async function deleteCurrentMailboxMessage(step) {
try {
const deleteButton = await waitForElement('.button-bar button.danger', 5000);
simulateClick(deleteButton);
log(`步骤 ${step}:已删除邮箱消息`, 'ok');
await sleep(1200);
} catch (err) {
log(`步骤 ${step}:删除邮箱消息失败:${err.message}`, 'warn');
}
}
async function handleMailboxPollEmail(step, payload) {
const {
codePatterns = [],
requiredKeywords = [],
senderFilters = [],
subjectFilters = [],
maxAttempts = 20,
intervalMs = 3000,
excludeCodes = [],
} = payload || {};
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
log(`步骤 ${step}:开始轮询 Inbucket 邮箱页面(最多 ${maxAttempts} 次)`);
try {
await waitForElement('.message-list, .message-list-entry', 15000);
log(`步骤 ${step}:邮箱页面已加载`);
} catch {
throw new Error('Inbucket 邮箱页面未加载完成,请确认已打开 /m/<mailbox>/ 页面。');
}
const existingMailIds = getCurrentMailboxIds();
log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧消息快照`);
const FALLBACK_AFTER = 3;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
log(`步骤 ${step}:正在轮询 Inbucket 邮箱,第 ${attempt}/${maxAttempts}`);
if (attempt > 1) {
await refreshMailbox();
}
const entries = Array.from(findMailboxEntries()).map(parseMailboxEntry);
const useFallback = attempt > FALLBACK_AFTER;
const candidates = [];
for (const mail of entries) {
if (!mail.unread) continue;
if (seenMailIds.has(mail.mailId)) continue;
if (!useFallback && existingMailIds.has(mail.mailId)) continue;
const match = rowMatchesFilters(mail, senderFilters, subjectFilters, '', {
codePatterns,
requiredKeywords,
});
if (!match.matched) continue;
candidates.push({ ...mail, code: match.code });
}
for (const mail of candidates) {
const code = mail.code || extractVerificationCode(mail.combinedText, {
codePatterns,
});
if (!code) continue;
if (excludedCodeSet.has(code)) {
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
continue;
}
await openMailboxEntry(mail.entry);
await deleteCurrentMailboxMessage(step);
seenMailIds.add(mail.mailId);
await persistSeenMailIds();
const source = existingMailIds.has(mail.mailId) ? '回退匹配邮件' : '新邮件';
log(
`步骤 ${step}:已找到验证码:${code}(来源:${source},发件人:${mail.sender || '未知'},主题:${(mail.subject || '').slice(0, 60)}`,
'ok'
);
return {
ok: true,
code,
emailTimestamp: Date.now(),
mailId: mail.mailId,
};
}
if (attempt === FALLBACK_AFTER + 1) {
log(`步骤 ${step}:暂未发现新消息,开始回退到较早的匹配邮件`, 'warn');
}
if (attempt < maxAttempts) {
await sleep(intervalMs);
}
}
throw new Error(
`${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 Inbucket 邮箱中找到匹配的验证码邮件。` +
'请手动检查邮箱页面。'
);
}
async function handlePollEmail(step, payload) {
if (!location.pathname.startsWith('/m/')) {
throw new Error('当前 Inbucket 仅支持 /m/<mailbox>/ 这种邮箱页面。');
}
return handleMailboxPollEmail(step, payload);
}
} // end of isTopFrame else block
+783
View File
@@ -0,0 +1,783 @@
// content/mail-163.js — Content script for 163 Mail (steps 4, 7)
// Injected on: mail.163.com
//
// DOM structure:
// Mail item: div[sign="letter"] with aria-label="你的 ChatGPT 代码为 479637 发件人 OpenAI ..."
// Sender: .nui-user (e.g., "OpenAI")
// Subject: span.da0 (e.g., "你的 ChatGPT 代码为 479637")
// Delete actions: hover trash icon on the row, or checkbox + toolbar delete button
const MAIL163_PREFIX = '[MultiPage:mail-163]';
const isTopFrame = window === window.top;
console.log(MAIL163_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
// Only operate in the top frame
if (!isTopFrame) {
console.log(MAIL163_PREFIX, 'Skipping child frame');
} else {
// Track codes we've already seen — persisted in chrome.storage.session to survive script re-injection
let seenCodes = new Set();
async function loadSeenCodes() {
try {
const data = await chrome.storage.session.get('seenCodes');
if (data.seenCodes && Array.isArray(data.seenCodes)) {
seenCodes = new Set(data.seenCodes);
console.log(MAIL163_PREFIX, `Loaded ${seenCodes.size} previously seen codes`);
}
} catch (err) {
console.warn(MAIL163_PREFIX, 'Session storage unavailable, using in-memory seen codes:', err?.message || err);
}
}
// Load previously seen codes on startup
loadSeenCodes();
async function persistSeenCodes() {
try {
await chrome.storage.session.set({ seenCodes: [...seenCodes] });
} catch (err) {
console.warn(MAIL163_PREFIX, 'Could not persist seen codes, continuing in-memory only:', err?.message || err);
}
}
// ============================================================
// Message Handler (top frame only)
// ============================================================
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'POLL_EMAIL') {
resetStopState();
handlePollEmail(message.step, message.payload).then(result => {
sendResponse(result);
}).catch(err => {
if (isStopError(err)) {
log(`步骤 ${message.step}:已被用户停止。`, 'warn');
sendResponse({ stopped: true, error: err.message });
return;
}
log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn');
sendResponse({ error: err.message });
});
return true;
}
});
// ============================================================
// Find mail items
// ============================================================
function normalizeText(value) {
return String(value || '').replace(/\s+/g, ' ').trim();
}
function normalizeRulePatternList(patterns = []) {
return Array.isArray(patterns) ? patterns : [];
}
function extractCodeByRulePatterns(text, patterns = []) {
const normalizedText = String(text || '');
for (const pattern of normalizeRulePatternList(patterns)) {
try {
const source = String(pattern?.source || '').trim();
if (!source) {
continue;
}
const flags = String(pattern?.flags || '').replace(/[^dgimsuvy]/g, '');
const match = normalizedText.match(new RegExp(source, flags));
if (!match) {
continue;
}
for (let index = 1; index < match.length; index += 1) {
const candidate = String(match[index] || '').trim();
if (candidate) {
return candidate;
}
}
if (String(match[0] || '').trim()) {
return String(match[0] || '').trim();
}
} catch (_) {
// Ignore invalid runtime rule patterns and continue with other candidates.
}
}
return null;
}
function getNetEaseMailLabel(hostname) {
const currentHostname = String(
hostname || (typeof location !== 'undefined' ? location.hostname : '') || ''
).toLowerCase();
if (currentHostname === 'mail.126.com' || currentHostname.endsWith('.mail.126.com')) {
return '126 邮箱';
}
if (currentHostname === 'webmail.vip.163.com') {
return '163 VIP 邮箱';
}
return '163 邮箱';
}
function isVisibleNode(node) {
if (!node) return false;
if (node.hidden) return false;
const style = typeof window.getComputedStyle === 'function'
? window.getComputedStyle(node)
: null;
if (style && (style.display === 'none' || style.visibility === 'hidden')) {
return false;
}
const rect = typeof node.getBoundingClientRect === 'function'
? node.getBoundingClientRect()
: null;
if (rect && rect.width <= 0 && rect.height <= 0) {
return false;
}
return true;
}
function isLikelyMailItemNode(node) {
if (!isVisibleNode(node)) {
return false;
}
if (node.matches?.('div[sign="letter"]')) {
return true;
}
if (node.querySelector?.('.nui-user, span.da0, [sign="trash"], [title="删除邮件"], [class*="subject"], [class*="sender"]')) {
return true;
}
const summaryText = normalizeText(
node.getAttribute?.('aria-label')
|| node.getAttribute?.('title')
|| node.textContent
);
if (!summaryText) {
return false;
}
return /发件人|验证码|verification|code|log-?in/i.test(summaryText);
}
function findMailItems() {
const selectorGroups = [
'div[sign="letter"]',
'[role="option"][aria-label]',
'[role="listitem"][aria-label]',
'tr[aria-label]',
'li[aria-label]',
'div[aria-label]',
];
for (const selector of selectorGroups) {
const matches = Array.from(document.querySelectorAll(selector)).filter(isLikelyMailItemNode);
if (matches.length > 0) {
return matches;
}
}
return [];
}
function getMailTextBySelectors(item, selectors = []) {
for (const selector of selectors) {
const candidates = item.querySelectorAll(selector);
for (const candidate of candidates) {
const texts = [
candidate.getAttribute?.('title'),
candidate.getAttribute?.('aria-label'),
candidate.textContent,
];
for (const text of texts) {
const normalized = normalizeText(text);
if (normalized) {
return normalized;
}
}
}
}
return '';
}
function getMailSenderText(item) {
return getMailTextBySelectors(item, [
'.nui-user',
'[class*="sender"]',
'[class*="from"]',
'[data-sender]',
]);
}
function getMailSubjectText(item) {
return getMailTextBySelectors(item, [
'span.da0',
'[class*="subject"]',
'[data-subject]',
'strong',
]);
}
function getMailRowText(item) {
const ariaLabel = normalizeText(item.getAttribute('aria-label'));
const sender = getMailSenderText(item);
const subject = getMailSubjectText(item);
const fullText = normalizeText(item.textContent || '');
return normalizeText([ariaLabel, sender, subject, fullText].filter(Boolean).join(' '));
}
function getMailItemId(item, index = 0) {
const candidates = [
item.getAttribute('id'),
item.getAttribute('data-id'),
item.dataset?.id,
item.getAttribute('data-key'),
item.getAttribute('key'),
].filter(Boolean);
if (candidates.length > 0) {
return String(candidates[0]);
}
return `${index}|${getMailRowText(item).slice(0, 240)}`;
}
function getCurrentMailIds(items = []) {
const ids = new Set();
const sourceItems = items.length > 0 ? items : findMailItems();
sourceItems.forEach((item, index) => {
ids.add(getMailItemId(item, index));
});
return ids;
}
function normalizeMinuteTimestamp(timestamp) {
if (!Number.isFinite(timestamp) || timestamp <= 0) return 0;
const date = new Date(timestamp);
date.setSeconds(0, 0);
return date.getTime();
}
function parseMail163Timestamp(rawText) {
const text = normalizeText(rawText);
if (!text) return null;
let match = text.match(/(\d{4})年(\d{1,2})月(\d{1,2})日\s+(\d{1,2}):(\d{2})/);
if (match) {
const [, year, month, day, hour, minute] = match;
return new Date(
Number(year),
Number(month) - 1,
Number(day),
Number(hour),
Number(minute),
0,
0
).getTime();
}
match = text.match(/今天\s*(\d{1,2}):(\d{2})/);
if (match) {
const [, hour, minute] = match;
const now = new Date();
return new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
Number(hour),
Number(minute),
0,
0
).getTime();
}
match = text.match(/昨天\s*(\d{1,2}):(\d{2})/);
if (match) {
const [, hour, minute] = match;
const now = new Date();
now.setDate(now.getDate() - 1);
return new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
Number(hour),
Number(minute),
0,
0
).getTime();
}
match = text.match(/\b(\d{1,2}):(\d{2})\b/);
if (match) {
const [, hour, minute] = match;
const now = new Date();
return new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
Number(hour),
Number(minute),
0,
0
).getTime();
}
return null;
}
function isLikelyMailTimestampText(value) {
const text = normalizeText(value);
if (!text) {
return false;
}
return /(\d{4}年\d{1,2}月\d{1,2}日\s+\d{1,2}:\d{2})|今天\s*\d{1,2}:\d{2}|昨天\s*\d{1,2}:\d{2}|\b\d{1,2}:\d{2}\b/.test(text);
}
function collectMailTimestampCandidates(item) {
const candidates = [];
const seen = new Set();
const pushCandidate = (value) => {
const normalized = normalizeText(value);
if (!normalized || !isLikelyMailTimestampText(normalized) || seen.has(normalized)) {
return;
}
seen.add(normalized);
candidates.push(normalized);
};
const priorityNodes = item.querySelectorAll('.e00, [title], [aria-label], time, [class*="time"], [class*="date"]');
priorityNodes.forEach((node) => {
pushCandidate(node.getAttribute?.('title'));
pushCandidate(node.getAttribute?.('aria-label'));
pushCandidate(node.textContent);
});
const textNodes = item.querySelectorAll('span, div, td, strong, b');
textNodes.forEach((node) => {
const text = normalizeText(node.textContent);
if (text && text.length <= 24) {
pushCandidate(text);
}
});
pushCandidate(item.getAttribute('aria-label'));
pushCandidate(item.getAttribute('title'));
return candidates;
}
function getMailTimestamp(item) {
const candidates = collectMailTimestampCandidates(item);
for (const candidate of candidates) {
const parsed = parseMail163Timestamp(candidate);
if (parsed) return parsed;
}
return null;
}
function collectOpenedMailTextCandidates() {
const texts = [];
const seen = new Set();
const pushText = (value) => {
const normalized = normalizeText(value);
if (!normalized || seen.has(normalized)) {
return;
}
seen.add(normalized);
texts.push(normalized);
};
const selectors = [
'.readHtml',
'[class*="readmail"]',
'[class*="mailread"]',
'[class*="mailBody"]',
'[class*="mailbody"]',
'[class*="mail-content"]',
'[class*="mailContent"]',
'[class*="mail-detail"]',
'[class*="mailDetail"]',
'[class*="detail"] [class*="content"]',
'[class*="read"] [class*="content"]',
'[role="main"]',
];
selectors.forEach((selector) => {
document.querySelectorAll(selector).forEach((node) => {
pushText(node.innerText || node.textContent);
});
});
document.querySelectorAll('iframe').forEach((frame) => {
try {
pushText(frame.contentDocument?.body?.innerText || frame.contentDocument?.body?.textContent);
} catch {
// Ignore cross-frame access errors and keep trying other candidates.
}
});
pushText(document.body?.innerText || document.body?.textContent);
return texts.sort((a, b) => b.length - a.length);
}
function selectOpenedMailTextCandidate(item, candidates = [], options = {}) {
const subject = normalizeText(getMailSubjectText(item)).toLowerCase();
const sender = normalizeText(getMailSenderText(item)).toLowerCase();
const excludedSet = new Set((options.excludedTexts || []).map((value) => normalizeText(value)));
const allowExcludedFallback = options.allowExcludedFallback !== false;
const pickCandidate = (source) => source.find((candidate) => {
const lower = candidate.toLowerCase();
if (subject && lower.includes(subject)) {
return true;
}
if (sender && lower.includes(sender)) {
return true;
}
return Boolean(extractVerificationCode(candidate, { codePatterns: options.codePatterns }) && /verification|验证码|log-?in\s+code|enter\s+this\s+code|code/i.test(lower));
}) || source[0] || '';
const filteredCandidates = candidates.filter((candidate) => !excludedSet.has(normalizeText(candidate)));
const preferred = pickCandidate(filteredCandidates);
if (preferred || !allowExcludedFallback) {
return preferred;
}
return pickCandidate(candidates);
}
function readOpenedMailText(item, options = {}) {
const candidates = collectOpenedMailTextCandidates();
return selectOpenedMailTextCandidate(item, candidates, options);
}
async function returnToInbox() {
const inboxLink = document.querySelector('.nui-tree-item-text[title="收件箱"], [title="收件箱"]');
if (inboxLink) {
if (typeof simulateClick === 'function') {
simulateClick(inboxLink);
} else {
inboxLink.click();
}
}
for (let i = 0; i < 20; i += 1) {
if (findMailItems().length > 0) {
return true;
}
await sleep(250);
}
return false;
}
async function openMailAndGetMessageText(item, options = {}) {
const beforeCandidates = collectOpenedMailTextCandidates();
const beforeText = selectOpenedMailTextCandidate(item, beforeCandidates, {
codePatterns: options.codePatterns,
});
if (typeof simulateClick === 'function') {
simulateClick(item);
} else {
item.click();
}
let openedText = '';
for (let i = 0; i < 24; i += 1) {
await sleep(250);
const candidate = readOpenedMailText(item, {
codePatterns: options.codePatterns,
excludedTexts: beforeCandidates,
allowExcludedFallback: false,
});
if (!candidate) {
continue;
}
openedText = candidate;
if (extractVerificationCode(candidate, { codePatterns: options.codePatterns })) {
break;
}
if (candidate !== beforeText && candidate.length > beforeText.length + 24) {
break;
}
}
await returnToInbox();
return openedText;
}
function scheduleEmailCleanup(item, step) {
setTimeout(() => {
Promise.resolve(deleteEmail(item, step)).catch(() => {
// Cleanup is best effort only and must never affect the main verification flow.
});
}, 0);
}
// ============================================================
// Email Polling
// ============================================================
async function handlePollEmail(step, payload) {
const {
codePatterns = [],
senderFilters,
subjectFilters,
maxAttempts,
intervalMs,
excludeCodes = [],
filterAfterTimestamp = 0,
} = payload;
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
const filterAfterMinute = normalizeMinuteTimestamp(Number(filterAfterTimestamp) || 0);
const mailLabel = getNetEaseMailLabel();
log(`步骤 ${step}:开始轮询 ${mailLabel}(最多 ${maxAttempts} 次)`);
if (filterAfterMinute) {
log(`步骤 ${step}:仅尝试 ${new Date(filterAfterMinute).toLocaleString('zh-CN', { hour12: false })} 及之后时间的邮件。`);
}
// Click inbox in sidebar to ensure we're in inbox view
log(`步骤 ${step}:正在等待侧边栏加载...`);
try {
const inboxLink = await waitForElement('.nui-tree-item-text[title="收件箱"]', 5000);
inboxLink.click();
log(`步骤 ${step}:已点击收件箱`);
} catch {
log(`步骤 ${step}:未找到收件箱入口,继续尝试后续流程...`, 'warn');
}
// Wait for mail list to appear
log(`步骤 ${step}:正在等待邮件列表加载...`);
let items = [];
for (let i = 0; i < 20; i++) {
items = findMailItems();
if (items.length > 0) break;
await sleep(500);
}
if (items.length === 0) {
await refreshInbox();
await sleep(2000);
items = findMailItems();
}
if (items.length === 0) {
throw new Error(`${mailLabel}列表未加载完成,请确认当前已打开收件箱。`);
}
log(`步骤 ${step}:邮件列表已加载,共 ${items.length} 封邮件`);
// Snapshot existing mail IDs
const existingMailIds = getCurrentMailIds(items);
log(`步骤 ${step}:已记录当前 ${existingMailIds.size} 封旧邮件快照`);
const FALLBACK_AFTER = 3;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
log(`步骤 ${step}:正在轮询 ${mailLabel},第 ${attempt}/${maxAttempts}`);
if (attempt > 1) {
await refreshInbox();
await sleep(1000);
}
const allItems = findMailItems();
const useFallback = attempt > FALLBACK_AFTER;
for (let index = 0; index < allItems.length; index++) {
const item = allItems[index];
const id = getMailItemId(item, index);
const mailTimestamp = getMailTimestamp(item);
const mailMinute = normalizeMinuteTimestamp(mailTimestamp || 0);
const passesTimeFilter = !filterAfterMinute || (mailMinute && mailMinute >= filterAfterMinute);
if (!passesTimeFilter) {
continue;
}
if (!useFallback && existingMailIds.has(id)) {
continue;
}
const sender = getMailSenderText(item).toLowerCase();
const subject = getMailSubjectText(item);
const rowText = getMailRowText(item);
const ariaLabel = normalizeText(item.getAttribute('aria-label')).toLowerCase();
const combinedText = normalizeText([subject, ariaLabel, rowText].filter(Boolean).join(' '));
if (!mailTimestamp) {
log(`步骤 ${step}:邮件 ${id.slice(0, 60)} 未读取到时间,已跳过时间窗口校验后的文本匹配阶段。`, 'info');
}
const senderMatch = senderFilters.some((filter) => {
const normalizedFilter = String(filter || '').toLowerCase();
return normalizedFilter && (sender.includes(normalizedFilter) || ariaLabel.includes(normalizedFilter) || rowText.toLowerCase().includes(normalizedFilter));
});
const subjectMatch = subjectFilters.some((filter) => {
const normalizedFilter = String(filter || '').toLowerCase();
return normalizedFilter && (subject.toLowerCase().includes(normalizedFilter) || ariaLabel.includes(normalizedFilter) || rowText.toLowerCase().includes(normalizedFilter));
});
if (senderMatch || subjectMatch) {
let code = extractVerificationCode(combinedText, { codePatterns });
let codeSource = '邮件列表';
if (!code) {
const openedText = await openMailAndGetMessageText(item, { codePatterns });
code = extractVerificationCode(openedText, { codePatterns });
if (code) {
codeSource = '邮件正文';
}
}
if (code && excludedCodeSet.has(code)) {
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
} else if (code && !seenCodes.has(code)) {
seenCodes.add(code);
persistSeenCodes();
const source = useFallback && existingMailIds.has(id) ? `回退匹配${codeSource}` : `新邮件${codeSource}`;
const timeLabel = mailTimestamp ? `,时间:${new Date(mailTimestamp).toLocaleString('zh-CN', { hour12: false })}` : '';
log(`步骤 ${step}:已找到验证码:${code}(来源:${source}${timeLabel},主题:${subject.slice(0, 40)}`, 'ok');
// Trigger cleanup only as a best-effort side effect.
scheduleEmailCleanup(item, step);
return { ok: true, code, emailTimestamp: Date.now(), mailId: id };
} else if (code && seenCodes.has(code)) {
log(`步骤 ${step}:跳过已处理过的验证码:${code}`, 'info');
}
}
}
if (attempt === FALLBACK_AFTER + 1) {
log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn');
}
if (attempt < maxAttempts) {
await sleep(intervalMs);
}
}
throw new Error(
`${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未在 ${mailLabel}中找到新的匹配邮件。` +
'请手动检查收件箱。'
);
}
// ============================================================
// Delete Email via Hover Trash / Toolbar Fallback
// ============================================================
async function deleteEmail(item, step) {
try {
log(`步骤 ${step}:正在删除邮件...`);
// Strategy 1: Click the trash icon inside the mail item
// Each mail item has: <b class="nui-ico nui-ico-delete" title="删除邮件" sign="trash">
// These icons appear on hover, so we trigger mouseover first
item.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
item.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
await sleep(300);
const trashIcon = item.querySelector('[sign="trash"], .nui-ico-delete, [title="删除邮件"]');
if (trashIcon) {
trashIcon.click();
log(`步骤 ${step}:已点击删除图标`, 'ok');
await sleep(1500);
// Check if item disappeared (confirm deletion)
const stillExists = document.getElementById(item.id);
if (!stillExists || stillExists.style.display === 'none') {
log(`步骤 ${step}:邮件已成功删除`);
} else {
log(`步骤 ${step}:邮件可能尚未删除,列表中仍可见`, 'warn');
}
return;
}
// Strategy 2: Select checkbox then click toolbar delete button
log(`步骤 ${step}:未找到删除图标,尝试使用复选框加工具栏删除...`);
const checkbox = item.querySelector('[sign="checkbox"], .nui-chk');
if (checkbox) {
checkbox.click();
await sleep(300);
// Click toolbar delete button
const toolbarBtns = document.querySelectorAll('.nui-btn .nui-btn-text');
for (const btn of toolbarBtns) {
if (btn.textContent.replace(/\s/g, '').includes('删除')) {
btn.closest('.nui-btn').click();
log(`步骤 ${step}:已点击工具栏删除`, 'ok');
await sleep(1500);
return;
}
}
}
log(`步骤 ${step}:无法删除邮件(未找到删除按钮)`, 'warn');
} catch (err) {
log(`步骤 ${step}:删除邮件失败:${err.message}`, 'warn');
}
}
// ============================================================
// Inbox Refresh
// ============================================================
async function refreshInbox() {
// Try toolbar "刷 新" button
const toolbarBtns = document.querySelectorAll('.nui-btn .nui-btn-text');
for (const btn of toolbarBtns) {
if (btn.textContent.replace(/\s/g, '') === '刷新') {
btn.closest('.nui-btn').click();
console.log(MAIL163_PREFIX, 'Clicked "刷新" button');
await sleep(800);
return;
}
}
// Fallback: click sidebar "收 信"
const shouXinBtns = document.querySelectorAll('.ra0');
for (const btn of shouXinBtns) {
if (btn.textContent.replace(/\s/g, '').includes('收信')) {
btn.click();
console.log(MAIL163_PREFIX, 'Clicked "收信" button');
await sleep(800);
return;
}
}
console.log(MAIL163_PREFIX, 'Could not find refresh button');
}
// ============================================================
// Verification Code Extraction
// ============================================================
function extractVerificationCode(text, options = {}) {
const matchedByRule = extractCodeByRulePatterns(text, options?.codePatterns);
if (matchedByRule) {
return matchedByRule;
}
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s:]*(\d{6})/);
if (matchCn) return matchCn[1];
const matchLoginCode = text.match(/(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
if (matchLoginCode) return matchLoginCode[1];
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
if (matchEn) return matchEn[1] || matchEn[2];
const match6 = text.match(/\b(\d{6})\b/);
if (match6) return match6[1];
return null;
}
} // end of isTopFrame else block
+1411
View File
File diff suppressed because it is too large Load Diff
+98
View File
@@ -0,0 +1,98 @@
(function attachOperationDelay(root) {
const OPERATION_DELAY_MS = 2000;
const SETTING_RESTORE_FALLBACK_MS = 50;
const EXCLUDED_STEP_KEYS = new Set(['confirm-oauth', 'platform-verify']);
let operationDelayEnabled = true;
let operationDelaySettingReady = null;
let operationDelaySettingRevision = 0;
function normalizeOperationDelayEnabled(value) {
return typeof value === 'boolean' ? value : true;
}
function getOperationDelayEnabled() {
return operationDelayEnabled;
}
async function refreshOperationDelaySetting() {
const restoreRevision = ++operationDelaySettingRevision;
const ready = (async () => {
let nextEnabled = true;
try {
const data = await root.chrome?.storage?.local?.get?.(['operationDelayEnabled']);
nextEnabled = normalizeOperationDelayEnabled(data?.operationDelayEnabled);
} catch {
nextEnabled = true;
}
if (operationDelaySettingRevision === restoreRevision) {
operationDelayEnabled = nextEnabled;
}
return operationDelayEnabled;
})();
operationDelaySettingReady = ready;
try {
return await ready;
} finally {
if (operationDelaySettingReady === ready) {
operationDelaySettingReady = null;
}
}
}
function shouldDelayOperation(metadata = {}) {
if (metadata.skipOperationDelay === true) return false;
if (EXCLUDED_STEP_KEYS.has(String(metadata.stepKey || '').trim())) return false;
const enabled = Object.prototype.hasOwnProperty.call(metadata, 'enabled')
? normalizeOperationDelayEnabled(metadata.enabled)
: getOperationDelayEnabled();
if (enabled === false) return false;
return true;
}
function waitForOperationDelaySettingFallback() {
return new Promise((resolve) => {
const schedule = root.setTimeout || (typeof setTimeout === 'function' ? setTimeout : null);
if (typeof schedule === 'function') {
schedule(() => resolve(getOperationDelayEnabled()), SETTING_RESTORE_FALLBACK_MS);
return;
}
resolve(getOperationDelayEnabled());
});
}
async function resolveOperationDelayEnabled(metadata = {}, options = {}) {
if (typeof options.getEnabled === 'function') {
return normalizeOperationDelayEnabled(options.getEnabled());
}
if (Object.prototype.hasOwnProperty.call(metadata, 'enabled')) {
return normalizeOperationDelayEnabled(metadata.enabled);
}
if (operationDelaySettingReady) {
return normalizeOperationDelayEnabled(await Promise.race([
operationDelaySettingReady,
waitForOperationDelaySettingFallback(),
]));
}
return getOperationDelayEnabled();
}
async function performOperationWithDelay(metadata = {}, operation, options = {}) {
const result = await operation();
const enabled = await resolveOperationDelayEnabled(metadata, options);
if (shouldDelayOperation({ ...metadata, enabled })) {
const wait = options.sleep || root.sleep;
await wait(OPERATION_DELAY_MS);
}
return result;
}
root.chrome?.storage?.onChanged?.addListener?.((changes, areaName) => {
if (areaName === 'local' && Object.prototype.hasOwnProperty.call(changes, 'operationDelayEnabled')) {
operationDelaySettingRevision += 1;
operationDelayEnabled = normalizeOperationDelayEnabled(changes.operationDelayEnabled?.newValue);
}
});
refreshOperationDelaySetting().catch(() => { operationDelayEnabled = true; });
root.CodexOperationDelay = { OPERATION_DELAY_MS, normalizeOperationDelayEnabled, refreshOperationDelaySetting, getOperationDelayEnabled, shouldDelayOperation, performOperationWithDelay };
})(typeof self !== 'undefined' ? self : globalThis);
+957
View File
@@ -0,0 +1,957 @@
// 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_VERIFICATION = 'verification';
const PAYPAL_HOSTED_STAGE_REVIEW = 'review_consent';
const PAYPAL_HOSTED_STAGE_APPROVAL = 'approval';
const PAYPAL_HOSTED_STAGE_UNKNOWN = 'unknown';
const PAYPAL_HOSTED_HERMES_AUTORUN_SENTINEL = '__MULTIPAGE_PAYPAL_HOSTED_HERMES_AUTORUN__';
const PAYPAL_HOSTED_GUEST_SUBMIT_SENTINEL = '__MULTIPAGE_PAYPAL_HOSTED_GUEST_SUBMIT__';
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 inspectPayPalState();
case 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP':
return runHostedCheckoutStep(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 getPayPalHostedPathname() {
return String(location?.pathname || '').trim();
}
function isPayPalHostedLoginPage() {
const pathname = getPayPalHostedPathname();
return pathname === '/pay'
|| Boolean(document.getElementById('email'));
}
function isPayPalHostedGuestCheckoutPage() {
const pathname = getPayPalHostedPathname();
return /\/checkoutweb\//i.test(pathname)
|| Boolean(document.getElementById('cardNumber'))
|| Boolean(document.getElementById('billingLine1'));
}
function isPayPalHostedReviewPage() {
return /\/webapps\/hermes/i.test(getPayPalHostedPathname());
}
function findHostedVerificationInputs() {
return Array.from({ length: 6 }, (_, index) => document.getElementById(`ci-ciBasic-${index}`))
.filter((input) => isVisibleElement(input));
}
function hasHostedVerificationInputs() {
return findHostedVerificationInputs().length >= 6;
}
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 detectPayPalHostedCheckoutStage() {
if (!/paypal\./i.test(String(location?.host || ''))) {
return PAYPAL_HOSTED_STAGE_OUTSIDE;
}
if (hasHostedVerificationInputs()) {
return PAYPAL_HOSTED_STAGE_VERIFICATION;
}
if (isPayPalHostedGuestCheckoutPage()) {
return PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT;
}
if (isPayPalHostedReviewPage() && findHostedReviewConsentButton()) {
return PAYPAL_HOSTED_STAGE_REVIEW;
}
if (isPayPalHostedLoginPage()) {
return PAYPAL_HOSTED_STAGE_LOGIN;
}
if (Boolean(findApproveButton())) {
return PAYPAL_HOSTED_STAGE_APPROVAL;
}
return 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, text) {
const select = document.getElementById(String(id || '').trim());
const expectedText = normalizeText(text);
if (!select || !expectedText || !Array.isArray(Array.from(select.options || []))) {
return false;
}
const match = Array.from(select.options || []).find((option) => {
const label = normalizeText(option?.textContent || option?.label || '');
const value = normalizeText(option?.value || '');
return label.toLowerCase().includes(expectedText.toLowerCase())
|| value.toLowerCase().includes(expectedText.toLowerCase());
});
if (!match) {
return false;
}
select.value = match.value;
select.dispatchEvent(new Event('change', { bubbles: true }));
return true;
}
function removeHostedCaptchaArtifacts() {
let removed = false;
const selectors = [
'#captcha-standalone',
'.captcha-overlay',
'.captcha-container',
];
selectors.forEach((selector) => {
document.querySelectorAll(selector).forEach((node) => {
try {
node.remove();
removed = true;
} catch {
// Ignore non-removable overlays.
}
});
});
return removed;
}
function startHostedCaptchaCleanupObserver(timeoutMs = 15000) {
const observer = new MutationObserver(() => {
removeHostedCaptchaArtifacts();
});
observer.observe(document.documentElement || document.body, {
childList: true,
subtree: true,
});
setTimeout(() => observer.disconnect(), Math.max(1000, Number(timeoutMs) || 15000));
return observer;
}
function findHostedGuestSubmitButton() {
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')
|| findClickableByText([
/pay|continue|next|agree|subscribe/i,
/支付|继续|下一步|同意|订阅/i,
]);
}
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 lowercase = 'abcdefghijklmnopqrstuvwxyz';
const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const digits = '0123456789';
const symbols = '!@#$%^';
const alphabet = `${lowercase}${uppercase}${digits}${symbols}`;
const value = [
lowercase[Math.floor(Math.random() * lowercase.length)],
uppercase[Math.floor(Math.random() * uppercase.length)],
digits[Math.floor(Math.random() * digits.length)],
symbols[Math.floor(Math.random() * symbols.length)],
];
while (value.length < 14) {
value.push(alphabet[Math.floor(Math.random() * alphabet.length)]);
}
return value.sort(() => Math.random() - 0.5).join('');
}
function buildHostedVisaCard() {
const prefixes = [
[4, 1, 4, 7],
[4, 1, 0, 0],
];
const digits = prefixes[Math.floor(Math.random() * prefixes.length)].slice();
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;
}
const checkDigit = (10 - (sum % 10)) % 10;
digits.push(checkDigit);
const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0');
const currentYear = new Date().getFullYear() % 100;
const year = currentYear + Math.floor(Math.random() * 4) + 2;
const cvv = String(Math.floor(100 + Math.random() * 900));
return {
number: digits.join(''),
expiry: `${month} / ${year}`,
cvv,
};
}
function dispatchHostedGenericClick(button) {
const rect = button.getBoundingClientRect();
const clientX = rect.left + rect.width / 2;
const clientY = rect.top + rect.height / 2;
const eventInit = {
bubbles: true,
cancelable: true,
view: window,
clientX,
clientY,
};
button.dispatchEvent(new PointerEvent('pointerdown', eventInit));
button.dispatchEvent(new MouseEvent('mousedown', eventInit));
button.dispatchEvent(new PointerEvent('pointerup', eventInit));
button.dispatchEvent(new MouseEvent('mouseup', eventInit));
button.dispatchEvent(new MouseEvent('click', eventInit));
}
async function clickHostedGenericSubmitButton(retries = 0) {
removeHostedCaptchaArtifacts();
const button = findHostedGuestSubmitButton() || findEmailNextButton() || findLoginNextButton();
if (!button) {
if (retries >= 10) {
throw new Error('PayPal hosted checkout 未找到可点击的继续/提交按钮。');
}
await sleep(1000);
return clickHostedGenericSubmitButton(retries + 1);
}
const buttonText = normalizeText(button.textContent || '');
if (button.disabled) {
if (retries >= 10) {
throw new Error('PayPal hosted checkout 按钮长时间处于 disabled 状态。');
}
await sleep(1000);
return clickHostedGenericSubmitButton(retries + 1);
}
const rect = button.getBoundingClientRect();
if (rect.height === 0) {
if (retries >= 10) {
throw new Error('PayPal hosted checkout 按钮长时间不可见。');
}
await sleep(1000);
return clickHostedGenericSubmitButton(retries + 1);
}
dispatchHostedGenericClick(button);
await sleep(1000);
removeHostedCaptchaArtifacts();
if (hasHostedVerificationInputs()) {
return {
clicked: true,
verificationRequired: true,
buttonText,
};
}
const currentText = normalizeText(button.textContent || '');
if (!/processing/i.test(currentText) && currentText === buttonText) {
if (retries >= 10) {
return {
clicked: true,
verificationRequired: false,
buttonText,
retried: true,
};
}
await sleep(2000);
return clickHostedGenericSubmitButton(retries + 1);
}
return {
clicked: true,
verificationRequired: false,
buttonText,
};
}
function normalizeHostedVerificationCode(value = '') {
const digits = String(value || '').replace(/\D+/g, '');
return digits.slice(0, 6);
}
async function submitHostedPayLogin(payload = {}) {
await waitForDocumentComplete();
removeHostedCaptchaArtifacts();
const email = normalizeText(payload.email || buildHostedRandomEmail());
if (!email) {
throw new Error('PayPal hosted checkout 缺少邮箱。');
}
const emailInput = document.getElementById('email') || findEmailInput();
if (!emailInput) {
throw new Error('PayPal hosted checkout 未找到邮箱输入框。');
}
await sleep(2000);
refillPayPalEmailInput(emailInput, email);
await sleep(1000);
const clickResult = await clickHostedGenericSubmitButton(0);
return {
stage: PAYPAL_HOSTED_STAGE_LOGIN,
submitted: true,
generatedEmail: email,
verificationRequired: Boolean(clickResult?.verificationRequired),
nextExpected: 'guest_checkout_or_verification',
};
}
async function fillHostedVerificationCode(payload = {}) {
const delayOperation = typeof performPayPalOperationWithDelay === 'function'
? performPayPalOperationWithDelay
: async (_metadata, operation) => operation();
await waitForDocumentComplete();
const code = normalizeHostedVerificationCode(payload.verificationCode || payload.code || '');
if (code.length !== 6) {
throw new Error('PayPal hosted checkout 验证码无效。');
}
const inputs = findHostedVerificationInputs();
if (inputs.length < 6) {
throw new Error('PayPal hosted checkout 当前页面未显示验证码输入框。');
}
await delayOperation({ stepKey: 'plus-checkout-create', kind: 'fill', label: 'hosted-paypal-verification-code' }, async () => {
inputs.forEach((input, index) => {
fillInput(input, code[index] || '');
});
});
return {
stage: PAYPAL_HOSTED_STAGE_VERIFICATION,
codeSubmitted: true,
};
}
async function fillHostedGuestCheckout(payload = {}) {
await waitForDocumentComplete();
startHostedCaptchaCleanupObserver();
removeHostedCaptchaArtifacts();
log(`PayPal guest checkout:收到 payload.phone=${String(payload?.phone || '').trim() || '(空)'}payload.address=${JSON.stringify(payload?.address || {})}`, 'info');
await sleep(2000);
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(3000);
}
const card = buildHostedVisaCard();
const email = normalizeText(payload.email || buildHostedRandomEmail());
const phone = normalizeText(payload.phone || PAYPAL_HOSTED_DEFAULT_PHONE);
const password = String(payload.password || buildHostedRandomPassword());
const firstName = normalizeText(payload.firstName || 'James');
const lastName = normalizeText(payload.lastName || 'Smith');
const cardNumber = String(payload.cardNumber || card.number).replace(/\s+/g, '');
const cardExpiry = normalizeText(payload.cardExpiry || card.expiry);
const cardCvv = normalizeText(payload.cardCvv || card.cvv);
const address = payload.address && typeof payload.address === 'object' ? payload.address : {};
if (!email || !password || !cardNumber || !cardExpiry || !cardCvv) {
throw new Error('PayPal hosted checkout 缺少卡支付所需资料。');
}
fillHostedInputById('email', email);
fillHostedInputById('phone', phone);
fillHostedInputById('cardNumber', cardNumber);
fillHostedInputById('cardExpiry', cardExpiry);
fillHostedInputById('cardCvv', cardCvv);
fillHostedInputById('password', password);
fillHostedInputById('firstName', firstName);
fillHostedInputById('lastName', lastName);
fillHostedInputById('billingLine1', address.street || '');
fillHostedInputById('billingCity', address.city || '');
fillHostedInputById('billingPostalCode', address.zip || '');
fillHostedInputById('billingLine1', address.street || '');
selectHostedOptionByIdText('billingState', address.state || '');
const rootScope = typeof window !== 'undefined' ? window : globalThis;
if (!rootScope[PAYPAL_HOSTED_GUEST_SUBMIT_SENTINEL]) {
rootScope[PAYPAL_HOSTED_GUEST_SUBMIT_SENTINEL] = true;
setTimeout(() => {
clickHostedGenericSubmitButton(0).catch((error) => {
log(`PayPal hosted checkout guest submit 失败:${error?.message || error}`, 'warn');
}).finally(() => {
rootScope[PAYPAL_HOSTED_GUEST_SUBMIT_SENTINEL] = false;
});
}, 500);
}
return {
stage: PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT,
submitted: true,
verificationRequired: Boolean(hasHostedVerificationInputs()),
submitScheduled: true,
};
}
async function clickHostedReviewConsent() {
await waitForDocumentComplete();
log(`PayPal Hermes:开始等待账单确认文案。当前 URL:${location.href}`, 'info');
let waited = 0;
while (waited < 30) {
waited += 1;
const pageText = document.body ? document.body.innerText : '';
if (String(pageText || '').includes('Set up once. Pay faster next time')) {
log(`PayPal Hermes:第 ${waited}/30 秒命中目标文案,开始寻找 consentButton。`, 'info');
let button = document.getElementById('consentButton')
|| document.querySelector('button[data-testid="consentButton"]');
if (button) {
log('PayPal Hermes:已找到 consentButton,准备点击 Agree and Continue。', 'info');
button.click();
return {
stage: PAYPAL_HOSTED_STAGE_REVIEW,
submitted: true,
};
}
log('PayPal Hermes:首次未找到 consentButton2 秒后重试一次。', 'warn');
await sleep(2000);
button = document.getElementById('consentButton');
if (button) {
log('PayPal Hermes:重试后找到 consentButton,准备点击 Agree and Continue。', 'info');
button.click();
return {
stage: PAYPAL_HOSTED_STAGE_REVIEW,
submitted: true,
};
}
log('PayPal Hermes:重试后仍未找到 consentButton。', 'warn');
throw new Error('PayPal hosted checkout 未找到 consentButton。');
}
if (waited === 1 || waited % 5 === 0) {
log(`PayPal Hermes:尚未命中目标文案,继续等待(${waited}/30)。`, 'info');
}
await sleep(1000);
}
log('PayPal Hermes:等待 30 秒后仍未命中目标文案。', 'warn');
throw new Error('PayPal hosted checkout 账单确认页超时,未检测到目标文案。');
}
async function runHostedCheckoutStep(payload = {}) {
if (isPayPalHostedReviewPage()) {
return clickHostedReviewConsent();
}
const stage = detectPayPalHostedCheckoutStage();
if (stage === PAYPAL_HOSTED_STAGE_VERIFICATION) {
if (!payload.verificationCode && !payload.code) {
return {
stage,
requiresVerificationCode: true,
};
}
return fillHostedVerificationCode(payload);
}
if (stage === PAYPAL_HOSTED_STAGE_LOGIN) {
return submitHostedPayLogin(payload);
}
if (stage === PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT) {
return fillHostedGuestCheckout(payload);
}
if (stage === PAYPAL_HOSTED_STAGE_REVIEW) {
return clickHostedReviewConsent();
}
return {
stage,
submitted: false,
approveReady: Boolean(findApproveButton()),
};
}
function shouldAutoRunHostedHermesReview() {
const rootScope = typeof window !== 'undefined' ? window : globalThis;
if (!isPayPalHostedReviewPage()) {
return false;
}
if (rootScope[PAYPAL_HOSTED_HERMES_AUTORUN_SENTINEL]) {
return false;
}
rootScope[PAYPAL_HOSTED_HERMES_AUTORUN_SENTINEL] = true;
return true;
}
function scheduleHostedHermesAutoRun() {
if (!shouldAutoRunHostedHermesReview()) {
return;
}
log(`PayPal Hermes 页面已命中,按油猴脚本方式自动等待并点击 Agree and Continue。当前 URL${location.href}`, 'info');
setTimeout(() => {
clickHostedReviewConsent().then(() => {
log('PayPal Hermes:已按油猴脚本方式执行 Agree and Continue。', 'ok');
}).catch((error) => {
log(`PayPal Hermes:自动点击 Agree and Continue 失败:${error?.message || error}`, 'warn');
});
}, 0);
}
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);
const hostedStage = detectPayPalHostedCheckoutStage();
return {
url: location.href,
readyState: document.readyState,
hostedStage,
needsLogin: Boolean(loginPhase),
loginPhase,
hasEmailInput: Boolean(emailInput),
hasPasswordInput: Boolean(passwordInput),
hasHostedGuestCheckout: hostedStage === PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT,
verificationInputsVisible: hasHostedVerificationInputs(),
reviewConsentReady: Boolean(findHostedReviewConsentButton()),
approveReady: Boolean(approveButton && isEnabledControl(approveButton)),
approveButtonText: approveButton ? getActionText(approveButton) : '',
hasPasskeyPrompt: hasPasskeyPrompt(),
bodyTextPreview: normalizeText(document.body?.innerText || '').slice(0, 240),
};
}
scheduleHostedHermesAutoRun();
File diff suppressed because it is too large Load Diff
+237
View File
@@ -0,0 +1,237 @@
(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
+234
View File
@@ -0,0 +1,234 @@
// content/qq-mail.js — Content script for QQ Mail (steps 4, 7)
// Injected on: mail.qq.com, wx.mail.qq.com
// NOTE: all_frames: true
//
// Strategy for avoiding stale codes:
// 1. On poll start, snapshot all existing mail IDs as "old"
// 2. On each poll cycle, refresh inbox and look for NEW items (not in snapshot)
// 3. Only extract codes from NEW items that match sender/subject filters
const QQ_MAIL_PREFIX = '[MultiPage:qq-mail]';
const isTopFrame = window === window.top;
console.log(QQ_MAIL_PREFIX, 'Content script loaded on', location.href, 'frame:', isTopFrame ? 'top' : 'child');
// ============================================================
// Message Handler
// ============================================================
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'POLL_EMAIL') {
if (!isTopFrame) {
sendResponse({ ok: false, reason: 'wrong-frame' });
return;
}
resetStopState();
handlePollEmail(message.step, message.payload).then(result => {
sendResponse(result);
}).catch(err => {
if (isStopError(err)) {
log(`步骤 ${message.step}:已被用户停止。`, 'warn');
sendResponse({ stopped: true, error: err.message });
return;
}
log(`步骤 ${message.step}:邮箱轮询失败:${err.message}`, 'warn');
sendResponse({ error: err.message });
});
return true; // async response
}
});
// ============================================================
// Get all current mail IDs from the list
// ============================================================
function getCurrentMailIds() {
const ids = new Set();
document.querySelectorAll('.mail-list-page-item[data-mailid]').forEach(item => {
ids.add(item.getAttribute('data-mailid'));
});
return ids;
}
function normalizeRulePatternList(patterns = []) {
return Array.isArray(patterns) ? patterns : [];
}
function extractCodeByRulePatterns(text, patterns = []) {
const normalizedText = String(text || '');
for (const pattern of normalizeRulePatternList(patterns)) {
try {
const source = String(pattern?.source || '').trim();
if (!source) {
continue;
}
const flags = String(pattern?.flags || '').replace(/[^dgimsuvy]/g, '');
const match = normalizedText.match(new RegExp(source, flags));
if (!match) {
continue;
}
for (let index = 1; index < match.length; index += 1) {
const candidate = String(match[index] || '').trim();
if (candidate) {
return candidate;
}
}
if (String(match[0] || '').trim()) {
return String(match[0] || '').trim();
}
} catch (_) {
// Ignore invalid runtime rule patterns and continue with other candidates.
}
}
return null;
}
// ============================================================
// Email Polling
// ============================================================
async function handlePollEmail(step, payload) {
const {
codePatterns = [],
senderFilters,
subjectFilters,
maxAttempts,
intervalMs,
excludeCodes = [],
} = payload;
const excludedCodeSet = new Set(excludeCodes.filter(Boolean));
log(`步骤 ${step}:开始轮询邮箱(最多 ${maxAttempts} 次,每 ${intervalMs / 1000} 秒一次)`);
// Wait for mail list to load
try {
await waitForElement('.mail-list-page-item', 10000);
log(`步骤 ${step}:邮件列表已加载`);
} catch {
throw new Error('邮件列表未加载完成,请确认 QQ 邮箱已打开收件箱。');
}
// Step 1: Snapshot existing mail IDs BEFORE we start waiting for new email
const existingMailIds = getCurrentMailIds();
log(`步骤 ${step}:已将当前 ${existingMailIds.size} 封邮件标记为旧邮件快照`);
// Fallback after just 3 attempts (~10s). In practice, the email is usually
// already in the list but has the same mailid (page was already open).
const FALLBACK_AFTER = 3;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
log(`步骤 ${step}:正在轮询 QQ 邮箱,第 ${attempt}/${maxAttempts}`);
// Refresh inbox (skip on first attempt, list is fresh)
if (attempt > 1) {
await refreshInbox();
await sleep(800);
}
const allItems = document.querySelectorAll('.mail-list-page-item[data-mailid]');
const useFallback = attempt > FALLBACK_AFTER;
// Phase 1 (attempt 1~3): only look at NEW emails (not in snapshot)
// Phase 2 (attempt 4+): fallback to first matching email in list
for (const item of allItems) {
const mailId = item.getAttribute('data-mailid');
if (!useFallback && existingMailIds.has(mailId)) continue;
const sender = (item.querySelector('.cmp-account-nick')?.textContent || '').toLowerCase();
const subject = (item.querySelector('.mail-subject')?.textContent || '').toLowerCase();
const digest = item.querySelector('.mail-digest')?.textContent || '';
const senderMatch = senderFilters.some(f => sender.includes(f.toLowerCase()));
const subjectMatch = subjectFilters.some(f => subject.includes(f.toLowerCase()));
if (senderMatch || subjectMatch) {
const code = extractVerificationCode(subject + ' ' + digest, {
codePatterns,
});
if (code) {
if (excludedCodeSet.has(code)) {
log(`步骤 ${step}:跳过排除的验证码:${code}`, 'info');
continue;
}
const source = useFallback && existingMailIds.has(mailId) ? '回退首封匹配邮件' : '新邮件';
log(`步骤 ${step}:已找到验证码:${code}(来源:${source},主题:${subject.slice(0, 40)}`, 'ok');
return { ok: true, code, emailTimestamp: Date.now(), mailId };
}
}
}
if (attempt === FALLBACK_AFTER + 1) {
log(`步骤 ${step}:连续 ${FALLBACK_AFTER} 次未发现新邮件,开始回退到首封匹配邮件`, 'warn');
}
if (attempt < maxAttempts) {
await sleep(intervalMs);
}
}
throw new Error(
`${(maxAttempts * intervalMs / 1000).toFixed(0)} 秒后仍未找到新的匹配邮件。` +
'请手动检查 QQ 邮箱,邮件可能延迟到达或进入垃圾箱。'
);
}
// ============================================================
// Inbox Refresh
// ============================================================
async function refreshInbox() {
// Try multiple strategies to refresh the mail list
// Strategy 1: Click any visible refresh button
const refreshBtn = document.querySelector('[class*="refresh"], [title*="刷新"]');
if (refreshBtn) {
simulateClick(refreshBtn);
console.log(QQ_MAIL_PREFIX, 'Clicked refresh button');
await sleep(500);
return;
}
// Strategy 2: Click inbox in sidebar to reload list
const sidebarInbox = document.querySelector('a[href*="inbox"], [class*="folder-item"][class*="inbox"], [title="收件箱"]');
if (sidebarInbox) {
simulateClick(sidebarInbox);
console.log(QQ_MAIL_PREFIX, 'Clicked sidebar inbox');
await sleep(500);
return;
}
// Strategy 3: Click the folder name in toolbar
const folderName = document.querySelector('.toolbar-folder-name');
if (folderName) {
simulateClick(folderName);
console.log(QQ_MAIL_PREFIX, 'Clicked toolbar folder name');
await sleep(500);
}
}
// ============================================================
// Verification Code Extraction
// ============================================================
function extractVerificationCode(text, options = {}) {
const matchedByRule = extractCodeByRulePatterns(text, options?.codePatterns);
if (matchedByRule) return matchedByRule;
// Pattern 1: Chinese format "代码为 370794" or "验证码...370794"
const matchCn = text.match(/(?:代码为|验证码[^0-9]*?)[\s:]*(\d{6})/);
if (matchCn) return matchCn[1];
const matchLoginCode = text.match(/(?:log-?in\s+code|enter\s+this\s+code)[^0-9]{0,24}(\d{6})/i);
if (matchLoginCode) return matchLoginCode[1];
// Pattern 2: English format "code is 370794" or "code: 370794"
const matchEn = text.match(/code[:\s]+is[:\s]+(\d{6})|code[:\s]+(\d{6})/i);
if (matchEn) return matchEn[1] || matchEn[2];
// Pattern 3: standalone 6-digit number (first occurrence)
const match6 = text.match(/\b(\d{6})\b/);
if (match6) return match6[1];
return null;
}
File diff suppressed because it is too large Load Diff
+686
View File
@@ -0,0 +1,686 @@
// 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();
+574
View File
@@ -0,0 +1,574 @@
// content/utils.js — Shared utilities for all content scripts
const getActivationStrategy = self.MultiPageActivationUtils?.getActivationStrategy;
function detectScriptSource({
injectedSource,
url = '',
hostname = '',
} = {}) {
const sourceRegistry = globalThis?.MultiPageSourceRegistry?.createSourceRegistry?.();
if (sourceRegistry?.detectSourceFromLocation) {
return sourceRegistry.detectSourceFromLocation({
injectedSource,
url,
hostname,
});
}
if (injectedSource) return injectedSource;
if (url.includes('auth0.openai.com') || url.includes('auth.openai.com') || url.includes('accounts.openai.com')) return 'openai-auth';
if (hostname === 'mail.qq.com' || hostname === 'wx.mail.qq.com') return 'qq-mail';
if (
hostname === 'mail.163.com'
|| hostname.endsWith('.mail.163.com')
|| hostname === 'webmail.vip.163.com'
|| hostname === 'mail.126.com'
|| hostname.endsWith('.mail.126.com')
) return 'mail-163';
if (hostname === 'mail.google.com') return 'gmail-mail';
if (hostname === 'www.icloud.com' || hostname === 'www.icloud.com.cn') return 'icloud-mail';
if (url.includes('duckduckgo.com/email/settings/autofill')) return 'duck-mail';
if (url.includes('chatgpt.com')) return 'chatgpt';
if (url.includes("2925.com")) return "mail-2925";
return 'unknown-source';
}
const SCRIPT_SOURCE = (() => {
return detectScriptSource({
injectedSource: window.__MULTIPAGE_SOURCE,
url: location.href,
hostname: location.hostname,
});
})();
function getRuntimeScriptSource() {
return window.__MULTIPAGE_SOURCE || SCRIPT_SOURCE;
}
const LOG_PREFIX = `[MultiPage:${SCRIPT_SOURCE}]`;
const STOP_ERROR_MESSAGE = '流程已被用户停止。';
let flowStopped = false;
if (!window.__MULTIPAGE_UTILS_LISTENER_READY__) {
window.__MULTIPAGE_UTILS_LISTENER_READY__ = true;
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'STOP_FLOW') {
flowStopped = true;
console.warn(LOG_PREFIX, STOP_ERROR_MESSAGE);
return;
}
if (message.type === 'PING') {
sendResponse({
ok: true,
source: getRuntimeScriptSource(),
plusCheckoutReady: Boolean(window.__MULTIPAGE_PLUS_CHECKOUT_READY__),
});
}
});
}
function resetStopState() {
flowStopped = false;
}
function isStopError(error) {
const message = typeof error === 'string' ? error : error?.message;
return message === STOP_ERROR_MESSAGE;
}
function throwIfStopped() {
if (flowStopped) {
throw new Error(STOP_ERROR_MESSAGE);
}
}
/**
* Wait for a DOM element to appear.
* @param {string} selector - CSS selector
* @param {number} timeout - Max wait time in ms (default 10000)
* @returns {Promise<Element>}
*/
function waitForElement(selector, timeout = 10000) {
return new Promise((resolve, reject) => {
throwIfStopped();
const existing = document.querySelector(selector);
if (existing) {
console.log(LOG_PREFIX, `立即找到元素: ${selector}`);
log(`已找到元素:${selector}`);
resolve(existing);
return;
}
console.log(LOG_PREFIX, `等待元素: ${selector}(超时 ${timeout}ms`);
log(`正在等待选择器:${selector}...`);
let settled = false;
let stopTimer = null;
const cleanup = () => {
if (settled) return;
settled = true;
observer.disconnect();
clearTimeout(timer);
clearTimeout(stopTimer);
};
const observer = new MutationObserver(() => {
if (flowStopped) {
cleanup();
reject(new Error(STOP_ERROR_MESSAGE));
return;
}
const el = document.querySelector(selector);
if (el) {
cleanup();
console.log(LOG_PREFIX, `等待后找到元素: ${selector}`);
log(`已找到元素:${selector}`);
resolve(el);
}
});
observer.observe(document.body || document.documentElement, {
childList: true,
subtree: true,
});
const timer = setTimeout(() => {
cleanup();
const msg = `${location.href} 等待 ${selector} 超时,已超过 ${timeout}ms`;
console.error(LOG_PREFIX, msg);
reject(new Error(msg));
}, timeout);
const pollStop = () => {
if (settled) return;
if (flowStopped) {
cleanup();
reject(new Error(STOP_ERROR_MESSAGE));
return;
}
stopTimer = setTimeout(pollStop, 100);
};
pollStop();
});
}
/**
* Wait for an element matching a text pattern among multiple candidates.
* @param {string} containerSelector - Selector for candidate elements
* @param {RegExp} textPattern - Regex to match against textContent
* @param {number} timeout - Max wait time in ms
* @returns {Promise<Element>}
*/
function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
return new Promise((resolve, reject) => {
throwIfStopped();
function search() {
const candidates = document.querySelectorAll(containerSelector);
for (const el of candidates) {
if (textPattern.test(el.textContent)) {
return el;
}
}
return null;
}
const existing = search();
if (existing) {
console.log(LOG_PREFIX, `立即按文本找到元素: ${containerSelector} 匹配 ${textPattern}`);
log(`已按文本找到元素:${textPattern}`);
resolve(existing);
return;
}
console.log(LOG_PREFIX, `等待文本匹配: ${containerSelector} / ${textPattern}`);
log(`正在等待包含文本的元素:${textPattern}...`);
let settled = false;
let stopTimer = null;
const cleanup = () => {
if (settled) return;
settled = true;
observer.disconnect();
clearTimeout(timer);
clearTimeout(stopTimer);
};
const observer = new MutationObserver(() => {
if (flowStopped) {
cleanup();
reject(new Error(STOP_ERROR_MESSAGE));
return;
}
const el = search();
if (el) {
cleanup();
console.log(LOG_PREFIX, `等待后按文本找到元素: ${textPattern}`);
log(`已按文本找到元素:${textPattern}`);
resolve(el);
}
});
observer.observe(document.body || document.documentElement, {
childList: true,
subtree: true,
});
const timer = setTimeout(() => {
cleanup();
const msg = `${location.href}${containerSelector} 中等待文本 "${textPattern}" 超时,已超过 ${timeout}ms`;
console.error(LOG_PREFIX, msg);
reject(new Error(msg));
}, timeout);
const pollStop = () => {
if (settled) return;
if (flowStopped) {
cleanup();
reject(new Error(STOP_ERROR_MESSAGE));
return;
}
stopTimer = setTimeout(pollStop, 100);
};
pollStop();
});
}
/**
* React-compatible form filling.
* Sets value via native setter and dispatches input + change events.
* @param {HTMLInputElement} el
* @param {string} value
*/
function fillInput(el, value) {
throwIfStopped();
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value'
).set;
nativeInputValueSetter.call(el, value);
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
console.log(LOG_PREFIX, `已填写输入框 ${el.name || el.id || el.type}: ${value}`);
log(`已填写输入框 [${el.name || el.id || el.type || '未知'}]`);
}
/**
* Fill a select element by setting its value and triggering change.
* @param {HTMLSelectElement} el
* @param {string} value
*/
function fillSelect(el, value) {
throwIfStopped();
el.value = value;
el.dispatchEvent(new Event('change', { bubbles: true }));
console.log(LOG_PREFIX, `已在 ${el.name || el.id} 中选择值: ${value}`);
log(`已选择 [${el.name || el.id || '未知'}] = ${value}`);
}
function normalizeLogStep(value) {
const step = Math.floor(Number(value) || 0);
return step > 0 ? step : null;
}
const DEFAULT_OPENAI_NODE_BY_STEP = Object.freeze({
1: 'open-chatgpt',
2: 'submit-signup-email',
3: 'fill-password',
4: 'fetch-signup-code',
5: 'fill-profile',
6: 'wait-registration-success',
7: 'oauth-login',
8: 'fetch-login-code',
9: 'post-login-phone-verification',
10: 'confirm-oauth',
11: 'fetch-login-code',
12: 'post-login-phone-verification',
13: 'confirm-oauth',
14: 'platform-verify',
15: 'platform-verify',
16: 'confirm-oauth',
17: 'platform-verify',
});
function resolveReportNodeId(stepOrNodeId, data = {}) {
const explicitNodeId = String(data?.nodeId || data?.nodeKey || '').trim();
if (explicitNodeId) {
return explicitNodeId;
}
const directNodeId = String(stepOrNodeId || '').trim();
if (directNodeId && !/^\d+$/.test(directNodeId)) {
return directNodeId;
}
const step = normalizeLogStep(stepOrNodeId || data?.step || data?.visibleStep);
return step ? DEFAULT_OPENAI_NODE_BY_STEP[step] || '' : '';
}
/**
* Send a log message to Side Panel via Background.
* @param {string} message
* @param {string} level - 'info' | 'ok' | 'warn' | 'error'
* @param {{ step?: number, stepKey?: string }} options
*/
function log(message, level = 'info', options = {}) {
const step = normalizeLogStep(options?.step);
chrome.runtime.sendMessage({
type: 'LOG',
source: getRuntimeScriptSource(),
step,
payload: {
message: String(message || ''),
level,
timestamp: Date.now(),
step,
stepKey: String(options?.stepKey || '').trim(),
},
error: null,
});
}
/**
* Report that this content script is loaded and ready.
*/
function reportReady() {
if (getRuntimeScriptSource() === 'unknown-source') {
console.warn(LOG_PREFIX, 'skip CONTENT_SCRIPT_READY for unknown source');
return;
}
console.log(LOG_PREFIX, '内容脚本已就绪');
const message = {
type: 'CONTENT_SCRIPT_READY',
source: getRuntimeScriptSource(),
step: null,
payload: {},
error: null,
};
Promise.resolve(chrome.runtime.sendMessage(message))
.then((response) => {
console.log(LOG_PREFIX, 'CONTENT_SCRIPT_READY sent successfully', { response, url: location.href });
})
.catch((err) => {
console.error(LOG_PREFIX, 'CONTENT_SCRIPT_READY send failed', err?.message || err, { url: location.href });
});
}
/**
* Report node completion.
* @param {string|number} stepOrNodeId
* @param {Object} data - Node output data
*/
function reportComplete(stepOrNodeId, data = {}) {
const nodeId = resolveReportNodeId(stepOrNodeId, data);
const step = normalizeLogStep(stepOrNodeId || data?.step || data?.visibleStep);
console.log(LOG_PREFIX, `节点 ${nodeId || stepOrNodeId} 已完成`, data);
log('已成功完成', 'ok', { step, stepKey: nodeId });
const message = {
type: 'NODE_COMPLETE',
source: getRuntimeScriptSource(),
nodeId,
payload: {
...(data || {}),
...(nodeId ? { nodeId } : {}),
...(step ? { step } : {}),
},
error: null,
};
Promise.resolve(chrome.runtime.sendMessage(message))
.then((response) => {
console.log(LOG_PREFIX, `NODE_COMPLETE sent successfully for node ${nodeId || stepOrNodeId}`, {
response,
url: location.href,
payloadKeys: Object.keys(data || {}),
});
})
.catch((err) => {
console.error(LOG_PREFIX, `NODE_COMPLETE send failed for node ${nodeId || stepOrNodeId}`, err?.message || err, {
url: location.href,
payloadKeys: Object.keys(data || {}),
});
});
}
function reportNodeComplete(nodeId, data = {}) {
const normalizedNodeId = String(nodeId || '').trim();
console.log(LOG_PREFIX, `节点 ${normalizedNodeId} 已完成`, data);
const message = {
type: 'NODE_COMPLETE',
source: getRuntimeScriptSource(),
nodeId: normalizedNodeId,
payload: {
...(data || {}),
nodeId: normalizedNodeId,
},
error: null,
};
Promise.resolve(chrome.runtime.sendMessage(message))
.then((response) => {
console.log(LOG_PREFIX, `NODE_COMPLETE sent successfully for node ${normalizedNodeId}`, {
response,
url: location.href,
payloadKeys: Object.keys(data || {}),
});
})
.catch((err) => {
console.error(LOG_PREFIX, `NODE_COMPLETE send failed for node ${normalizedNodeId}`, err?.message || err, {
url: location.href,
payloadKeys: Object.keys(data || {}),
});
});
}
/**
* Report node error.
* @param {string|number} stepOrNodeId
* @param {string} errorMessage
*/
function reportError(stepOrNodeId, errorMessage) {
const nodeId = resolveReportNodeId(stepOrNodeId);
const step = normalizeLogStep(stepOrNodeId);
console.error(LOG_PREFIX, `节点 ${nodeId || stepOrNodeId} 失败: ${errorMessage}`);
const message = {
type: 'NODE_ERROR',
source: getRuntimeScriptSource(),
nodeId,
payload: {
...(nodeId ? { nodeId } : {}),
...(step ? { step } : {}),
},
error: errorMessage,
};
Promise.resolve(chrome.runtime.sendMessage(message))
.then((response) => {
console.log(LOG_PREFIX, `NODE_ERROR sent successfully for node ${nodeId || stepOrNodeId}`, {
response,
url: location.href,
errorMessage,
});
})
.catch((err) => {
console.error(LOG_PREFIX, `NODE_ERROR send failed for node ${nodeId || stepOrNodeId}`, err?.message || err, {
url: location.href,
errorMessage,
});
});
}
function reportNodeError(nodeId, errorMessage) {
const normalizedNodeId = String(nodeId || '').trim();
console.error(LOG_PREFIX, `节点 ${normalizedNodeId} 失败: ${errorMessage}`);
const message = {
type: 'NODE_ERROR',
source: getRuntimeScriptSource(),
nodeId: normalizedNodeId,
payload: {
nodeId: normalizedNodeId,
},
error: errorMessage,
};
Promise.resolve(chrome.runtime.sendMessage(message))
.then((response) => {
console.log(LOG_PREFIX, `NODE_ERROR sent successfully for node ${normalizedNodeId}`, {
response,
url: location.href,
errorMessage,
});
})
.catch((err) => {
console.error(LOG_PREFIX, `NODE_ERROR send failed for node ${normalizedNodeId}`, err?.message || err, {
url: location.href,
errorMessage,
});
});
}
/**
* Simulate a click with proper event dispatching.
* @param {Element} el
*/
function simulateClick(el) {
throwIfStopped();
if (!el) {
throw new Error('无法点击空元素。');
}
const form = el.form || el.closest?.('form') || null;
const strategy = typeof getActivationStrategy === 'function'
? getActivationStrategy({
tagName: el.tagName,
type: el.getAttribute?.('type') || el.type || '',
hasForm: Boolean(form),
pathname: location.pathname || '',
})
: { method: 'click' };
let method = strategy.method || 'click';
if (method === 'requestSubmit' && form && typeof form.requestSubmit === 'function') {
form.requestSubmit(el);
} else if (typeof el.click === 'function') {
method = 'click';
el.click();
} else {
method = 'dispatchEvent';
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
}
console.log(LOG_PREFIX, `已点击(${method}): ${el.tagName} ${el.textContent?.slice(0, 30) || ''}`);
log(`已点击(${method}) [${el.tagName}] "${el.textContent?.trim().slice(0, 30) || ''}"`);
}
/**
* Wait a specified number of milliseconds.
* @param {number} ms
* @returns {Promise<void>}
*/
function sleep(ms) {
return new Promise((resolve, reject) => {
const start = Date.now();
function tick() {
if (flowStopped) {
reject(new Error(STOP_ERROR_MESSAGE));
return;
}
if (Date.now() - start >= ms) {
resolve();
return;
}
setTimeout(tick, Math.min(100, Math.max(25, ms - (Date.now() - start))));
}
tick();
});
}
async function humanPause(min = 250, max = 850) {
const duration = Math.floor(Math.random() * (max - min + 1)) + min;
await sleep(duration);
}
function shouldReportReadyForFrame(source, isChildFrame) {
const sourceRegistry = globalThis?.MultiPageSourceRegistry?.createSourceRegistry?.();
if (sourceRegistry?.shouldReportReadyForFrame) {
return sourceRegistry.shouldReportReadyForFrame(source, isChildFrame);
}
if (!isChildFrame) return true;
return ![
'qq-mail',
'mail-163',
'gmail-mail',
'mail-2925',
'inbucket-mail',
'plus-checkout',
'unknown-source',
].includes(source);
}
// Auto-report ready on load. Child frames are probed explicitly by frameId, so
// they should not overwrite the tab-level registration or spam the side panel.
if (shouldReportReadyForFrame(getRuntimeScriptSource(), window !== window.top)) {
reportReady();
}
+1092
View File
File diff suppressed because it is too large Load Diff
+144
View File
@@ -0,0 +1,144 @@
// 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),
};
}