Skip to content

plugin-tool-browser — 浏览器自动化 ​

包名: @aalis/plugin-tool-browser
源码: packages/plugin-tool-browser/src/index.ts

概述 ​

基于 Puppeteer 的浏览器自动化工具(默认无头模式,可由 headless 关闭),为 AI 提供导航、获取页面文本与链接、截图、点击、输入等工具。

插件声明 ​

typescript
export default definePlugin({
  name: '@aalis/plugin-tool-browser',
  displayName: '浏览器工具',
  subsystem: 'tools',
  uses: {
    config,
    logger,
    lifecycle,
    events,
    tools: optional(tools),
    sessionManager: optional(sessionManager),
    webui: optional(webuiServer),
    proc: optional(processService),
    storage: optional(storage),
  },
  apply(caps) { /* 见源码 */ },
});

配置 ​

字段类型默认值说明
headlessbooleantrue无头模式:是否以无头模式运行浏览器(无 GUI 窗口)。
defaultTimeoutnumber30000默认超时(ms):页面导航和操作的默认超时时间。
viewportWidthnumber1280视口宽度
viewportHeightnumber720视口高度
maxPagesnumber5最大页面数:同时打开的最大页面数量。超出后关闭最早打开的页面。
executablePathstring''Chrome 路径:自定义 Chrome/Chromium 可执行文件的绝对路径。留空则使用 Puppeteer 内置 Chromium。
maxContentLengthnumber50000最大内容长度:返回给 Agent 的页面文本最大字符数。
blockPrivatebooleantrue封锁内网与本地:拒绝 localhost / 127.x / ::1 / 10.x / 172.16-31.x / 192.168.x / 169.254.x / 0.0.0.0,防止 SSRF。
allowedProtocolsmultiselect["http","https"]允许的协议:浏览器只允许访问这些协议的 URL。
allowedHostsmultiselect[]主机白名单:允许访问的主机(含内网时需在此显式列出)。留空 = 仅按 blockPrivate 判定。

注册工具 ​

工具说明
browser_navigate打开指定 URL(可用 pageId 复用本会话打开的页面、用 waitFor 等待 CSS 选择器),返回 pageId、标题、URL 和截断后的页面文本
browser_get_text获取页面文本,可按 CSS 选择器取特定元素
browser_click点击页面元素
browser_type在输入框中输入文本(默认先清空,可选回车提交)
browser_screenshot对指定页面截图(可截整页或指定元素)。PNG 一律先落在 tmp 根,文本结果恒带 storage_uri;这次调用的主模型能看图时,PNG 同时随工具结果的 images 交出。两种情况下文本结果都不含 base64(见下节)
browser_get_links获取页面上的链接(href 与文本,默认上限 50)
browser_close_page关闭指定页面

页面与登录态的归属 ​

页面与登录态都归打开它的根会话,子会话归它的根会话(取法同 api-session-manager 的 rootSessionOf)。每个根会话用自己的浏览器上下文(puppeteer 的 createBrowserContext),页面开在它里面,cookie、localStorage 与缓存只在这个会话里有效:在一个会话里登录的站点,别的会话打开不带这个登录态。上下文按需建,浏览器崩溃或重启后随之清空、再按需重建,登录态不保留。

带 pageId 的工具只取得到调用所在的根会话打开的页面:同一根会话里(根会话与它的子会话)能取、能关;别的会话打开的页面与不存在的一样,回「页面不存在」,browser_navigate 带别的会话的 pageId 时开一个新页面。没有会话管理、或会话不在会话表里(编排、mcp-server 等)时按调用所在的会话自己算,没有会话的调用(sessionId 为空)共用一个上下文,只取得到同样没有会话打开的页面。

会话删除时(session:deleted)关掉它的上下文,连同页面与登录态;删子会话不动根会话的。

会话里在场的人共用这个会话的页面与登录态:群聊等多人会话里,谁都能操作别人在这个会话里打开的页面、带着别人在这个会话里登录的身份访问网站,所以写类操作(browser_navigate、browser_click、browser_type、browser_close_page)声明 risk: 'sensitive'。maxPages 是进程级的上限,超出时关闭全部页面里最久没访问的那个,可能属于别的会话。

SSRF 防护 ​

browser_navigate 打开 URL 前先做校验:协议须在 allowedProtocols 内;blockPrivate=true 时再用 isPrivateHost 做字符串级私网判定,这一步不解析域名。

blockPrivate=true 时,插件在本进程的 127.0.0.1 随机端口起一道网络闸(只支持无认证 CONNECT 的 SOCKS5 服务),浏览器以 --proxy-server 把全部 TCP 连接交给它,并以 --proxy-bypass-list=<-loopback> 撤掉 Chrome 让 localhost、回环与链路本地地址默认绕过代理的规则。页面、页面用 window.open 打开的窗口,以及 dedicated / shared / service worker 发出的连接都经过它,包括导航、点击与表单提交、重定向的每一跳、子资源、fetch 与 WebSocket。每个连接先按进程级网络策略的 allowedPorts 判定目标端口(allowedHosts 里的主机也不例外),再按目标主机判定:

  • allowedHosts 里的主机按名字直连,不做私网判定;
  • IP 字面量经 assertAddressesSafe 判定(按数值认,写法不限),按 canonicalAddress 给出的规范写法连接,连的就是判的那个地址;
  • 域名经 pinnedLookup 解析,全部地址通过判定后,连接只用这次解析得到的地址。判定与连接之间不再解析第二次,DNS 重绑定(判定时解析到公网地址、连接时解析到内网地址)因此无效。

判定不过、解析失败或连接失败,浏览器侧的请求都以 net::ERR_SOCKS_CONNECTION_FAILED 失败;browser_navigate 遇到这个错误时在报错后附一句说明,指出目标可能被 blockPrivate 拦截,宿主配置的 network 段写错时也报此错。判定遵循进程级网络策略,即宿主配置文档 network 字段的 blockPrivate、denyCidrs 与 allowedPorts 三项。WebRTC 以 --webrtc-ip-handling-policy=disable_non_proxied_udp 启动,不发不经代理的 UDP(以随附的 Chrome 实测;executablePath 指向较旧的 Chrome 时未核实)。

闸在首次启动浏览器之前起好,此后一直监听到插件停用;起不来时报错、不启动浏览器,下次调用重试,浏览器不会在没有闸的情况下运行。闸不做认证,本机进程都可以连到它,经它连接的目标同样按上述规则判定;问候与请求 10 秒内没有收齐的连接会被断开。插件停用时先关闭闸并断开全部在途连接,再关闭页面与浏览器,闸的关闭不等浏览器关闭落定。浏览器的全部流量经插件所在进程转发,打开重页面、视频时会多占该进程的 CPU,闸解析域名用的 dns.lookup 占用 libuv 线程池,域名多的页面可能与同进程的文件 I/O 争用线程;走代理后 Chrome 不使用 QUIC,也不再使用系统代理设置,出站连接由插件所在进程直连目标。blockPrivate=false 时不做私网判定,也不起闸,进程级网络策略不作用于浏览器。

Chrome 自带的本地网络访问限制只对浏览器直连的请求起作用:经闸的连接由闸解析目标,浏览器无从判断目标属于内网还是公网。本插件的防护不以它为前提。

判定函数均来自 @aalis/util-network-guard,私网段清单见 network-guard。

allowedHosts 的条目与小写化后的主机名(不含端口)做精确比较,不支持通配或网段;IPv6 字面量带方括号写(如 [::1])。命中即跳过上述私网判定,仅在 blockPrivate=true 时生效。

截图的交付形态 ​

browser_screenshot 的 PNG 一律先落盘到 tmp:/browser/{会话目录}/shot-{内容 sha256 前 16 位}.png(会话目录名按 会话媒体的目录约定 的 sessionDirName 换,会话 id 为空时回工具错误),文本结果里恒带 storage_uri,附带的说明按两条路分写:这次调用的主模型能看图时(acceptsImages,agent 按模型服务登记的能力填),PNG 随结果的 images 一并交出,说明是「图已随结果附上;若你看不到图,可把 storage_uri 交给看图工具(如有)或 send_attachment」;主模型不能看图、或调用方只读 content(mcp-server、workflow)时拿不到 images,说明是「图未随结果附上,可把 storage_uri 交给看图工具(如有)查看或 send_attachment 发送」。

base64 任何情况下都不进文本结果:整张 PNG 的 base64 有几十万字符,主模型看不到图,还会灌满上下文并落进历史。落盘失败时才退回「只给 images」;若此时调用方也接不住图,工具直接返回错误。

文件名取内容哈希,同一张图反复截图落在同一个文件上(零增量)。这些文件不会自动清理:tmp 根下的 browser/ 目录由使用者自行清理(或交给宿主对 tmp 根的清理策略),插件既不设 TTL 也不在拆卸时删。

浏览器实例 ​

Chromium 按需启动,进程级共享一个实例与一张页面表;并发的首次调用共用同一次启动。取实例时检查连接是否存活:崩溃或被杀之后再调工具会重新启动浏览器,并清空页面表(此前的 pageId 随之失效,需重新 browser_navigate)。插件停用时若浏览器正在启动,启动完成后随即关闭,发起启动的调用返回错误。

每个页面(pageId)独占一个浏览器窗口,headless=false 时即各自一个系统窗口。browser_click、browser_type 与 browser_screenshot 操作前先把目标页切到前台:页面自己用 window.open 或 target=_blank 开出的窗口会把原页压到后台,而无头 Chrome 的后台页不做渲染,点击、默认先清空的输入与按选择器截图会一直等不到结果。

browser_screenshot(含切到前台与按选择器找元素)限时 defaultTimeout。截图卡住(如页面的渲染进程卡死)时,底层的截图命令一直挂着,并持有浏览器级的锁,同一浏览器里开新页、关页面与其它页面的截图都排在它后面;到时限后工具返回超时错误,并关掉这一代浏览器(不等关闭落定),页面表随之清空,此前的 pageId 全部失效,下次调用重新启动浏览器。其余操作里的单条 CDP 命令以 defaultTimeout 的两倍为上限(启动时设为 protocolTimeout,puppeteer 默认 180 秒),挂住时返回 puppeteer 的超时错误。defaultTimeout 为 0 时两者都不设时限。

浏览器页 ​

WebUI 插件详情里的「浏览器」页(key: 'browser')列出打开着的页面(不分会话;ID、标题、地址、最后访问),行动作「关闭」,另有「关闭所有页面」(确认框)。关闭不存在的页面回错误,不当成功。开页、关页、导航或点击改了标题与地址、浏览器重启清空页面表时,页面都重取。

配置校验 ​

blockPrivate、allowedProtocols、allowedHosts 和 executablePath 的显式无效值使实例进入错误态,不会注册浏览器工具。协议只接受 http 与 https;白名单必须是字符串数组。其他错误类型回落默认值并告警。

文档对应 dev@c4e046e41,core 0.21.0,构建于 2026-11-02
基于 MIT 许可发布