feat: 内置受管浏览器(CDP + AX 快照 ref 体系)+ 本地浏览器配置导入
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* 受管浏览器的 URL 与观察策略。
|
||||
* 独立成模块便于单测与复用(渲染层不需要,主进程专用)。
|
||||
*/
|
||||
|
||||
const SEARCH_URL = 'https://www.google.com/search?q='
|
||||
|
||||
/** 导航白名单:普通 web、本地开发服务与 about:blank;file:/javascript:/data: 一律拒绝 */
|
||||
export function assertSafeBrowserUrl(input: string): string {
|
||||
const value = input.trim()
|
||||
if (!value) throw new Error('URL 不能为空。')
|
||||
if (value === 'about:blank') return value
|
||||
let parsed: URL
|
||||
try {
|
||||
parsed = new URL(value)
|
||||
} catch {
|
||||
throw new Error(`无法解析的 URL: ${value}`)
|
||||
}
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
throw new Error(`不支持的地址协议(仅允许 http/https 与 about:blank): ${parsed.protocol}`)
|
||||
}
|
||||
return parsed.toString()
|
||||
}
|
||||
|
||||
function isLikelyUrl(value: string): boolean {
|
||||
const candidate = `http://${value}`
|
||||
try {
|
||||
const url = new URL(candidate)
|
||||
const host = url.hostname.toLowerCase()
|
||||
// 域名 / IP(v4/v6)/ localhost 与 *.localhost 视为直达地址
|
||||
const isHost =
|
||||
host === 'localhost' ||
|
||||
host.endsWith('.localhost') ||
|
||||
host.includes('.') ||
|
||||
host.includes(':')
|
||||
// 无空格且形如域名/IP/localhost
|
||||
return isHost && !/\s/.test(value)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** 输入归一化:URL/域名/IP/localhost 直达,普通文本走搜索 */
|
||||
export function resolveBrowserDestination(input: string): string {
|
||||
const value = input.trim()
|
||||
if (!value) throw new Error('地址不能为空。')
|
||||
if (value === 'about:blank') return value
|
||||
if (/^https?:\/\//i.test(value)) return assertSafeBrowserUrl(value)
|
||||
if (isLikelyUrl(value)) return assertSafeBrowserUrl(`https://${value}`)
|
||||
return SEARCH_URL + encodeURIComponent(value)
|
||||
}
|
||||
|
||||
// ---- Observe 观察策略 ----
|
||||
|
||||
export const DEFAULT_BROWSER_OBSERVE_MAX_ELEMENTS = 240
|
||||
export const MAX_BROWSER_OBSERVE_MAX_ELEMENTS = 400
|
||||
export const MIN_BROWSER_OBSERVE_MAX_ELEMENTS = 20
|
||||
|
||||
/** 可交互 AX role 白名单(占观察结果前 2/3 配额) */
|
||||
export const INTERACTIVE_AX_ROLES = new Set([
|
||||
'button',
|
||||
'link',
|
||||
'textfield',
|
||||
'textbox',
|
||||
'searchbox',
|
||||
'checkbox',
|
||||
'radiobutton',
|
||||
'combobox',
|
||||
'menuitem',
|
||||
'menuitemcheckbox',
|
||||
'menuitemradio',
|
||||
'tab',
|
||||
'option',
|
||||
'switch',
|
||||
'slider'
|
||||
])
|
||||
|
||||
/** 无名称也值得返回的 role(依赖 role 本身可定位) */
|
||||
export const NAMELESS_ALLOWED_ROLES = new Set(['button', 'textfield', 'textbox', 'link', 'checkbox', 'combobox'])
|
||||
|
||||
export function resolveBrowserObserveMaxElements(requested?: number): number {
|
||||
if (typeof requested !== 'number' || !Number.isFinite(requested)) return DEFAULT_BROWSER_OBSERVE_MAX_ELEMENTS
|
||||
return Math.min(MAX_BROWSER_OBSERVE_MAX_ELEMENTS, Math.max(MIN_BROWSER_OBSERVE_MAX_ELEMENTS, Math.round(requested)))
|
||||
}
|
||||
|
||||
export function isInteractiveAxRole(role: string): boolean {
|
||||
return INTERACTIVE_AX_ROLES.has(role.toLowerCase())
|
||||
}
|
||||
|
||||
export interface BrowserObservationCandidate {
|
||||
role: string
|
||||
name: string
|
||||
editable?: boolean
|
||||
}
|
||||
|
||||
/** 可交互元素优先(约 2/3 配额),其余语义上下文补足 */
|
||||
export function prioritizeBrowserObservationCandidates<T extends BrowserObservationCandidate>(
|
||||
candidates: readonly T[],
|
||||
maxElements: number
|
||||
): T[] {
|
||||
const interactiveLimit = Math.ceil(maxElements * (2 / 3))
|
||||
const interactive: T[] = []
|
||||
const context: T[] = []
|
||||
for (const candidate of candidates) {
|
||||
if (interactive.length < interactiveLimit && (isInteractiveAxRole(candidate.role) || candidate.editable)) {
|
||||
interactive.push(candidate)
|
||||
} else {
|
||||
context.push(candidate)
|
||||
}
|
||||
}
|
||||
const contextLimit = maxElements - interactive.length
|
||||
return [...interactive, ...context.slice(0, Math.max(0, contextLimit))]
|
||||
}
|
||||
|
||||
/** 不同 role 的可访问名称截断上限,避免长文本撑爆快照 */
|
||||
export function browserObservationNameLimit(role: string): number {
|
||||
if (role === 'link' || role === 'button' || role === 'menuitem') return 80
|
||||
if (role === 'heading') return 120
|
||||
return 160
|
||||
}
|
||||
Reference in New Issue
Block a user