数据截至 (上游 commit d02db1ee7c41)
视觉反馈与跨标签页:遮罩、Panel 与扩展
30 秒导读: 前四章讲的是 Page Agent 的核心——反思循环、DOM 脱水、工具与动作、LLM 客户端。本章讲核心外面的两圈壳:一圈是运行时视觉反馈(自动化时盖一层遮罩、画个假光标、拦住真人的手),另一圈是跨标签页与外部控制(浏览器扩展让 agent 在多个 tab 间跳,MCP 让外部客户端遥控你的浏览器)。这两圈都不碰核心的内部机制,只是把核心包起来、复用它。
1. 这是什么(零基础也能懂)
前面四章把一个「网页里的 GUI Agent」讲透了:它能看懂页面、想清楚下一步、点对按钮、填对表单。但那是核心引擎。真把它交到用户手里,还差两样外围能力。
- 第一样:让人看得见、且不添乱。 agent 自动点页面时,屏幕上得有反馈——不然用户以为卡死了;同时,agent 点到一半你 的鼠标别乱插一脚。这就是 SimulatorMask(模拟器遮罩):自动化期间盖一层半透明遮罩,画一个会动的"AI 光标"演示它在点哪,顺手把真人的鼠标键盘都拦在遮罩外。
- 第二样:突破"一个页面"的边界。 核心默认只管当前这一个网页。可真实任务经常要开新标签页、跨站点跳转,甚至由另一个 AI(比如 Claude、Cursor)从外部下命令。这就是浏览器扩展里的
MultiPageAgent和 MCP 包干的事。
一句话直觉: 把核心引擎想成汽车的发动机。本章讲的是仪表盘 + 挡风玻璃(遮罩/Panel:让你看见、保护你的手)和换成越野底盘(扩展:同一台发动机,换个能跑多地形的底盘,就能跨 tab)。发动机内部一个零件都没改。
本章的边界(重要): 这是一章外延章。它只讲"外围如何包裹并复用核心",不重复循环怎么反思、DOM 怎么脱水、动作怎么落地、LLM 怎么容错——那些请看前四章。
2. 顶层全景(两圈壳怎么套在核心上)
先看清楚"核心"和"两圈壳"的关系。核心是 PageAgentCore(在 @page-agent/core),它只依赖 LLM 客户端和一个 PageController。两种真实产品,都是给核心换外围换出来的:
┌─────────────────────────────┐
│ PageAgentCore │ ← 核心(前四章)
│ 反思循环 · 工具 · 系统提示 │ 两种产品都复用它
└───────────┬─────────────────┘
│ 依赖一个 pageController
┌──────────────────────┼──────────────────────┐
│ │
┌──────┴───────┐ ┌────────┴────────┐
│ 单页产品 │ │ 扩展产品 │
│ PageAgent │ │ MultiPageAgent │
└──────┬───────┘ └────────┬────────┘
│ 加了两个外围 │ 换了外围
① PageController(enableMask:true) ① pageController → RemotePageController
└ 里面挂 SimulatorMask └ 经 TabsController 跨 tab 代理
② new Panel(this) ← UI 反馈 ② 注入 tabTools + 扩展版 system_prompt
③ 心跳/状态投影到 chrome.storage
怎么读这张图: 中间那个框(核心)两边都一样;差别全在下面两片"外围"。左边 PageAgent 给核心加了遮罩和 Panel;右边 MultiPageAgent 把核心的 pageController 换成一个跨 tab 的代理,再补上多 tab 工具。
各外围件一句话职责:
| 外围件 | 属于哪圈 | 干什么 | 文件 |
|---|---|---|---|
SimulatorMask | 视觉反馈 | 自动化时盖遮罩、画 AI 光标、拦真人输入 | packages/page-controller/src/mask/SimulatorMask.ts |
PageAgent | 视觉反馈 | 单页入口:默认开遮罩 + 挂 Panel | packages/page-agent/src/PageAgent.ts |
Panel | 视觉反馈 | 控制面板 UI,经 PanelAgentAdapter 与 agent 解耦 | packages/ui/src/panel/Panel.ts |
MultiPageAgent | 跨页/外部 | 扩展入口:复用核心,换成跨 tab 底盘 | packages/extension/src/agent/MultiPageAgent.ts |
RemotePageController | 跨页/外部 | agent 侧的 DOM 代理,经消息打到远端 tab | packages/extension/src/agent/RemotePageController.ts |
TabsController | 跨页/外部 | 管理一组 tab:开/切/关、镜像状态 | packages/extension/src/agent/TabsController.ts |
tabTools | 跨页/外部 | 给核心注入 open/switch/close tab 三个工具 | packages/extension/src/agent/tabTools.ts |
| MCP server | 外部控制 | 让外部 agent 客户端下发 execute_task | packages/mcp/src/index.js |
第一圈:运行时视觉反馈
3. SimulatorMask:自动化时的"挡风玻璃"
3.1 它要解决的小问题
agent 正在替你点页面。此刻有两个矛盾的需求:
- 要让 DOM 上真的发生点击——否则动作落不下去。
- 又不能让真人的手同时乱动——你手一抖点了别处,agent 的世界模型就和真实页面对不上了。
SimulatorMask 用一个巧办法同时满足:盖一层透明遮罩层罩住整页,平时它 pointerEvents: auto 吃掉所有真人输入;需要"真的点下去"时再瞬间放行。
3.2 三件事:拦输入、画光标、自我排除
① 拦住真人的鼠标键盘。 遮罩 wrapper 上挂了一整排事件监听,click / mousedown / mouseup / mousemove / wheel / keydown / keyup 全部 stopPropagation + preventDefault——真人的操作到这层就被"吃掉",传不到底下的页面。
引用 SimulatorMask.ts:44-71(构造函数里的一排 addEventListener):真人的每一类指针/键盘事件都在遮罩层被拦截并阻止冒泡。
② 画一个会动的 AI 光标。 遮罩里有一个 #cursor 元素,用 requestAnimationFrame 每帧朝目标位置插值移动(而不是瞬移),看起来像真手在滑动。
下面这段是简化示意,非源码,演示"