// 使用 CommonJS 格式 const { app, BrowserWindow, ipcMain, shell, session, Menu, Tray, nativeImage, webContents } = require('electron'); const { join } = require('path'); const fs = require('fs'); let mainWindow: typeof BrowserWindow.prototype | null = null; let tray: typeof Tray.prototype | null = null; let isQuitting = false; const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL; // 获取图标路径 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, // 无边框窗口,自定义标题栏 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(); } }); app.whenReady().then(() => { createTray(); createWindow(); // 配置 webview session,允许第三方 cookies 和跨域请求 setupWebviewSessions(); 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(); } }); // 拦截新窗口打开(包括自定义协议) contents.setWindowOpenHandler(({ url }: { url: string }) => { if (!isAllowedUrl(url)) { console.log('[WebView] 阻止打开自定义协议窗口:', url); return { action: 'deny' }; } // 对于正常的 http/https 链接,在当前 webview 中打开 console.log('[WebView] 拦截新窗口,在当前页面打开:', url); contents.loadURL(url); return { action: 'deny' }; }); // 允许所有的权限请求(如摄像头、地理位置等) contents.session.setPermissionRequestHandler((_webContents: unknown, permission: string, callback: (granted: boolean) => void) => { // 允许所有权限请求 callback(true); }); // 配置 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', () => { if (tray) { tray.destroy(); tray = null; } }); // 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); }); } // 获取 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 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: Electron.CookiesSetDetails[]) => { try { const ses = session.fromPartition(partition); // 逐个设置 cookie for (const cookie of cookies) { await ses.cookies.set(cookie); } return true; } catch (error) { console.error('设置 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; } const result = await wc.executeJavaScript(` (function() { const el = document.querySelector('${selector.replace(/'/g, "\\'")}'); 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 position = await wc.executeJavaScript(` (function() { const searchText = '${text.replace(/'/g, "\\'")}'; // 查找可点击元素 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] 未找到包含 "${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('通过文本点击失败:', error); return false; } }); // ========== CDP 网络拦截功能 ========== // 存储每个 webContents 的网络拦截配置 const networkInterceptors: Map; pendingRequests: Map; }> = new Map(); // 启用 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'); } // 解析 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) { // 忽略 } } 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,webContentsId: ${webContentsId}`); return true; } return false; });