utils.js 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. // content/utils.js — Shared utilities for all content scripts
  2. const SCRIPT_SOURCE = (() => {
  3. if (window.__MULTIPAGE_SOURCE) return window.__MULTIPAGE_SOURCE;
  4. const url = location.href;
  5. if (url.includes('auth0.openai.com') || url.includes('auth.openai.com') || url.includes('accounts.openai.com')) return 'signup-page';
  6. if (url.includes('mail.qq.com')) return 'qq-mail';
  7. if (url.includes('mail.163.com')) return 'mail-163';
  8. if (url.includes('duckduckgo.com/email/settings/autofill')) return 'duck-mail';
  9. if (url.includes('chatgpt.com')) return 'chatgpt';
  10. // VPS panel — detected dynamically since URL is configurable
  11. return 'vps-panel';
  12. })();
  13. const LOG_PREFIX = `[MultiPage:${SCRIPT_SOURCE}]`;
  14. const STOP_ERROR_MESSAGE = 'Flow stopped by user.';
  15. let flowStopped = false;
  16. chrome.runtime.onMessage.addListener((message) => {
  17. if (message.type === 'STOP_FLOW') {
  18. flowStopped = true;
  19. console.warn(LOG_PREFIX, STOP_ERROR_MESSAGE);
  20. }
  21. });
  22. function resetStopState() {
  23. flowStopped = false;
  24. }
  25. function isStopError(error) {
  26. const message = typeof error === 'string' ? error : error?.message;
  27. return message === STOP_ERROR_MESSAGE;
  28. }
  29. function throwIfStopped() {
  30. if (flowStopped) {
  31. throw new Error(STOP_ERROR_MESSAGE);
  32. }
  33. }
  34. /**
  35. * Wait for a DOM element to appear.
  36. * @param {string} selector - CSS selector
  37. * @param {number} timeout - Max wait time in ms (default 10000)
  38. * @returns {Promise<Element>}
  39. */
  40. function waitForElement(selector, timeout = 10000) {
  41. return new Promise((resolve, reject) => {
  42. throwIfStopped();
  43. const existing = document.querySelector(selector);
  44. if (existing) {
  45. console.log(LOG_PREFIX, `Found immediately: ${selector}`);
  46. log(`Found element: ${selector}`);
  47. resolve(existing);
  48. return;
  49. }
  50. console.log(LOG_PREFIX, `Waiting for: ${selector} (timeout: ${timeout}ms)`);
  51. log(`Waiting for selector: ${selector}...`);
  52. let settled = false;
  53. let stopTimer = null;
  54. const cleanup = () => {
  55. if (settled) return;
  56. settled = true;
  57. observer.disconnect();
  58. clearTimeout(timer);
  59. clearTimeout(stopTimer);
  60. };
  61. const observer = new MutationObserver(() => {
  62. if (flowStopped) {
  63. cleanup();
  64. reject(new Error(STOP_ERROR_MESSAGE));
  65. return;
  66. }
  67. const el = document.querySelector(selector);
  68. if (el) {
  69. cleanup();
  70. console.log(LOG_PREFIX, `Found after wait: ${selector}`);
  71. log(`Found element: ${selector}`);
  72. resolve(el);
  73. }
  74. });
  75. observer.observe(document.body || document.documentElement, {
  76. childList: true,
  77. subtree: true,
  78. });
  79. const timer = setTimeout(() => {
  80. cleanup();
  81. const msg = `Timeout waiting for ${selector} after ${timeout}ms on ${location.href}`;
  82. console.error(LOG_PREFIX, msg);
  83. reject(new Error(msg));
  84. }, timeout);
  85. const pollStop = () => {
  86. if (settled) return;
  87. if (flowStopped) {
  88. cleanup();
  89. reject(new Error(STOP_ERROR_MESSAGE));
  90. return;
  91. }
  92. stopTimer = setTimeout(pollStop, 100);
  93. };
  94. pollStop();
  95. });
  96. }
  97. /**
  98. * Wait for an element matching a text pattern among multiple candidates.
  99. * @param {string} containerSelector - Selector for candidate elements
  100. * @param {RegExp} textPattern - Regex to match against textContent
  101. * @param {number} timeout - Max wait time in ms
  102. * @returns {Promise<Element>}
  103. */
  104. function waitForElementByText(containerSelector, textPattern, timeout = 10000) {
  105. return new Promise((resolve, reject) => {
  106. throwIfStopped();
  107. function search() {
  108. const candidates = document.querySelectorAll(containerSelector);
  109. for (const el of candidates) {
  110. if (textPattern.test(el.textContent)) {
  111. return el;
  112. }
  113. }
  114. return null;
  115. }
  116. const existing = search();
  117. if (existing) {
  118. console.log(LOG_PREFIX, `Found by text immediately: ${containerSelector} matching ${textPattern}`);
  119. log(`Found element by text: ${textPattern}`);
  120. resolve(existing);
  121. return;
  122. }
  123. console.log(LOG_PREFIX, `Waiting for text match: ${containerSelector} / ${textPattern}`);
  124. log(`Waiting for element with text: ${textPattern}...`);
  125. let settled = false;
  126. let stopTimer = null;
  127. const cleanup = () => {
  128. if (settled) return;
  129. settled = true;
  130. observer.disconnect();
  131. clearTimeout(timer);
  132. clearTimeout(stopTimer);
  133. };
  134. const observer = new MutationObserver(() => {
  135. if (flowStopped) {
  136. cleanup();
  137. reject(new Error(STOP_ERROR_MESSAGE));
  138. return;
  139. }
  140. const el = search();
  141. if (el) {
  142. cleanup();
  143. console.log(LOG_PREFIX, `Found by text after wait: ${textPattern}`);
  144. log(`Found element by text: ${textPattern}`);
  145. resolve(el);
  146. }
  147. });
  148. observer.observe(document.body || document.documentElement, {
  149. childList: true,
  150. subtree: true,
  151. });
  152. const timer = setTimeout(() => {
  153. cleanup();
  154. const msg = `Timeout waiting for text "${textPattern}" in "${containerSelector}" after ${timeout}ms on ${location.href}`;
  155. console.error(LOG_PREFIX, msg);
  156. reject(new Error(msg));
  157. }, timeout);
  158. const pollStop = () => {
  159. if (settled) return;
  160. if (flowStopped) {
  161. cleanup();
  162. reject(new Error(STOP_ERROR_MESSAGE));
  163. return;
  164. }
  165. stopTimer = setTimeout(pollStop, 100);
  166. };
  167. pollStop();
  168. });
  169. }
  170. /**
  171. * React-compatible form filling.
  172. * Sets value via native setter and dispatches input + change events.
  173. * @param {HTMLInputElement} el
  174. * @param {string} value
  175. */
  176. function fillInput(el, value) {
  177. throwIfStopped();
  178. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
  179. window.HTMLInputElement.prototype,
  180. 'value'
  181. ).set;
  182. nativeInputValueSetter.call(el, value);
  183. el.dispatchEvent(new Event('input', { bubbles: true }));
  184. el.dispatchEvent(new Event('change', { bubbles: true }));
  185. console.log(LOG_PREFIX, `Filled input ${el.name || el.id || el.type} with: ${value}`);
  186. log(`Filled input [${el.name || el.id || el.type || 'unknown'}]`);
  187. }
  188. /**
  189. * Fill a select element by setting its value and triggering change.
  190. * @param {HTMLSelectElement} el
  191. * @param {string} value
  192. */
  193. function fillSelect(el, value) {
  194. throwIfStopped();
  195. el.value = value;
  196. el.dispatchEvent(new Event('change', { bubbles: true }));
  197. console.log(LOG_PREFIX, `Selected value ${value} in ${el.name || el.id}`);
  198. log(`Selected [${el.name || el.id || 'unknown'}] = ${value}`);
  199. }
  200. /**
  201. * Send a log message to Side Panel via Background.
  202. * @param {string} message
  203. * @param {string} level - 'info' | 'ok' | 'warn' | 'error'
  204. */
  205. function log(message, level = 'info') {
  206. chrome.runtime.sendMessage({
  207. type: 'LOG',
  208. source: SCRIPT_SOURCE,
  209. step: null,
  210. payload: { message, level, timestamp: Date.now() },
  211. error: null,
  212. });
  213. }
  214. /**
  215. * Report that this content script is loaded and ready.
  216. */
  217. function reportReady() {
  218. console.log(LOG_PREFIX, 'Content script ready');
  219. chrome.runtime.sendMessage({
  220. type: 'CONTENT_SCRIPT_READY',
  221. source: SCRIPT_SOURCE,
  222. step: null,
  223. payload: {},
  224. error: null,
  225. });
  226. }
  227. /**
  228. * Report step completion.
  229. * @param {number} step
  230. * @param {Object} data - Step output data
  231. */
  232. function reportComplete(step, data = {}) {
  233. console.log(LOG_PREFIX, `Step ${step} completed`, data);
  234. log(`Step ${step} completed successfully`, 'ok');
  235. chrome.runtime.sendMessage({
  236. type: 'STEP_COMPLETE',
  237. source: SCRIPT_SOURCE,
  238. step,
  239. payload: data,
  240. error: null,
  241. });
  242. }
  243. /**
  244. * Report step error.
  245. * @param {number} step
  246. * @param {string} errorMessage
  247. */
  248. function reportError(step, errorMessage) {
  249. console.error(LOG_PREFIX, `Step ${step} failed: ${errorMessage}`);
  250. log(`Step ${step} failed: ${errorMessage}`, 'error');
  251. chrome.runtime.sendMessage({
  252. type: 'STEP_ERROR',
  253. source: SCRIPT_SOURCE,
  254. step,
  255. payload: {},
  256. error: errorMessage,
  257. });
  258. }
  259. /**
  260. * Simulate a click with proper event dispatching.
  261. * @param {Element} el
  262. */
  263. function simulateClick(el) {
  264. throwIfStopped();
  265. el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
  266. console.log(LOG_PREFIX, `Clicked: ${el.tagName} ${el.textContent?.slice(0, 30) || ''}`);
  267. log(`Clicked [${el.tagName}] "${el.textContent?.trim().slice(0, 30) || ''}"`);
  268. }
  269. /**
  270. * Wait a specified number of milliseconds.
  271. * @param {number} ms
  272. * @returns {Promise<void>}
  273. */
  274. function sleep(ms) {
  275. return new Promise((resolve, reject) => {
  276. const start = Date.now();
  277. function tick() {
  278. if (flowStopped) {
  279. reject(new Error(STOP_ERROR_MESSAGE));
  280. return;
  281. }
  282. if (Date.now() - start >= ms) {
  283. resolve();
  284. return;
  285. }
  286. setTimeout(tick, Math.min(100, Math.max(25, ms - (Date.now() - start))));
  287. }
  288. tick();
  289. });
  290. }
  291. async function humanPause(min = 250, max = 850) {
  292. const duration = Math.floor(Math.random() * (max - min + 1)) + min;
  293. await sleep(duration);
  294. }
  295. // Auto-report ready on load
  296. // Skip ready signal from child iframes of mail pages to avoid overwriting the top frame's registration
  297. const _isMailChildFrame = (SCRIPT_SOURCE === 'qq-mail' || SCRIPT_SOURCE === 'mail-163' || SCRIPT_SOURCE === 'inbucket-mail') && window !== window.top;
  298. if (!_isMailChildFrame) {
  299. reportReady();
  300. }