main.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. "use strict";
  2. const { app, BrowserWindow, ipcMain, shell, session, Menu, Tray, nativeImage, webContents } = require("electron");
  3. const { join } = require("path");
  4. require("fs");
  5. let mainWindow = null;
  6. let tray = null;
  7. let isQuitting = false;
  8. const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;
  9. function getIconPath() {
  10. return VITE_DEV_SERVER_URL ? join(__dirname, "../public/icons/icon-256.png") : join(__dirname, "../dist/icons/icon-256.png");
  11. }
  12. function getTrayIconPath() {
  13. return VITE_DEV_SERVER_URL ? join(__dirname, "../public/icons/tray-icon.png") : join(__dirname, "../dist/icons/tray-icon.png");
  14. }
  15. function createTrayIcon() {
  16. const trayIconPath = getTrayIconPath();
  17. return nativeImage.createFromPath(trayIconPath);
  18. }
  19. function createTray() {
  20. const trayIcon = createTrayIcon();
  21. tray = new Tray(trayIcon);
  22. const contextMenu = Menu.buildFromTemplate([
  23. {
  24. label: "显示主窗口",
  25. click: () => {
  26. if (mainWindow) {
  27. mainWindow.show();
  28. mainWindow.focus();
  29. }
  30. }
  31. },
  32. {
  33. label: "最小化到托盘",
  34. click: () => {
  35. mainWindow == null ? void 0 : mainWindow.hide();
  36. }
  37. },
  38. { type: "separator" },
  39. {
  40. label: "退出",
  41. click: () => {
  42. isQuitting = true;
  43. app.quit();
  44. }
  45. }
  46. ]);
  47. tray.setToolTip("多平台媒体管理系统");
  48. tray.setContextMenu(contextMenu);
  49. tray.on("click", () => {
  50. if (mainWindow) {
  51. if (mainWindow.isVisible()) {
  52. mainWindow.focus();
  53. } else {
  54. mainWindow.show();
  55. mainWindow.focus();
  56. }
  57. }
  58. });
  59. tray.on("double-click", () => {
  60. if (mainWindow) {
  61. mainWindow.show();
  62. mainWindow.focus();
  63. }
  64. });
  65. }
  66. function createWindow() {
  67. Menu.setApplicationMenu(null);
  68. const iconPath = getIconPath();
  69. mainWindow = new BrowserWindow({
  70. width: 1400,
  71. height: 900,
  72. minWidth: 1200,
  73. minHeight: 700,
  74. icon: iconPath,
  75. webPreferences: {
  76. preload: join(__dirname, "preload.js"),
  77. nodeIntegration: false,
  78. contextIsolation: true,
  79. webviewTag: true
  80. // 启用 webview 标签
  81. },
  82. frame: false,
  83. // 无边框窗口,自定义标题栏
  84. transparent: false,
  85. backgroundColor: "#f0f2f5",
  86. show: false
  87. });
  88. mainWindow.once("ready-to-show", () => {
  89. mainWindow == null ? void 0 : mainWindow.show();
  90. setupWindowEvents();
  91. });
  92. if (VITE_DEV_SERVER_URL) {
  93. mainWindow.loadURL(VITE_DEV_SERVER_URL);
  94. mainWindow.webContents.openDevTools();
  95. } else {
  96. mainWindow.loadFile(join(__dirname, "../dist/index.html"));
  97. }
  98. mainWindow.webContents.setWindowOpenHandler(({ url }) => {
  99. shell.openExternal(url);
  100. return { action: "deny" };
  101. });
  102. mainWindow.on("close", (event) => {
  103. if (!isQuitting) {
  104. event.preventDefault();
  105. mainWindow == null ? void 0 : mainWindow.hide();
  106. if (tray && !app.isPackaged) ;
  107. }
  108. });
  109. mainWindow.on("closed", () => {
  110. mainWindow = null;
  111. });
  112. }
  113. const gotTheLock = app.requestSingleInstanceLock();
  114. if (!gotTheLock) {
  115. app.quit();
  116. } else {
  117. app.on("second-instance", () => {
  118. if (mainWindow) {
  119. mainWindow.show();
  120. if (mainWindow.isMinimized()) mainWindow.restore();
  121. mainWindow.focus();
  122. }
  123. });
  124. app.whenReady().then(() => {
  125. createTray();
  126. createWindow();
  127. setupWebviewSessions();
  128. app.on("activate", () => {
  129. if (BrowserWindow.getAllWindows().length === 0) {
  130. createWindow();
  131. } else if (mainWindow) {
  132. mainWindow.show();
  133. }
  134. });
  135. });
  136. }
  137. function setupWebviewSessions() {
  138. app.on("web-contents-created", (_event, contents) => {
  139. if (contents.getType() === "webview") {
  140. contents.setUserAgent(
  141. "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
  142. );
  143. contents.on("will-navigate", (event, url) => {
  144. if (!isAllowedUrl(url)) {
  145. console.log("[WebView] 阻止导航到自定义协议:", url);
  146. event.preventDefault();
  147. }
  148. });
  149. contents.setWindowOpenHandler(({ url }) => {
  150. if (!isAllowedUrl(url)) {
  151. console.log("[WebView] 阻止打开自定义协议窗口:", url);
  152. return { action: "deny" };
  153. }
  154. console.log("[WebView] 拦截新窗口,在当前页面打开:", url);
  155. contents.loadURL(url);
  156. return { action: "deny" };
  157. });
  158. contents.session.setPermissionRequestHandler((_webContents, permission, callback) => {
  159. callback(true);
  160. });
  161. contents.session.webRequest.onBeforeSendHeaders((details, callback) => {
  162. delete details.requestHeaders["X-DevTools-Emulate-Network-Conditions-Client-Id"];
  163. if (!details.requestHeaders["Origin"] && !details.requestHeaders["origin"]) ;
  164. callback({ requestHeaders: details.requestHeaders });
  165. });
  166. }
  167. });
  168. }
  169. function isAllowedUrl(url) {
  170. if (!url) return false;
  171. const lowerUrl = url.toLowerCase();
  172. return lowerUrl.startsWith("http://") || lowerUrl.startsWith("https://") || lowerUrl.startsWith("about:") || lowerUrl.startsWith("data:");
  173. }
  174. app.on("window-all-closed", () => {
  175. });
  176. app.on("before-quit", () => {
  177. isQuitting = true;
  178. });
  179. app.on("quit", () => {
  180. if (tray) {
  181. tray.destroy();
  182. tray = null;
  183. }
  184. });
  185. ipcMain.handle("get-app-version", () => {
  186. return app.getVersion();
  187. });
  188. ipcMain.handle("get-platform", () => {
  189. return process.platform;
  190. });
  191. ipcMain.on("window-minimize", () => {
  192. mainWindow == null ? void 0 : mainWindow.minimize();
  193. });
  194. ipcMain.on("window-maximize", () => {
  195. if (mainWindow == null ? void 0 : mainWindow.isMaximized()) {
  196. mainWindow.unmaximize();
  197. } else {
  198. mainWindow == null ? void 0 : mainWindow.maximize();
  199. }
  200. });
  201. ipcMain.on("window-close", () => {
  202. mainWindow == null ? void 0 : mainWindow.hide();
  203. });
  204. ipcMain.on("app-quit", () => {
  205. isQuitting = true;
  206. app.quit();
  207. });
  208. ipcMain.handle("window-is-maximized", () => {
  209. return (mainWindow == null ? void 0 : mainWindow.isMaximized()) || false;
  210. });
  211. function setupWindowEvents() {
  212. mainWindow == null ? void 0 : mainWindow.on("maximize", () => {
  213. mainWindow == null ? void 0 : mainWindow.webContents.send("window-maximized", true);
  214. });
  215. mainWindow == null ? void 0 : mainWindow.on("unmaximize", () => {
  216. mainWindow == null ? void 0 : mainWindow.webContents.send("window-maximized", false);
  217. });
  218. }
  219. ipcMain.handle("get-webview-cookies", async (_event, partition, url) => {
  220. try {
  221. const ses = session.fromPartition(partition);
  222. const cookies = await ses.cookies.get({ url });
  223. return cookies;
  224. } catch (error) {
  225. console.error("获取 cookies 失败:", error);
  226. return [];
  227. }
  228. });
  229. ipcMain.handle("clear-webview-cookies", async (_event, partition) => {
  230. try {
  231. const ses = session.fromPartition(partition);
  232. await ses.clearStorageData({ storages: ["cookies"] });
  233. return true;
  234. } catch (error) {
  235. console.error("清除 cookies 失败:", error);
  236. return false;
  237. }
  238. });
  239. ipcMain.handle("set-webview-cookies", async (_event, partition, cookies) => {
  240. try {
  241. const ses = session.fromPartition(partition);
  242. for (const cookie of cookies) {
  243. await ses.cookies.set(cookie);
  244. }
  245. return true;
  246. } catch (error) {
  247. console.error("设置 cookies 失败:", error);
  248. return false;
  249. }
  250. });
  251. ipcMain.handle("capture-webview-page", async (_event, webContentsId) => {
  252. try {
  253. const wc = webContents.fromId(webContentsId);
  254. if (!wc) {
  255. console.error("找不到 webContents:", webContentsId);
  256. return null;
  257. }
  258. const image = await wc.capturePage();
  259. if (!image || image.isEmpty()) {
  260. console.warn("截图为空");
  261. return null;
  262. }
  263. const buffer = image.toJPEG(80);
  264. return buffer.toString("base64");
  265. } catch (error) {
  266. console.error("截图失败:", error);
  267. return null;
  268. }
  269. });
  270. ipcMain.handle("webview-send-mouse-click", async (_event, webContentsId, x, y) => {
  271. try {
  272. const wc = webContents.fromId(webContentsId);
  273. if (!wc) {
  274. console.error("找不到 webContents:", webContentsId);
  275. return false;
  276. }
  277. wc.sendInputEvent({
  278. type: "mouseMove",
  279. x: Math.round(x),
  280. y: Math.round(y)
  281. });
  282. await new Promise((resolve) => setTimeout(resolve, 50));
  283. wc.sendInputEvent({
  284. type: "mouseDown",
  285. x: Math.round(x),
  286. y: Math.round(y),
  287. button: "left",
  288. clickCount: 1
  289. });
  290. await new Promise((resolve) => setTimeout(resolve, 50));
  291. wc.sendInputEvent({
  292. type: "mouseUp",
  293. x: Math.round(x),
  294. y: Math.round(y),
  295. button: "left",
  296. clickCount: 1
  297. });
  298. console.log(`[webview-send-mouse-click] Clicked at (${x}, ${y})`);
  299. return true;
  300. } catch (error) {
  301. console.error("发送点击事件失败:", error);
  302. return false;
  303. }
  304. });
  305. ipcMain.handle("webview-send-text-input", async (_event, webContentsId, text) => {
  306. try {
  307. const wc = webContents.fromId(webContentsId);
  308. if (!wc) {
  309. console.error("找不到 webContents:", webContentsId);
  310. return false;
  311. }
  312. for (const char of text) {
  313. wc.sendInputEvent({
  314. type: "char",
  315. keyCode: char
  316. });
  317. await new Promise((resolve) => setTimeout(resolve, 30));
  318. }
  319. console.log(`[webview-send-text-input] Typed: ${text}`);
  320. return true;
  321. } catch (error) {
  322. console.error("发送输入事件失败:", error);
  323. return false;
  324. }
  325. });
  326. ipcMain.handle("webview-get-element-position", async (_event, webContentsId, selector) => {
  327. try {
  328. const wc = webContents.fromId(webContentsId);
  329. if (!wc) {
  330. console.error("找不到 webContents:", webContentsId);
  331. return null;
  332. }
  333. const result = await wc.executeJavaScript(`
  334. (function() {
  335. const el = document.querySelector('${selector.replace(/'/g, "\\'")}');
  336. if (!el) return null;
  337. const rect = el.getBoundingClientRect();
  338. return {
  339. x: rect.left + rect.width / 2,
  340. y: rect.top + rect.height / 2,
  341. width: rect.width,
  342. height: rect.height
  343. };
  344. })()
  345. `);
  346. return result;
  347. } catch (error) {
  348. console.error("获取元素位置失败:", error);
  349. return null;
  350. }
  351. });
  352. ipcMain.handle("webview-click-by-text", async (_event, webContentsId, text) => {
  353. try {
  354. const wc = webContents.fromId(webContentsId);
  355. if (!wc) {
  356. console.error("找不到 webContents:", webContentsId);
  357. return false;
  358. }
  359. const position = await wc.executeJavaScript(`
  360. (function() {
  361. const searchText = '${text.replace(/'/g, "\\'")}';
  362. // 查找可点击元素
  363. const clickables = document.querySelectorAll('a, button, [role="button"], [onclick], input[type="submit"], input[type="button"]');
  364. for (const el of clickables) {
  365. if (el.textContent?.includes(searchText) || el.getAttribute('aria-label')?.includes(searchText) || el.getAttribute('title')?.includes(searchText)) {
  366. const rect = el.getBoundingClientRect();
  367. if (rect.width > 0 && rect.height > 0) {
  368. return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
  369. }
  370. }
  371. }
  372. // 查找所有包含文本的元素
  373. const allElements = document.querySelectorAll('*');
  374. for (const el of allElements) {
  375. const text = el.innerText?.trim();
  376. if (text && text.length < 100 && text.includes(searchText)) {
  377. const rect = el.getBoundingClientRect();
  378. if (rect.width > 0 && rect.height > 0 && rect.width < 500) {
  379. return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
  380. }
  381. }
  382. }
  383. return null;
  384. })()
  385. `);
  386. if (!position) {
  387. console.warn(`[webview-click-by-text] 未找到包含 "${text}" 的元素`);
  388. return false;
  389. }
  390. wc.sendInputEvent({ type: "mouseMove", x: Math.round(position.x), y: Math.round(position.y) });
  391. await new Promise((resolve) => setTimeout(resolve, 50));
  392. wc.sendInputEvent({ type: "mouseDown", x: Math.round(position.x), y: Math.round(position.y), button: "left", clickCount: 1 });
  393. await new Promise((resolve) => setTimeout(resolve, 50));
  394. wc.sendInputEvent({ type: "mouseUp", x: Math.round(position.x), y: Math.round(position.y), button: "left", clickCount: 1 });
  395. console.log(`[webview-click-by-text] Clicked "${text}" at (${position.x}, ${position.y})`);
  396. return true;
  397. } catch (error) {
  398. console.error("通过文本点击失败:", error);
  399. return false;
  400. }
  401. });
  402. const networkInterceptors = /* @__PURE__ */ new Map();
  403. ipcMain.handle("enable-network-intercept", async (_event, webContentsId, patterns) => {
  404. var _a;
  405. try {
  406. const wc = webContents.fromId(webContentsId);
  407. if (!wc) {
  408. console.error("[CDP] 找不到 webContents:", webContentsId);
  409. return false;
  410. }
  411. if (networkInterceptors.has(webContentsId)) {
  412. try {
  413. wc.debugger.detach();
  414. } catch (e) {
  415. }
  416. }
  417. networkInterceptors.set(webContentsId, {
  418. patterns,
  419. pendingRequests: /* @__PURE__ */ new Map()
  420. });
  421. try {
  422. wc.debugger.attach("1.3");
  423. } catch (err) {
  424. const error = err;
  425. if (!((_a = error.message) == null ? void 0 : _a.includes("Already attached"))) {
  426. throw err;
  427. }
  428. }
  429. await wc.debugger.sendCommand("Network.enable");
  430. wc.debugger.on("message", async (_e, method, params) => {
  431. const config = networkInterceptors.get(webContentsId);
  432. if (!config) return;
  433. if (method === "Network.responseReceived") {
  434. const { requestId, response } = params;
  435. if (!requestId || !(response == null ? void 0 : response.url)) return;
  436. if (response.url.includes("baijiahao.baidu.com")) {
  437. if (response.url.includes("/pcui/") || response.url.includes("/article")) {
  438. console.log(`[CDP DEBUG] 百家号 API: ${response.url}`);
  439. }
  440. }
  441. for (const pattern of config.patterns) {
  442. if (response.url.includes(pattern.match)) {
  443. config.pendingRequests.set(requestId, {
  444. url: response.url,
  445. timestamp: Date.now()
  446. });
  447. console.log(`[CDP] 匹配到 API: ${pattern.key} - ${response.url}`);
  448. break;
  449. }
  450. }
  451. }
  452. if (method === "Network.loadingFinished") {
  453. const { requestId } = params;
  454. if (!requestId) return;
  455. const pending = config.pendingRequests.get(requestId);
  456. if (!pending) return;
  457. config.pendingRequests.delete(requestId);
  458. try {
  459. const result = await wc.debugger.sendCommand("Network.getResponseBody", { requestId });
  460. let body = result.body;
  461. if (result.base64Encoded) {
  462. body = Buffer.from(body, "base64").toString("utf8");
  463. }
  464. const data = JSON.parse(body);
  465. let matchedKey = "";
  466. for (const pattern of config.patterns) {
  467. if (pending.url.includes(pattern.match)) {
  468. matchedKey = pattern.key;
  469. break;
  470. }
  471. }
  472. if (matchedKey) {
  473. console.log(`[CDP] 获取到响应: ${matchedKey}`, JSON.stringify(data).substring(0, 200));
  474. mainWindow == null ? void 0 : mainWindow.webContents.send("network-intercept-data", {
  475. webContentsId,
  476. key: matchedKey,
  477. url: pending.url,
  478. data
  479. });
  480. }
  481. } catch (err) {
  482. console.warn(`[CDP] 获取响应体失败:`, err);
  483. }
  484. }
  485. });
  486. console.log(`[CDP] 已启用网络拦截,webContentsId: ${webContentsId}, patterns:`, patterns.map((p) => p.key));
  487. return true;
  488. } catch (error) {
  489. console.error("[CDP] 启用网络拦截失败:", error);
  490. return false;
  491. }
  492. });
  493. ipcMain.handle("disable-network-intercept", async (_event, webContentsId) => {
  494. try {
  495. const wc = webContents.fromId(webContentsId);
  496. if (wc) {
  497. try {
  498. wc.debugger.detach();
  499. } catch (e) {
  500. }
  501. }
  502. networkInterceptors.delete(webContentsId);
  503. console.log(`[CDP] 已禁用网络拦截,webContentsId: ${webContentsId}`);
  504. return true;
  505. } catch (error) {
  506. console.error("[CDP] 禁用网络拦截失败:", error);
  507. return false;
  508. }
  509. });
  510. ipcMain.handle("update-network-patterns", async (_event, webContentsId, patterns) => {
  511. const config = networkInterceptors.get(webContentsId);
  512. if (config) {
  513. config.patterns = patterns;
  514. console.log(`[CDP] 已更新 patterns,webContentsId: ${webContentsId}`);
  515. return true;
  516. }
  517. return false;
  518. });
  519. //# sourceMappingURL=main.js.map