main.ts 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079
  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 } 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. // 鍏堝垱寤虹獥鍙f樉绀?splash screen
  289. createWindow();
  290. createTray();
  291. // 鍚庡彴鍚姩鏈湴 Node 鏈嶅姟锛屼笉闃诲绐楀彛鏄剧ず
  292. console.log('[Main] 姝e湪鍚庡彴鍚姩鏈湴鏈嶅姟...');
  293. startLocalServices().then(({ nodeOk }) => {
  294. console.log(`[Main] local services status: Node=${nodeOk ? 'ready' : 'not_ready'}`);
  295. mainWindow?.webContents.send('services-status-changed', { nodeOk });
  296. });
  297. // 閰嶇疆 webview session锛屽厑璁哥涓夋柟 cookies 鍜岃法鍩熻姹?
  298. setupWebviewSessions();
  299. setupCertificateBypass();
  300. setupCorsBypassForApiRequests();
  301. app.on('activate', () => {
  302. if (BrowserWindow.getAllWindows().length === 0) {
  303. createWindow();
  304. } else if (mainWindow) {
  305. mainWindow.show();
  306. }
  307. });
  308. });
  309. }
  310. // 閰嶇疆 webview sessions
  311. function setupWebviewSessions() {
  312. // 鐩戝惉鏂扮殑 webContents 鍒涘缓
  313. app.on('web-contents-created', (_event: unknown, contents: typeof webContents.prototype) => {
  314. // 涓?webview 绫诲瀷鐨?webContents 閰嶇疆
  315. if (contents.getType() === 'webview') {
  316. // 璁剧疆 User-Agent锛堟ā鎷?Chrome 娴忚鍣級
  317. contents.setUserAgent(
  318. 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
  319. );
  320. // 鎷︽埅鑷畾涔夊崗璁摼鎺ワ紙濡?bitbrowser://锛夌殑瀵艰埅
  321. contents.on('will-navigate', (event: Event, url: string) => {
  322. if (!isAllowedUrl(url)) {
  323. console.log('[WebView] 闃绘瀵艰埅鍒拌嚜瀹氫箟鍗忚:', url);
  324. event.preventDefault();
  325. }
  326. });
  327. // 鎷︽埅鏂扮獥鍙f墦寮€锛堝寘鎷嚜瀹氫箟鍗忚锛?
  328. contents.setWindowOpenHandler(({ url }: { url: string }) => {
  329. if (!isAllowedUrl(url)) {
  330. console.log('[WebView] 闃绘鎵撳紑鑷畾涔夊崗璁獥鍙?', url);
  331. return { action: 'deny' };
  332. }
  333. // 瀵逛簬姝e父鐨?http/https 閾炬帴锛屽湪褰撳墠 webview 涓墦寮€
  334. console.log('[WebView] 鎷︽埅鏂扮獥鍙o紝鍦ㄥ綋鍓嶉〉闈㈡墦寮€:', url);
  335. contents.loadURL(url);
  336. return { action: 'deny' };
  337. });
  338. // 浠呭厑璁镐笟鍔℃墍闇€鐨勬潈闄愯姹?
  339. const allowedPermissions = ['clipboard-read', 'clipboard-write', 'notifications'];
  340. contents.session.setPermissionRequestHandler((_webContents: unknown, permission: string, callback: (granted: boolean) => void) => {
  341. callback(allowedPermissions.includes(permission));
  342. });
  343. // 閰嶇疆 webRequest 淇敼璇锋眰澶达紝绉婚櫎鍙兘鏆撮湶 Electron 鐨勭壒寰?
  344. contents.session.webRequest.onBeforeSendHeaders((details: { requestHeaders: Record<string, string> }, callback: (response: { requestHeaders: Record<string, string> }) => void) => {
  345. // 绉婚櫎鍙兘鏆撮湶 Electron 鐨勮姹傚ご
  346. delete details.requestHeaders['X-DevTools-Emulate-Network-Conditions-Client-Id'];
  347. // 纭繚鏈夋甯哥殑 Origin 鍜?Referer
  348. if (!details.requestHeaders['Origin'] && !details.requestHeaders['origin']) {
  349. // 涓嶆坊鍔?Origin锛岃娴忚鍣ㄨ嚜鍔ㄥ鐞?
  350. }
  351. callback({ requestHeaders: details.requestHeaders });
  352. });
  353. }
  354. });
  355. }
  356. // 妫€鏌?URL 鏄惁鏄厑璁哥殑鍗忚
  357. function isAllowedUrl(url: string): boolean {
  358. if (!url) return false;
  359. const lowerUrl = url.toLowerCase();
  360. return lowerUrl.startsWith('http://') ||
  361. lowerUrl.startsWith('https://') ||
  362. lowerUrl.startsWith('about:') ||
  363. lowerUrl.startsWith('data:');
  364. }
  365. // 闃绘榛樿鐨?window-all-closed 琛屼负锛屼繚鎸佹墭鐩樿繍琛?
  366. app.on('window-all-closed', () => {
  367. // 涓嶉€€鍑哄簲鐢紝淇濇寔鎵樼洏杩愯
  368. // 鍙湁鍦?isQuitting 涓?true 鏃舵墠鐪熸閫€鍑?
  369. });
  370. // 搴旂敤閫€鍑哄墠娓呯悊鎵樼洏
  371. app.on('before-quit', () => {
  372. isQuitting = true;
  373. });
  374. app.on('quit', () => {
  375. stopLocalServices();
  376. if (tray) {
  377. tray.destroy();
  378. tray = null;
  379. }
  380. });
  381. ipcMain.handle('test-server-connection', async (_event: unknown, args: { url: string }) => {
  382. try {
  383. const baseUrl = normalizeBaseUrl(args?.url);
  384. if (!baseUrl) return { ok: false, error: 'Server URL is required' };
  385. const result = await requestJson(`${baseUrl}/api/health`, 5000);
  386. if (!result.ok) return { ok: false, error: result.error || 'Connection failed' };
  387. if (result.data?.status === 'ok') return { ok: true };
  388. return { ok: false, error: 'Unexpected service response' };
  389. } catch (e: any) {
  390. return { ok: false, error: e?.message || 'Connection failed' };
  391. }
  392. });
  393. ipcMain.handle('get-local-services-status', () => {
  394. return getServiceStatus();
  395. });
  396. ipcMain.handle('get-local-urls', () => {
  397. return { nodeUrl: LOCAL_NODE_URL };
  398. });
  399. ipcMain.handle('get-service-log', () => {
  400. try {
  401. const logPath = getLogPath();
  402. if (fs.existsSync(logPath)) {
  403. return { path: logPath, content: fs.readFileSync(logPath, 'utf-8') };
  404. }
  405. return { path: logPath, content: '(log file not found)' };
  406. } catch (e: any) {
  407. return { path: '', content: `Read failed: ${e.message}` };
  408. }
  409. });
  410. ipcMain.handle('open-log-file', () => {
  411. try {
  412. const logPath = getLogPath();
  413. if (fs.existsSync(logPath)) {
  414. shell.showItemInFolder(logPath);
  415. }
  416. } catch { /* ignore */ }
  417. });
  418. // IPC 澶勭悊
  419. ipcMain.handle('get-app-version', () => {
  420. return app.getVersion();
  421. });
  422. ipcMain.handle('get-platform', () => {
  423. return process.platform;
  424. });
  425. // 绐楀彛鎺у埗
  426. ipcMain.on('window-minimize', () => {
  427. mainWindow?.minimize();
  428. });
  429. ipcMain.on('window-maximize', () => {
  430. if (mainWindow?.isMaximized()) {
  431. mainWindow.unmaximize();
  432. } else {
  433. mainWindow?.maximize();
  434. }
  435. });
  436. // 鍏抽棴绐楀彛锛堟渶灏忓寲鍒版墭鐩橈級
  437. ipcMain.on('window-close', () => {
  438. mainWindow?.hide();
  439. });
  440. // 鐪熸閫€鍑哄簲鐢?
  441. ipcMain.on('app-quit', () => {
  442. isQuitting = true;
  443. app.quit();
  444. });
  445. // 鑾峰彇绐楀彛鏈€澶у寲鐘舵€?
  446. ipcMain.handle('window-is-maximized', () => {
  447. return mainWindow?.isMaximized() || false;
  448. });
  449. // 鐩戝惉绐楀彛鏈€澶у寲/杩樺師浜嬩欢锛岄€氱煡娓叉煋杩涚▼
  450. function setupWindowEvents() {
  451. mainWindow?.on('maximize', () => {
  452. mainWindow?.webContents.send('window-maximized', true);
  453. });
  454. mainWindow?.on('unmaximize', () => {
  455. mainWindow?.webContents.send('window-maximized', false);
  456. });
  457. }
  458. // 寮圭獥鎵撳紑骞冲彴鍚庡彴锛堢嫭绔嬬獥鍙o紝涓嶅祵鍏ワ紱鐢ㄤ簬瀹為獙锛屽彲鍥炲綊涓哄祵鍏ワ級
  459. ipcMain.handle('open-backend-external', async (_event: unknown, payload: { url: string; cookieData?: string; title?: string }) => {
  460. const { url, cookieData, title } = payload || {};
  461. if (!url || typeof url !== 'string') return;
  462. const partition = 'persist:backend-popup-' + Date.now();
  463. const ses = session.fromPartition(partition);
  464. if (cookieData && typeof cookieData === 'string' && cookieData.trim()) {
  465. const raw = cookieData.trim();
  466. let cookiesToSet: Array<{ name: string; value: string; domain?: string; path?: string }> = [];
  467. try {
  468. if (raw.startsWith('[') || raw.startsWith('{')) {
  469. const parsed = JSON.parse(raw);
  470. const arr = Array.isArray(parsed) ? parsed : (parsed?.cookies || []);
  471. cookiesToSet = arr.map((c: { name?: string; value?: string; domain?: string; path?: string }) => ({
  472. name: String(c?.name ?? '').trim(),
  473. value: String(c?.value ?? '').trim(),
  474. domain: c?.domain ? String(c.domain) : undefined,
  475. path: c?.path ? String(c.path) : '/',
  476. })).filter((c: { name: string }) => c.name);
  477. } else {
  478. raw.split(';').forEach((p: string) => {
  479. const idx = p.indexOf('=');
  480. if (idx > 0) {
  481. const name = p.slice(0, idx).trim();
  482. const value = p.slice(idx + 1).trim();
  483. if (name) cookiesToSet.push({ name, value, path: '/' });
  484. }
  485. });
  486. }
  487. } catch (e) {
  488. console.warn('[open-backend-external] 瑙f瀽 cookie 澶辫触', e);
  489. }
  490. const origin = new URL(url).origin;
  491. const hostname = new URL(url).hostname;
  492. const defaultDomain = hostname.startsWith('www.') ? hostname.slice(4) : hostname;
  493. const domainWithDot = defaultDomain.includes('.') ? '.' + defaultDomain.split('.').slice(-2).join('.') : undefined;
  494. for (const c of cookiesToSet) {
  495. try {
  496. await ses.cookies.set({
  497. url: origin + '/',
  498. name: c.name,
  499. value: c.value,
  500. domain: c.domain || domainWithDot || hostname,
  501. path: c.path || '/',
  502. });
  503. } catch (err) {
  504. console.warn('[open-backend-external] 璁剧疆 cookie 澶辫触', c.name, err);
  505. }
  506. }
  507. }
  508. const win = new BrowserWindow({
  509. width: 1280,
  510. height: 800,
  511. title: title || '骞冲彴鍚庡彴',
  512. icon: getIconPath(),
  513. webPreferences: {
  514. session: ses,
  515. nodeIntegration: false,
  516. contextIsolation: true,
  517. },
  518. show: false,
  519. });
  520. win.once('ready-to-show', () => {
  521. win.show();
  522. });
  523. await win.loadURL(url);
  524. return { ok: true };
  525. });
  526. // 鑾峰彇 webview 鐨?cookies
  527. ipcMain.handle('get-webview-cookies', async (_event: unknown, partition: string, url: string) => {
  528. try {
  529. const ses = session.fromPartition(partition);
  530. const cookies = await ses.cookies.get({ url });
  531. return cookies;
  532. } catch (error) {
  533. console.error('鑾峰彇 cookies 澶辫触:', error);
  534. return [];
  535. }
  536. });
  537. // 鑾峰彇 webview 鐨勫叏閮?cookies锛堟寜 partition锛?
  538. ipcMain.handle('get-webview-all-cookies', async (_event: unknown, partition: string) => {
  539. try {
  540. const ses = session.fromPartition(partition);
  541. return await ses.cookies.get({});
  542. } catch (error) {
  543. console.error('鑾峰彇鍏ㄩ儴 cookies 澶辫触:', error);
  544. return [];
  545. }
  546. });
  547. // 娓呴櫎 webview 鐨?cookies
  548. ipcMain.handle('clear-webview-cookies', async (_event: unknown, partition: string) => {
  549. try {
  550. const ses = session.fromPartition(partition);
  551. await ses.clearStorageData({ storages: ['cookies'] });
  552. return true;
  553. } catch (error) {
  554. console.error('娓呴櫎 cookies 澶辫触:', error);
  555. return false;
  556. }
  557. });
  558. // 璁剧疆 webview 鐨?cookies
  559. ipcMain.handle('set-webview-cookies', async (_event: unknown, partition: string, cookies: any[]) => {
  560. try {
  561. if (!Array.isArray(cookies) || cookies.length === 0) {
  562. console.warn(`[Main] set-webview-cookies: cookies 涓虹┖, partition=${partition}`);
  563. return false;
  564. }
  565. console.log(`[Main] 璁剧疆 webview cookies, partition=${partition}, count=${cookies.length}`);
  566. const ses = session.fromPartition(partition);
  567. // 閫愪釜璁剧疆 cookie
  568. let successCount = 0;
  569. for (const cookie of cookies) {
  570. try {
  571. // 纭繚 Cookie 鏍煎紡姝g‘
  572. const cookieToSet: Record<string, any> = {
  573. url: cookie.url,
  574. name: cookie.name,
  575. value: cookie.value,
  576. domain: cookie.domain,
  577. path: cookie.path || '/',
  578. };
  579. // 鍙€夊瓧娈?
  580. if (typeof cookie.expirationDate === 'number' && Number.isFinite(cookie.expirationDate) && cookie.expirationDate > 0) {
  581. cookieToSet.expirationDate = cookie.expirationDate;
  582. }
  583. if (cookie.httpOnly !== undefined) {
  584. cookieToSet.httpOnly = cookie.httpOnly;
  585. }
  586. if (cookie.secure !== undefined) {
  587. cookieToSet.secure = cookie.secure;
  588. }
  589. if (cookie.sameSite) {
  590. cookieToSet.sameSite = cookie.sameSite as 'no_restriction' | 'lax' | 'strict';
  591. }
  592. await ses.cookies.set(cookieToSet);
  593. successCount++;
  594. // 璁板綍鍏抽敭 Cookie
  595. if (cookie.name === 'BDUSS' || cookie.name === 'STOKEN' || cookie.name === 'sessionid') {
  596. console.log(`[Main] 鎴愬姛璁剧疆鍏抽敭 Cookie: ${cookie.name}, domain: ${cookie.domain}`);
  597. }
  598. } catch (error) {
  599. console.error(`[Main] 璁剧疆 cookie 澶辫触 (${cookie.name}):`, error);
  600. }
  601. }
  602. console.log(`[Main] 鎴愬姛璁剧疆 ${successCount}/${cookies.length} 涓?cookies`);
  603. // 楠岃瘉 Cookie 鏄惁鐪熺殑璁剧疆鎴愬姛
  604. try {
  605. const setCookies = await ses.cookies.get({ domain: '.baidu.com' });
  606. console.log(`[Main] 楠岃瘉锛氬綋鍓?session 涓湁 ${setCookies.length} 涓櫨搴?Cookie`);
  607. const keyNames = setCookies.slice(0, 5).map((c: any) => c.name).join(', ');
  608. console.log(`[Main] 鍏抽敭 Cookie 鍚嶇О: ${keyNames}`);
  609. } catch (verifyError) {
  610. console.error('[Main] 楠岃瘉 Cookie 澶辫触:', verifyError);
  611. }
  612. return successCount > 0;
  613. } catch (error) {
  614. console.error('[Main] 璁剧疆 cookies 澶辫触:', error);
  615. return false;
  616. }
  617. });
  618. // 鎴彇 webview 椤甸潰鎴浘锛堢敤浜?AI 鍒嗘瀽锛?
  619. ipcMain.handle('capture-webview-page', async (_event: unknown, webContentsId: number) => {
  620. try {
  621. const wc = webContents.fromId(webContentsId);
  622. if (!wc) {
  623. console.error('鎵句笉鍒?webContents:', webContentsId);
  624. return null;
  625. }
  626. const image = await wc.capturePage();
  627. if (!image || image.isEmpty()) {
  628. console.warn('鎴浘涓虹┖');
  629. return null;
  630. }
  631. // 杞崲涓?JPEG 鏍煎紡鐨?Base64
  632. const buffer = image.toJPEG(80);
  633. return buffer.toString('base64');
  634. } catch (error) {
  635. console.error('鎴浘澶辫触:', error);
  636. return null;
  637. }
  638. });
  639. // 鍚?webview 鍙戦€侀紶鏍囩偣鍑讳簨浠?
  640. ipcMain.handle('webview-send-mouse-click', async (_event: unknown, webContentsId: number, x: number, y: number) => {
  641. try {
  642. const wc = webContents.fromId(webContentsId);
  643. if (!wc) {
  644. console.error('鎵句笉鍒?webContents:', webContentsId);
  645. return false;
  646. }
  647. // 鍙戦€侀紶鏍囩Щ鍔ㄤ簨浠?
  648. wc.sendInputEvent({
  649. type: 'mouseMove',
  650. x: Math.round(x),
  651. y: Math.round(y),
  652. });
  653. // 鐭殏寤惰繜鍚庡彂閫佺偣鍑讳簨浠?
  654. await new Promise(resolve => setTimeout(resolve, 50));
  655. // 鍙戦€侀紶鏍囨寜涓嬩簨浠?
  656. wc.sendInputEvent({
  657. type: 'mouseDown',
  658. x: Math.round(x),
  659. y: Math.round(y),
  660. button: 'left',
  661. clickCount: 1,
  662. });
  663. // 鐭殏寤惰繜鍚庡彂閫侀紶鏍囨姮璧蜂簨浠?
  664. await new Promise(resolve => setTimeout(resolve, 50));
  665. wc.sendInputEvent({
  666. type: 'mouseUp',
  667. x: Math.round(x),
  668. y: Math.round(y),
  669. button: 'left',
  670. clickCount: 1,
  671. });
  672. console.log(`[webview-send-mouse-click] Clicked at (${x}, ${y})`);
  673. return true;
  674. } catch (error) {
  675. console.error('鍙戦€佺偣鍑讳簨浠跺け璐?', error);
  676. return false;
  677. }
  678. });
  679. // 鍚?webview 鍙戦€侀敭鐩樿緭鍏ヤ簨浠?
  680. ipcMain.handle('webview-send-text-input', async (_event: unknown, webContentsId: number, text: string) => {
  681. try {
  682. const wc = webContents.fromId(webContentsId);
  683. if (!wc) {
  684. console.error('鎵句笉鍒?webContents:', webContentsId);
  685. return false;
  686. }
  687. // 閫愬瓧绗﹁緭鍏?
  688. for (const char of text) {
  689. wc.sendInputEvent({
  690. type: 'char',
  691. keyCode: char,
  692. });
  693. await new Promise(resolve => setTimeout(resolve, 30));
  694. }
  695. console.log(`[webview-send-text-input] Typed: ${text}`);
  696. return true;
  697. } catch (error) {
  698. console.error('鍙戦€佽緭鍏ヤ簨浠跺け璐?', error);
  699. return false;
  700. }
  701. });
  702. // 鑾峰彇 webview 椤甸潰鍏冪礌浣嶇疆
  703. ipcMain.handle('webview-get-element-position', async (_event: unknown, webContentsId: number, selector: string) => {
  704. try {
  705. const wc = webContents.fromId(webContentsId);
  706. if (!wc) {
  707. console.error('鎵句笉鍒?webContents:', webContentsId);
  708. return null;
  709. }
  710. // 鐧藉悕鍗曢獙璇?selector锛屼粎鍏佽鍚堟硶 CSS 閫夋嫨鍣ㄥ瓧绗?
  711. if (!/^[a-zA-Z0-9_\-.# :\[\]="'>~+*,\\]+$/.test(selector)) {
  712. console.error('Invalid selector:', selector);
  713. return null;
  714. }
  715. const result = await wc.executeJavaScript(`
  716. (function() {
  717. const el = document.querySelector(${JSON.stringify(selector)});
  718. if (!el) return null;
  719. const rect = el.getBoundingClientRect();
  720. return {
  721. x: rect.left + rect.width / 2,
  722. y: rect.top + rect.height / 2,
  723. width: rect.width,
  724. height: rect.height
  725. };
  726. })()
  727. `);
  728. return result;
  729. } catch (error) {
  730. console.error('鑾峰彇鍏冪礌浣嶇疆澶辫触:', error);
  731. return null;
  732. }
  733. });
  734. // 閫氳繃鏂囨湰鍐呭鏌ユ壘骞剁偣鍑诲厓绱?
  735. ipcMain.handle('webview-click-by-text', async (_event: unknown, webContentsId: number, text: string) => {
  736. try {
  737. const wc = webContents.fromId(webContentsId);
  738. if (!wc) {
  739. console.error('鎵句笉鍒?webContents:', webContentsId);
  740. return false;
  741. }
  742. // 鏌ユ壘鍖呭惈鎸囧畾鏂囨湰鐨勫彲鐐瑰嚮鍏冪礌鐨勪綅缃?
  743. const sanitizedText = (text || '').replace(/[<>"'`\\]/g, '');
  744. const position = await wc.executeJavaScript(`
  745. (function() {
  746. const searchText = ${JSON.stringify(sanitizedText)};
  747. // 鏌ユ壘鍙偣鍑诲厓绱?
  748. const clickables = document.querySelectorAll('a, button, [role="button"], [onclick], input[type="submit"], input[type="button"]');
  749. for (const el of clickables) {
  750. if (el.textContent?.includes(searchText) || el.getAttribute('aria-label')?.includes(searchText) || el.getAttribute('title')?.includes(searchText)) {
  751. const rect = el.getBoundingClientRect();
  752. if (rect.width > 0 && rect.height > 0) {
  753. return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
  754. }
  755. }
  756. }
  757. // 鏌ユ壘鎵€鏈夊寘鍚枃鏈殑鍏冪礌
  758. const allElements = document.querySelectorAll('*');
  759. for (const el of allElements) {
  760. const text = el.innerText?.trim();
  761. if (text && text.length < 100 && text.includes(searchText)) {
  762. const rect = el.getBoundingClientRect();
  763. if (rect.width > 0 && rect.height > 0 && rect.width < 500) {
  764. return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
  765. }
  766. }
  767. }
  768. return null;
  769. })()
  770. `);
  771. if (!position) {
  772. console.warn(`[webview-click-by-text] target not found: "${text}"`);
  773. return false;
  774. }
  775. // 鍙戦€佺偣鍑讳簨浠?
  776. wc.sendInputEvent({ type: 'mouseMove', x: Math.round(position.x), y: Math.round(position.y) });
  777. await new Promise(resolve => setTimeout(resolve, 50));
  778. wc.sendInputEvent({ type: 'mouseDown', x: Math.round(position.x), y: Math.round(position.y), button: 'left', clickCount: 1 });
  779. await new Promise(resolve => setTimeout(resolve, 50));
  780. wc.sendInputEvent({ type: 'mouseUp', x: Math.round(position.x), y: Math.round(position.y), button: 'left', clickCount: 1 });
  781. console.log(`[webview-click-by-text] Clicked "${text}" at (${position.x}, ${position.y})`);
  782. return true;
  783. } catch (error) {
  784. console.error('click by text failed:', error);
  785. return false;
  786. }
  787. });
  788. // ========== CDP 缃戠粶鎷︽埅鍔熻兘 ==========
  789. // 瀛樺偍姣忎釜 webContents 鐨勭綉缁滄嫤鎴厤缃?
  790. const networkInterceptors: Map<number, {
  791. patterns: Array<{ match: string, key: string }>;
  792. pendingRequests: Map<string, { url: string, timestamp: number }>;
  793. }> = new Map();
  794. // 娓呯悊宸查攢姣佺殑 webContents
  795. app.on('web-contents-destroyed', (_event: unknown, contents: typeof webContents.prototype) => {
  796. const webContentsId = contents.id;
  797. if (networkInterceptors.has(webContentsId)) {
  798. // 娓呯悊缃戠粶鎷︽埅鍣?
  799. try {
  800. contents.debugger.detach();
  801. } catch (e) {
  802. // 蹇界暐閿欒
  803. }
  804. networkInterceptors.delete(webContentsId);
  805. console.log(`[CDP] 宸叉竻鐞嗗凡閿€姣佺殑 webContents 鎷︽埅鍣? ${webContentsId}`);
  806. }
  807. });
  808. // 鍚敤 CDP 缃戠粶鎷︽埅
  809. ipcMain.handle('enable-network-intercept', async (_event: unknown, webContentsId: number, patterns: Array<{ match: string, key: string }>) => {
  810. try {
  811. const wc = webContents.fromId(webContentsId);
  812. if (!wc) {
  813. console.error('[CDP] 鎵句笉鍒?webContents:', webContentsId);
  814. return false;
  815. }
  816. // 濡傛灉宸茬粡鏈夋嫤鎴櫒锛屽厛娓呯悊
  817. if (networkInterceptors.has(webContentsId)) {
  818. try {
  819. wc.debugger.detach();
  820. } catch (e) {
  821. // 蹇界暐
  822. }
  823. }
  824. // 瀛樺偍閰嶇疆
  825. networkInterceptors.set(webContentsId, {
  826. patterns,
  827. pendingRequests: new Map()
  828. });
  829. // 闄勫姞璋冭瘯鍣?
  830. try {
  831. wc.debugger.attach('1.3');
  832. } catch (err: unknown) {
  833. const error = err as Error;
  834. if (!error.message?.includes('Already attached')) {
  835. throw err;
  836. }
  837. }
  838. // 鍚敤缃戠粶鐩戝惉
  839. await wc.debugger.sendCommand('Network.enable');
  840. // 鐩戝惉缃戠粶鍝嶅簲
  841. wc.debugger.on('message', async (_e: unknown, method: string, params: {
  842. requestId?: string;
  843. response?: { url?: string; status?: number; mimeType?: string };
  844. encodedDataLength?: number;
  845. }) => {
  846. const config = networkInterceptors.get(webContentsId);
  847. if (!config) return;
  848. if (method === 'Network.responseReceived') {
  849. const { requestId, response } = params;
  850. if (!requestId || !response?.url) return;
  851. // 璋冭瘯锛氭墦鍗扮櫨瀹跺彿鐩稿叧鐨勬墍鏈?API 璇锋眰
  852. if (response.url.includes('baijiahao.baidu.com')) {
  853. if (response.url.includes('/pcui/') || response.url.includes('/article')) {
  854. console.log(`[CDP DEBUG] 鐧惧鍙?API: ${response.url}`);
  855. }
  856. }
  857. // 妫€鏌ユ槸鍚﹀尮閰嶆垜浠叧娉ㄧ殑 API
  858. for (const pattern of config.patterns) {
  859. if (response.url.includes(pattern.match)) {
  860. // 璁板綍璇锋眰锛岀瓑寰呭搷搴斿畬鎴?
  861. config.pendingRequests.set(requestId, {
  862. url: response.url,
  863. timestamp: Date.now()
  864. });
  865. console.log(`[CDP] 鍖归厤鍒?API: ${pattern.key} - ${response.url}`);
  866. break;
  867. }
  868. }
  869. }
  870. if (method === 'Network.loadingFinished') {
  871. const { requestId } = params;
  872. if (!requestId) return;
  873. const pending = config.pendingRequests.get(requestId);
  874. if (!pending) return;
  875. config.pendingRequests.delete(requestId);
  876. try {
  877. // 鑾峰彇鍝嶅簲浣?
  878. const result = await wc.debugger.sendCommand('Network.getResponseBody', { requestId }) as { body: string; base64Encoded: boolean };
  879. let body = result.body;
  880. // 濡傛灉鏄?base64 缂栫爜锛岃В鐮?
  881. if (result.base64Encoded) {
  882. body = Buffer.from(body, 'base64').toString('utf8');
  883. }
  884. // 瑙f瀽 JSON
  885. const data = JSON.parse(body);
  886. // 鎵惧埌鍖归厤鐨?key
  887. let matchedKey = '';
  888. for (const pattern of config.patterns) {
  889. if (pending.url.includes(pattern.match)) {
  890. matchedKey = pattern.key;
  891. break;
  892. }
  893. }
  894. if (matchedKey) {
  895. console.log(`[CDP] 鑾峰彇鍒板搷搴? ${matchedKey}`, JSON.stringify(data).substring(0, 200));
  896. // 鍙戦€佸埌娓叉煋杩涚▼
  897. mainWindow?.webContents.send('network-intercept-data', {
  898. webContentsId,
  899. key: matchedKey,
  900. url: pending.url,
  901. data
  902. });
  903. }
  904. } catch (err) {
  905. console.warn(`[CDP] 鑾峰彇鍝嶅簲浣撳け璐?`, err);
  906. }
  907. }
  908. });
  909. console.log(`[CDP] 宸插惎鐢ㄧ綉缁滄嫤鎴紝webContentsId: ${webContentsId}, patterns:`, patterns.map(p => p.key));
  910. return true;
  911. } catch (error) {
  912. console.error('[CDP] 鍚敤缃戠粶鎷︽埅澶辫触:', error);
  913. return false;
  914. }
  915. });
  916. // 绂佺敤 CDP 缃戠粶鎷︽埅
  917. ipcMain.handle('disable-network-intercept', async (_event: unknown, webContentsId: number) => {
  918. try {
  919. const wc = webContents.fromId(webContentsId);
  920. if (wc) {
  921. try {
  922. wc.debugger.detach();
  923. } catch (e) {
  924. // 蹇界暐
  925. }
  926. }
  927. const config = networkInterceptors.get(webContentsId);
  928. if (config) {
  929. // 娓呯悊寰呭鐞嗚姹傜殑 Map
  930. config.pendingRequests.clear();
  931. }
  932. networkInterceptors.delete(webContentsId);
  933. console.log(`[CDP] 宸茬鐢ㄧ綉缁滄嫤鎴紝webContentsId: ${webContentsId}`);
  934. return true;
  935. } catch (error) {
  936. console.error('[CDP] 绂佺敤缃戠粶鎷︽埅澶辫触:', error);
  937. return false;
  938. }
  939. });
  940. // 鏇存柊缃戠粶鎷︽埅鐨?patterns
  941. ipcMain.handle('update-network-patterns', async (_event: unknown, webContentsId: number, patterns: Array<{ match: string, key: string }>) => {
  942. const config = networkInterceptors.get(webContentsId);
  943. if (config) {
  944. config.patterns = patterns;
  945. console.log(`[CDP] 宸叉洿鏂?patterns锛寃ebContentsId: ${webContentsId}`);
  946. return true;
  947. }
  948. return false;
  949. });