文章

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

适用场景

场景

描述

SaaS AI Copilot

快速集成 AI 助手到产品

智能表单填充

ERP/CRM 系统自动化

无障碍访问

自然语言控制网页

多页面自动化

浏览器扩展跨标签任务


二、核心架构

┌─────────────────────────────────────────────────────────────────┐
│                        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 依赖


三、包结构详解

包名

npm 包

职责

依赖关系

page-agent

page-agent

主入口,带 UI

core, ui, page-controller

core

@page-agent/core

核心代理逻辑(无 UI)

llms, page-controller

llms

@page-agent/llms

LLM 客户端 + 重试

无依赖

page-controller

@page-agent/page-controller

DOM 操作 + Mask

无 LLM 依赖

ui

@page-agent/ui

Panel + i18n

解耦于 PageAgent

extension

-

浏览器扩展(多页面)

WXT + React

website

-

文档网站

React + Vite


四、Agent 执行循环

Re-Act 模式

┌─────────────────────────────────────────────────────────────┐
│                    Re-Act Agent Loop                        │
│                                                             │
│   ┌──────────┐    ┌──────────────┐    ┌──────────────┐      │
│   │ Observe  │───▶│    Think     │───▶│     Act      │      │
│   │ (观察)    │    │ (LLM思考)    │    │  (执行工具)    │     │
│   └──────────┘    └──────────────┘    └──────────────┘      │
│        ▲                                     │              │
│        └─────────────────────────────────────┘              │
│                      Loop until done                        │
└─────────────────────────────────────────────────────────────┘

每一步包含

  1. Observe — 收集浏览器状态、页面信息、观察事件

  2. Think — LLM 调用,包含:

    • evaluation_previous_goal — 评估上一步

    • memory — 记忆

    • next_goal — 下一步目标

    • action — 具体动作

  3. Act — 执行工具,返回结果

事件系统

事件

触发时机

用途

statuschange

状态转换

UI 更新

historychange

历史更新

渲染历史卡片

activity

实时活动

临时 UI 反馈

dispose

代理销毁

清理资源


五、工具系统

工具

功能

参数

done

完成任务

text, success

wait

等待

seconds (1-10)

ask_user

询问用户

question

click_element_by_index

点击元素

index

input_text

输入文本

index, text

select_dropdown_option

选择下拉选项

index, text

scroll

垂直滚动

down, num_pages, pixels, index

scroll_horizontally

水平滚动

right, pixels, index

execute_javascript

执行 JS

script


六、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 样式判断

极高

极低

极低

原因:现代前端框架会自动为可交互元素设置正确的 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 }))
}

九、为什么效果好?核心原因总结

维度

技术实现

效果提升

元素识别

基于 cursor 样式判断

准确率极高,几乎无漏报

视口感知

只处理可见元素

减少 token 消耗,提高响应速度

去重机制

isElementDistinctInteraction

避免父子元素重复标记

新元素标记

*[index] + WeakMap 缓存

帮助 LLM 快速识别页面变化

滚动检测

data-scrollable 属性

支持复杂交互场景

原生事件

完整事件序列模拟

兼容各种前端框架

React 兼容

原生 setter + patch

绕过受控组件限制

视觉反馈

Mask + AI 光标

用户可观察,增强信任


十、关键代码位置索引

功能

文件位置

主入口类

packages/page-agent/src/PageAgent.ts

核心代理类

packages/core/src/PageAgentCore.ts

DOM 树提取

packages/page-controller/src/dom/dom_tree/index.js

可交互元素识别

packages/page-controller/src/dom/dom_tree/index.js#L694-L953

元素高亮

packages/page-controller/src/dom/dom_tree/index.js#L165-L412

文本化输出

packages/page-controller/src/dom/index.ts#L120-L372

点击/输入操作

packages/page-controller/src/actions.ts

视觉遮罩

packages/page-controller/src/mask/SimulatorMask.ts

工具定义

packages/core/src/tools/index.ts

系统提示词

packages/core/src/prompts/system_prompt.md

LLM 客户端

packages/llms/src/index.ts

UI Panel

packages/ui/src/panel/Panel.ts

多页面代理

packages/extension/src/agent/MultiPageAgent.ts


十一、配置能力

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
}

十二、项目成熟度

维度

状态

核心功能

✅ 完整

TypeScript

✅ 严格类型

文档

✅ 完整文档站

测试

⚠️ 需补充

浏览器扩展

🚧 WIP

国际化

✅ 中英双语


参考资料