Chrome系列11-Alibaba PageAgent项目拆解
背景
近期在做ScriptCat的二开,期望可以借助AI让脚本开发的更简单,但是html内容与大模型的交互是一个问题,遂需要分析Browser Agent或类似产品能力。
PageAgent项目分析
官网仓库:https://github.com/alibaba/page-agent
Page Agent 项目深度解析
本文档系统性地分析了 Page Agent 项目的架构设计、核心能力与技术实现细节。
一、项目定位
Page Agent 是一个 AI 驱动的 Web UI 自动化代理,核心特点是:
直接在网页中运行 — 无需浏览器扩展、Python 或无头浏览器
纯文本 DOM 操作 — 不需要截图、OCR 或多模态 LLM
Bring Your Own LLM — 支持任何 OpenAI 兼容的 API
适用场景
二、核心架构
┌─────────────────────────────────────────────────────────────────┐
│ PageAgent (主入口) │
│ 带内置 UI Panel + Mask │
├─────────────────────────────────────────────────────────────────┤
│ PageAgentCore (核心) │
│ Re-Act Agent 循环 + 工具系统 + 事件系统 │
├──────────────────────────┬──────────────────────────────────────┤
│ PageController │ LLM Client │
│ DOM操作 + 视觉反馈 │ OpenAI兼容 + 重试机制 │
├──────────────────────────┴──────────────────────────────────────┤
│ UI Panel │
│ 可折叠面板 + i18n + 动画效果 │
└─────────────────────────────────────────────────────────────────┘模块边界
Page Agent: 主入口,继承 PageAgentCore 并添加 Panel
Core: 无 UI 的核心代理逻辑,可独立使用
LLMs: LLM 客户端,无 page-agent 依赖
UI: Panel 和 i18n,通过 PanelAgentAdapter 接口解耦
Page Controller: DOM 操作,无 LLM 依赖
三、包结构详解
四、Agent 执行循环
Re-Act 模式
┌─────────────────────────────────────────────────────────────┐
│ Re-Act Agent Loop │
│ │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Observe │───▶│ Think │───▶│ Act │ │
│ │ (观察) │ │ (LLM思考) │ │ (执行工具) │ │
│ └──────────┘ └──────────────┘ └──────────────┘ │
│ ▲ │ │
│ └─────────────────────────────────────┘ │
│ Loop until done │
└─────────────────────────────────────────────────────────────┘每一步包含
Observe — 收集浏览器状态、页面信息、观察事件
Think — LLM 调用,包含:
evaluation_previous_goal— 评估上一步memory— 记忆next_goal— 下一步目标action— 具体动作
Act — 执行工具,返回结果
事件系统
五、工具系统
六、DOM 处理管道(核心)
完整流程
┌─────────────────────────────────────────────────────────────────────────────┐
│ DOM 处理完整流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Live DOM │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ 1. DOM Tree Extraction (dom_tree/index.js) │ │
│ │ • 遍历 DOM 树,识别可交互元素 │ │
│ │ • 使用 WeakMap 缓存优化性能 │ │
│ │ • 支持 Shadow DOM / iframe │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ 2. Interactive Element Detection (isInteractiveElement) │ │
│ │ • cursor 样式判断 ⭐ 最关键 │ │
│ │ • 标签类型 + 属性 + 事件监听器 │ │
│ │ • 可滚动元素检测 │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ 3. Index Assignment (handleHighlighting) │ │
│ │ • 分配唯一索引 [0], [1], [2]... │ │
│ │ • 存储到 selectorMap │ │
│ │ • 直接引用 DOM 元素 (ref) │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ 4. Dehydration (flatTreeToString) │ │
│ │ • 转换为 LLM 可读文本格式 │ │
│ │ • 过滤属性,保留关键信息 │ │
│ │ • 标记新元素 *[index] │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Simplified HTML for LLM │
│ │
└─────────────────────────────────────────────────────────────────────────────┘可交互元素识别算法 ⭐ 核心创新
这是项目效果好的关键原因:
function isInteractiveElement(element) {
// 1️⃣ 黑白名单机制
if (interactiveBlacklist.includes(element)) return false
if (interactiveWhitelist.includes(element)) return true
// 2️⃣ ⭐ 基于 cursor 样式判断 - 最关键的优化!
const interactiveCursors = new Set([
'pointer', 'move', 'text', 'grab', 'grabbing',
'cell', 'copy', 'alias', 'all-scroll', 'col-resize',
'context-menu', 'crosshair', 'e-resize', 'zoom-in', ...
])
if (style?.cursor && interactiveCursors.has(style.cursor)) {
return true // ⭐ Genius fix for almost all interactive elements
}
// 3️⃣ 标签类型判断
const interactiveElements = new Set([
'a', 'button', 'input', 'select', 'textarea',
'details', 'summary', 'label', 'option', ...
])
// 4️⃣ 检查 disabled/readonly 状态
if (element.disabled || element.readOnly) return false
// 5️⃣ ARIA 角色判断
const interactiveRoles = new Set([
'button', 'menu', 'menuitem', 'radio', 'checkbox',
'tab', 'switch', 'slider', 'combobox', ...
])
// 6️⃣ 事件监听器检测(开发环境)
if (getEventListeners(element)['click']?.length > 0) return true
// 7️⃣ 可滚动元素检测
if (isScrollableElement(element)) return true
return false
}为什么基于 cursor 判断效果最好?
原因:现代前端框架会自动为可交互元素设置正确的 cursor 样式,这是开发者语义的直接体现。
数据处理:从 DOM 到 LLM 输入
属性过滤策略
只保留关键属性:
const DEFAULT_INCLUDE_ATTRIBUTES = [
'title', 'type', 'checked', 'name', 'role', 'value',
'placeholder', 'aria-label', 'aria-expanded', 'data-state',
'id', 'for', 'target', 'aria-haspopup', 'contenteditable', ...
]文本输出格式
[0]<a aria-label=page-agent.js 首页 />
[1]<div >P />
[2]<div >page-agent.js
UI Agent in your webpage />
[3]<a >文档 />
*[5]<button class=new-element>快速开始 /> ← 新元素标记
[6]<div data-scrollable="top=0, bottom=500" /> ← 滚动信息关键设计:
[index]— 可交互元素索引*[index]— 新出现的元素(帮助 LLM 识别变化)data-scrollable— 滚动距离信息缩进表示父子关系
七、视觉反馈系统
元素高亮
function highlightElement(element, index, parentIframe = null) {
// 1. 创建高亮容器
const container = document.createElement('div')
container.style.position = 'fixed'
container.style.pointerEvents = 'none'
container.style.zIndex = '2147483640' // 最高层级
// 2. 为每个 clientRect 创建覆盖层
const rects = element.getClientRects()
for (const rect of rects) {
const overlay = document.createElement('div')
overlay.style.border = `2px solid ${color}`
overlay.style.backgroundColor = `${color}1A` // 10% 透明度
}
// 3. 创建索引标签
const label = document.createElement('div')
label.textContent = index.toString()
// 4. 监听滚动/resize 更新位置
window.addEventListener('scroll', throttledUpdatePositions, true)
}SimulatorMask 遮罩
export class SimulatorMask {
constructor() {
// 1. 阻止所有用户交互
this.wrapper.addEventListener('click', (e) => {
e.stopPropagation()
e.preventDefault()
})
// 2. 创建 AI 光标
this.#createCursor()
// 3. 监听光标移动事件
window.addEventListener('PageAgent::MovePointerTo', (event) => {
const { x, y } = event.detail
this.setCursorPosition(x, y)
})
}
// 平滑光标移动动画
#moveCursorToTarget() {
const newX = this.#currentCursorX + (this.#targetCursorX - this.#currentCursorX) * 0.2
requestAnimationFrame(() => this.#moveCursorToTarget())
}
}八、元素操作实现
点击操作
export async function clickElement(element: HTMLElement) {
// 1. 滚动到可见区域
await scrollIntoViewIfNeeded(element)
// 2. 移动 AI 光标到元素中心
await movePointerToElement(element)
// 3. 触发点击动画
window.dispatchEvent(new CustomEvent('PageAgent::ClickPointer'))
// 4. 模拟完整的事件序列
element.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }))
element.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }))
element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
element.focus()
element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }))
element.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await waitFor(0.2) // 等待事件处理完成
}输入操作
export async function inputTextElement(element: HTMLElement, text: string) {
await clickElement(element) // 先聚焦
// 使用原生 setter 绕过 React 受控组件限制
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
)!.set!
nativeInputValueSetter.call(element, text)
element.dispatchEvent(new Event('input', { bubbles: true }))
}九、为什么效果好?核心原因总结
十、关键代码位置索引
十一、配置能力
interface AgentConfig {
// LLM 配置
baseURL: string
apiKey: string
model: string
temperature?: number
maxRetries?: number
// Agent 配置
maxSteps?: number // 最大步数 (默认 40)
language?: 'en-US' | 'zh-CN'
// 自定义
customTools?: Record<string, Tool | null>
customSystemPrompt?: string
instructions?: {
system?: string
getPageInstructions?: (url: string) => string
}
// 回调
onBeforeStep?: (agent, step) => Promise<void>
onAfterStep?: (agent, history) => Promise<void>
onBeforeTask?: (agent) => Promise<void>
onAfterTask?: (agent, result) => Promise<void>
onDispose?: (agent) => void
// 实验性
experimentalScriptExecutionTool?: boolean
experimentalLlmsTxt?: boolean
}十二、项目成熟度
参考资料
browser-use — DOM 处理组件来源