main.ts 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082
  1. // 浣跨敤 CommonJS 鏍煎紡
  2. const { app, BrowserWindow, ipcMain, shell, session, Menu, Tray, nativeImage, webContents } = require('electron');
  3. const { join } = require('path');
  4. const fs = require('fs');
  5. const http = require('http');
  6. const https = require('https');
  7. const os = require('os');
  8. import { startLocalServices, stopLocalServices, getServiceStatus, LOCAL_NODE_URL, getLogPath, setPlaywrightProgressHandler } from './local-services';
  9. let mainWindow: typeof BrowserWindow.prototype | null = null;
  10. // ========== 鍐呭瓨鐩戞帶 ==========
  11. const MEMORY_THRESHOLD_MB = 512; // 瓒呰繃 512MB 瑙﹀彂璀﹀憡 / 娓呯悊
  12. let lastMemoryReport = 0;
  13. function getMemoryUsageMB(): number {
  14. const used = process.memoryUsage();
  15. return Math.round(used.heapUsed / 1024 / 1024);
  16. }
  17. function logMemory(prefix: string): void {
  18. const used = getMemoryUsageMB();
  19. const total = Math.round(os.totalmem() / 1024 / 1024);
  20. const free = Math.round(os.freemem() / 1024 / 1024);
  21. const usagePct = Math.round((used / total) * 100);
  22. console.log(`[MEM] ${prefix} heap=${used}MB total=${total}MB free=${free}MB usage=${usagePct}%`);
  23. }
  24. function monitorMemory(): void {
  25. const used = getMemoryUsageMB();
  26. const now = Date.now();
  27. // 姣?60 绉掓姤鍛婁竴娆?
  28. if (now - lastMemoryReport > 60000) {
  29. logMemory('Electron');
  30. lastMemoryReport = now;
  31. }
  32. // 瓒呰繃闃堝€硷紝瑙﹀彂 GC 骞舵姤鍛?
  33. if (used > MEMORY_THRESHOLD_MB) {
  34. console.warn(`[MEM] Memory high (${used}MB), triggering GC...`);
  35. if (global.gc) {
  36. global.gc();
  37. }
  38. // 鍏抽棴澶氫綑鐨?webContents
  39. if (mainWindow?.webContents) {
  40. const wc = mainWindow.webContents;
  41. // 灏濊瘯娓呯悊 devtools extension
  42. try {
  43. session.defaultSession?.webContents.forEach((w: any) => {
  44. if (w !== wc && !w.isDestroyed()) {
  45. console.warn('[MEM] Closing idle webContents');
  46. w.close();
  47. }
  48. });
  49. } catch {}
  50. }
  51. }
  52. }
  53. // 鍚姩鍐呭瓨鐩戞帶
  54. setInterval(monitorMemory, 30000);
  55. let tray: typeof Tray.prototype | null = null;
  56. let isQuitting = false;
  57. const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;
  58. function setupCertificateBypass() {
  59. // 浠呭湪寮€鍙戠幆澧冭烦杩囨湰鍦版湇鍔$殑璇佷功楠岃瘉锛岀敓浜х幆澧冧笉鍋氬叏灞€缁曡繃
  60. if (!VITE_DEV_SERVER_URL) return;
  61. const allowedHosts = ['localhost', '127.0.0.1'];
  62. app.on('certificate-error', (event: Event, _webContents: typeof webContents.prototype, url: string, _error: string, _certificate: unknown, callback: (isTrusted: boolean) => void) => {
  63. try {
  64. const { hostname } = new URL(url);
  65. if (allowedHosts.includes(hostname)) {
  66. event.preventDefault();
  67. callback(true);
  68. return;
  69. }
  70. } catch { /* ignore invalid URLs */ }
  71. callback(false);
  72. });
  73. }
  74. function setupCorsBypassForApiRequests() {
  75. const ses = session.defaultSession;
  76. if (!ses) return;
  77. ses.webRequest.onHeadersReceived((details: { url: string; responseHeaders?: Record<string, string[] | string> }, callback: (response: { responseHeaders: Record<string, string[] | string> }) => void) => {
  78. const url = String(details.url || '');
  79. const isHttp = url.startsWith('http://') || url.startsWith('https://');
  80. const isApiLike = url.includes('/api/') || url.includes('/uploads/');
  81. if (!isHttp || !isApiLike) {
  82. callback({ responseHeaders: details.responseHeaders || {} });
  83. return;
  84. }
  85. const responseHeaders = { ...(details.responseHeaders || {}) };
  86. // 绉婚櫎鏈嶅姟绔凡鏈夌殑 CORS 澶达紝閬垮厤涓庝笅闈㈣缃殑鍊煎悎骞舵垚 "origin1, *" 瀵艰嚧杩濊
  87. const corsKeys = ['access-control-allow-origin', 'Access-Control-Allow-Origin'];
  88. corsKeys.forEach((k) => delete responseHeaders[k]);
  89. responseHeaders['access-control-allow-origin'] = ['*'];
  90. responseHeaders['access-control-allow-methods'] = ['GET,POST,PUT,PATCH,DELETE,OPTIONS'];
  91. responseHeaders['access-control-allow-headers'] = ['Authorization,Content-Type,X-Requested-With'];
  92. responseHeaders['access-control-expose-headers'] = ['Content-Disposition,Content-Type'];
  93. callback({ responseHeaders });
  94. });
  95. }
  96. function normalizeBaseUrl(url: string): string {
  97. const raw = String(url || '').trim();
  98. if (!raw) return '';
  99. try {
  100. const u = new URL(raw);
  101. return `${u.protocol}//${u.host}`.replace(/\/$/, '');
  102. } catch {
  103. return raw.replace(/\/$/, '');
  104. }
  105. }
  106. function requestJson(url: string, timeoutMs: number): Promise<{ ok: boolean; status?: number; data?: any; error?: string }> {
  107. return new Promise((resolve) => {
  108. const u = new URL(url);
  109. const isHttps = u.protocol === 'https:';
  110. const lib = isHttps ? https : http;
  111. // Windows 涓?localhost 甯歌瑙f瀽涓?::1锛岃€屽悗绔粎鐩戝惉 127.0.0.1锛屽鑷?ECONNREFUSED
  112. const hostname = (u.hostname === 'localhost' || u.hostname === '::1') ? '127.0.0.1' : u.hostname;
  113. const req = lib.request({
  114. method: 'GET',
  115. protocol: u.protocol,
  116. hostname,
  117. port: u.port || (isHttps ? 443 : 80),
  118. path: `${u.pathname}${u.search}`,
  119. headers: {
  120. Accept: 'application/json',
  121. },
  122. timeout: timeoutMs,
  123. rejectUnauthorized: !VITE_DEV_SERVER_URL, // 浠呭紑鍙戠幆澧冭烦杩囪瘉涔﹂獙璇?
  124. }, (res: any) => {
  125. const chunks: Buffer[] = [];
  126. res.on('data', (c: Buffer) => chunks.push(c));
  127. res.on('end', () => {
  128. const status = Number(res.statusCode || 0);
  129. const rawText = Buffer.concat(chunks).toString('utf-8');
  130. if (status < 200 || status >= 300) {
  131. resolve({ ok: false, status, error: `HTTP ${status}` });
  132. return;
  133. }
  134. try {
  135. const json = rawText ? JSON.parse(rawText) : null;
  136. resolve({ ok: true, status, data: json });
  137. } catch {
  138. resolve({ ok: false, status, error: '鍝嶅簲涓嶆槸 JSON' });
  139. }
  140. });
  141. });
  142. req.on('timeout', () => {
  143. req.destroy(new Error('timeout'));
  144. });
  145. req.on('error', (err: any) => {
  146. resolve({ ok: false, error: err?.message || '缃戠粶閿欒' });
  147. });
  148. req.end();
  149. });
  150. }
  151. // 鑾峰彇鍥炬爣璺緞
  152. function getIconPath() {
  153. return VITE_DEV_SERVER_URL
  154. ? join(__dirname, '../public/icons/icon-256.png')
  155. : join(__dirname, '../dist/icons/icon-256.png');
  156. }
  157. // 鑾峰彇鎵樼洏鍥炬爣璺緞
  158. function getTrayIconPath() {
  159. return VITE_DEV_SERVER_URL
  160. ? join(__dirname, '../public/icons/tray-icon.png')
  161. : join(__dirname, '../dist/icons/tray-icon.png');
  162. }
  163. // 鍒涘缓鎵樼洏鍥炬爣
  164. function createTrayIcon(): typeof nativeImage.prototype {
  165. const trayIconPath = getTrayIconPath();
  166. return nativeImage.createFromPath(trayIconPath);
  167. }
  168. // 鍒涘缓绯荤粺鎵樼洏
  169. function createTray() {
  170. const trayIcon = createTrayIcon();
  171. tray = new Tray(trayIcon);
  172. const contextMenu = Menu.buildFromTemplate([
  173. {
  174. label: '显示主窗口',
  175. click: () => {
  176. if (mainWindow) {
  177. mainWindow.show();
  178. mainWindow.focus();
  179. }
  180. }
  181. },
  182. {
  183. label: '最小化到托盘',
  184. click: () => {
  185. mainWindow?.hide();
  186. }
  187. },
  188. { type: 'separator' },
  189. {
  190. label: '退出',
  191. click: () => {
  192. isQuitting = true;
  193. app.quit();
  194. }
  195. }
  196. ]);
  197. tray.setToolTip('智媒通');
  198. tray.setContextMenu(contextMenu);
  199. // 鐐瑰嚮鎵樼洏鍥炬爣鏄剧ず绐楀彛
  200. tray.on('click', () => {
  201. if (mainWindow) {
  202. if (mainWindow.isVisible()) {
  203. mainWindow.focus();
  204. } else {
  205. mainWindow.show();
  206. mainWindow.focus();
  207. }
  208. }
  209. });
  210. // 鍙屽嚮鎵樼洏鍥炬爣鏄剧ず绐楀彛
  211. tray.on('double-click', () => {
  212. if (mainWindow) {
  213. mainWindow.show();
  214. mainWindow.focus();
  215. }
  216. });
  217. }
  218. function createWindow() {
  219. // 闅愯棌榛樿鑿滃崟鏍?
  220. Menu.setApplicationMenu(null);
  221. const iconPath = getIconPath();
  222. mainWindow = new BrowserWindow({
  223. width: 1400,
  224. height: 900,
  225. minWidth: 1200,
  226. minHeight: 700,
  227. icon: iconPath,
  228. webPreferences: {
  229. preload: join(__dirname, 'preload.js'),
  230. nodeIntegration: false,
  231. contextIsolation: true,
  232. webviewTag: true, // 鍚敤 webview 鏍囩
  233. },
  234. frame: false, // 鏃犺竟妗嗙獥鍙o紝鑷畾涔夋爣棰樻爮
  235. transparent: false,
  236. backgroundColor: '#f0f2f5',
  237. show: false,
  238. });
  239. // 绐楀彛鍑嗗濂藉悗鍐嶆樉绀猴紝閬垮厤鐧藉睆
  240. mainWindow.once('ready-to-show', () => {
  241. mainWindow?.show();
  242. setupWindowEvents();
  243. });
  244. // 鍔犺浇椤甸潰
  245. if (VITE_DEV_SERVER_URL) {
  246. mainWindow.loadURL(VITE_DEV_SERVER_URL);
  247. mainWindow.webContents.openDevTools();
  248. } else {
  249. mainWindow.loadFile(join(__dirname, '../dist/index.html'));
  250. }
  251. // 澶勭悊澶栭儴閾炬帴
  252. mainWindow.webContents.setWindowOpenHandler(({ url }: { url: string }) => {
  253. shell.openExternal(url);
  254. return { action: 'deny' };
  255. });
  256. // 鍏抽棴鎸夐挳榛樿鏈€灏忓寲鍒版墭鐩?
  257. mainWindow.on('close', (event: Event) => {
  258. if (!isQuitting) {
  259. event.preventDefault();
  260. mainWindow?.hide();
  261. // 鏄剧ず鎵樼洏閫氱煡锛堜粎棣栨锛?
  262. if (tray && !app.isPackaged) {
  263. // 寮€鍙戞ā寮忎笅鍙互鏄剧ず閫氱煡
  264. }
  265. }
  266. });
  267. mainWindow.on('closed', () => {
  268. mainWindow = null;
  269. });
  270. }
  271. // 鍗曞疄渚嬮攣瀹?
  272. const gotTheLock = app.requestSingleInstanceLock();
  273. if (!gotTheLock) {
  274. app.quit();
  275. } else {
  276. app.on('second-instance', () => {
  277. if (mainWindow) {
  278. mainWindow.show();
  279. if (mainWindow.isMinimized()) mainWindow.restore();
  280. mainWindow.focus();
  281. }
  282. });
  283. // ========== 闄嶄綆 Electron 鍐呭瓨鍗犵敤锛堝繀椤诲湪 app.whenReady 涔嬪墠锛?==========
  284. app.commandLine.appendSwitch('js-flags', '--max-old-space-size=512');
  285. app.commandLine.appendSwitch('renderer-process-limit', '2');
  286. app.whenReady().then(async () => {
  287. logMemory('AppReady');
  288. setPlaywrightProgressHandler((p) => {
  289. mainWindow?.webContents.send('playwright-install-progress', p);
  290. });
  291. // 鍏堝垱寤虹獥鍙f樉绀?splash screen
  292. createWindow();
  293. createTray();
  294. // 鍚庡彴鍚姩鏈湴 Node 鏈嶅姟锛屼笉闃诲绐楀彛鏄剧ず
  295. console.log('[Main] 姝e湪鍚庡彴鍚姩鏈湴鏈嶅姟...');
  296. startLocalServices().then(({ nodeOk }) => {
  297. console.log(`[Main] local services status: Node=${nodeOk ? 'ready' : 'not_ready'}`);
  298. mainWindow?.webContents.send('services-status-changed', { nodeOk });
  299. });
  300. // 閰嶇疆 webview session锛屽厑璁哥涓夋柟 cookies 鍜岃法鍩熻姹?
  301. setupWebviewSessions();
  302. setupCertificateBypass();
  303. setupCorsBypassForApiRequests();
  304. app.on('activate', () => {
  305. if (BrowserWindow.getAllWindows().length === 0) {
  306. createWindow();
  307. } else if (mainWindow) {
  308. mainWindow.show();
  309. }
  310. });
  311. });
  312. }
  313. // 閰嶇疆 webview sessions
  314. function setupWebviewSessions() {
  315. // 鐩戝惉鏂扮殑 webContents 鍒涘缓
  316. app.on('web-contents-created', (_event: unknown, contents: typeof webContents.prototype) => {
  317. // 涓?webview 绫诲瀷鐨?webContents 閰嶇疆
  318. if (contents.getType() === 'webview') {
  319. // 璁剧疆 User-Agent锛堟ā鎷?Chrome 娴忚鍣級
  320. contents.setUserAgent(
  321. 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
  322. );
  323. // 鎷︽埅鑷畾涔夊崗璁摼鎺ワ紙濡?bitbrowser://锛夌殑瀵艰埅
  324. contents.on('will-navigate', (event: Event, url: string) => {
  325. if (!isAllowedUrl(url)) {
  326. console.log('[WebView] 闃绘瀵艰埅鍒拌嚜瀹氫箟鍗忚:', url);
  327. event.preventDefault();
  328. }
  329. });
  330. // 鎷︽埅鏂扮獥鍙f墦寮€锛堝寘鎷嚜瀹氫箟鍗忚锛?
  331. contents.setWindowOpenHandler(({ url }: { url: string }) => {
  332. if (!isAllowedUrl(url)) {
  333. console.log('[WebView] 闃绘鎵撳紑鑷畾涔夊崗璁獥鍙?', url);
  334. return { action: 'deny' };
  335. }
  336. // 瀵逛簬姝e父鐨?http/https 閾炬帴锛屽湪褰撳墠 webview 涓墦寮€
  337. console.log('[WebView] 鎷︽埅鏂扮獥鍙o紝鍦ㄥ綋鍓嶉〉闈㈡墦寮€:', url);
  338. contents.loadURL(url);
  339. return { action: 'deny' };
  340. });
  341. // 浠呭厑璁镐笟鍔℃墍闇€鐨勬潈闄愯姹?
  342. const allowedPermissions = ['clipboard-read', 'clipboard-write', 'notifications'];
  343. contents.session.setPermissionRequestHandler((_webContents: unknown, permission: string, callback: (granted: boolean) => void) => {
  344. callback(allowedPermissions.includes(permission));
  345. });
  346. // 閰嶇疆 webRequest 淇敼璇锋眰澶达紝绉婚櫎鍙兘鏆撮湶 Electron 鐨勭壒寰?
  347. contents.session.webRequest.onBeforeSendHeaders((details: { requestHeaders: Record<string, string> }, callback: (response: { requestHeaders: Record<string, string> }) => void) => {
  348. // 绉婚櫎鍙兘鏆撮湶 Electron 鐨勮姹傚ご
  349. delete details.requestHeaders['X-DevTools-Emulate-Network-Conditions-Client-Id'];
  350. // 纭繚鏈夋甯哥殑 Origin 鍜?Referer
  351. if (!details.requestHeaders['Origin'] && !details.requestHeaders['origin']) {
  352. // 涓嶆坊鍔?Origin锛岃娴忚鍣ㄨ嚜鍔ㄥ鐞?
  353. }
  354. callback({ requestHeaders: details.requestHeaders });
  355. });
  356. }
  357. });
  358. }
  359. // 妫€鏌?URL 鏄惁鏄厑璁哥殑鍗忚
  360. function isAllowedUrl(url: string): boolean {
  361. if (!url) return false;
  362. const lowerUrl = url.toLowerCase();
  363. return lowerUrl.startsWith('http://') ||
  364. lowerUrl.startsWith('https://') ||
  365. lowerUrl.startsWith('about:') ||
  366. lowerUrl.startsWith('data:');
  367. }
  368. // 闃绘榛樿鐨?window-all-closed 琛屼负锛屼繚鎸佹墭鐩樿繍琛?
  369. app.on('window-all-closed', () => {
  370. // 涓嶉€€鍑哄簲鐢紝淇濇寔鎵樼洏杩愯
  371. // 鍙湁鍦?isQuitting 涓?true 鏃舵墠鐪熸閫€鍑?
  372. });
  373. // 搴旂敤閫€鍑哄墠娓呯悊鎵樼洏
  374. app.on('before-quit', () => {
  375. isQuitting = true;
  376. });
  377. app.on('quit', () => {
  378. stopLocalServices();
  379. if (tray) {
  380. tray.destroy();
  381. tray = null;
  382. }
  383. });
  384. ipcMain.handle('test-server-connection', async (_event: unknown, args: { url: string }) => {
  385. try {
  386. const baseUrl = normalizeBaseUrl(args?.url);
  387. if (!baseUrl) return { ok: false, error: 'Server URL is required' };
  388. const result = await requestJson(`${baseUrl}/api/health`, 5000);
  389. if (!result.ok) return { ok: false, error: result.error || 'Connection failed' };
  390. if (result.data?.status === 'ok') return { ok: true };
  391. return { ok: false, error: 'Unexpected service response' };
  392. } catch (e: any) {
  393. return { ok: false, error: e?.message || 'Connection failed' };
  394. }
  395. });
  396. ipcMain.handle('get-local-services-status', () => {
  397. return getServiceStatus();
  398. });
  399. ipcMain.handle('get-local-urls', () => {
  400. return { nodeUrl: LOCAL_NODE_URL };
  401. });
  402. ipcMain.handle('get-service-log', () => {
  403. try {
  404. const logPath = getLogPath();
  405. if (fs.existsSync(logPath)) {
  406. return { path: logPath, content: fs.readFileSync(logPath, 'utf-8') };
  407. }
  408. return { path: logPath, content: '(log file not found)' };
  409. } catch (e: any) {
  410. return { path: '', content: `Read failed: ${e.message}` };
  411. }
  412. });
  413. ipcMain.handle('open-log-file', () => {
  414. try {
  415. const logPath = getLogPath();
  416. if (fs.existsSync(logPath)) {
  417. shell.showItemInFolder(logPath);
  418. }
  419. } catch { /* ignore */ }
  420. });
  421. // IPC 澶勭悊
  422. ipcMain.handle('get-app-version', () => {
  423. return app.getVersion();
  424. });
  425. ipcMain.handle('get-platform', () => {
  426. return process.platform;
  427. });
  428. // 绐楀彛鎺у埗
  429. ipcMain.on('window-minimize', () => {
  430. mainWindow?.minimize();
  431. });
  432. ipcMain.on('window-maximize', () => {
  433. if (mainWindow?.isMaximized()) {
  434. mainWindow.unmaximize();
  435. } else {
  436. mainWindow?.maximize();
  437. }
  438. });
  439. // 鍏抽棴绐楀彛锛堟渶灏忓寲鍒版墭鐩橈級
  440. ipcMain.on('window-close', () => {
  441. mainWindow?.hide();
  442. });
  443. // 鐪熸閫€鍑哄簲鐢?
  444. ipcMain.on('app-quit', () => {
  445. isQuitting = true;
  446. app.quit();
  447. });
  448. // 鑾峰彇绐楀彛鏈€澶у寲鐘舵€?
  449. ipcMain.handle('window-is-maximized', () => {
  450. return mainWindow?.isMaximized() || false;
  451. });
  452. // 鐩戝惉绐楀彛鏈€澶у寲/杩樺師浜嬩欢锛岄€氱煡娓叉煋杩涚▼
  453. function setupWindowEvents() {
  454. mainWindow?.on('maximize', () => {
  455. mainWindow?.webContents.send('window-maximized', true);
  456. });
  457. mainWindow?.on('unmaximize', () => {
  458. mainWindow?.webContents.send('window-maximized', false);
  459. });
  460. }
  461. // 寮圭獥鎵撳紑骞冲彴鍚庡彴锛堢嫭绔嬬獥鍙o紝涓嶅祵鍏ワ紱鐢ㄤ簬瀹為獙锛屽彲鍥炲綊涓哄祵鍏ワ級
  462. ipcMain.handle('open-backend-external', async (_event: unknown, payload: { url: string; cookieData?: string; title?: string }) => {
  463. const { url, cookieData, title } = payload || {};
  464. if (!url || typeof url !== 'string') return;
  465. const partition = 'persist:backend-popup-' + Date.now();
  466. const ses = session.fromPartition(partition);
  467. if (cookieData && typeof cookieData === 'string' && cookieData.trim()) {
  468. const raw = cookieData.trim();
  469. let cookiesToSet: Array<{ name: string; value: string; domain?: string; path?: string }> = [];
  470. try {
  471. if (raw.startsWith('[') || raw.startsWith('{')) {
  472. const parsed = JSON.parse(raw);
  473. const arr = Array.isArray(parsed) ? parsed : (parsed?.cookies || []);
  474. cookiesToSet = arr.map((c: { name?: string; value?: string; domain?: string; path?: string }) => ({
  475. name: String(c?.name ?? '').trim(),
  476. value: String(c?.value ?? '').trim(),
  477. domain: c?.domain ? String(c.domain) : undefined,
  478. path: c?.path ? String(c.path) : '/',
  479. })).filter((c: { name: string }) => c.name);
  480. } else {
  481. raw.split(';').forEach((p: string) => {
  482. const idx = p.indexOf('=');
  483. if (idx > 0) {
  484. const name = p.slice(0, idx).trim();
  485. const value = p.slice(idx + 1).trim();
  486. if (name) cookiesToSet.push({ name, value, path: '/' });
  487. }
  488. });
  489. }
  490. } catch (e) {
  491. console.warn('[open-backend-external] 瑙f瀽 cookie 澶辫触', e);
  492. }
  493. const origin = new URL(url).origin;
  494. const hostname = new URL(url).hostname;
  495. const defaultDomain = hostname.startsWith('www.') ? hostname.slice(4) : hostname;
  496. const domainWithDot = defaultDomain.includes('.') ? '.' + defaultDomain.split('.').slice(-2).join('.') : undefined;
  497. for (const c of cookiesToSet) {
  498. try {
  499. await ses.cookies.set({
  500. url: origin + '/',
  501. name: c.name,
  502. value: c.value,
  503. domain: c.domain || domainWithDot || hostname,
  504. path: c.path || '/',
  505. });
  506. } catch (err) {
  507. console.warn('[open-backend-external] 璁剧疆 cookie 澶辫触', c.name, err);
  508. }
  509. }
  510. }
  511. const win = new BrowserWindow({
  512. width: 1280,
  513. height: 800,
  514. title: title || '骞冲彴鍚庡彴',
  515. icon: getIconPath(),
  516. webPreferences: {
  517. session: ses,
  518. nodeIntegration: false,
  519. contextIsolation: true,
  520. },
  521. show: false,
  522. });
  523. win.once('ready-to-show', () => {
  524. win.show();
  525. });
  526. await win.loadURL(url);
  527. return { ok: true };
  528. });
  529. // 鑾峰彇 webview 鐨?cookies
  530. ipcMain.handle('get-webview-cookies', async (_event: unknown, partition: string, url: string) => {
  531. try {
  532. const ses = session.fromPartition(partition);
  533. const cookies = await ses.cookies.get({ url });
  534. return cookies;
  535. } catch (error) {
  536. console.error('鑾峰彇 cookies 澶辫触:', error);
  537. return [];
  538. }
  539. });
  540. // 鑾峰彇 webview 鐨勫叏閮?cookies锛堟寜 partition锛?
  541. ipcMain.handle('get-webview-all-cookies', async (_event: unknown, partition: string) => {
  542. try {
  543. const ses = session.fromPartition(partition);
  544. return await ses.cookies.get({});
  545. } catch (error) {
  546. console.error('鑾峰彇鍏ㄩ儴 cookies 澶辫触:', error);
  547. return [];
  548. }
  549. });
  550. // 娓呴櫎 webview 鐨?cookies
  551. ipcMain.handle('clear-webview-cookies', async (_event: unknown, partition: string) => {
  552. try {
  553. const ses = session.fromPartition(partition);
  554. await ses.clearStorageData({ storages: ['cookies'] });
  555. return true;
  556. } catch (error) {
  557. console.error('娓呴櫎 cookies 澶辫触:', error);
  558. return false;
  559. }
  560. });
  561. // 璁剧疆 webview 鐨?cookies
  562. ipcMain.handle('set-webview-cookies', async (_event: unknown, partition: string, cookies: any[]) => {
  563. try {
  564. if (!Array.isArray(cookies) || cookies.length === 0) {
  565. console.warn(`[Main] set-webview-cookies: cookies 涓虹┖, partition=${partition}`);
  566. return false;
  567. }
  568. console.log(`[Main] 璁剧疆 webview cookies, partition=${partition}, count=${cookies.length}`);
  569. const ses = session.fromPartition(partition);
  570. // 閫愪釜璁剧疆 cookie
  571. let successCount = 0;
  572. for (const cookie of cookies) {
  573. try {
  574. // 纭繚 Cookie 鏍煎紡姝g‘
  575. const cookieToSet: Record<string, any> = {
  576. url: cookie.url,
  577. name: cookie.name,
  578. value: cookie.value,
  579. domain: cookie.domain,
  580. path: cookie.path || '/',
  581. };
  582. // 鍙€夊瓧娈?
  583. if (typeof cookie.expirationDate === 'number' && Number.isFinite(cookie.expirationDate) && cookie.expirationDate > 0) {
  584. cookieToSet.expirationDate = cookie.expirationDate;
  585. }
  586. if (cookie.httpOnly !== undefined) {
  587. cookieToSet.httpOnly = cookie.httpOnly;
  588. }
  589. if (cookie.secure !== undefined) {
  590. cookieToSet.secure = cookie.secure;
  591. }
  592. if (cookie.sameSite) {
  593. cookieToSet.sameSite = cookie.sameSite as 'no_restriction' | 'lax' | 'strict';
  594. }
  595. await ses.cookies.set(cookieToSet);
  596. successCount++;
  597. // 璁板綍鍏抽敭 Cookie
  598. if (cookie.name === 'BDUSS' || cookie.name === 'STOKEN' || cookie.name === 'sessionid') {
  599. console.log(`[Main] 鎴愬姛璁剧疆鍏抽敭 Cookie: ${cookie.name}, domain: ${cookie.domain}`);
  600. }
  601. } catch (error) {
  602. console.error(`[Main] 璁剧疆 cookie 澶辫触 (${cookie.name}):`, error);
  603. }
  604. }
  605. console.log(`[Main] 鎴愬姛璁剧疆 ${successCount}/${cookies.length} 涓?cookies`);
  606. // 楠岃瘉 Cookie 鏄惁鐪熺殑璁剧疆鎴愬姛
  607. try {
  608. const setCookies = await ses.cookies.get({ domain: '.baidu.com' });
  609. console.log(`[Main] 楠岃瘉锛氬綋鍓?session 涓湁 ${setCookies.length} 涓櫨搴?Cookie`);
  610. const keyNames = setCookies.slice(0, 5).map((c: any) => c.name).join(', ');
  611. console.log(`[Main] 鍏抽敭 Cookie 鍚嶇О: ${keyNames}`);
  612. } catch (verifyError) {
  613. console.error('[Main] 楠岃瘉 Cookie 澶辫触:', verifyError);
  614. }
  615. return successCount > 0;
  616. } catch (error) {
  617. console.error('[Main] 璁剧疆 cookies 澶辫触:', error);
  618. return false;
  619. }
  620. });
  621. // 鎴彇 webview 椤甸潰鎴浘锛堢敤浜?AI 鍒嗘瀽锛?
  622. ipcMain.handle('capture-webview-page', async (_event: unknown, webContentsId: number) => {
  623. try {
  624. const wc = webContents.fromId(webContentsId);
  625. if (!wc) {
  626. console.error('鎵句笉鍒?webContents:', webContentsId);
  627. return null;
  628. }
  629. const image = await wc.capturePage();
  630. if (!image || image.isEmpty()) {
  631. console.warn('鎴浘涓虹┖');
  632. return null;
  633. }
  634. // 杞崲涓?JPEG 鏍煎紡鐨?Base64
  635. const buffer = image.toJPEG(80);
  636. return buffer.toString('base64');
  637. } catch (error) {
  638. console.error('鎴浘澶辫触:', error);
  639. return null;
  640. }
  641. });
  642. // 鍚?webview 鍙戦€侀紶鏍囩偣鍑讳簨浠?
  643. ipcMain.handle('webview-send-mouse-click', async (_event: unknown, webContentsId: number, x: number, y: number) => {
  644. try {
  645. const wc = webContents.fromId(webContentsId);
  646. if (!wc) {
  647. console.error('鎵句笉鍒?webContents:', webContentsId);
  648. return false;
  649. }
  650. // 鍙戦€侀紶鏍囩Щ鍔ㄤ簨浠?
  651. wc.sendInputEvent({
  652. type: 'mouseMove',
  653. x: Math.round(x),
  654. y: Math.round(y),
  655. });
  656. // 鐭殏寤惰繜鍚庡彂閫佺偣鍑讳簨浠?
  657. await new Promise(resolve => setTimeout(resolve, 50));
  658. // 鍙戦€侀紶鏍囨寜涓嬩簨浠?
  659. wc.sendInputEvent({
  660. type: 'mouseDown',
  661. x: Math.round(x),
  662. y: Math.round(y),
  663. button: 'left',
  664. clickCount: 1,
  665. });
  666. // 鐭殏寤惰繜鍚庡彂閫侀紶鏍囨姮璧蜂簨浠?
  667. await new Promise(resolve => setTimeout(resolve, 50));
  668. wc.sendInputEvent({
  669. type: 'mouseUp',
  670. x: Math.round(x),
  671. y: Math.round(y),
  672. button: 'left',
  673. clickCount: 1,
  674. });
  675. console.log(`[webview-send-mouse-click] Clicked at (${x}, ${y})`);
  676. return true;
  677. } catch (error) {
  678. console.error('鍙戦€佺偣鍑讳簨浠跺け璐?', error);
  679. return false;
  680. }
  681. });
  682. // 鍚?webview 鍙戦€侀敭鐩樿緭鍏ヤ簨浠?
  683. ipcMain.handle('webview-send-text-input', async (_event: unknown, webContentsId: number, text: string) => {
  684. try {
  685. const wc = webContents.fromId(webContentsId);
  686. if (!wc) {
  687. console.error('鎵句笉鍒?webContents:', webContentsId);
  688. return false;
  689. }
  690. // 閫愬瓧绗﹁緭鍏?
  691. for (const char of text) {
  692. wc.sendInputEvent({
  693. type: 'char',
  694. keyCode: char,
  695. });
  696. await new Promise(resolve => setTimeout(resolve, 30));
  697. }
  698. console.log(`[webview-send-text-input] Typed: ${text}`);
  699. return true;
  700. } catch (error) {
  701. console.error('鍙戦€佽緭鍏ヤ簨浠跺け璐?', error);
  702. return false;
  703. }
  704. });
  705. // 鑾峰彇 webview 椤甸潰鍏冪礌浣嶇疆
  706. ipcMain.handle('webview-get-element-position', async (_event: unknown, webContentsId: number, selector: string) => {
  707. try {
  708. const wc = webContents.fromId(webContentsId);
  709. if (!wc) {
  710. console.error('鎵句笉鍒?webContents:', webContentsId);
  711. return null;
  712. }
  713. // 鐧藉悕鍗曢獙璇?selector锛屼粎鍏佽鍚堟硶 CSS 閫夋嫨鍣ㄥ瓧绗?
  714. if (!/^[a-zA-Z0-9_\-.# :\[\]="'>~+*,\\]+$/.test(selector)) {
  715. console.error('Invalid selector:', selector);
  716. return null;
  717. }
  718. const result = await wc.executeJavaScript(`
  719. (function() {
  720. const el = document.querySelector(${JSON.stringify(selector)});
  721. if (!el) return null;
  722. const rect = el.getBoundingClientRect();
  723. return {
  724. x: rect.left + rect.width / 2,
  725. y: rect.top + rect.height / 2,
  726. width: rect.width,
  727. height: rect.height
  728. };
  729. })()
  730. `);
  731. return result;
  732. } catch (error) {
  733. console.error('鑾峰彇鍏冪礌浣嶇疆澶辫触:', error);
  734. return null;
  735. }
  736. });
  737. // 閫氳繃鏂囨湰鍐呭鏌ユ壘骞剁偣鍑诲厓绱?
  738. ipcMain.handle('webview-click-by-text', async (_event: unknown, webContentsId: number, text: string) => {
  739. try {
  740. const wc = webContents.fromId(webContentsId);
  741. if (!wc) {
  742. console.error('鎵句笉鍒?webContents:', webContentsId);
  743. return false;
  744. }
  745. // 鏌ユ壘鍖呭惈鎸囧畾鏂囨湰鐨勫彲鐐瑰嚮鍏冪礌鐨勪綅缃?
  746. const sanitizedText = (text || '').replace(/[<>"'`\\]/g, '');
  747. const position = await wc.executeJavaScript(`
  748. (function() {
  749. const searchText = ${JSON.stringify(sanitizedText)};
  750. // 鏌ユ壘鍙偣鍑诲厓绱?
  751. const clickables = document.querySelectorAll('a, button, [role="button"], [onclick], input[type="submit"], input[type="button"]');
  752. for (const el of clickables) {
  753. if (el.textContent?.includes(searchText) || el.getAttribute('aria-label')?.includes(searchText) || el.getAttribute('title')?.includes(searchText)) {
  754. const rect = el.getBoundingClientRect();
  755. if (rect.width > 0 && rect.height > 0) {
  756. return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
  757. }
  758. }
  759. }
  760. // 鏌ユ壘鎵€鏈夊寘鍚枃鏈殑鍏冪礌
  761. const allElements = document.querySelectorAll('*');
  762. for (const el of allElements) {
  763. const text = el.innerText?.trim();
  764. if (text && text.length < 100 && text.includes(searchText)) {
  765. const rect = el.getBoundingClientRect();
  766. if (rect.width > 0 && rect.height > 0 && rect.width < 500) {
  767. return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
  768. }
  769. }
  770. }
  771. return null;
  772. })()
  773. `);
  774. if (!position) {
  775. console.warn(`[webview-click-by-text] target not found: "${text}"`);
  776. return false;
  777. }
  778. // 鍙戦€佺偣鍑讳簨浠?
  779. wc.sendInputEvent({ type: 'mouseMove', x: Math.round(position.x), y: Math.round(position.y) });
  780. await new Promise(resolve => setTimeout(resolve, 50));
  781. wc.sendInputEvent({ type: 'mouseDown', x: Math.round(position.x), y: Math.round(position.y), button: 'left', clickCount: 1 });
  782. await new Promise(resolve => setTimeout(resolve, 50));
  783. wc.sendInputEvent({ type: 'mouseUp', x: Math.round(position.x), y: Math.round(position.y), button: 'left', clickCount: 1 });
  784. console.log(`[webview-click-by-text] Clicked "${text}" at (${position.x}, ${position.y})`);
  785. return true;
  786. } catch (error) {
  787. console.error('click by text failed:', error);
  788. return false;
  789. }
  790. });
  791. // ========== CDP 缃戠粶鎷︽埅鍔熻兘 ==========
  792. // 瀛樺偍姣忎釜 webContents 鐨勭綉缁滄嫤鎴厤缃?
  793. const networkInterceptors: Map<number, {
  794. patterns: Array<{ match: string, key: string }>;
  795. pendingRequests: Map<string, { url: string, timestamp: number }>;
  796. }> = new Map();
  797. // 娓呯悊宸查攢姣佺殑 webContents
  798. app.on('web-contents-destroyed', (_event: unknown, contents: typeof webContents.prototype) => {
  799. const webContentsId = contents.id;
  800. if (networkInterceptors.has(webContentsId)) {
  801. // 娓呯悊缃戠粶鎷︽埅鍣?
  802. try {
  803. contents.debugger.detach();
  804. } catch (e) {
  805. // 蹇界暐閿欒
  806. }
  807. networkInterceptors.delete(webContentsId);
  808. console.log(`[CDP] 宸叉竻鐞嗗凡閿€姣佺殑 webContents 鎷︽埅鍣? ${webContentsId}`);
  809. }
  810. });
  811. // 鍚敤 CDP 缃戠粶鎷︽埅
  812. ipcMain.handle('enable-network-intercept', async (_event: unknown, webContentsId: number, patterns: Array<{ match: string, key: string }>) => {
  813. try {
  814. const wc = webContents.fromId(webContentsId);
  815. if (!wc) {
  816. console.error('[CDP] 鎵句笉鍒?webContents:', webContentsId);
  817. return false;
  818. }
  819. // 濡傛灉宸茬粡鏈夋嫤鎴櫒锛屽厛娓呯悊
  820. if (networkInterceptors.has(webContentsId)) {
  821. try {
  822. wc.debugger.detach();
  823. } catch (e) {
  824. // 蹇界暐
  825. }
  826. }
  827. // 瀛樺偍閰嶇疆
  828. networkInterceptors.set(webContentsId, {
  829. patterns,
  830. pendingRequests: new Map()
  831. });
  832. // 闄勫姞璋冭瘯鍣?
  833. try {
  834. wc.debugger.attach('1.3');
  835. } catch (err: unknown) {
  836. const error = err as Error;
  837. if (!error.message?.includes('Already attached')) {
  838. throw err;
  839. }
  840. }
  841. // 鍚敤缃戠粶鐩戝惉
  842. await wc.debugger.sendCommand('Network.enable');
  843. // 鐩戝惉缃戠粶鍝嶅簲
  844. wc.debugger.on('message', async (_e: unknown, method: string, params: {
  845. requestId?: string;
  846. response?: { url?: string; status?: number; mimeType?: string };
  847. encodedDataLength?: number;
  848. }) => {
  849. const config = networkInterceptors.get(webContentsId);
  850. if (!config) return;
  851. if (method === 'Network.responseReceived') {
  852. const { requestId, response } = params;
  853. if (!requestId || !response?.url) return;
  854. // 璋冭瘯锛氭墦鍗扮櫨瀹跺彿鐩稿叧鐨勬墍鏈?API 璇锋眰
  855. if (response.url.includes('baijiahao.baidu.com')) {
  856. if (response.url.includes('/pcui/') || response.url.includes('/article')) {
  857. console.log(`[CDP DEBUG] 鐧惧鍙?API: ${response.url}`);
  858. }
  859. }
  860. // 妫€鏌ユ槸鍚﹀尮閰嶆垜浠叧娉ㄧ殑 API
  861. for (const pattern of config.patterns) {
  862. if (response.url.includes(pattern.match)) {
  863. // 璁板綍璇锋眰锛岀瓑寰呭搷搴斿畬鎴?
  864. config.pendingRequests.set(requestId, {
  865. url: response.url,
  866. timestamp: Date.now()
  867. });
  868. console.log(`[CDP] 鍖归厤鍒?API: ${pattern.key} - ${response.url}`);
  869. break;
  870. }
  871. }
  872. }
  873. if (method === 'Network.loadingFinished') {
  874. const { requestId } = params;
  875. if (!requestId) return;
  876. const pending = config.pendingRequests.get(requestId);
  877. if (!pending) return;
  878. config.pendingRequests.delete(requestId);
  879. try {
  880. // 鑾峰彇鍝嶅簲浣?
  881. const result = await wc.debugger.sendCommand('Network.getResponseBody', { requestId }) as { body: string; base64Encoded: boolean };
  882. let body = result.body;
  883. // 濡傛灉鏄?base64 缂栫爜锛岃В鐮?
  884. if (result.base64Encoded) {
  885. body = Buffer.from(body, 'base64').toString('utf8');
  886. }
  887. // 瑙f瀽 JSON
  888. const data = JSON.parse(body);
  889. // 鎵惧埌鍖归厤鐨?key
  890. let matchedKey = '';
  891. for (const pattern of config.patterns) {
  892. if (pending.url.includes(pattern.match)) {
  893. matchedKey = pattern.key;
  894. break;
  895. }
  896. }
  897. if (matchedKey) {
  898. console.log(`[CDP] 鑾峰彇鍒板搷搴? ${matchedKey}`, JSON.stringify(data).substring(0, 200));
  899. // 鍙戦€佸埌娓叉煋杩涚▼
  900. mainWindow?.webContents.send('network-intercept-data', {
  901. webContentsId,
  902. key: matchedKey,
  903. url: pending.url,
  904. data
  905. });
  906. }
  907. } catch (err) {
  908. console.warn(`[CDP] 鑾峰彇鍝嶅簲浣撳け璐?`, err);
  909. }
  910. }
  911. });
  912. console.log(`[CDP] 宸插惎鐢ㄧ綉缁滄嫤鎴紝webContentsId: ${webContentsId}, patterns:`, patterns.map(p => p.key));
  913. return true;
  914. } catch (error) {
  915. console.error('[CDP] 鍚敤缃戠粶鎷︽埅澶辫触:', error);
  916. return false;
  917. }
  918. });
  919. // 绂佺敤 CDP 缃戠粶鎷︽埅
  920. ipcMain.handle('disable-network-intercept', async (_event: unknown, webContentsId: number) => {
  921. try {
  922. const wc = webContents.fromId(webContentsId);
  923. if (wc) {
  924. try {
  925. wc.debugger.detach();
  926. } catch (e) {
  927. // 蹇界暐
  928. }
  929. }
  930. const config = networkInterceptors.get(webContentsId);
  931. if (config) {
  932. // 娓呯悊寰呭鐞嗚姹傜殑 Map
  933. config.pendingRequests.clear();
  934. }
  935. networkInterceptors.delete(webContentsId);
  936. console.log(`[CDP] 宸茬鐢ㄧ綉缁滄嫤鎴紝webContentsId: ${webContentsId}`);
  937. return true;
  938. } catch (error) {
  939. console.error('[CDP] 绂佺敤缃戠粶鎷︽埅澶辫触:', error);
  940. return false;
  941. }
  942. });
  943. // 鏇存柊缃戠粶鎷︽埅鐨?patterns
  944. ipcMain.handle('update-network-patterns', async (_event: unknown, webContentsId: number, patterns: Array<{ match: string, key: string }>) => {
  945. const config = networkInterceptors.get(webContentsId);
  946. if (config) {
  947. config.patterns = patterns;
  948. console.log(`[CDP] 宸叉洿鏂?patterns锛寃ebContentsId: ${webContentsId}`);
  949. return true;
  950. }
  951. return false;
  952. });