| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- // content/utils.js — Shared utilities for all content scripts
- const SCRIPT_SOURCE = (() => {
- if (window.__MULTIPAGE_SOURCE) return window.__MULTIPAGE_SOURCE;
- const url = location.href;
- if (url.includes('auth0.openai.com') || url.includes('auth.openai.com') || url.includes('accounts.openai.com')) return 'signup-page';
- if (url.includes('mail.qq.com')) return 'qq-mail';
- if (url.includes('mail.163.com')) return 'mail-163';
- if (url.includes('duckduckgo.com/email/settings/autofill')) return 'duck-mail';
- if (url.includes('chatgpt.com')) return 'chatgpt';
- // VPS panel — detected dynamically since URL is configurable
- return 'vps-panel';
- })();
- const LOG_PREFIX = `[MultiPage:${SCRIPT_SOURCE}]`;
- const STOP_ERROR_MESSAGE = '流程已被用户停止。';
- let flowStopped = false;
- chrome.runtime.onMessage.addListener((message) => {
- if (message.type === 'STOP_FLOW') {
- flowStopped = true;
- console.warn(LOG_PREFIX, STOP_ERROR_MESSAGE);
- }
- });
- 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}`);
- }
- /**
- * Send a log message to Side Panel via Background.
- * @param {string} message
- * @param {string} level - 'info' | 'ok' | 'warn' | 'error'
- */
- function log(message, level = 'info') {
- chrome.runtime.sendMessage({
- type: 'LOG',
- source: SCRIPT_SOURCE,
- step: null,
- payload: { message, level, timestamp: Date.now() },
- error: null,
- });
- }
- /**
- * Report that this content script is loaded and ready.
- */
- function reportReady() {
- console.log(LOG_PREFIX, '内容脚本已就绪');
- chrome.runtime.sendMessage({
- type: 'CONTENT_SCRIPT_READY',
- source: SCRIPT_SOURCE,
- step: null,
- payload: {},
- error: null,
- });
- }
- /**
- * Report step completion.
- * @param {number} step
- * @param {Object} data - Step output data
- */
- function reportComplete(step, data = {}) {
- console.log(LOG_PREFIX, `步骤 ${step} 已完成`, data);
- log(`步骤 ${step} 已成功完成`, 'ok');
- chrome.runtime.sendMessage({
- type: 'STEP_COMPLETE',
- source: SCRIPT_SOURCE,
- step,
- payload: data,
- error: null,
- });
- }
- /**
- * Report step error.
- * @param {number} step
- * @param {string} errorMessage
- */
- function reportError(step, errorMessage) {
- console.error(LOG_PREFIX, `步骤 ${step} 失败: ${errorMessage}`);
- log(`步骤 ${step} 失败:${errorMessage}`, 'error');
- chrome.runtime.sendMessage({
- type: 'STEP_ERROR',
- source: SCRIPT_SOURCE,
- step,
- payload: {},
- error: errorMessage,
- });
- }
- /**
- * Simulate a click with proper event dispatching.
- * @param {Element} el
- */
- function simulateClick(el) {
- throwIfStopped();
- el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
- console.log(LOG_PREFIX, `已点击: ${el.tagName} ${el.textContent?.slice(0, 30) || ''}`);
- log(`已点击 [${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);
- }
- // Auto-report ready on load
- // Skip ready signal from child iframes of mail pages to avoid overwriting the top frame's registration
- const _isMailChildFrame = (SCRIPT_SOURCE === 'qq-mail' || SCRIPT_SOURCE === 'mail-163' || SCRIPT_SOURCE === 'inbucket-mail') && window !== window.top;
- if (!_isMailChildFrame) {
- reportReady();
- }
|