跳到主要内容

总览:网页里的 GUI Agent

30 秒导读: Page Agent(阿里开源,npm 包名 page-agent)让你一行 <script> 就给任意网页装一个 AI 助手。用户用大白话说"帮我登录""把这张表填好",助手自己去读页面、自己找按钮、自己点击输入。它的独门取舍是:不用截图、不用多模态大模型、不用浏览器扩展、不用后端 headless 浏览器——一切都在页面自己的 JavaScript 里发生,你只要自带一个 OpenAI 兼容的 LLM 接口。

本章是这组文档的门厅和路由:先讲清"这是什么、怎么用、七个包怎么拼、一条数据怎么流",再把你送进对应的分章。机制的细节(循环内部、DOM 算法、事件合成)都留给后面五章。


1. 这是什么(零基础也能懂)

一句话定义: Page Agent 是一个活在网页里的 GUI Agent——GUI Agent 指"像人一样用图形界面的 AI 智能体":它不调你的 API,而是看着页面、点着按钮替你操作。

它要解决谁的什么问题。 假设你做了一个 SaaS 后台,用户要点 20 下、跨 3 个页面才能建好一张工单。传统做法是再写一个 Copilot:接后端、编排 API、维护一套指令。Page Agent 换了思路——页面已经能被人操作了,那就让 AI 也去操作这个页面,不碰你的后端。

它面向的典型场景:

场景一句话
SaaS AI 副驾几行代码给产品加个 AI 助手,后端零改动
智能填表把"20 次点击的流程"压成一句话,适合 ERP/CRM/后台
无障碍让任意 Web 应用能用自然语言/语音驱动
跨页任务配可选 Chrome 扩展,让 agent 跨标签页干活
MCP让外部 agent 客户端反过来控制这个浏览器

它的四个卖点(README「Features」逐条对应源码事实):

  • 纯 in-page JS——不需要浏览器扩展、不需要 Python、不需要 headless 浏览器。装 agent 就是往页面塞一段脚本(见 packages/page-agent/src/demo.ts:16PageAgent 挂上 window)。
  • 文本化 DOM——不截图、不需要多模态模型或特殊权限。它把活 DOM"脱水"成一份带编号的纯文本元素表喂给模型(见 packages/page-controller/src/PageController.ts:174 updateTree)。
  • 自带 LLM——兼容大多数主流模型,包括本地部署的;只要给个 OpenAI 兼容的 baseURL(见 packages/llms/src/index.ts:19 LLM 默认用 OpenAIClient)。
  • 可选扩展跨标签页——单页不够用时,装官方 Chrome 扩展,同一套 agent 内核就能跨 tab(见 packages/extension/src/agent/MultiPageAgent.ts MultiPageAgent)。

用起来什么样(最小示例)

装好 npm 包后,核心 API 就是"new 一个,execute 一句话"(源自 README「NPM Installation」):

import { PageAgent } from 'page-agent'

const agent = new PageAgent({
model: 'qwen3.5-plus',
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
apiKey: 'YOUR_API_KEY',
language: 'en-US',
})

// 用大白话下指令,剩下的它自己看页面、自己点
await agent.execute('Click the login button')

想更快试玩,连模型都不用自己配——README 给了一行 CDN <script>,加载即用官方免费测试 LLM 自动初始化(实现见 demo.ts:25autoInit 分支,默认还会 panel.show() 弹出对话面板)。

一句话直觉/类比: 把网页的 DOM 想成一张带编号的遥控器按钮表。Page Agent 先把满屏花哨的界面简化成"① 登录按钮、② 用户名输入框、③ 提交……"这样一份编号清单递给模型;模型不用"看图",只需回一句"按 ①";系统再把 ① 翻译成对真实那个按钮的点击。编号是模型和真实页面之间唯一的握手——这条主线贯穿全篇。


2. 顶层全景(七个包怎么拼)

Page Agent 是一个 npm workspaces monorepo,package.jsonworkspaces 按拓扑序列了七个包(外加一个私有 website)。理解它先理解分层:一个无 UI 的核心,外面包一层 UI + DOM 控制器,再外面是两种"外壳"(浏览器扩展 / MCP 服务)。

2.1 各包一句话职责

包(npm 名)干什么入口(main)
page-agent对外主入口:在核心之上加 UI 面板 + DOM 控制器,还产出 demo/IIFE 构建packages/page-agent/src/PageAgent.ts
@page-agent/core无头核心:ReAct 主循环、记忆流、工具编排、prompt 组装packages/core/src/PageAgentCore.ts
@page-agent/llmsLLM 客户端:OpenAI 兼容调用、单次结构化工具调用、重试容错packages/llms/src/index.ts
@page-agent/page-controllerDOM 层:DOM 提取/脱水、按编号点击输入、可选视觉遮罩packages/page-controller/src/PageController.ts
@page-agent/ui面板与 i18n:对话 Panel,通过适配器与内核解耦packages/ui/src/index.ts
@page-agent/ext浏览器扩展(WXT + React):跨标签页的 MultiPageAgentpackages/extension/src/agent/MultiPageAgent.ts
@page-agent/mcpMCP 服务(Beta):让外部 agent 客户端控制这个浏览器packages/mcp/src/index.js

2.2 依赖流向(谁装谁)

从各包 package.jsondependencies 读出这张图。核心内核不依赖 UI、也不依赖任何具体页面——这是它能同时被"页面内脚本""浏览器扩展""MCP"三种外壳复用的原因。

怎么读:箭头表示"依赖 / 装配"方向,越靠右越底层、越无 UI。

┌─────────── 三种外壳(选一种用)───────────┐
│ │
page-agent @page-agent/ext @page-agent/mcp
(页面内脚本 + Panel) (跨标签页扩展) (外部 agent 控制)
│ \ │ │
│ \ @page-agent/ui │ │(通过 WebSocket
│ (对话面板) │ │ 桥到页面内 agent)
▼ ▼ ▼ ▼
┌──────────────────────────────────────┐
│ @page-agent/core ⭐ │ ← 无头内核:ReAct 循环 + 记忆
└───────────────┬───────────────┬───────┘
│ │
@page-agent/llms @page-agent/page-controller
(自带的大模型) (看页面 / 点页面 / 遮罩)

三点值得记住:

  • page-agent = core + ui + page-controller PageAgent 类只有 20 来行:new 一个 PageControllersuper() 进核心、再挂一个 Panel(packages/page-agent/src/PageAgent.ts:13-28)。它是"把零件装成开箱即用整机"的那层。
  • core 只认两个下游:llmspage-controller 核心通过 page-controller异步接口读写 DOM,自己完全不碰真实 DOM;通过 llms 调模型。UI 是可选的、被 page-agent 从外面挂上去的。
  • 扩展和 MCP 都复用同一个 core MultiPageAgent extends PageAgentCore,只是把"单页控制器"换成"能跨 tab 的远程控制器"(详见 05 章)。MCP 则是另起一个进程,通过 WebSocket 桥把外部指令送进页面内的 agent。

2.3 一条主线数据流:observe → dehydrate → LLM → indexed act

不管哪种外壳,内核每走一步(step)都是同一条四段流水线。这是理解整个项目的主动脉,后面每一章都是在放大其中一段。

怎么读:从左到右是一个 step 内的先后顺序;走完一圈回到最左,进入下一 step。

┌──────────────────────────────────────────────────────────────┐
│ 一个 step(在 PageAgentCore.execute 的 while 循环里) │
│ │
│ ① observe ② dehydrate ③ LLM ④ act │
│ 看现在的页面 → 脱水成带编号 → 模型回一个 → 按编号 │
│ (活 DOM) 纯文本元素表 动作(选编号) 真实操作 │
│ │
│ getBrowserState flatTreeToString llm.invoke clickElement│
│ updateTree + getSelectorMap (AgentOutput) inputText │
└───────────────────────────┐ └──── 回到 ① 下一步 ◄─┘
「编号」是 ②③④ 之间唯一的通用货币
阶段白话关键符号 · 文件展开在
① observe抓当前页面快照(URL、标题、滚动位置、可交互元素)getBrowserState · page-controller/src/PageController.ts:12902
② dehydrate把活 DOM 树压成一份带 [index] 编号的简化文本,并建立"编号→真实元素"映射flatTreeToStringgetSelectorMap · page-controller/src/dom/index.ts:193,49602
③ LLM组装 system+user prompt,强制模型只调一个结构化工具 AgentOutput,里面含反思 + 一个动作invoke · llms/src/index.ts:36;#packMacroTool · core/src/PageAgentCore.ts:3860104
④ act动作里的编号被翻译回真实元素,执行点击/输入/滚动clickElementinputText · page-controller/src/PageController.ts:244,27403

主循环本身(何时停、怎么攒记忆、怎么处理导航与超时)是 01 章的主题:它在 PageAgentCore.executewhile (true) 里(core/src/PageAgentCore.ts:251),每步产出一条 step 历史事件,直到模型调用 done 工具或步数触顶(默认 maxSteps: 40,见 PageAgentCore.ts:111)。


3. 巧妙之处清单(精华一览)

这些是读完全篇你该带走的"设计巧思"。每条只点"妙在哪",细节指向对应分章。

  • 文本化 DOM 代替截图。 不用多模态模型、不要截图权限,把界面降维成一份带编号的文本清单——门槛和成本都砍掉一大截。见 02
  • 「编号」作为模型与真实页面的唯一握手。 模型永远只说"操作第 N 号",从不直接碰 DOM;系统用 selectorMap 把编号翻译回真实元素。既稳又可审计。见 0203
  • 单结构化工具 AgentOutput 打包整步输出。 把"反思 + 记忆 + 下一目标 + 一个动作"塞进一次工具调用的 schema,用 z.union 把所有工具并成一个可选动作——一次往返拿到该走的下一步。见 0104
  • 两条信息流分离:持久「历史」vs 瞬时「活动」。 history 进模型上下文当记忆,activity 只喂 UI 不污染推理;错误事件甚至刻意不进 LLM 上下文(PageAgentCore.ts:625-628)。见 01
  • 野模型容错(auto-fix + retry)。 面向"什么模型都可能接"的现实,llms 包一层 withRetry,还有 autoFixer 收拾不守规矩的结构化输出。见 04
  • 无头内核 + 可插外壳。 core 不依赖 UI/页面,于是"页面内脚本 / 扩展 / MCP"三种形态复用同一循环。见 §2、05
  • SimulatorMask 视觉遮罩。 自动化期间盖一层遮罩挡住用户误触,提取 DOM 时又临时放行——细节见 updateTree 里对 mask.wrapper.style.pointerEvents 的开合。见 05

4. 代码地图(导航索引)

主题 | 文件 | 符号 三列。符号名比行号抗漂移——上游更新后优先用符号名 grep 定位。所有路径相对克隆根 packages/

主题文件符号
对外主入口(装配整机)page-agent/src/PageAgent.tsPageAgent
一行脚本 / IIFE 自动初始化page-agent/src/demo.tsautoInitwindow.PageAgent
⭐ 无头内核 + 主循环core/src/PageAgentCore.tsPageAgentCoreexecute
单结构化工具打包core/src/PageAgentCore.ts#packMacroTool(AgentOutput)
user prompt 组装(历史/状态/页面)core/src/PageAgentCore.ts#assembleUserPrompt
工具集(动作定义)core/src/tools/index.tstoolsdone/click_element_by_index/input_text/scroll
结构化输出容错core/src/utils/autoFixer.tsautoFixer
LLM 客户端 + 重试llms/src/index.tsLLMinvokewithRetry
OpenAI 兼容客户端llms/src/OpenAIClient.tsOpenAIClient
DOM 快照(observe)page-controller/src/PageController.tsgetBrowserStateupdateTree
DOM 脱水成文本page-controller/src/dom/index.tsgetFlatTreeflatTreeToString
编号→真实元素映射page-controller/src/dom/index.tsgetSelectorMap
按编号点击/输入(act)page-controller/src/PageController.tsclickElementinputText
视觉遮罩page-controller/src/SimulatorMask.tsSimulatorMask
对话面板ui/src/index.tsPanel
跨标签页 agentextension/src/agent/MultiPageAgent.tsMultiPageAgent
远程 DOM 控制器extension/src/agent/RemotePageController.tsRemotePageController
MCP 服务mcp/src/index.jsMcpServerHubBridge

5. 接下来读哪章

由浅入深,建议按顺序;也可按任务直接跳。

  • 01-agent-loop.md — ReAct 反思循环与记忆流。 主循环怎么转:observe→think→act 一步的生命周期,history/activity 两条信息流,done 与步数上限,导航/等待观察。
  • 02-dom-pipeline.md — 纯文本 DOM 脱水。 从活 DOM 到带编号元素表:getFlatTreeflatTreeToStringselectorMap,以及 header/footer 的滚动提示。
  • 03-tools-and-actions.md — 工具层与动作落地。 九个内置工具,如何把模型给的编号变成真实点击/输入/滚动。
  • 04-llm-and-autofix.md — LLM 客户端。 单结构化工具调用契约、withRetryautoFixer 对野模型的容错。
  • 05-mask-and-multipage.md — 视觉反馈与跨标签页。 SimulatorMask 遮罩、Panel、Chrome 扩展的 MultiPageAgent 与 MCP 桥。

本文档基线: alibaba/page-agent @ fa4664dfa5379e6e91deaf85bc1db2ae14d8e1d7。所有 file:line 引用 as-of 该提交;行号可能随上游漂移,优先用符号名定位。