utils.js 9.1 KB

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