utils.js 9.5 KB

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