utils.js 10 KB

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