feat(browser): 受管浏览器代理设置(profile 导入已有实现,本轮补全代理)

This commit is contained in:
Rick Ye
2026-09-11 17:02:11 +08:00
parent 6507948f01
commit b6e6031415
7 changed files with 263 additions and 7 deletions
@@ -4,7 +4,7 @@ const log = getLogger('browser')
import { app, BrowserWindow, session as electronSession, WebContentsView, type Rectangle, type Session } from 'electron'
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import type { BrowserImportInfo, BrowserTabInfo, BrowserUiState } from '@shared/types'
import type { BrowserImportInfo, BrowserProxySettings, BrowserTabInfo, BrowserUiState } from '@shared/types'
import {
BrowserCdpTimeoutError,
BROWSER_OBSERVE_TIMEOUT_MS,
@@ -32,6 +32,9 @@ import {
* + Input.dispatch* 真实输入事件;不在页面里执行模拟点击。
* - 登录态隔离在 persist:aso-browser 分区;本地浏览器配置导入由 browser-profile.ts
* 直接迁移分区目录数据(Cookies/Local Storage + 密钥移植)。
* - 代理设置作用于同一分区会话(session.setProxy):跟随系统 / 直连 / 手动规则
* (Chromium proxyRules 语法 + 绕过列表),持久化进 aso-browser.json,
* 启动即应用、修改后断开在途连接立即生效(对 Agent 的 browser_* 工具同样生效)。
*/
const PARTITION = 'persist:aso-browser'
@@ -71,6 +74,7 @@ interface TabRecord {
interface BrowserSettings {
riskAcknowledged: boolean
imported?: BrowserImportInfo | null
proxy?: BrowserProxySettings
}
export interface BrowserObservation {
@@ -111,6 +115,36 @@ function textValue(v: unknown): string {
return ''
}
/** 校验/规范化代理输入(fixed 模式必须有可解析的 host:port 规则;其余模式丢弃规则字段) */
function normalizeProxy(input: BrowserProxySettings): BrowserProxySettings {
if (input.mode !== 'fixed') return { mode: input.mode }
const entries = (input.rules ?? '')
.split(';')
.map((s) => s.trim())
.filter(Boolean)
if (entries.length === 0) {
throw new Error('手动代理需要填写规则,如 127.0.0.1:7890(多协议用分号分隔,如 http=127.0.0.1:7890;https=127.0.0.1:7890)。')
}
for (const entry of entries) {
const hostPort = entry.includes('://')
? entry.slice(entry.indexOf('://') + 3)
: entry.includes('=')
? entry.slice(entry.indexOf('=') + 1)
: entry
const colon = hostPort.lastIndexOf(':')
const port = colon >= 0 ? hostPort.slice(colon + 1) : ''
if (!hostPort || colon < 0 || !/^\d{1,5}$/.test(port) || Number(port) < 1 || Number(port) > 65535) {
throw new Error(`代理规则「${entry}」无效:应为 host:port(如 127.0.0.1:7890)。`)
}
}
const bypass = (input.bypass ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
.join(',')
return { mode: 'fixed', rules: entries.join(';'), bypass: bypass || undefined }
}
function isEditableAxNode(node: Record<string, unknown>): boolean {
const role = textValue(node.role).toLowerCase()
if (role === 'textfield' || role === 'textbox' || role === 'searchbox') return true
@@ -310,6 +344,10 @@ export class BrowserController {
item.setSavePath(target)
})
this.settings = this.loadSettings()
// 启动即应用持久化的代理设置(setProxy 异步;失败不阻塞浏览器初始化,日志可查)
this.applyProxy(this.proxySettings()).catch((err: unknown) => {
log.error('启动时代理设置应用失败:', { err })
})
}
// ---- 设置持久化 ----
@@ -321,12 +359,17 @@ export class BrowserController {
private loadSettings(): BrowserSettings {
try {
const raw = JSON.parse(readFileSync(this.settingsFile, 'utf8')) as Partial<BrowserSettings>
const proxy = raw.proxy
return {
riskAcknowledged: raw.riskAcknowledged === true,
imported: raw.imported ?? null
imported: raw.imported ?? null,
proxy:
proxy && (proxy.mode === 'direct' || proxy.mode === 'fixed' || proxy.mode === 'system')
? proxy
: { mode: 'system' }
}
} catch {
return { riskAcknowledged: false }
return { riskAcknowledged: false, proxy: { mode: 'system' } }
}
}
@@ -349,6 +392,38 @@ export class BrowserController {
return this.settings.imported ?? null
}
// ---- 代理设置 ----
private proxySettings(): BrowserProxySettings {
return this.settings.proxy ?? { mode: 'system' }
}
getProxySettings(): BrowserProxySettings {
return this.proxySettings()
}
/** 应用到分区会话;切换后断开在途连接,让新规则对已打开的标签立即生效 */
private async applyProxy(proxy: BrowserProxySettings): Promise<void> {
await this.browserSession.setProxy(
proxy.mode === 'fixed'
? { mode: 'fixed_servers', proxyRules: proxy.rules ?? '', proxyBypassRules: proxy.bypass }
: proxy.mode === 'direct'
? { mode: 'direct' }
: { mode: 'system' }
)
await this.browserSession.closeAllConnections()
}
/** 更新并持久化代理设置:先应用成功才落盘(失败抛错给 UI,原设置保持不变) */
async setProxySettings(input: BrowserProxySettings): Promise<BrowserProxySettings> {
const next = normalizeProxy(input)
await this.applyProxy(next)
this.settings.proxy = next
this.saveSettings()
this.emitState()
return next
}
/** 当前是否有活跃标签页(导入配置前必须没有) */
get hasTabs(): boolean {
return this.tabs.size > 0
@@ -395,7 +470,8 @@ export class BrowserController {
activeTabId: this.activeTabId,
recentClosed: this.closedTabs.map(({ title, url }) => ({ title, url })),
trace: this.traceText,
imported: this.settings.imported ?? null
imported: this.settings.imported ?? null,
proxy: this.proxySettings()
}
}