// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import { ipcMain, WebContentsView, BrowserWindow } from 'electron' interface WebViewInfo { id: string view: WebContentsView initialUrl: string currentUrl: string isActive: boolean isShow: boolean } interface Size { x: number y: number width: number height: number } export class WebViewManager { private webViews = new Map() private win: BrowserWindow | null = null private size: Size = { x: 0, y: 0, width: 0, height: 0 } private maxInactiveWebviews = 5 private lastCleanupTime = Date.now() constructor(window: BrowserWindow) { this.win = window } // Remove automatic IPC handler registration from constructor // IPC handlers should be registered once in the main process public async captureWebview(webviewId: string) { const webContents = this.webViews.get(webviewId); if (!webContents) return null; const image = await webContents.view.webContents.capturePage(); const jpegBuffer = image.toJPEG(10); return 'data:image/jpeg;base64,' + jpegBuffer.toString('base64'); } public setSize(size: Size) { this.size = size this.webViews.forEach((webview) => { if (webview.isActive && webview.isShow) { this.changeViewSize(webview.id, size) } }) } public getActiveWebview() { const activeWebviews = Array.from(this.webViews.values()).filter(webview => webview.isActive) return activeWebviews.map(webview => webview.id) } public async createWebview(id: string = '1', url: string = 'about:blank?use=0') { try { // If webview with this id already exists, return error if (this.webViews.has(id)) { return { success: false, error: `Webview with id ${id} already exists` } } const view = new WebContentsView({ webPreferences: { // Use a separate session partition for webviews to isolate storage from main window // This ensures clearing webview storage won't affect main window's auth data partition: 'persist:user_login', nodeIntegration: false, contextIsolation: true, backgroundThrottling: true, offscreen: false, sandbox: true, disableBlinkFeatures: 'Accelerated2dCanvas,AutomationControlled', enableBlinkFeatures: 'IdleDetection', autoplayPolicy: 'document-user-activation-required', }, }) view.webContents.on('did-finish-load', () => { // Inject stealth script to avoid bot detection view.webContents.executeJavaScript(` // Save original values before overriding to maintain consistency const originalLanguages = navigator.languages ? [...navigator.languages] : ['en-US', 'en']; const originalHardwareConcurrency = navigator.hardwareConcurrency || 8; const originalDeviceMemory = navigator.deviceMemory || 8; // Hide webdriver property Object.defineProperty(navigator, 'webdriver', { get: () => undefined, configurable: true }); // Override plugins with proper PluginArray-like behavior Object.defineProperty(navigator, 'plugins', { get: () => { const plugins = { length: 3, 0: { name: 'Chrome PDF Plugin', description: 'Portable Document Format', filename: 'internal-pdf-viewer' }, 1: { name: 'Chrome PDF Viewer', description: '', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' }, 2: { name: 'Native Client', description: '', filename: 'internal-nacl-plugin' }, item: function(index) { return this[index] || null; }, namedItem: function(name) { for (let i = 0; i < this.length; i++) { if (this[i].name === name) return this[i]; } return null; }, refresh: function() {}, [Symbol.iterator]: function* () { for (let i = 0; i < this.length; i++) { yield this[i]; } } }; return plugins; }, configurable: true }); // Use original system languages for consistency with other browser data Object.defineProperty(navigator, 'languages', { get: () => originalLanguages, configurable: true }); // Use original hardwareConcurrency, clamped to common range (4-16) to avoid extreme fingerprints Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => Math.min(Math.max(originalHardwareConcurrency, 4), 16), configurable: true }); // Use original deviceMemory, clamped to common range (4-16) to avoid extreme fingerprints Object.defineProperty(navigator, 'deviceMemory', { get: () => Math.min(Math.max(originalDeviceMemory, 4), 16), configurable: true }); // Fix WebGL vendor/renderer for both WebGL and WebGL2 const getParameter = WebGLRenderingContext.prototype.getParameter; WebGLRenderingContext.prototype.getParameter = function(parameter) { if (parameter === 37445) return 'Intel Inc.'; if (parameter === 37446) return 'Intel(R) Iris(TM) Graphics 6100'; return getParameter.call(this, parameter); }; // Also patch WebGL2RenderingContext if (typeof WebGL2RenderingContext !== 'undefined') { const getParameter2 = WebGL2RenderingContext.prototype.getParameter; WebGL2RenderingContext.prototype.getParameter = function(parameter) { if (parameter === 37445) return 'Intel Inc.'; if (parameter === 37446) return 'Intel(R) Iris(TM) Graphics 6100'; return getParameter2.call(this, parameter); }; } // Override chrome runtime - real Chrome has window.chrome but runtime is undefined if (!window.chrome) { window.chrome = {}; } // In real Chrome, runtime exists but is undefined outside extensions // Don't set it to an object, that's detectable // Hide automation variables const automationVars = ['__webdriver_evaluate', '__selenium_evaluate', '__webdriver_script_fn', '__driver_evaluate', '__fxdriver_evaluate', '__driver_unwrapped', 'domAutomation', 'domAutomationController']; automationVars.forEach(v => { Object.defineProperty(window, v, { get: () => undefined, set: () => {}, configurable: true, enumerable: false }); }); // Mouse event handler window.addEventListener('mousedown', (e) => { if (!(e.target instanceof HTMLButtonElement || e.target instanceof HTMLInputElement)) { e.preventDefault(); } }, true); `); }); // Set to muted state when created view.webContents.audioMuted = true let newId = Number(id) view.setBounds({ x: -9999 + newId * 100, y: -9999 + newId * 100, width: 100, height: 100 }) view.setBorderRadius(16) await view.webContents.loadURL(url) const webViewInfo: WebViewInfo = { id, view, initialUrl: url, currentUrl: url, isActive: false, isShow: false, } // view.webContents.on("did-navigate", (event, url) => { // const win = BrowserWindow.fromWebContents(event.sender); // win?.webContents.send("url-updated", url); // }); view.webContents.on("did-navigate-in-page", (event, url) => { if (webViewInfo.isActive && webViewInfo.isShow && url !== 'about:blank?use=0' && url !== 'about:blank') { console.log("did-navigate-in-page", id, url) this.win?.webContents.send("url-updated", url); return } }); // Listen for URL change events view.webContents.on('did-navigate', (event, navigationUrl) => { webViewInfo.currentUrl = navigationUrl if (navigationUrl !== webViewInfo.initialUrl) { webViewInfo.isActive = true } console.log(`Webview ${id} navigated to: ${navigationUrl}`) if (webViewInfo.isActive && webViewInfo.isShow && navigationUrl !== 'about:blank?use=0' && navigationUrl !== 'about:blank') { console.log("did-navigate", id, navigationUrl) this.win?.webContents.send("url-updated", navigationUrl); return } webViewInfo.view.setBounds({ x: -1919, y: -1079, width: 1920, height: 1080 }) const activeSize = this.getActiveWebview().length const allSize = Array.from(this.webViews.values()).length const inactiveSize = allSize - activeSize // Clean up inactive webviews if too many if (inactiveSize > this.maxInactiveWebviews && Date.now() - this.lastCleanupTime > 30000) { this.cleanupInactiveWebviews() this.lastCleanupTime = Date.now() } // Create new webviews if needed if (inactiveSize <= 2) { const existingKeys = Array.from(this.webViews.keys()).map(Number).filter(n => !isNaN(n)) const maxId = existingKeys.length > 0 ? Math.max(...existingKeys) : 0 const startId = maxId + 1 // Create only 2 new webviews to reduce memory usage for (let i = 0; i < 2; i++) { const nextId = (startId + i).toString() this.createWebview(nextId, 'about:blank?use=0') } } // setTimeout(() => { // let newId = Number(id) // view.setBounds({ x: -9999 + newId * 100, y: -9999 + newId * 100, width: 100, height: 100 }) // }, 500) // Notify frontend when URL changes if (this.win && !this.win.isDestroyed()) { this.win.webContents.send('webview-navigated', id, navigationUrl) } }) view.webContents.setWindowOpenHandler(({ url }) => { view.webContents.loadURL(url) return { action: 'deny' } }) // Store in Map this.webViews.set(id, webViewInfo) this.win?.contentView.addChildView(view) return { success: true, id, hidden: true } } catch (error: any) { console.error(`Failed to create hidden webview ${id}:`, error) return { success: false, error: error.message } } } public changeViewSize(id: string, size: Size) { try { const webViewInfo = this.webViews.get(id) if (!webViewInfo) { return { success: false, error: `Webview with id ${id} not found` } } const { x, y, width, height } = size if (webViewInfo.isActive && webViewInfo.isShow) { webViewInfo.view.setBounds({ x, y, width: Math.max(width, 100), height: Math.max(height, 100) }) } else { let newId = Number(id) webViewInfo.view.setBounds({ x: -9999 + newId * 100, y: -9999 + newId * 100, width: Math.max(width, 100), height: Math.max(height, 100) }) } return { success: true } } catch (error: any) { console.error(`Failed to resize all webviews:`, error) return { success: false, error: error.message } } } public hideWebview(id: string) { const webViewInfo = this.webViews.get(id) if (!webViewInfo) { return { success: false, error: `Webview with id ${id} not found` } } let newId = Number(id) webViewInfo.view.setBounds({ x: -9999 + newId * 100, y: -9999 + newId * 100, width: 100, height: 100 }) webViewInfo.isShow = false if (webViewInfo.view.webContents && !webViewInfo.view.webContents.isDestroyed()) { webViewInfo.view.webContents.setBackgroundThrottling(true) } return { success: true } } public hideAllWebview() { this.webViews.forEach(webview => { let newId = Number(webview.id) webview.view.setBounds({ x: -9999 + newId * 100, y: -9999 + newId * 100, width: 100, height: 100 }) webview.isShow = false if (webview.view.webContents && !webview.view.webContents.isDestroyed()) { webview.view.webContents.setBackgroundThrottling(true) } }) } public async showWebview(id: string) { let webViewInfo = this.webViews.get(id) // If webview doesn't exist, create it if (!webViewInfo) { console.log(`Webview ${id} not found, creating new one`) const createResult = await this.createWebview(id, 'about:blank?use=0') if (!createResult.success) { return { success: false, error: `Failed to create webview ${id}` } } webViewInfo = this.webViews.get(id)! } const currentUrl = webViewInfo.view.webContents.getURL(); this.win?.webContents.send("url-updated", currentUrl); webViewInfo.isShow = true this.changeViewSize(id, this.size) console.log("showWebview", id, this.size) if (webViewInfo.view.webContents && !webViewInfo.view.webContents.isDestroyed()) { webViewInfo.view.webContents.setBackgroundThrottling(false) } if (this.win && !this.win.isDestroyed()) { this.win.webContents.send('webview-show', id) } return { success: true } } public getShowWebview() { return JSON.parse(JSON.stringify(Array.from(this.webViews.values()).filter(webview => webview.isShow).map(webview => webview.id))) } public destroyWebview(id: string) { try { const webViewInfo = this.webViews.get(id) if (!webViewInfo) { return { success: false, error: `Webview with id ${id} not found` } } if (!webViewInfo.view.webContents.isDestroyed()) { webViewInfo.view.webContents.removeAllListeners() // DO NOT clear storage data here! // Multiple webviews share the same partition 'persist:user_login' // Clearing storage would affect ALL webviews and remove login cookies // Only clear cache which is per-webContents webViewInfo.view.webContents.session.clearCache() } // remove webview from parent container if (this.win?.contentView) { this.win.contentView.removeChildView(webViewInfo.view) } // destroy webview webViewInfo.view.webContents.close() // remove from Map this.webViews.delete(id) console.log(`Webview ${id} destroyed successfully`) return { success: true } } catch (error: any) { console.error(`Failed to destroy webview ${id}:`, error) return { success: false, error: error.message } } } public destroy() { // Destroy all webviews Array.from(this.webViews.keys()).forEach(id => { this.destroyWebview(id) }) this.webViews.clear() } private cleanupInactiveWebviews() { const inactiveWebviews = Array.from(this.webViews.entries()) .filter(([id, info]) => !info.isActive && !info.isShow && info.currentUrl === 'about:blank?use=0') .sort((a, b) => parseInt(a[0]) - parseInt(b[0])) const toRemove = inactiveWebviews.slice(this.maxInactiveWebviews) toRemove.forEach(([id, _]) => { console.log(`Cleaning up inactive webview: ${id}`) this.destroyWebview(id) }) } }