feat: 内置受管浏览器(CDP + AX 快照 ref 体系)+ 本地浏览器配置导入

This commit is contained in:
Rick Ye
2026-09-07 11:20:51 +08:00
parent 5dd645b745
commit eafbe8c15d
16 changed files with 2490 additions and 37 deletions
+67
View File
@@ -2,6 +2,8 @@ import { BrowserWindow, dialog, ipcMain, shell } from 'electron'
import { mkdirSync } from 'node:fs'
import { userInfo } from 'node:os'
import { AgentService } from './pi/agent-service'
import { getBrowserController } from './browser/browser-controller'
import { detectBrowserSources, importBrowserProfile } from './browser/browser-profile'
/**
* 注册 IPC 通道并把 agent 事件转发到所有渲染窗口。
@@ -257,5 +259,70 @@ export function registerIpc(): AgentService {
return service.pickAttachments(win ?? undefined)
})
// ---- 内置受管浏览器 ----
const browser = getBrowserController()
// 面板状态广播(与 agent:event 同样转发到所有窗口)
browser.onState((state) => {
for (const win of BrowserWindow.getAllWindows()) {
if (!win.isDestroyed()) win.webContents.send('browser:event', { type: 'state', state })
}
})
const browserPass = <T,>(fn: () => T): T => {
try {
return fn()
} catch (err) {
throw new Error(err instanceof Error ? err.message : String(err))
}
}
ipcMain.handle('browser:getState', () => browser.getState())
ipcMain.handle('browser:navigate', (_e, input: unknown) =>
browserPass(() => browser.navigateDisplay(typeof input === 'string' ? input : ''))
)
ipcMain.handle('browser:goBack', () => browserPass(() => browser.goBack()))
ipcMain.handle('browser:goForward', () => browserPass(() => browser.goForward()))
ipcMain.handle('browser:reload', () => browserPass(() => browser.reload()))
ipcMain.handle('browser:newTab', (_e, url: unknown) =>
browserPass(() => browser.newTab(typeof url === 'string' && url ? url : undefined, false))
)
ipcMain.handle('browser:selectTab', (_e, tabId: unknown) =>
browserPass(() => browser.selectTab(typeof tabId === 'string' ? tabId : ''))
)
ipcMain.handle('browser:closeTab', (_e, tabId: unknown) =>
browserPass(() => browser.closeTab(typeof tabId === 'string' ? tabId : undefined))
)
ipcMain.handle('browser:close', () => browserPass(() => browser.close()))
ipcMain.handle('browser:acknowledgeRisk', () => browserPass(() => browser.acknowledgeRisk()))
// 渲染层 BrowserSlot 量出的占位矩形(原生 WebContentsView 对齐到 DOM 布局)
ipcMain.handle('browser:layout', (_e, layout: unknown) => {
const l = layout as { tabId?: unknown; visible?: unknown; revision?: unknown; bounds?: unknown }
const { tabId, visible, revision } = l
const bounds = l.bounds as { x?: unknown; y?: unknown; width?: unknown; height?: unknown } | undefined
const x = bounds?.x
const y = bounds?.y
const width = bounds?.width
const height = bounds?.height
if (
typeof tabId !== 'string' ||
typeof visible !== 'boolean' ||
typeof revision !== 'number' ||
typeof x !== 'number' ||
typeof y !== 'number' ||
typeof width !== 'number' ||
typeof height !== 'number'
) {
throw new Error('invalid browser layout')
}
return browserPass(() => browser.applyLayout({ tabId, visible, revision, bounds: { x, y, width, height } }))
})
// ---- 本地浏览器配置导入 ----
ipcMain.handle('browser:detectSources', () => browserPass(() => detectBrowserSources()))
ipcMain.handle('browser:importProfile', (_e, sourceId: unknown, profileDir: unknown) => {
if (typeof sourceId !== 'string' || typeof profileDir !== 'string') throw new Error('invalid import source')
return browserPass(() => importBrowserProfile(sourceId, profileDir))
})
return service
}