// 浣跨敤 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 }, callback: (response: { responseHeaders: Record }) => 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 }, callback: (response: { requestHeaders: Record }) => 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 = { 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; pendingRequests: Map; }> = 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; });