Files
2026-05-20 12:11:19 +08:00

576 lines
17 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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';
const textBeforeClick = el.textContent || '';
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} ${textBeforeClick.slice(0, 30)}`);
log(`已点击(${method}) [${el.tagName}] "${textBeforeClick.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();
}