| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079 |
- // 浣跨敤 CommonJS 鏍煎紡
- const { app, BrowserWindow, ipcMain, shell, session, Menu, Tray, nativeImage, webContents } = require('electron');
- const { join } = require('path');
- const fs = require('fs');
- const http = require('http');
- const https = require('https');
- const os = require('os');
- import { startLocalServices, stopLocalServices, getServiceStatus, LOCAL_NODE_URL, getLogPath } from './local-services';
- let mainWindow: typeof BrowserWindow.prototype | null = null;
- // ========== 鍐呭瓨鐩戞帶 ==========
- const MEMORY_THRESHOLD_MB = 512; // 瓒呰繃 512MB 瑙﹀彂璀﹀憡 / 娓呯悊
- let lastMemoryReport = 0;
- function getMemoryUsageMB(): number {
- const used = process.memoryUsage();
- return Math.round(used.heapUsed / 1024 / 1024);
- }
- function logMemory(prefix: string): void {
- const used = getMemoryUsageMB();
- const total = Math.round(os.totalmem() / 1024 / 1024);
- const free = Math.round(os.freemem() / 1024 / 1024);
- const usagePct = Math.round((used / total) * 100);
- console.log(`[MEM] ${prefix} heap=${used}MB total=${total}MB free=${free}MB usage=${usagePct}%`);
- }
- function monitorMemory(): void {
- const used = getMemoryUsageMB();
- const now = Date.now();
- // 姣?60 绉掓姤鍛婁竴娆?
- if (now - lastMemoryReport > 60000) {
- logMemory('Electron');
- lastMemoryReport = now;
- }
- // 瓒呰繃闃堝€硷紝瑙﹀彂 GC 骞舵姤鍛?
- if (used > MEMORY_THRESHOLD_MB) {
- console.warn(`[MEM] Memory high (${used}MB), triggering GC...`);
- if (global.gc) {
- global.gc();
- }
- // 鍏抽棴澶氫綑鐨?webContents
- if (mainWindow?.webContents) {
- const wc = mainWindow.webContents;
- // 灏濊瘯娓呯悊 devtools extension
- try {
- session.defaultSession?.webContents.forEach((w: any) => {
- if (w !== wc && !w.isDestroyed()) {
- console.warn('[MEM] Closing idle webContents');
- w.close();
- }
- });
- } catch {}
- }
- }
- }
- // 鍚姩鍐呭瓨鐩戞帶
- setInterval(monitorMemory, 30000);
- let tray: typeof Tray.prototype | null = null;
- let isQuitting = false;
- const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;
- function setupCertificateBypass() {
- // 浠呭湪寮€鍙戠幆澧冭烦杩囨湰鍦版湇鍔$殑璇佷功楠岃瘉锛岀敓浜х幆澧冧笉鍋氬叏灞€缁曡繃
- if (!VITE_DEV_SERVER_URL) return;
- const allowedHosts = ['localhost', '127.0.0.1'];
- app.on('certificate-error', (event: Event, _webContents: typeof webContents.prototype, url: string, _error: string, _certificate: unknown, callback: (isTrusted: boolean) => void) => {
- try {
- const { hostname } = new URL(url);
- if (allowedHosts.includes(hostname)) {
- event.preventDefault();
- callback(true);
- return;
- }
- } catch { /* ignore invalid URLs */ }
- callback(false);
- });
- }
- function setupCorsBypassForApiRequests() {
- const ses = session.defaultSession;
- if (!ses) return;
- ses.webRequest.onHeadersReceived((details: { url: string; responseHeaders?: Record<string, string[] | string> }, callback: (response: { responseHeaders: Record<string, string[] | string> }) => void) => {
- const url = String(details.url || '');
- const isHttp = url.startsWith('http://') || url.startsWith('https://');
- const isApiLike = url.includes('/api/') || url.includes('/uploads/');
- if (!isHttp || !isApiLike) {
- callback({ responseHeaders: details.responseHeaders || {} });
- return;
- }
- const responseHeaders = { ...(details.responseHeaders || {}) };
- // 绉婚櫎鏈嶅姟绔凡鏈夌殑 CORS 澶达紝閬垮厤涓庝笅闈㈣缃殑鍊煎悎骞舵垚 "origin1, *" 瀵艰嚧杩濊
- const corsKeys = ['access-control-allow-origin', 'Access-Control-Allow-Origin'];
- corsKeys.forEach((k) => delete responseHeaders[k]);
- responseHeaders['access-control-allow-origin'] = ['*'];
- responseHeaders['access-control-allow-methods'] = ['GET,POST,PUT,PATCH,DELETE,OPTIONS'];
- responseHeaders['access-control-allow-headers'] = ['Authorization,Content-Type,X-Requested-With'];
- responseHeaders['access-control-expose-headers'] = ['Content-Disposition,Content-Type'];
- callback({ responseHeaders });
- });
- }
- function normalizeBaseUrl(url: string): string {
- const raw = String(url || '').trim();
- if (!raw) return '';
- try {
- const u = new URL(raw);
- return `${u.protocol}//${u.host}`.replace(/\/$/, '');
- } catch {
- return raw.replace(/\/$/, '');
- }
- }
- function requestJson(url: string, timeoutMs: number): Promise<{ ok: boolean; status?: number; data?: any; error?: string }> {
- return new Promise((resolve) => {
- const u = new URL(url);
- const isHttps = u.protocol === 'https:';
- const lib = isHttps ? https : http;
- // Windows 涓?localhost 甯歌瑙f瀽涓?::1锛岃€屽悗绔粎鐩戝惉 127.0.0.1锛屽鑷?ECONNREFUSED
- const hostname = (u.hostname === 'localhost' || u.hostname === '::1') ? '127.0.0.1' : u.hostname;
- const req = lib.request({
- method: 'GET',
- protocol: u.protocol,
- hostname,
- port: u.port || (isHttps ? 443 : 80),
- path: `${u.pathname}${u.search}`,
- headers: {
- Accept: 'application/json',
- },
- timeout: timeoutMs,
- rejectUnauthorized: !VITE_DEV_SERVER_URL, // 浠呭紑鍙戠幆澧冭烦杩囪瘉涔﹂獙璇?
- }, (res: any) => {
- const chunks: Buffer[] = [];
- res.on('data', (c: Buffer) => chunks.push(c));
- res.on('end', () => {
- const status = Number(res.statusCode || 0);
- const rawText = Buffer.concat(chunks).toString('utf-8');
- if (status < 200 || status >= 300) {
- resolve({ ok: false, status, error: `HTTP ${status}` });
- return;
- }
- try {
- const json = rawText ? JSON.parse(rawText) : null;
- resolve({ ok: true, status, data: json });
- } catch {
- resolve({ ok: false, status, error: '鍝嶅簲涓嶆槸 JSON' });
- }
- });
- });
- req.on('timeout', () => {
- req.destroy(new Error('timeout'));
- });
- req.on('error', (err: any) => {
- resolve({ ok: false, error: err?.message || '缃戠粶閿欒' });
- });
- req.end();
- });
- }
- // 鑾峰彇鍥炬爣璺緞
- function getIconPath() {
- return VITE_DEV_SERVER_URL
- ? join(__dirname, '../public/icons/icon-256.png')
- : join(__dirname, '../dist/icons/icon-256.png');
- }
- // 鑾峰彇鎵樼洏鍥炬爣璺緞
- function getTrayIconPath() {
- return VITE_DEV_SERVER_URL
- ? join(__dirname, '../public/icons/tray-icon.png')
- : join(__dirname, '../dist/icons/tray-icon.png');
- }
- // 鍒涘缓鎵樼洏鍥炬爣
- function createTrayIcon(): typeof nativeImage.prototype {
- const trayIconPath = getTrayIconPath();
- return nativeImage.createFromPath(trayIconPath);
- }
- // 鍒涘缓绯荤粺鎵樼洏
- function createTray() {
- const trayIcon = createTrayIcon();
- tray = new Tray(trayIcon);
- const contextMenu = Menu.buildFromTemplate([
- {
- label: '显示主窗口',
- click: () => {
- if (mainWindow) {
- mainWindow.show();
- mainWindow.focus();
- }
- }
- },
- {
- label: '最小化到托盘',
- click: () => {
- mainWindow?.hide();
- }
- },
- { type: 'separator' },
- {
- label: '退出',
- click: () => {
- isQuitting = true;
- app.quit();
- }
- }
- ]);
- tray.setToolTip('智媒通');
- tray.setContextMenu(contextMenu);
- // 鐐瑰嚮鎵樼洏鍥炬爣鏄剧ず绐楀彛
- tray.on('click', () => {
- if (mainWindow) {
- if (mainWindow.isVisible()) {
- mainWindow.focus();
- } else {
- mainWindow.show();
- mainWindow.focus();
- }
- }
- });
- // 鍙屽嚮鎵樼洏鍥炬爣鏄剧ず绐楀彛
- tray.on('double-click', () => {
- if (mainWindow) {
- mainWindow.show();
- mainWindow.focus();
- }
- });
- }
- function createWindow() {
- // 闅愯棌榛樿鑿滃崟鏍?
- Menu.setApplicationMenu(null);
- const iconPath = getIconPath();
- mainWindow = new BrowserWindow({
- width: 1400,
- height: 900,
- minWidth: 1200,
- minHeight: 700,
- icon: iconPath,
- webPreferences: {
- preload: join(__dirname, 'preload.js'),
- nodeIntegration: false,
- contextIsolation: true,
- webviewTag: true, // 鍚敤 webview 鏍囩
- },
- frame: false, // 鏃犺竟妗嗙獥鍙o紝鑷畾涔夋爣棰樻爮
- transparent: false,
- backgroundColor: '#f0f2f5',
- show: false,
- });
- // 绐楀彛鍑嗗濂藉悗鍐嶆樉绀猴紝閬垮厤鐧藉睆
- mainWindow.once('ready-to-show', () => {
- mainWindow?.show();
- setupWindowEvents();
- });
- // 鍔犺浇椤甸潰
- if (VITE_DEV_SERVER_URL) {
- mainWindow.loadURL(VITE_DEV_SERVER_URL);
- mainWindow.webContents.openDevTools();
- } else {
- mainWindow.loadFile(join(__dirname, '../dist/index.html'));
- }
- // 澶勭悊澶栭儴閾炬帴
- mainWindow.webContents.setWindowOpenHandler(({ url }: { url: string }) => {
- shell.openExternal(url);
- return { action: 'deny' };
- });
- // 鍏抽棴鎸夐挳榛樿鏈€灏忓寲鍒版墭鐩?
- mainWindow.on('close', (event: Event) => {
- if (!isQuitting) {
- event.preventDefault();
- mainWindow?.hide();
- // 鏄剧ず鎵樼洏閫氱煡锛堜粎棣栨锛?
- if (tray && !app.isPackaged) {
- // 寮€鍙戞ā寮忎笅鍙互鏄剧ず閫氱煡
- }
- }
- });
- mainWindow.on('closed', () => {
- mainWindow = null;
- });
- }
- // 鍗曞疄渚嬮攣瀹?
- const gotTheLock = app.requestSingleInstanceLock();
- if (!gotTheLock) {
- app.quit();
- } else {
- app.on('second-instance', () => {
- if (mainWindow) {
- mainWindow.show();
- if (mainWindow.isMinimized()) mainWindow.restore();
- mainWindow.focus();
- }
- });
- // ========== 闄嶄綆 Electron 鍐呭瓨鍗犵敤锛堝繀椤诲湪 app.whenReady 涔嬪墠锛?==========
- app.commandLine.appendSwitch('js-flags', '--max-old-space-size=512');
- app.commandLine.appendSwitch('renderer-process-limit', '2');
- app.whenReady().then(async () => {
- logMemory('AppReady');
- // 鍏堝垱寤虹獥鍙f樉绀?splash screen
- createWindow();
- createTray();
- // 鍚庡彴鍚姩鏈湴 Node 鏈嶅姟锛屼笉闃诲绐楀彛鏄剧ず
- console.log('[Main] 姝e湪鍚庡彴鍚姩鏈湴鏈嶅姟...');
- startLocalServices().then(({ nodeOk }) => {
- console.log(`[Main] local services status: Node=${nodeOk ? 'ready' : 'not_ready'}`);
- mainWindow?.webContents.send('services-status-changed', { nodeOk });
- });
- // 閰嶇疆 webview session锛屽厑璁哥涓夋柟 cookies 鍜岃法鍩熻姹?
- setupWebviewSessions();
- setupCertificateBypass();
- setupCorsBypassForApiRequests();
- app.on('activate', () => {
- if (BrowserWindow.getAllWindows().length === 0) {
- createWindow();
- } else if (mainWindow) {
- mainWindow.show();
- }
- });
- });
- }
- // 閰嶇疆 webview sessions
- function setupWebviewSessions() {
- // 鐩戝惉鏂扮殑 webContents 鍒涘缓
- app.on('web-contents-created', (_event: unknown, contents: typeof webContents.prototype) => {
- // 涓?webview 绫诲瀷鐨?webContents 閰嶇疆
- if (contents.getType() === 'webview') {
- // 璁剧疆 User-Agent锛堟ā鎷?Chrome 娴忚鍣級
- contents.setUserAgent(
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
- );
- // 鎷︽埅鑷畾涔夊崗璁摼鎺ワ紙濡?bitbrowser://锛夌殑瀵艰埅
- contents.on('will-navigate', (event: Event, url: string) => {
- if (!isAllowedUrl(url)) {
- console.log('[WebView] 闃绘瀵艰埅鍒拌嚜瀹氫箟鍗忚:', url);
- event.preventDefault();
- }
- });
- // 鎷︽埅鏂扮獥鍙f墦寮€锛堝寘鎷嚜瀹氫箟鍗忚锛?
- contents.setWindowOpenHandler(({ url }: { url: string }) => {
- if (!isAllowedUrl(url)) {
- console.log('[WebView] 闃绘鎵撳紑鑷畾涔夊崗璁獥鍙?', url);
- return { action: 'deny' };
- }
- // 瀵逛簬姝e父鐨?http/https 閾炬帴锛屽湪褰撳墠 webview 涓墦寮€
- console.log('[WebView] 鎷︽埅鏂扮獥鍙o紝鍦ㄥ綋鍓嶉〉闈㈡墦寮€:', url);
- contents.loadURL(url);
- return { action: 'deny' };
- });
- // 浠呭厑璁镐笟鍔℃墍闇€鐨勬潈闄愯姹?
- const allowedPermissions = ['clipboard-read', 'clipboard-write', 'notifications'];
- contents.session.setPermissionRequestHandler((_webContents: unknown, permission: string, callback: (granted: boolean) => void) => {
- callback(allowedPermissions.includes(permission));
- });
- // 閰嶇疆 webRequest 淇敼璇锋眰澶达紝绉婚櫎鍙兘鏆撮湶 Electron 鐨勭壒寰?
- contents.session.webRequest.onBeforeSendHeaders((details: { requestHeaders: Record<string, string> }, callback: (response: { requestHeaders: Record<string, string> }) => void) => {
- // 绉婚櫎鍙兘鏆撮湶 Electron 鐨勮姹傚ご
- delete details.requestHeaders['X-DevTools-Emulate-Network-Conditions-Client-Id'];
- // 纭繚鏈夋甯哥殑 Origin 鍜?Referer
- if (!details.requestHeaders['Origin'] && !details.requestHeaders['origin']) {
- // 涓嶆坊鍔?Origin锛岃娴忚鍣ㄨ嚜鍔ㄥ鐞?
- }
- callback({ requestHeaders: details.requestHeaders });
- });
- }
- });
- }
- // 妫€鏌?URL 鏄惁鏄厑璁哥殑鍗忚
- function isAllowedUrl(url: string): boolean {
- if (!url) return false;
- const lowerUrl = url.toLowerCase();
- return lowerUrl.startsWith('http://') ||
- lowerUrl.startsWith('https://') ||
- lowerUrl.startsWith('about:') ||
- lowerUrl.startsWith('data:');
- }
- // 闃绘榛樿鐨?window-all-closed 琛屼负锛屼繚鎸佹墭鐩樿繍琛?
- app.on('window-all-closed', () => {
- // 涓嶉€€鍑哄簲鐢紝淇濇寔鎵樼洏杩愯
- // 鍙湁鍦?isQuitting 涓?true 鏃舵墠鐪熸閫€鍑?
- });
- // 搴旂敤閫€鍑哄墠娓呯悊鎵樼洏
- app.on('before-quit', () => {
- isQuitting = true;
- });
- app.on('quit', () => {
- stopLocalServices();
- if (tray) {
- tray.destroy();
- tray = null;
- }
- });
- ipcMain.handle('test-server-connection', async (_event: unknown, args: { url: string }) => {
- try {
- const baseUrl = normalizeBaseUrl(args?.url);
- if (!baseUrl) return { ok: false, error: 'Server URL is required' };
- const result = await requestJson(`${baseUrl}/api/health`, 5000);
- if (!result.ok) return { ok: false, error: result.error || 'Connection failed' };
- if (result.data?.status === 'ok') return { ok: true };
- return { ok: false, error: 'Unexpected service response' };
- } catch (e: any) {
- return { ok: false, error: e?.message || 'Connection failed' };
- }
- });
- ipcMain.handle('get-local-services-status', () => {
- return getServiceStatus();
- });
- ipcMain.handle('get-local-urls', () => {
- return { nodeUrl: LOCAL_NODE_URL };
- });
- ipcMain.handle('get-service-log', () => {
- try {
- const logPath = getLogPath();
- if (fs.existsSync(logPath)) {
- return { path: logPath, content: fs.readFileSync(logPath, 'utf-8') };
- }
- return { path: logPath, content: '(log file not found)' };
- } catch (e: any) {
- return { path: '', content: `Read failed: ${e.message}` };
- }
- });
- ipcMain.handle('open-log-file', () => {
- try {
- const logPath = getLogPath();
- if (fs.existsSync(logPath)) {
- shell.showItemInFolder(logPath);
- }
- } catch { /* ignore */ }
- });
- // IPC 澶勭悊
- ipcMain.handle('get-app-version', () => {
- return app.getVersion();
- });
- ipcMain.handle('get-platform', () => {
- return process.platform;
- });
- // 绐楀彛鎺у埗
- ipcMain.on('window-minimize', () => {
- mainWindow?.minimize();
- });
- ipcMain.on('window-maximize', () => {
- if (mainWindow?.isMaximized()) {
- mainWindow.unmaximize();
- } else {
- mainWindow?.maximize();
- }
- });
- // 鍏抽棴绐楀彛锛堟渶灏忓寲鍒版墭鐩橈級
- ipcMain.on('window-close', () => {
- mainWindow?.hide();
- });
- // 鐪熸閫€鍑哄簲鐢?
- ipcMain.on('app-quit', () => {
- isQuitting = true;
- app.quit();
- });
- // 鑾峰彇绐楀彛鏈€澶у寲鐘舵€?
- ipcMain.handle('window-is-maximized', () => {
- return mainWindow?.isMaximized() || false;
- });
- // 鐩戝惉绐楀彛鏈€澶у寲/杩樺師浜嬩欢锛岄€氱煡娓叉煋杩涚▼
- function setupWindowEvents() {
- mainWindow?.on('maximize', () => {
- mainWindow?.webContents.send('window-maximized', true);
- });
- mainWindow?.on('unmaximize', () => {
- mainWindow?.webContents.send('window-maximized', false);
- });
- }
- // 寮圭獥鎵撳紑骞冲彴鍚庡彴锛堢嫭绔嬬獥鍙o紝涓嶅祵鍏ワ紱鐢ㄤ簬瀹為獙锛屽彲鍥炲綊涓哄祵鍏ワ級
- ipcMain.handle('open-backend-external', async (_event: unknown, payload: { url: string; cookieData?: string; title?: string }) => {
- const { url, cookieData, title } = payload || {};
- if (!url || typeof url !== 'string') return;
- const partition = 'persist:backend-popup-' + Date.now();
- const ses = session.fromPartition(partition);
- if (cookieData && typeof cookieData === 'string' && cookieData.trim()) {
- const raw = cookieData.trim();
- let cookiesToSet: Array<{ name: string; value: string; domain?: string; path?: string }> = [];
- try {
- if (raw.startsWith('[') || raw.startsWith('{')) {
- const parsed = JSON.parse(raw);
- const arr = Array.isArray(parsed) ? parsed : (parsed?.cookies || []);
- cookiesToSet = arr.map((c: { name?: string; value?: string; domain?: string; path?: string }) => ({
- name: String(c?.name ?? '').trim(),
- value: String(c?.value ?? '').trim(),
- domain: c?.domain ? String(c.domain) : undefined,
- path: c?.path ? String(c.path) : '/',
- })).filter((c: { name: string }) => c.name);
- } else {
- raw.split(';').forEach((p: string) => {
- const idx = p.indexOf('=');
- if (idx > 0) {
- const name = p.slice(0, idx).trim();
- const value = p.slice(idx + 1).trim();
- if (name) cookiesToSet.push({ name, value, path: '/' });
- }
- });
- }
- } catch (e) {
- console.warn('[open-backend-external] 瑙f瀽 cookie 澶辫触', e);
- }
- const origin = new URL(url).origin;
- const hostname = new URL(url).hostname;
- const defaultDomain = hostname.startsWith('www.') ? hostname.slice(4) : hostname;
- const domainWithDot = defaultDomain.includes('.') ? '.' + defaultDomain.split('.').slice(-2).join('.') : undefined;
- for (const c of cookiesToSet) {
- try {
- await ses.cookies.set({
- url: origin + '/',
- name: c.name,
- value: c.value,
- domain: c.domain || domainWithDot || hostname,
- path: c.path || '/',
- });
- } catch (err) {
- console.warn('[open-backend-external] 璁剧疆 cookie 澶辫触', c.name, err);
- }
- }
- }
- const win = new BrowserWindow({
- width: 1280,
- height: 800,
- title: title || '骞冲彴鍚庡彴',
- icon: getIconPath(),
- webPreferences: {
- session: ses,
- nodeIntegration: false,
- contextIsolation: true,
- },
- show: false,
- });
- win.once('ready-to-show', () => {
- win.show();
- });
- await win.loadURL(url);
- return { ok: true };
- });
- // 鑾峰彇 webview 鐨?cookies
- ipcMain.handle('get-webview-cookies', async (_event: unknown, partition: string, url: string) => {
- try {
- const ses = session.fromPartition(partition);
- const cookies = await ses.cookies.get({ url });
- return cookies;
- } catch (error) {
- console.error('鑾峰彇 cookies 澶辫触:', error);
- return [];
- }
- });
- // 鑾峰彇 webview 鐨勫叏閮?cookies锛堟寜 partition锛?
- ipcMain.handle('get-webview-all-cookies', async (_event: unknown, partition: string) => {
- try {
- const ses = session.fromPartition(partition);
- return await ses.cookies.get({});
- } catch (error) {
- console.error('鑾峰彇鍏ㄩ儴 cookies 澶辫触:', error);
- return [];
- }
- });
- // 娓呴櫎 webview 鐨?cookies
- ipcMain.handle('clear-webview-cookies', async (_event: unknown, partition: string) => {
- try {
- const ses = session.fromPartition(partition);
- await ses.clearStorageData({ storages: ['cookies'] });
- return true;
- } catch (error) {
- console.error('娓呴櫎 cookies 澶辫触:', error);
- return false;
- }
- });
- // 璁剧疆 webview 鐨?cookies
- ipcMain.handle('set-webview-cookies', async (_event: unknown, partition: string, cookies: any[]) => {
- try {
- if (!Array.isArray(cookies) || cookies.length === 0) {
- console.warn(`[Main] set-webview-cookies: cookies 涓虹┖, partition=${partition}`);
- return false;
- }
- console.log(`[Main] 璁剧疆 webview cookies, partition=${partition}, count=${cookies.length}`);
- const ses = session.fromPartition(partition);
- // 閫愪釜璁剧疆 cookie
- let successCount = 0;
- for (const cookie of cookies) {
- try {
- // 纭繚 Cookie 鏍煎紡姝g‘
- const cookieToSet: Record<string, any> = {
- url: cookie.url,
- name: cookie.name,
- value: cookie.value,
- domain: cookie.domain,
- path: cookie.path || '/',
- };
- // 鍙€夊瓧娈?
- if (typeof cookie.expirationDate === 'number' && Number.isFinite(cookie.expirationDate) && cookie.expirationDate > 0) {
- cookieToSet.expirationDate = cookie.expirationDate;
- }
- if (cookie.httpOnly !== undefined) {
- cookieToSet.httpOnly = cookie.httpOnly;
- }
- if (cookie.secure !== undefined) {
- cookieToSet.secure = cookie.secure;
- }
- if (cookie.sameSite) {
- cookieToSet.sameSite = cookie.sameSite as 'no_restriction' | 'lax' | 'strict';
- }
- await ses.cookies.set(cookieToSet);
- successCount++;
- // 璁板綍鍏抽敭 Cookie
- if (cookie.name === 'BDUSS' || cookie.name === 'STOKEN' || cookie.name === 'sessionid') {
- console.log(`[Main] 鎴愬姛璁剧疆鍏抽敭 Cookie: ${cookie.name}, domain: ${cookie.domain}`);
- }
- } catch (error) {
- console.error(`[Main] 璁剧疆 cookie 澶辫触 (${cookie.name}):`, error);
- }
- }
- console.log(`[Main] 鎴愬姛璁剧疆 ${successCount}/${cookies.length} 涓?cookies`);
- // 楠岃瘉 Cookie 鏄惁鐪熺殑璁剧疆鎴愬姛
- try {
- const setCookies = await ses.cookies.get({ domain: '.baidu.com' });
- console.log(`[Main] 楠岃瘉锛氬綋鍓?session 涓湁 ${setCookies.length} 涓櫨搴?Cookie`);
- const keyNames = setCookies.slice(0, 5).map((c: any) => c.name).join(', ');
- console.log(`[Main] 鍏抽敭 Cookie 鍚嶇О: ${keyNames}`);
- } catch (verifyError) {
- console.error('[Main] 楠岃瘉 Cookie 澶辫触:', verifyError);
- }
- return successCount > 0;
- } catch (error) {
- console.error('[Main] 璁剧疆 cookies 澶辫触:', error);
- return false;
- }
- });
- // 鎴彇 webview 椤甸潰鎴浘锛堢敤浜?AI 鍒嗘瀽锛?
- ipcMain.handle('capture-webview-page', async (_event: unknown, webContentsId: number) => {
- try {
- const wc = webContents.fromId(webContentsId);
- if (!wc) {
- console.error('鎵句笉鍒?webContents:', webContentsId);
- return null;
- }
- const image = await wc.capturePage();
- if (!image || image.isEmpty()) {
- console.warn('鎴浘涓虹┖');
- return null;
- }
- // 杞崲涓?JPEG 鏍煎紡鐨?Base64
- const buffer = image.toJPEG(80);
- return buffer.toString('base64');
- } catch (error) {
- console.error('鎴浘澶辫触:', error);
- return null;
- }
- });
- // 鍚?webview 鍙戦€侀紶鏍囩偣鍑讳簨浠?
- ipcMain.handle('webview-send-mouse-click', async (_event: unknown, webContentsId: number, x: number, y: number) => {
- try {
- const wc = webContents.fromId(webContentsId);
- if (!wc) {
- console.error('鎵句笉鍒?webContents:', webContentsId);
- return false;
- }
- // 鍙戦€侀紶鏍囩Щ鍔ㄤ簨浠?
- wc.sendInputEvent({
- type: 'mouseMove',
- x: Math.round(x),
- y: Math.round(y),
- });
- // 鐭殏寤惰繜鍚庡彂閫佺偣鍑讳簨浠?
- await new Promise(resolve => setTimeout(resolve, 50));
- // 鍙戦€侀紶鏍囨寜涓嬩簨浠?
- wc.sendInputEvent({
- type: 'mouseDown',
- x: Math.round(x),
- y: Math.round(y),
- button: 'left',
- clickCount: 1,
- });
- // 鐭殏寤惰繜鍚庡彂閫侀紶鏍囨姮璧蜂簨浠?
- await new Promise(resolve => setTimeout(resolve, 50));
- wc.sendInputEvent({
- type: 'mouseUp',
- x: Math.round(x),
- y: Math.round(y),
- button: 'left',
- clickCount: 1,
- });
- console.log(`[webview-send-mouse-click] Clicked at (${x}, ${y})`);
- return true;
- } catch (error) {
- console.error('鍙戦€佺偣鍑讳簨浠跺け璐?', error);
- return false;
- }
- });
- // 鍚?webview 鍙戦€侀敭鐩樿緭鍏ヤ簨浠?
- ipcMain.handle('webview-send-text-input', async (_event: unknown, webContentsId: number, text: string) => {
- try {
- const wc = webContents.fromId(webContentsId);
- if (!wc) {
- console.error('鎵句笉鍒?webContents:', webContentsId);
- return false;
- }
- // 閫愬瓧绗﹁緭鍏?
- for (const char of text) {
- wc.sendInputEvent({
- type: 'char',
- keyCode: char,
- });
- await new Promise(resolve => setTimeout(resolve, 30));
- }
- console.log(`[webview-send-text-input] Typed: ${text}`);
- return true;
- } catch (error) {
- console.error('鍙戦€佽緭鍏ヤ簨浠跺け璐?', error);
- return false;
- }
- });
- // 鑾峰彇 webview 椤甸潰鍏冪礌浣嶇疆
- ipcMain.handle('webview-get-element-position', async (_event: unknown, webContentsId: number, selector: string) => {
- try {
- const wc = webContents.fromId(webContentsId);
- if (!wc) {
- console.error('鎵句笉鍒?webContents:', webContentsId);
- return null;
- }
- // 鐧藉悕鍗曢獙璇?selector锛屼粎鍏佽鍚堟硶 CSS 閫夋嫨鍣ㄥ瓧绗?
- if (!/^[a-zA-Z0-9_\-.# :\[\]="'>~+*,\\]+$/.test(selector)) {
- console.error('Invalid selector:', selector);
- return null;
- }
- const result = await wc.executeJavaScript(`
- (function() {
- const el = document.querySelector(${JSON.stringify(selector)});
- if (!el) return null;
- const rect = el.getBoundingClientRect();
- return {
- x: rect.left + rect.width / 2,
- y: rect.top + rect.height / 2,
- width: rect.width,
- height: rect.height
- };
- })()
- `);
- return result;
- } catch (error) {
- console.error('鑾峰彇鍏冪礌浣嶇疆澶辫触:', error);
- return null;
- }
- });
- // 閫氳繃鏂囨湰鍐呭鏌ユ壘骞剁偣鍑诲厓绱?
- ipcMain.handle('webview-click-by-text', async (_event: unknown, webContentsId: number, text: string) => {
- try {
- const wc = webContents.fromId(webContentsId);
- if (!wc) {
- console.error('鎵句笉鍒?webContents:', webContentsId);
- return false;
- }
- // 鏌ユ壘鍖呭惈鎸囧畾鏂囨湰鐨勫彲鐐瑰嚮鍏冪礌鐨勪綅缃?
- const sanitizedText = (text || '').replace(/[<>"'`\\]/g, '');
- const position = await wc.executeJavaScript(`
- (function() {
- const searchText = ${JSON.stringify(sanitizedText)};
-
- // 鏌ユ壘鍙偣鍑诲厓绱?
- const clickables = document.querySelectorAll('a, button, [role="button"], [onclick], input[type="submit"], input[type="button"]');
- for (const el of clickables) {
- if (el.textContent?.includes(searchText) || el.getAttribute('aria-label')?.includes(searchText) || el.getAttribute('title')?.includes(searchText)) {
- const rect = el.getBoundingClientRect();
- if (rect.width > 0 && rect.height > 0) {
- return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
- }
- }
- }
-
- // 鏌ユ壘鎵€鏈夊寘鍚枃鏈殑鍏冪礌
- const allElements = document.querySelectorAll('*');
- for (const el of allElements) {
- const text = el.innerText?.trim();
- if (text && text.length < 100 && text.includes(searchText)) {
- const rect = el.getBoundingClientRect();
- if (rect.width > 0 && rect.height > 0 && rect.width < 500) {
- return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
- }
- }
- }
-
- return null;
- })()
- `);
- if (!position) {
- console.warn(`[webview-click-by-text] target not found: "${text}"`);
- return false;
- }
- // 鍙戦€佺偣鍑讳簨浠?
- wc.sendInputEvent({ type: 'mouseMove', x: Math.round(position.x), y: Math.round(position.y) });
- await new Promise(resolve => setTimeout(resolve, 50));
- wc.sendInputEvent({ type: 'mouseDown', x: Math.round(position.x), y: Math.round(position.y), button: 'left', clickCount: 1 });
- await new Promise(resolve => setTimeout(resolve, 50));
- wc.sendInputEvent({ type: 'mouseUp', x: Math.round(position.x), y: Math.round(position.y), button: 'left', clickCount: 1 });
- console.log(`[webview-click-by-text] Clicked "${text}" at (${position.x}, ${position.y})`);
- return true;
- } catch (error) {
- console.error('click by text failed:', error);
- return false;
- }
- });
- // ========== CDP 缃戠粶鎷︽埅鍔熻兘 ==========
- // 瀛樺偍姣忎釜 webContents 鐨勭綉缁滄嫤鎴厤缃?
- const networkInterceptors: Map<number, {
- patterns: Array<{ match: string, key: string }>;
- pendingRequests: Map<string, { url: string, timestamp: number }>;
- }> = new Map();
- // 娓呯悊宸查攢姣佺殑 webContents
- app.on('web-contents-destroyed', (_event: unknown, contents: typeof webContents.prototype) => {
- const webContentsId = contents.id;
- if (networkInterceptors.has(webContentsId)) {
- // 娓呯悊缃戠粶鎷︽埅鍣?
- try {
- contents.debugger.detach();
- } catch (e) {
- // 蹇界暐閿欒
- }
- networkInterceptors.delete(webContentsId);
- console.log(`[CDP] 宸叉竻鐞嗗凡閿€姣佺殑 webContents 鎷︽埅鍣? ${webContentsId}`);
- }
- });
- // 鍚敤 CDP 缃戠粶鎷︽埅
- ipcMain.handle('enable-network-intercept', async (_event: unknown, webContentsId: number, patterns: Array<{ match: string, key: string }>) => {
- try {
- const wc = webContents.fromId(webContentsId);
- if (!wc) {
- console.error('[CDP] 鎵句笉鍒?webContents:', webContentsId);
- return false;
- }
- // 濡傛灉宸茬粡鏈夋嫤鎴櫒锛屽厛娓呯悊
- if (networkInterceptors.has(webContentsId)) {
- try {
- wc.debugger.detach();
- } catch (e) {
- // 蹇界暐
- }
- }
- // 瀛樺偍閰嶇疆
- networkInterceptors.set(webContentsId, {
- patterns,
- pendingRequests: new Map()
- });
- // 闄勫姞璋冭瘯鍣?
- try {
- wc.debugger.attach('1.3');
- } catch (err: unknown) {
- const error = err as Error;
- if (!error.message?.includes('Already attached')) {
- throw err;
- }
- }
- // 鍚敤缃戠粶鐩戝惉
- await wc.debugger.sendCommand('Network.enable');
- // 鐩戝惉缃戠粶鍝嶅簲
- wc.debugger.on('message', async (_e: unknown, method: string, params: {
- requestId?: string;
- response?: { url?: string; status?: number; mimeType?: string };
- encodedDataLength?: number;
- }) => {
- const config = networkInterceptors.get(webContentsId);
- if (!config) return;
- if (method === 'Network.responseReceived') {
- const { requestId, response } = params;
- if (!requestId || !response?.url) return;
- // 璋冭瘯锛氭墦鍗扮櫨瀹跺彿鐩稿叧鐨勬墍鏈?API 璇锋眰
- if (response.url.includes('baijiahao.baidu.com')) {
- if (response.url.includes('/pcui/') || response.url.includes('/article')) {
- console.log(`[CDP DEBUG] 鐧惧鍙?API: ${response.url}`);
- }
- }
- // 妫€鏌ユ槸鍚﹀尮閰嶆垜浠叧娉ㄧ殑 API
- for (const pattern of config.patterns) {
- if (response.url.includes(pattern.match)) {
- // 璁板綍璇锋眰锛岀瓑寰呭搷搴斿畬鎴?
- config.pendingRequests.set(requestId, {
- url: response.url,
- timestamp: Date.now()
- });
- console.log(`[CDP] 鍖归厤鍒?API: ${pattern.key} - ${response.url}`);
- break;
- }
- }
- }
- if (method === 'Network.loadingFinished') {
- const { requestId } = params;
- if (!requestId) return;
- const pending = config.pendingRequests.get(requestId);
- if (!pending) return;
- config.pendingRequests.delete(requestId);
- try {
- // 鑾峰彇鍝嶅簲浣?
- const result = await wc.debugger.sendCommand('Network.getResponseBody', { requestId }) as { body: string; base64Encoded: boolean };
- let body = result.body;
- // 濡傛灉鏄?base64 缂栫爜锛岃В鐮?
- if (result.base64Encoded) {
- body = Buffer.from(body, 'base64').toString('utf8');
- }
- // 瑙f瀽 JSON
- const data = JSON.parse(body);
- // 鎵惧埌鍖归厤鐨?key
- let matchedKey = '';
- for (const pattern of config.patterns) {
- if (pending.url.includes(pattern.match)) {
- matchedKey = pattern.key;
- break;
- }
- }
- if (matchedKey) {
- console.log(`[CDP] 鑾峰彇鍒板搷搴? ${matchedKey}`, JSON.stringify(data).substring(0, 200));
- // 鍙戦€佸埌娓叉煋杩涚▼
- mainWindow?.webContents.send('network-intercept-data', {
- webContentsId,
- key: matchedKey,
- url: pending.url,
- data
- });
- }
- } catch (err) {
- console.warn(`[CDP] 鑾峰彇鍝嶅簲浣撳け璐?`, err);
- }
- }
- });
- console.log(`[CDP] 宸插惎鐢ㄧ綉缁滄嫤鎴紝webContentsId: ${webContentsId}, patterns:`, patterns.map(p => p.key));
- return true;
- } catch (error) {
- console.error('[CDP] 鍚敤缃戠粶鎷︽埅澶辫触:', error);
- return false;
- }
- });
- // 绂佺敤 CDP 缃戠粶鎷︽埅
- ipcMain.handle('disable-network-intercept', async (_event: unknown, webContentsId: number) => {
- try {
- const wc = webContents.fromId(webContentsId);
- if (wc) {
- try {
- wc.debugger.detach();
- } catch (e) {
- // 蹇界暐
- }
- }
- const config = networkInterceptors.get(webContentsId);
- if (config) {
- // 娓呯悊寰呭鐞嗚姹傜殑 Map
- config.pendingRequests.clear();
- }
- networkInterceptors.delete(webContentsId);
- console.log(`[CDP] 宸茬鐢ㄧ綉缁滄嫤鎴紝webContentsId: ${webContentsId}`);
- return true;
- } catch (error) {
- console.error('[CDP] 绂佺敤缃戠粶鎷︽埅澶辫触:', error);
- return false;
- }
- });
- // 鏇存柊缃戠粶鎷︽埅鐨?patterns
- ipcMain.handle('update-network-patterns', async (_event: unknown, webContentsId: number, patterns: Array<{ match: string, key: string }>) => {
- const config = networkInterceptors.get(webContentsId);
- if (config) {
- config.patterns = patterns;
- console.log(`[CDP] 宸叉洿鏂?patterns锛寃ebContentsId: ${webContentsId}`);
- return true;
- }
- return false;
- });
|