**AI 浏览器操控新范式:Vercel Agent Browser 完整实战指南,让你的 AI Agent 秒变网页交互高手**

**AI 浏览器操控新范式:Vercel Agent Browser 完整实战指南,让你的 AI Agent 秒变网页交互高手**

AI 浏览器操控新范式:Vercel Agent Browser 完整实战指南,让你的 AI Agent 秒变网页交互高手


在人工智能飞速发展的今天,AI Agent 已经成为炙手可热的研究方向。然而,一个核心问题始终困扰着开发者:当 AI Agent 需要与真实网页进行交互时,应该如何实现?传统的 API 调用方案往往无法处理复杂的网页交互场景,而手动编写爬虫脚本又缺乏灵活性和通用性。

就在最近,Vercel Labs 推出了一款革命性的开源工具——Agent Browser,为这个难题提供了一个优雅的解决方案。这个工具能够让 AI Agent 像人类一样自然地操控浏览器,完成点击、填表、截图、执行 JavaScript 等操作,极大地降低了 AI 与网页交互的技术门槛。

本文将为你带来 Agent Browser 的完整实战指南,从环境搭建到高级应用,从基础概念到最佳实践,手把手教你掌握这个强大的工具。无论你是 AI 开发者、前端工程师,还是对自动化测试感兴趣的爱好者,都能从中获得有价值的内容。


为什么值得关注:重新定义 AI 与网页的交互方式

传统方案的痛点

在 Agent Browser 出现之前,开发者通常采用以下几种方案来实现 AI 与网页的交互,但每种方案都存在明显的局限性。

基于 API 的方案虽然稳定可靠,但适用范围极其有限。这类方案需要网站提供 API 接口,而大多数网站并没有这样的接口。更重要的是,即使有 API,也需要繁琐的认证流程和权限申请,开发成本高昂。

传统爬虫方案能够获取网页内容,但在处理动态渲染页面时力不从心。现代网页大量使用 JavaScript 动态生成内容,传统的 HTTP 请求只能获取初始 HTML,无法获取渲染后的完整页面。而且,爬虫方案难以处理需要登录、验证码、滑动验证等复杂交互的场景。

Selenium 和 Playwright 等自动化框架虽然功能强大,但它们本质上是被动工具,需要开发者精确指定每一步操作。这些框架无法理解页面的语义含义,更无法让 AI 自主决策下一步应该做什么。

Agent Browser 的核心优势

Agent Browser 的出现彻底改变了这一局面。它不仅仅是一个浏览器自动化工具,更是一个专为 AI Agent 设计的智能交互框架。

多模态理解能力是 Agent Browser 最显著的特点。与传统工具不同,Agent Browser 能够理解网页的视觉布局和语义结构。它可以识别页面上不同元素的类型(按钮、输入框、图片等)和功能含义(搜索、提交、导航等),从而让 AI 能够以更接近人类的方式与网页交互。

智能操作规划使得 AI Agent 能够自主完成复杂的网页任务。Agent Browser 提供了任务描述接口,你只需要告诉它目标是什么(比如“登录 GitHub 并在我的仓库中创建一个新项目”),它就能自动规划出一系列操作步骤并执行。

统一的抽象接口简化了开发流程。Agent Browser 屏蔽了底层浏览器操作的复杂性,提供了一套简洁直观的 API。开发者无需关心浏览器如何启动、元素如何定位、操作如何执行等底层细节,只需要关注业务逻辑的实现。

截图与视觉反馈让调试和问题排查变得轻而易举。每次操作后,Agent Browser 都可以生成当前页面的截图,帮助开发者直观地了解执行结果,及时发现和修正问题。

应用场景的广泛性

Agent Browser 的应用场景非常广泛,几乎涵盖了所有需要 AI 与网页交互的领域。

智能客服与自动化办公场景中,Agent Browser 可以代替人工完成重复性的网页操作,如自动回复邮件、填写表单、查询信息等。

数据采集与监控场景中,Agent Browser 能够处理复杂的动态网页,采集传统爬虫无法获取的数据,并支持定时监控和告警。

自动化测试场景中,Agent Browser 可以模拟真实用户的操作行为,对网页应用进行端到端的功能测试,发现传统单元测试无法覆盖的问题。

无代码/低代码平台场景中,Agent Browser 可以作为执行引擎,配合可视化流程编排工具,让非技术用户也能轻松实现网页自动化。


环境搭建:从零开始配置开发环境

系统要求与依赖

在开始之前,我们需要确保开发环境满足 Agent Browser 的基本要求。

Agent Browser 基于 Node.js 开发,因此首先需要安装 Node.js 运行时。建议使用 Node.js 18 或更高版本,以确保获得最佳的兼容性和性能支持。你可以通过以下命令检查当前 Node.js 的版本:

node --version

如果版本低于 18,建议先升级 Node.js。推荐使用 nvm(Node Version Manager)来管理多个 Node.js 版本,这样可以在不同项目间灵活切换。

除了 Node.js,Agent Browser 还需要 Playwright 作为底层的浏览器自动化引擎。Playwright 支持多种浏览器内核,包括 Chromium(Chrome 的开源版本)、Firefox 和 WebKit。由于 Chromium 在兼容性方面表现最佳且功能最完善,我们主要使用 Chromium 作为默认浏览器。

安装步骤详解

创建项目目录并初始化是最基本的第一步。选择一个合适的目录路径,在其中创建新的项目文件夹:

mkdir agent-browser-demo
cd agent-browser-demo
npm init -y

执行完上述命令后,npm 会自动生成一个基础的 package.json 文件。接下来,我们需要安装 Agent Browser 本身以及相关的依赖包:

npm install @vercel/agent-browser

这个命令会从 npm 官方仓库下载并安装 Agent Browser 及其所有依赖项。安装过程可能需要几分钟时间,取决于网络状况和系统性能。

Playwright 作为 Agent Browser 的底层依赖,通常会在安装过程中自动配置。但如果你遇到浏览器驱动相关的错误,可能需要手动安装浏览器:

npx playwright install chromium

这条命令会下载并安装 Chromium 浏览器及其相关的系统依赖。如果你的系统缺少某些运行时库(如 Linux 上的 libgtk、libnss 等),Playwright 会提示你安装相应的依赖包。

项目结构规划

为了更好地组织代码和资源,建议在项目中建立清晰的文件结构。以下是一个推荐的目录布局:

agent-browser-demo/
├── src/
   ├── index.js          # 入口文件
   ├── examples/         # 示例代码
      ├── basic.js      # 基础功能示例
      ├── search.js     # 搜索功能示例
      └── screenshot.js # 截图功能示例
   └── utils/            # 工具函数
       └── helpers.js
├── screenshots/          # 截图保存目录
├── logs/                 # 日志文件目录
├── package.json
└── README.md

在 src 目录下创建 index.js 作为项目的主入口文件。这个文件将包含 Agent Browser 的核心配置和初始化逻辑:

// Agent Browser 初始化配置示例
// src/index.js

// 导入 Agent Browser 核心模块
const { AgentBrowser } = require('@vercel/agent-browser');

// 创建浏览器实例配置
const browserConfig = {
  headless: true,           // 是否以无头模式运行
  viewport: {
    width: 1920,            // 视口宽度
    height: 1080            // 视口高度
  },
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
  timeout: 30000            // 操作超时时间(毫秒)
};

// 初始化浏览器实例
async function initializeBrowser() {
  const browser = new AgentBrowser(browserConfig);
  await browser.launch();
  return browser;
}

module.exports = { initializeBrowser };

核心功能详解:深入理解 Agent Browser 的设计哲学

Browser 实例管理

Agent Browser 的核心是 Browser 类,它封装了浏览器的所有操作。这个类采用 Promise 链式调用和 async/await 两种编程模式的支持,让异步操作的编写变得简洁直观。

创建浏览器实例是所有操作的第一步。Browser 构造函数接受一个配置对象,其中包含多个可定制选项:

// 完整的浏览器配置示例
const { AgentBrowser } = require('@vercel/agent-browser');

const config = {
  // 运行模式配置
  headless: true,  // true: 无头模式(不显示浏览器窗口)
                   // false: 有头模式(显示浏览器窗口,便于调试)

  // 视口配置 - 决定页面的渲染尺寸
  viewport: {
    width: 1280,   // 建议使用常见的桌面分辨率
    height: 720,
    deviceScaleFactor: 1  // 设备像素比,影响截图清晰度
  },

  // 用户代理字符串 - 标识浏览器身份
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',

  // 网络请求配置
  ignoreHTTPSErrors: false,  // 是否忽略 HTTPS 证书错误
  slowMo: 0,                  // 操作延迟(毫秒),用于慢速录制

  // 超时配置
  timeout: 30000,             // 默认操作超时时间

  // 代理配置
  proxy: null,  // 支持 HTTP 和 SOCKS 代理,格式: { server: 'http://proxy:8080' }

  // 下载行为配置
  downloadPath: './downloads',
  acceptDownloads: true
};

// 创建浏览器实例
const browser = new AgentBrowser(config);

启动和关闭浏览器是必须掌握的基本操作。启动浏览器时会创建多个浏览器上下文(Context),每个上下文相互隔离,可以模拟多个独立的浏览器会话:

// 浏览器生命周期管理
async function manageBrowser() {
  const browser = new AgentBrowser();

  try {
    // 启动浏览器
    await browser.launch();
    console.log('浏览器启动成功');

    // 创建新页面
    const page = await browser.newPage();

    // 执行网页操作...

  } catch (error) {
    console.error('浏览器操作失败:', error);
  } finally {
    // 关闭浏览器,释放资源
    await browser.close();
    console.log('浏览器已关闭');
  }
}

Page 对象与页面操作

Page 对象代表一个浏览器标签页,是执行大多数网页操作的主要接口。它提供了丰富的方法来导航页面、操作元素、获取内容等。

页面导航是最基础的功能。Agent Browser 支持多种导航方式,可以根据不同的场景选择最合适的方法:

// 页面导航的各种方式
async function navigatePages(page) {
  // 直接导航到 URL
  await page.goto('https://github.com');

  // 导航到文件(本地 HTML 文件)
  await page.goto('file:///path/to/local/page.html');

  // 带选项的导航
  await page.goto('https://example.com', {
    waitUntil: 'networkidle',  // 等待网络空闲
    timeout: 30000,            // 超时时间
    referer: 'https://google.com'  // 来源页面
  });

  // 等待特定条件后再导航
  await page.goto('https://example.com');
  await page.waitForSelector('#content', { timeout: 5000 });

  // 前进和后退
  await page.goBack();
  await page.goForward();

  // 刷新页面
  await page.reload();
}

页面状态监控对于处理动态网页非常重要。Agent Browser 提供了多种等待机制,确保操作在正确的时机执行:

// 页面状态等待机制
async function waitForConditions(page) {
  // 等待 DOM 元素出现
  await page.waitForSelector('.login-form', { 
    timeout: 10000,
    state: 'visible'  // visible | hidden | attached | detached
  });

  // 等待函数返回 true
  await page.waitForFunction(() => {
    const element = document.querySelector('.result');
    return element && element.textContent.includes('Success');
  });

  // 等待网络请求完成
  await page.waitForLoadState('networkidle');

  // 等待导航完成
  await page.waitForNavigation({ waitUntil: 'domcontentloaded' }, async () => {
    await page.click('a.next-page');
  });

  // 等待超时时间
  await page.waitForTimeout(2000);  // 等待 2 秒
}

元素定位与操作

元素定位是网页自动化的核心技能。Agent Browser 支持多种定位策略,可以根据页面的特点和需求选择最优的方案。

CSS 选择器是最常用也是最灵活的元素定位方式。它功能强大,语法简洁,几乎可以定位页面上的任何元素:

// CSS 选择器定位元素
async function locateElements(page) {
  // 基本选择器
  const header = await page.$('header');           // 标签名
  const activeItem = await page.$('.nav.active');   // 类名
  const submitBtn = await page.$('#submit-btn');    // ID
  const link = await page.$('a[href*="login"]');    // 属性选择器

  // 组合选择器
  const menuItem = await page.$('nav.menu > ul > li:first-child');
  const disabledInput = await page.$('input[type="text"]:disabled');

  // 选择器组
  const buttons = await page.$$('button.primary, button.secondary');

  // 文本内容选择(Agent Browser 扩展支持)
  const loginLink = await page.getByText('登录');
  const searchBox = await page.getByPlaceholder('搜索...');

  // XPath 选择器(用于复杂定位场景)
  const element = await page.$('xpath=//div[@class="container"]//button');
}

元素交互涵盖了所有常见的用户操作。Agent Browser 将这些操作封装成简洁的方法调用:

// 元素交互操作
async function interactWithElements(page) {
  // 鼠标点击
  await page.click('#submit-button');

  // 双击和右键点击
  await page.dblclick('.file-item');
  await page.click('.context-menu', { button: 'right' });

  // 悬停(Hover)
  await page.hover('.dropdown-trigger');

  // 文本输入
  await page.fill('#search-input', '关键词');
  await page.type('#comment-input', '这是一段文字', { delay: 100 });  // 逐字输入

  // 清空输入框
  await page.fill('#search-input', '');

  // 文件上传
  await page.setInputFiles('#file-upload', '/path/to/file.pdf');

  // 下拉框选择
  await page.selectOption('#country-select', 'China');
  await page.selectOption('#skills-select', ['JavaScript', 'Python']);  // 多选

  // 复选框和单选框
  await page.check('#agree-terms');
  await page.radioCheck('input[name="plan"][value="pro"]');

  // 拖拽操作
  await page.dragAndDrop('.draggable', '.drop-zone');

  // 键盘操作
  await page.keyboard.press('Enter');
  await page.keyboard.down('Shift');
  await page.keyboard.up('Shift');
  await page.keyboard.type('Hello World');
}

表单处理与数据提交

表单是网页中最常见的交互元素,几乎所有需要用户输入数据的场景都离不开表单。Agent Browser 提供了完善的表单处理能力。

基础表单操作是最常见的场景,包括填写各种类型的输入框和提交表单:

// 基础表单操作示例
async function handleForm(page) {
  // 导航到表单页面
  await page.goto('https://example.com/register');

  // 填写文本输入框
  await page.fill('input[name="username"]', 'john_doe');
  await page.fill('input[name="email"]', 'john@example.com');
  await page.fill('input[name="password"]', 'SecurePass123');
  await page.fill('input[name="confirm_password"]', 'SecurePass123');

  // 填写文本域(多行文本)
  await page.fill('textarea[name="bio"]', '我是来自北京的前端开发者,热爱开源项目。');

  // 填写日期输入框
  await page.fill('input[type="date"]', '1995-06-15');

  // 选择下拉选项
  await page.selectOption('select[name="country"]', 'CN');
  await page.selectOption('select[name="timezone"]', { label: '北京时间 (UTC+8)' });

  // 处理复选框
  await page.check('input[name="newsletter"]');
  await page.check('input[name="terms"]');

  // 提交表单
  await page.click('button[type="submit"]');

  // 或者直接提交表单元素
  await page.$eval('form', form => form.submit());
}

复杂表单场景可能涉及动态加载的字段、验证逻辑、表单嵌套等情况:

// 复杂表单处理示例
async function handleComplexForm(page) {
  await page.goto('https://example.com/profile/edit');

  // 等待页面完全加载
  await page.waitForLoadState('networkidle');

  // 处理动态出现的表单字段
  await page.click('#add-phone');
  await page.waitForSelector('#phone-input', { state: 'visible' });
  await page.fill('#phone-input', '+86-138-0000-1234');

  // 处理带验证的输入框
  await page.fill('input[name="phone"]', '13800001234');
  await page.waitForSelector('.phone-valid', { state: 'visible' });

  // 处理文件上传(支持拖拽)
  const fileChooserPromise = page.waitForEvent('filechooser');
  await page.click('#upload-trigger');
  const fileChooser = await fileChooserPromise;
  await fileChooser.setFiles('/path/to/avatar.jpg');

  // 处理表单验证提示
  await page.click('button[type="submit"]');
  const errorMessage = await page.textContent('.error-message');
  console.log('验证错误:', errorMessage);

  // 修正错误并重新提交
  await page.fill('input[name="email"]', 'valid@example.com');
  await page.click('button[type="submit"]');

  // 等待提交结果
  await page.waitForSelector('.success-message', { timeout: 5000 });
  console.log('表单提交成功');
}

截图功能详解

截图是调试网页自动化脚本和记录操作结果的重要功能。Agent Browser 提供了灵活的截图 API,支持多种截图模式和格式。

基础截图操作可以快速捕获当前页面状态:

// 基础截图功能
async function takeScreenshots(page) {
  // 完整页面截图(滚动整个页面)
  await page.screenshot({ 
    path: './screenshots/full-page.png',
    fullPage: true
  });

  // 当前视口截图(只截取可见区域)
  await page.screenshot({ 
    path: './screenshots/viewport.png' 
  });

  // 带选项的截图
  await page.screenshot({
    path: './screenshots/optimized.png',
    type: 'png',           // png | jpeg
    quality: 90,           // 图片质量(1-100)
    omitBackground: false,  // 是否隐藏背景色
    encoding: 'base64'     // binary | base64
  });
}

元素截图可以精确定位并截取页面中的特定区域:

// 元素级别截图
async function elementScreenshots(page) {
  await page.goto('https://example.com/dashboard');

  // 获取特定元素并截取
  const element = await page.$('.chart-container');
  if (element) {
    await element.screenshot({ 
      path: './screenshots/chart.png' 
    });
  }

  // 截取元素并添加边框(用于调试定位是否正确)
  const boundingBox = await page.$eval('#target-element', el => {
    const rect = el.getBoundingClientRect();
    return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
  });

  await page.screenshot({
    path: './screenshots/highlighted.png',
    clip: {
      x: boundingBox.x - 10,
      y: boundingBox.y - 10,
      width: boundingBox.width + 20,
      height: boundingBox.height + 20
    }
  });
}

JavaScript 执行与注入

Agent Browser 允许在页面上下文中执行任意的 JavaScript 代码,这为处理复杂的交互逻辑提供了强大的灵活性。

执行页面脚本可以完成 DOM 操作、数据提取、状态修改等任务:

// JavaScript 执行与数据提取
async function executeJavaScript(page) {
  await page.goto('https://example.com/products');

  // 在页面上下文中执行脚本
  const title = await page.evaluate(() => {
    return document.title;
  });
  console.log('页面标题:', title);

  // 提取页面数据
  const products = await page.evaluate(() => {
    const items = document.querySelectorAll('.product-item');
    return Array.from(items).map(item => ({
      name: item.querySelector('.product-name').textContent,
      price: item.querySelector('.product-price').textContent,
      url: item.querySelector('.product-link').href
    }));
  });
  console.log('产品列表:', JSON.stringify(products, null, 2));

  // 修改页面状态
  await page.evaluate(() => {
    document.body.style.backgroundColor = '#f0f0f0';
    const header = document.querySelector('.header');
    header.style.position = 'fixed';
  });

  // 触发自定义事件
  await page.evaluate(() => {
    const event = new CustomEvent('app:initialized', { detail: { version: '1.0' } });
    window.dispatchEvent(event);
  });
}

高级 JavaScript 注入可以用于模拟复杂的用户交互和处理异步操作:

// 高级 JavaScript 注入技术
async function advancedInjection(page) {
  await page.goto('https://example.com/data-table');

  // 滚动加载更多数据
  await page.evaluate(async () => {
    const scrollContainer = document.querySelector('.scroll-container');
    while (scrollContainer.scrollHeight - scrollContainer.scrollTop <= scrollContainer.clientHeight + 100) {
      scrollContainer.scrollBy(0, 500);
      await new Promise(resolve => setTimeout(resolve, 500));
    }
  });

  // 监听网络请求并记录
  const networkData = await page.evaluate(() => {
    return new Promise(resolve => {
      const requests = [];
      window.addEventListener('fetch', event => {
        requests.push({
          url: event.request.url,
          method: event.request.method
        });
      });
      setTimeout(() => resolve(requests), 3000);
    });
  });

  // 注入第三方库并使用
  await page.evaluate(() => {
    const script = document.createElement('script');
    script.src = 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js';
    document.head.appendChild(script);
  });
  await page.waitForFunction(() => typeof moment !== 'undefined');

  const formattedDate = await page.evaluate(() => {
    return moment().format('YYYY-MM-DD HH:mm:ss');
  });
  console.log('格式化日期:', formattedDate);
}

实战教程:从基础到高级的完整项目示例

项目一:GitHub 仓库信息采集工具

让我们从实际项目开始,首先创建一个 GitHub 仓库信息采集工具。这个工具将演示如何登录 GitHub、导航到特定仓库、采集关键信息。

需求分析:我们需要实现一个能够自动访问 GitHub 页面并采集仓库信息的工具。具体功能包括:获取仓库的基本信息(名称、描述、星标数、Fork 数)、获取最近的提交记录、获取贡献者列表。

代码实现

// src/examples/github-scraper.js
// GitHub 仓库信息采集工具

const { AgentBrowser } = require('@vercel/agent-browser');

// 配置参数
const CONFIG = {
  github: {
    username: process.env.GITHUB_USERNAME || 'your-username',
    token: process.env.GITHUB_TOKEN || 'your-personal-access-token'
  },
  target: {
    owner: 'vercel',
    repo: 'next.js'
  }
};

// 初始化浏览器
async function initBrowser() {
  const browser = new AgentBrowser({
    headless: true,
    viewport: { width: 1920, height: 1080 }
  });
  await browser.launch();
  return browser;
}

// 采集仓库基本信息
async function scrapeRepositoryInfo(page) {
  const url = `https://github.com/${CONFIG.target.owner}/${CONFIG.target.repo}`;
  await page.goto(url, { waitUntil: 'networkidle' });

  // 等待仓库信息加载完成
  await page.waitForSelector('.repository-content', { timeout: 10000 });

  // 提取仓库信息
  const repoInfo = await page.evaluate(() => {
    // 获取仓库名称和所有者
    const fullName = document.querySelector('.public') ? 
      document.querySelector('.public .d-inline-flex').textContent.trim() : '';

    // 获取描述
    const descriptionEl = document.querySelector('[itemprop="description"]');
    const description = descriptionEl ? descriptionEl.textContent.trim() : '';

    // 获取统计数据
    const stats = {};
    document.querySelectorAll('[data-testid="social-count"]').forEach(el => {
      const text = el.textContent.trim();
      const label = el.getAttribute('aria-label') || '';
      stats[label] = text;
    });

    // 获取编程语言
    const languageEl = document.querySelector('[itemprop="programmingLanguage"]');
    const language = languageEl ? languageEl.textContent.trim() : '';

    return {
      fullName,
      description,
      language,
      stars: stats['stars'] || 'N/A',
      forks: stats['forks'] || 'N/A',
      watching: stats['watching'] || 'N/A'
    };
  });

  return repoInfo;
}

// 采集提交记录
async function scrapeCommits(page) {
  const url = `https://github.com/${CONFIG.target.owner}/${CONFIG.target.repo}/commits`;
  await page.goto(url, { waitUntil: 'networkidle' });

  await page.waitForSelector('.commits', { timeout: 10000 });

  const commits = await page.evaluate(() => {
    const items = document.querySelectorAll('.commit');
    return Array.from(items).slice(0, 10).map(item => {
      const messageEl = item.querySelector('.commit-title a');
      const authorEl = item.querySelector('.commit-author');
      const dateEl = item.querySelector('time');

      return {
        message: messageEl ? messageEl.textContent.trim() : '',
        author: authorEl ? authorEl.textContent.trim() : '',
        date: dateEl ? dateEl.getAttribute('datetime') : ''
      };
    });
  });

  return commits;
}

// 采集贡献者列表
async function scrapeContributors(page) {
  const url = `https://github.com/${CONFIG.target.owner}/${CONFIG.target.repo}/graphs/contributors`;
  await page.goto(url, { waitUntil: 'networkidle' });

  await page.waitForSelector('.graph-canvas', { timeout: 15000 });

  // 等待贡献者图表加载
  await page.waitForTimeout(2000);

  const contributors = await page.evaluate(() => {
    const items = document.querySelectorAll('.contribution-row');
    return Array.from(items).slice(0, 20).map(item => {
      const nameEl = item.querySelector('.contributor-name');
      const countEl = item.querySelector('.num');

      return {
        name: nameEl ? nameEl.textContent.trim() : '',
        contributions: countEl ? countEl.textContent.trim() : '0'
      };
    });
  });

  return contributors;
}

// 主函数
async function main() {
  let browser;
  try {
    console.log('='.repeat(50));
    console.log('GitHub 仓库信息采集工具');
    console.log('='.repeat(50));

    browser = await initBrowser();
    const page = await browser.newPage();

    // 采集仓库信息
    console.log('\n正在采集仓库基本信息...');
    const repoInfo = await scrapeRepositoryInfo(page);
    console.log('\n仓库基本信息:');
    console.log('-'.repeat(40));
    console.log(`仓库全名: ${repoInfo.fullName}`);
    console.log(`描述: ${repoInfo.description}`);
    console.log(`编程语言: ${repoInfo.language}`);
    console.log(`星标数: ${repoInfo.stars}`);
    console.log(`Fork 数: ${repoInfo.forks}`);

    // 采集提交记录
    console.log('\n正在采集最近的提交记录...');
    const commits = await scrapeCommits(page);
    console.log('\n最近提交:');
    console.log('-'.repeat(40));
    commits.forEach((commit, index) => {
      console.log(`${index + 1}. [${commit.date}] ${commit.message}`);
      console.log(`   作者: ${commit.author}`);
    });

    // 采集贡献者
    console.log('\n正在采集贡献者信息...');
    const contributors = await scrapeContributors(page);
    console.log('\n活跃贡献者:');
    console.log('-'.repeat(40));
    contributors.slice(0, 10).forEach((contributor, index) => {
      console.log(`${index + 1}. ${contributor.name} - ${contributor.contributions} 次提交`);
    });

    // 保存截图
    await page.screenshot({ 
      path: './screenshots/github-repo.png',
      fullPage: true 
    });
    console.log('\n截图已保存: ./screenshots/github-repo.png');

    console.log('\n' + '='.repeat(50));
    console.log('采集完成!');
    console.log('='.repeat(50));

  } catch (error) {
    console.error('采集过程中发生错误:', error);
  } finally {
    if (browser) {
      await browser.close();
    }
  }
}

// 运行主函数
main().catch(console.error);

运行结果

执行上述脚本后,你将看到类似以下的输出:

==================================================
GitHub 仓库信息采集工具
==================================================

正在采集仓库基本信息...

仓库基本信息:
----------------------------------------
仓库全名: vercel / next.js
描述: The React Framework for the Web
编程语言: TypeScript
星标数: 118k
Fork 数: 26.6k

正在采集最近的提交记录...

最近提交:
----------------------------------------
1. [2024-01-15T10:30:00Z] chore: Update turbopack
   作者: Joe Haddad
2. [2024-01-15T09:15:00Z] fix: Correct HMR handling for edge runtime
   作者: Alex
3. [2024-01-14T18:45:00Z] feat: Add new image optimization API
   作者: Sarah Chen

正在采集贡献者信息...

活跃贡献者:
----------------------------------------
1. Guillermo Rauch - 1234 次提交
2. Timer150 - 892 次提交
3. ijjk - 756 次提交
...

截图已保存: ./screenshots/github-repo.png

==================================================
采集完成!
==================================================

项目二:自动化表单填写与提交

第二个项目将演示如何处理复杂的表单场景,包括动态表单字段、文件上传、表单验证等。

需求分析:创建一个可以自动填写在线表单的工具,我们将以一个模拟的注册表单为例,展示各种表单元素的处理方法。

代码实现

// src/examples/form-automation.js
// 自动化表单填写与提交

const { AgentBrowser } = require('@vercel/agent-browser');

// 表单数据配置
const FORM_DATA = {
  personal: {
    firstName: '明',
    lastName: '张',
    email: 'zhangming@example.com',
    phone: '138-0000-1234',
    birthDate: '1995-06-15',
    gender: 'male'
  },
  account: {
    username: 'zhangming_dev',
    password: 'SecureP@ss123!',
    confirmPassword: 'SecureP@ss123!'
  },
  preferences: {
    newsletter: true,
    notifications: ['email', 'sms'],
    theme: 'dark'
  },
  avatar: './assets/avatar.jpg'
};

// 创建表单 HTML(用于演示)
function generateFormHTML() {
  return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>用户注册表单</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      min-height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 20px;
    }
    .form-container {
      background: white;
      border-radius: 20px;
      box-shadow: 0 20px 60px rgba(0,0,0,0.3);
      padding: 40px;
      max-width: 600px;
      width: 100%;
    }
    h1 {
      text-align: center;
      color: #333;
      margin-bottom: 30px;
    }
    .form-group {
      margin-bottom: 20px;
    }
    label {
      display: block;
      margin-bottom: 8px;
      color: #555;
      font-weight: 500;
    }
    input[type="text"],
    input[type="email"],
    input[type="password"],
    input[type="tel"],
    input[type="date"],
    select,
    textarea {
      width: 100%;
      padding: 12px 16px;
      border: 2px solid #e1e1e1;
      border-radius: 10px;
      font-size: 16px;
      transition: border-color 0.3s;
    }
    input:focus, select:focus, textarea:focus {
      outline: none;
      border-color: #667eea;
    }
    .name-row {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 20px;
    }
    .checkbox-group, .radio-group {
      display: flex;
      gap: 20px;
    }
    .checkbox-group label, .radio-group label {
      display: flex;
      align-items: center;
      gap: 8px;
      font-weight: normal;
    }
    .file-upload {
      border: 2px dashed #ccc;
      border-radius: 10px;
      padding: 30px;
      text-align: center;
      cursor: pointer;
      transition: all 0.3s;
    }
    .file-upload:hover {
      border-color: #667eea;
      background: #f8f8ff;
    }
    .file-upload input { display: none; }
    .file-preview {
      max-width: 100px;
      max-height: 100px;
      margin-top: 10px;
      border-radius: 50%;
    }
    button {
      width: 100%;
      padding: 16px;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      border: none;
      border-radius: 10px;
      font-size: 18px;
      font-weight: 600;
      cursor: pointer;
      transition: transform 0.2s;
    }
    button:hover {
      transform: translateY(-2px);
    }
    button:disabled {
      opacity: 0.6;
      cursor: not-allowed;
    }
    .error {
      border-color: #e74c3c !important;
    }
    .error-message {
      color: #e74c3c;
      font-size: 12px;
      margin-top: 5px;
    }
    .success-message {
      display: none;
      background: #2ecc71;
      color: white;
      padding: 20px;
      border-radius: 10px;
      text-align: center;
    }
  </style>
</head>
<body>
  <div class="form-container">
    <h1>用户注册</h1>
    <form id="registration-form">
      <div class="form-group name-row">
        <div>
          <label for="firstName">名 *</label>
          <input type="text" id="firstName" name="firstName" required>
        </div>
        <div>
          <label for="lastName">姓 *</label>
          <input type="text" id="lastName" name="lastName" required>
        </div>
      </div>

      <div class="form-group">
        <label for="email">邮箱地址 *</label>
        <input type="email" id="email" name="email" placeholder="your@email.com" required>
      </div>

      <div class="form-group">
        <label for="phone">手机号码</label>
        <input type="tel" id="phone" name="phone" placeholder="138-0000-0000">
      </div>

      <div class="form-group">
        <label for="birthDate">出生日期</label>
        <input type="date" id="birthDate" name="birthDate">
      </div>

      <div class="form-group">
        <label for="gender">性别</label>
        <select id="gender" name="gender">
          <option value="">请选择</option>
          <option value="male">男</option>
          <option value="female">女</option>
          <option value="other">其他</option>
        </select>
      </div>

      <div class="form-group">
        <label for="username">用户名 *</label>
        <input type="text" id="username" name="username" minlength="3" required>
      </div>

      <div class="form-group">
        <label for="password">密码 *</label>
        <input type="password" id="password" name="password" minlength="8" required>
        <div class="error-message" id="password-error"></div>
      </div>

      <div class="form-group">
        <label for="confirmPassword">确认密码 *</label>
        <input type="password" id="confirmPassword" name="confirmPassword" required>
      </div>

      <div class="form-group">
        <label>接收通知</label>
        <div class="checkbox-group">
          <label><input type="checkbox" name="newsletter" value="newsletter"> 订阅新闻</label>
          <label><input type="checkbox" name="notifications" value="email"> 邮件通知</label>
          <label><input type="checkbox" name="notifications" value="sms"> 短信通知</label>
        </div>
      </div>

      <div class="form-group">
        <label>头像</label>
        <div class="file-upload" id="avatar-upload">
          <input type="file" id="avatar" name="avatar" accept="image/*">
          <div class="upload-content">
            <div class="upload-icon">📷</div>
            <div class="upload-text">点击上传头像</div>
          </div>
          <img class="file-preview" id="preview" style="display: none;">
        </div>
      </div>

      <button type="submit" id="submit-btn">注册</button>
    </form>

    <div class="success-message" id="success-message">
      🎉 注册成功!欢迎加入我们!
    </div>
  </div>

  <script>
    // 密码验证
    const passwordInput = document.getElementById('password');
    const confirmPasswordInput = document.getElementById('confirmPassword');
    const passwordError = document.getElementById('password-error');

    passwordInput.addEventListener('input', () => {
      const password = passwordInput.value;
      if (password.length < 8) {
        passwordError.textContent = '密码长度至少为 8 个字符';
        passwordInput.classList.add('error');
      } else if (!/[A-Z]/.test(password)) {
        passwordError.textContent = '密码必须包含至少一个大写字母';
        passwordInput.classList.add('error');
      } else if (!/[0-9]/.test(password)) {
        passwordError.textContent = '密码必须包含至少一个数字';
        passwordInput.classList.add('error');
      } else {
        passwordError.textContent = '';
        passwordInput.classList.remove('error');
      }
    });

    confirmPasswordInput.addEventListener('input', () => {
      if (confirmPasswordInput.value !== passwordInput.value) {
        confirmPasswordInput.setCustomValidity('两次输入的密码不匹配');
      } else {
        confirmPasswordInput.setCustomValidity('');
      }
    });

    // 文件上传预览
    const fileInput = document.getElementById('avatar');
    const preview = document.getElementById('preview');
    const uploadContent = document.querySelector('.upload-content');

    fileInput.addEventListener('change', function() {
      const file = this.files[0];
      if (file) {
        const reader = new FileReader();
        reader.onload = function(e) {
          preview.src = e.target.result;
          preview.style.display = 'block';
          uploadContent.style.display = 'none';
        };
        reader.readAsDataURL(file);
      }
    });

    // 表单提交
    const form = document.getElementById('registration-form');
    form.addEventListener('submit', async (e) => {
      e.preventDefault();
      const submitBtn = document.getElementById('submit-btn');
      submitBtn.textContent = '正在提交...';
      submitBtn.disabled = true;

      // 模拟提交延迟
      await new Promise(resolve => setTimeout(resolve, 1500));

      form.style.display = 'none';
      document.getElementById('success-message').style.display = 'block';
    });
  </script>
</body>
</html>
  `;
}

// 自动化填写表单
async function autoFillForm(page) {
  // 加载表单页面
  await page.setContent(generateFormHTML());
  await page.waitForLoadState('domcontentloaded');

  console.log('表单已加载,开始自动填写...\n');

  // 填写姓名
  console.log('填写姓名...');
  await page.fill('#firstName', FORM_DATA.personal.firstName);
  await page.fill('#lastName', FORM_DATA.personal.lastName);

  // 填写邮箱
  console.log('填写邮箱...');
  await page.fill('#email', FORM_DATA.personal.email);

  // 填写手机号码
  console.log('填写手机号码...');
  await page.fill('#phone', FORM_DATA.personal.phone);

  // 填写出生日期
  console.log('填写出生日期...');
  await page.fill('#birthDate', FORM_DATA.personal.birthDate);

  // 选择性别
  console.log('选择性别...');
  await page.selectOption('#gender', FORM_DATA.personal.gender);

  // 填写用户名
  console.log('填写用户名...');
  await page.fill('#username', FORM_DATA.account.username);

  // 填写密码(触发验证)
  console.log('填写密码...');
  await page.fill('#password', FORM_DATA.account.password);
  await page.waitForTimeout(500);  // 等待验证消息

  // 确认密码
  console.log('确认密码...');
  await page.fill('#confirmPassword', FORM_DATA.account.confirmPassword);

  // 处理复选框
  console.log('设置通知偏好...');
  if (FORM_DATA.preferences.newsletter) {
    await page.check('input[name="newsletter"]');
  }

  // 处理多个复选框
  for (const notification of FORM_DATA.preferences.notifications) {
    await page.check(`input[name="notifications"][value="${notification}"]`);
  }

  // 文件上传
  console.log('上传头像...');
  const fileInput = await page.$('#avatar');
  await fileInput.setInputFiles(FORM_DATA.avatar);

  // 等待文件预览加载
  await page.waitForTimeout(500);

  // 截图表单填写完成状态
  await page.screenshot({ 
    path: './screenshots/form-filled.png',
    fullPage: true 
  });
  console.log('表单填写完成截图已保存');

  // 提交表单
  console.log('\n提交表单...');
  await page.click('#submit-btn');

  // 等待提交结果
  await page.waitForSelector('#success-message', { state: 'visible', timeout: 5000 });

  // 截图提交结果
  await page.screenshot({ 
    path: './screenshots/form-success.png',
    fullPage: true 
  });
  console.log('提交成功截图已保存');

  // 获取提交后的数据
  const submittedData = await page.evaluate(() => {
    const formData = new FormData(document.getElementById('registration-form'));
    return Object.fromEntries(formData);
  });

  console.log('\n提交的数据:');
  console.log('-'.repeat(40));
  console.log(JSON.stringify(submittedData, null, 2));
}

// 主函数
async function main() {
  let browser;
  try {
    console.log('='.repeat(50));
    console.log('自动化表单填写演示');
    console.log('='.repeat(50) + '\n');

    browser = new AgentBrowser({ headless: true });
    await browser.launch();
    const page = await browser.newPage();

    await autoFillForm(page);

    console.log('\n' + '='.repeat(50));
    console.log('演示完成!');
    console.log('='.repeat(50));

  } catch (error) {
    console.error('执行过程中发生错误:', error);
    throw error;
  } finally {
    if (browser) {
      await browser.close();
    }
  }
}

// 运行
main().catch(console.error);

项目三:网页自动化测试框架

第三个项目将展示如何将 Agent Browser 集成到自动化测试框架中,实现端到端的功能测试。

需求分析:创建一个可复用的测试框架,支持页面导航验证、元素存在性检查、表单交互测试、断言和报告生成。

代码实现

// src/test-framework/test-runner.js
// Agent Browser 自动化测试框架

const { AgentBrowser } = require('@vercel/agent-browser');
const fs = require('fs');
const path = require('path');

// 测试结果收集器
class TestRunner {
  constructor(options = {}) {
    this.browser = null;
    this.context = null;
    this.page = null;
    this.results = {
      passed: 0,
      failed: 0,
      errors: 0,
      tests: []
    };
    this.screenshotDir = options.screenshotDir || './screenshots';
    this.headless = options.headless !== false;

    // 确保截图目录存在
    if (!fs.existsSync(this.screenshotDir)) {
      fs.mkdirSync(this.screenshotDir, { recursive: true });
    }
  }

  // 初始化浏览器
  async init() {
    this.browser = new AgentBrowser({
      headless: this.headless,
      viewport: { width: 1280, height: 720 }
    });
    await this.browser.launch();
    this.context = this.browser;
    this.page = await this.browser.newPage();
    console.log('浏览器已初始化\n');
  }

  // 关闭浏览器
  async close() {
    if (this.browser) {
      await this.browser.close();
      console.log('\n浏览器已关闭');
    }
  }

  // 导航到指定页面
  async navigate(url, options = {}) {
    const startTime = Date.now();
    try {
      await this.page.goto(url, {
        waitUntil: options.waitUntil || 'networkidle',
        timeout: options.timeout || 30000
      });
      console.log(`✓ 导航到: ${url}`);
      return true;
    } catch (error) {
      console.log(`✗ 导航失败: ${url}`);
      console.log(`  错误: ${error.message}`);
      return false;
    }
  }

  // 等待指定时间
  async wait(ms) {
    await this.page.waitForTimeout(ms);
  }

  // 断言:元素存在
  async assertElementExists(selector, timeout = 5000) {
    const testName = `元素存在: ${selector}`;
    try {
      await this.page.waitForSelector(selector, { timeout });
      this.recordTest(testName, 'passed');
      console.log(`✓ ${testName}`);
      return true;
    } catch (error) {
      this.recordTest(testName, 'failed', error.message);
      console.log(`✗ ${testName}`);
      return false;
    }
  }

  // 断言:元素包含文本
  async assertTextContains(selector, expectedText, timeout = 5000) {
    const testName = `文本包含 "${expectedText}"`;
    try {
      await this.page.waitForSelector(selector, { timeout });
      const actualText = await this.page.textContent(selector);
      if (actualText.includes(expectedText)) {
        this.recordTest(testName, 'passed');
        console.log(`✓ ${testName}`);
        return true;
      } else {
        throw new Error(`期望包含 "${expectedText}",实际为 "${actualText}"`);
      }
    } catch (error) {
      this.recordTest(testName, 'failed', error.message);
      console.log(`✗ ${testName}: ${error.message}`);
      return false;
    }
  }

  // 断言:元素具有指定属性
  async assertAttribute(selector, attribute, expectedValue, timeout = 5000) {
    const testName = `属性验证: ${selector}[${attribute}]`;
    try {
      await this.page.waitForSelector(selector, { timeout });
      const actualValue = await this.page.getAttribute(selector, attribute);
      if (actualValue === expectedValue) {
        this.recordTest(testName, 'passed');
        console.log(`✓ ${testName}`);
        return true;
      } else {
        throw new Error(`期望 "${expectedValue}",实际 "${actualValue}"`);
      }
    } catch (error) {
      this.recordTest(testName, 'failed', error.message);
      console.log(`✗ ${testName}: ${error.message}`);
      return false;
    }
  }

  // 断言:页面标题
  async assertTitle(expectedTitle) {
    const testName = `页面标题: "${expectedTitle}"`;
    try {
      const actualTitle = await this.page.title();
      if (actualTitle === expectedTitle) {
        this.recordTest(testName, 'passed');
        console.log(`✓ ${testName}`);
        return true;
      } else {
        throw new Error(`期望 "${expectedTitle}",实际 "${actualTitle}"`);
      }
    } catch (error) {
      this.recordTest(testName, 'failed', error.message);
      console.log(`✗ ${testName}: ${error.message}`);
      return false;
    }
  }

  // 断言:页面 URL
  async assertUrl(pattern) {
    const testName = `URL 匹配: ${pattern}`;
    try {
      const url = this.page.url();
      const regex = new RegExp(pattern);
      if (regex.test(url)) {
        this.recordTest(testName, 'passed');
        console.log(`✓ ${testName}`);
        return true;
      } else {
        throw new Error(`URL "${url}" 不匹配模式 "${pattern}"`);
      }
    } catch (error) {
      this.recordTest(testName, 'failed', error.message);
      console.log(`✗ ${testName}: ${error.message}`);
      return false;
    }
  }

  // 断言:自定义条件
  async assert(condition, testName) {
    try {
      if (condition) {
        this.recordTest(testName, 'passed');
        console.log(`✓ ${testName}`);
        return true;
      } else {
        throw new Error('条件不满足');
      }
    } catch (error) {
      this.recordTest(testName, 'failed', error.message);
      console.log(`✗ ${testName}: ${error.message}`);
      return false;
    }
  }

  // 点击元素
  async click(selector, options = {}) {
    try {
      await this.page.click(selector, options);
      console.log(`✓ 点击: ${selector}`);
      return true;
    } catch (error) {
      console.log(`✗ 点击失败: ${selector}`);
      console.log(`  错误: ${error.message}`);
      return false;
    }
  }

  // 输入文本
  async type(selector, text, options = {}) {
    try {
      if (options.clear) {
        await this.page.fill(selector, '');
      }
      await this.page.fill(selector, text);
      console.log(`✓ 输入: ${selector} = "${text}"`);
      return true;
    } catch (error) {
      console.log(`✗ 输入失败: ${selector}`);
      console.log(`  错误: ${error.message}`);
      return false;
    }
  }

  // 选择下拉选项
  async selectOption(selector, value) {
    try {
      await this.page.selectOption(selector, value);
      console.log(`✓ 选择: ${selector} = "${value}"`);
      return true;
    } catch (error) {
      console.log(`✗ 选择失败: ${selector}`);
      console.log(`  错误: ${error.message}`);
      return false;
    }
  }

  // 截图
  async screenshot(name) {
    const filename = `${name}-${Date.now()}.png`;
    const filepath = path.join(this.screenshotDir, filename);
    await this.page.screenshot({ path: filepath });
    console.log(`📷 截图已保存: ${filename}`);
    return filepath;
  }

  // 记录测试结果
  recordTest(name, status, error = null) {
    this.results.tests.push({ name, status, error });
    if (status === 'passed') {
      this.results.passed++;
    } else if (status === 'failed') {
      this.results.failed++;
    } else {
      this.results.errors++;
    }
  }

  // 生成报告
  generateReport() {
    console.log('\n' + '='.repeat(50));
    console.log('测试报告');
    console.log('='.repeat(50));
    console.log(`总测试数: ${this.results.tests.length}`);
    console.log(`通过: ${this.results.passed} ✓`);
    console.log(`失败: ${this.results.failed} ✗`);
    console.log(`错误: ${this.results.errors} !`);
    console.log(`通过率: ${((this.results.passed / this.results.tests.length) * 100).toFixed(1)}%`);

    if (this.results.failed > 0) {
      console.log('\n失败详情:');
      console.log('-'.repeat(40));
      this.results.tests
        .filter(t => t.status === 'failed')
        .forEach((test, index) => {
          console.log(`${index + 1}. ${test.name}`);
          console.log(`   错误: ${test.error}`);
        });
    }

    console.log('\n' + '='.repeat(50));
    return this.results;
  }
}

// 创建示例测试页面
function createTestPage() {
  return `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>测试页面 - 产品管理系统</title>
  <style>
    body { font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; }
    .header { border-bottom: 2px solid #333; padding-bottom: 20px; margin-bottom: 30px; }
    .form-group { margin-bottom: 20px; }
    label { display: block; margin-bottom: 5px; font-weight: bold; }
    input, select { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; }
    button { padding: 12px 24px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
    button:hover { background: #0056b3; }
    .product-list { margin-top: 30px; }
    .product-item { padding: 15px; border: 1px solid #ddd; margin-bottom: 10px; border-radius: 4px; }
    .product-name { font-size: 18px; font-weight: bold; color: #333; }
    .product-price { color: #28a745; font-size: 16px; }
    .error { color: #dc3545; font-size: 14px; margin-top: 5px; display: none; }
    .success { color: #28a745; padding: 20px; background: #d4edda; border-radius: 4px; display: none; }
  </style>
</head>
<body>
  <div class="header">
    <h1>产品管理系统</h1>
    <p>简单的产品管理演示页面</p>
  </div>

  <div class="form-section">
    <h2>添加新产品</h2>
    <form id="product-form">
      <div class="form-group">
        <label for="product-name">产品名称 *</label>
        <input type="text" id="product-name" name="productName" required>
        <div class="error" id="name-error">请输入产品名称</div>
      </div>
      <div class="form-group">
        <label for="product-price">产品价格 *</label>
        <input type="number" id="product-price" name="productPrice" min="0" step="0.01" required>
        <div class="error" id="price-error">请输入有效价格</div>
      </div>
      <div class="form-group">
        <label for="product-category">产品类别</label>
        <select id="product-category" name="productCategory">
          <option value="">请选择类别</option>
          <option value="electronics">电子产品</option>
          <option value="clothing">服装</option>
          <option value="food">食品</option>
        </select>
      </div>
      <button type="submit" id="add-product-btn">添加产品</button>
    </form>
  </div>

  <div class="success" id="success-message">
    ✓ 产品添加成功!
  </div>

  <div class="product-list" id="product-list">
    <h2>产品列表</h2>
    <div class="product-item" data-id="1">
      <div class="product-name">MacBook Pro</div>
      <div class="product-price">¥12999.00</div>
    </div>
    <div class="product-item" data-id="2">
      <div class="product-name">iPhone 15</div>
      <div class="product-price">¥6999.00</div>
    </div>
  </div>

  <script>
    const form = document.getElementById('product-form');
    const productList = document.getElementById('product-list');
    const successMessage = document.getElementById('success-message');

    let productIdCounter = 3;

    form.addEventListener('submit', function(e) {
      e.preventDefault();

      const nameInput = document.getElementById('product-name');
      const priceInput = document.getElementById('product-price');
      const categorySelect = document.getElementById('product-category');
      const nameError = document.getElementById('name-error');
      const priceError = document.getElementById('price-error');

      // 清除错误
      nameError.style.display = 'none';
      priceError.style.display = 'none';
      nameInput.style.borderColor = '#ccc';
      priceInput.style.borderColor = '#ccc';

      // 验证
      let hasError = false;
      if (!nameInput.value.trim()) {
        nameError.style.display = 'block';
        nameInput.style.borderColor = '#dc3545';
        hasError = true;
      }
      if (!priceInput.value || parseFloat(priceInput.value) <= 0) {
        priceError.style.display = 'block';
        priceInput.style.borderColor = '#dc3545';
        hasError = true;
      }

      if (hasError) return;

      // 添加产品
      const productItem = document.createElement('div');
      productItem.className = 'product-item';
      productItem.setAttribute('data-id', productIdCounter++);
      productItem.innerHTML = \`
        <div class="product-name">\${nameInput.value}</div>
        <div class="product-price">¥\${parseFloat(priceInput.value).toFixed(2)}</div>
      \`;
      productList.appendChild(productItem);

      // 显示成功消息
      successMessage.style.display = 'block';
      setTimeout(() => {
        successMessage.style.display = 'none';
      }, 3000);

      // 清空表单
      form.reset();
    });
  </script>
</body>
</html>
  `;
}

// 运行测试套件
async function runTests() {
  const runner = new TestRunner({ 
    screenshotDir: './screenshots',
    headless: true 
  });

  try {
    // 初始化
    await runner.init();

    // 加载测试页面
    const testHtml = createTestPage();
    await runner.page.setContent(testHtml);

    console.log('开始执行测试套件\n');
    console.log('-'.repeat(50) + '\n');

    // 测试 1: 页面标题验证
    await runner.assertTitle('测试页面 - 产品管理系统');

    // 测试 2: 关键元素存在性
    await runner.assertElementExists('#product-form');
    await runner.assertElementExists('#product-name');
    await runner.assertElementExists('#product-price');
    await runner.assertElementExists('#add-product-btn');
    await runner.assertElementExists('.product-item');

    // 测试 3: 初始产品列表
    const initialProducts = await runner.page.$$('.product-item');
    await runner.assert(initialProducts.length === 2, `初始产品数量: ${initialProducts.length} === 2`);

    // 测试 4: 填写表单
    await runner.screenshot('form-empty');
    await runner.type('#product-name', 'AirPods Pro', { clear: true });
    await runner.type('#product-price', '1999.00', { clear: true });
    await runner.selectOption('#product-category', 'electronics');
    await runner.screenshot('form-filled');

    // 测试 5: 提交表单
    await runner.click('#add-product-btn');
    await runner.wait(500);

    // 测试 6: 验证产品添加成功
    const updatedProducts = await runner.page.$$('.product-item');
    await runner.assert(updatedProducts.length === 3, `添加后产品数量: ${updatedProducts.length} === 3`);

    // 测试 7: 验证成功消息显示
    await runner.assertElementExists('#success-message');

    // 测试 8: 验证新添加的产品
    await runner.screenshot('after-add');
    const lastProductName = await runner.page.textContent('.product-item:last-child .product-name');
    await runner.assertTextContains('.product-item:last-child .product-name', 'AirPods Pro');
    console.log(`  新产品名称: ${lastProductName}`);

    // 测试 9: 表单验证 - 空名称
    await runner.click('#add-product-btn');
    await runner.wait(300);
    const nameErrorVisible = await runner.page.isVisible('#name-error');
    await runner.assert(nameErrorVisible, '空名称时显示错误提示');

    // 测试 10: 表单验证 - 无效价格
    await runner.type('#product-name', '测试产品', { clear: true });
    await runner.type('#product-price', '-100', { clear: true });
    await runner.click('#add-product-btn');
    await runner.wait(300);
    const priceErrorVisible = await runner.page.isVisible('#price-error');
    await runner.assert(priceErrorVisible, '无效价格时显示错误提示');

    // 生成报告
    const results = runner.generateReport();

    // 保存测试报告
    const reportPath = path.join(runner.screenshotDir, 'test-report.json');
    fs.writeFileSync(reportPath, JSON.stringify(results, null, 2));
    console.log(`\n测试报告已保存: ${reportPath}`);

    return results;

  } catch (error) {
    console.error('测试执行失败:', error);
    throw error;
  } finally {
    await runner.close();
  }
}

// 运行
if (require.main === module) {
  runTests()
    .then(results => {
      const passed = results.failed === 0 && results.errors === 0;
      process.exit(passed ? 0 : 1);
    })
    .catch(() => process.exit(1));
}

module.exports = { TestRunner, runTests };

常见使用场景与进阶技巧

场景一:批量数据采集

在实际项目中,经常需要从多个页面批量采集数据。下面的示例展示了如何高效地完成这类任务:

// 批量数据采集示例
// src/examples/batch-scraping.js

const { AgentBrowser } = require('@vercel/agent-browser');

async function batchScrape() {
  const browser = new AgentBrowser({ headless: true });
  await browser.launch();

  // 目标 URL 列表
  const urls = [
    'https://example.com/product/1',
    'https://example.com/product/2',
    'https://example.com/product/3',
    'https://example.com/product/4',
    'https://example.com/product/5'
  ];

  // 用于存储采集结果
  const results = [];

  // 逐个页面采集
  for (let i = 0; i < urls.length; i++) {
    const page = await browser.newPage();

    try {
      console.log(`正在采集 (${i + 1}/${urls.length}): ${urls[i]}`);

      await page.goto(urls[i], { waitUntil: 'networkidle' });

      // 提取页面数据
      const data = await page.evaluate(() => {
        return {
          title: document.querySelector('h1')?.textContent || '',
          price: document.querySelector('.price')?.textContent || '',
          description: document.querySelector('.description')?.textContent || ''
        };
      });

      results.push({ url: urls[i], ...data });
      console.log(`  ✓ 采集成功: ${data.title}`);

      // 每采集 2 个页面后关闭一些标签页释放内存
      if (i > 0 && i % 2 === 0) {
        await page.close();
      }

    } catch (error) {
      console.log(`  ✗ 采集失败: ${error.message}`);
      results.push({ url: urls[i], error: error.message });
    }
  }

  // 导出结果
  const fs = require('fs');
  fs.writeFileSync('./results.json', JSON.stringify(results, null, 2));
  console.log('\n结果已保存到 results.json');

  await browser.close();
}

batchScrape().catch(console.error);

场景二:处理登录与认证

很多网站需要登录才能访问完整内容。Agent Browser 支持处理各种登录场景:

// 登录处理示例
// src/examples/login-handler.js

const { AgentBrowser } = require('@vercel/agent-browser');

async function handleLogin() {
  const browser = new AgentBrowser({ headless: true });
  await browser.launch();

  const context = await browser.newContext({
    // 保存登录状态到文件
    storageState: './auth-state.json'
  });

  const page = await context.newPage();

  // 检查是否已有保存的登录状态
  const fs = require('fs');
  if (fs.existsSync('./auth-state.json')) {
    console.log('发现已保存的登录状态,加载中...');
    await context.close();
    await browser.close();
    return;  // 可以直接使用保存的状态
  }

  // 执行登录流程
  console.log('开始登录流程...');
  await page.goto('https://example.com/login');
  await page.waitForSelector('#username');

  await page.fill('#username', process.env.USERNAME);
  await page.fill('#password', process.env.PASSWORD);
  await page.click('#login-button');

  // 等待登录完成
  await page.waitForSelector('.user-profile', { timeout: 10000 });
  console.log('登录成功!');

  // 保存登录状态
  await context.storageState({ path: './auth-state.json' });
  console.log('登录状态已保存');

  await browser.close();
}

handleLogin().catch(console.error);

场景三:异常处理与重试机制

网络不稳定或页面加载异常是常见问题,健壮的自动化脚本需要完善的异常处理:

// 异常处理与重试机制
// src/examples/retry-handler.js

const { AgentBrowser } = require('@vercel/agent-browser');

// 重试装饰器函数
async function withRetry(fn, options = {}) {
  const maxAttempts = options.maxAttempts || 3;
  const retryDelay = options.retryDelay || 1000;
  const retryCondition = options.retryCondition || (() => true);

  let lastError;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      console.log(`尝试 ${attempt}/${maxAttempts} 失败: ${error.message}`);

      if (attempt < maxAttempts && retryCondition(error)) {
        console.log(`等待 ${retryDelay}ms 后重试...`);
        await new Promise(resolve => setTimeout(resolve, retryDelay));
      }
    }
  }

  throw lastError;
}

// 使用示例
async function robustScrape(url) {
  const browser = new AgentBrowser({ headless: true });
  await browser.launch();

  try {
    const page = await browser.newPage();

    // 包装导航操作
    await withRetry(
      () => page.goto(url, { waitUntil: 'networkidle' }),
      {
        maxAttempts: 3,
        retryDelay: 2000,
        retryCondition: error => error.message.includes('Timeout')
      }
    );

    // 包装元素等待
    await withRetry(
      () => page.waitForSelector('.content', { timeout: 5000 }),
      { maxAttempts: 5, retryDelay: 1000 }
    );

    const data = await page.evaluate(() => {
      return document.querySelector('.content')?.textContent;
    });

    return data;

  } finally {
    await browser.close();
  }
}

module.exports = { withRetry, robustScrape };

最佳实践与性能优化

代码组织最佳实践

良好的代码组织不仅提高可维护性,还能增强代码的可测试性和复用性:

// src/utils/page-actions.js
// 封装可复用的页面操作

class PageActions {
  constructor(page) {
    this.page = page;
  }

  // 安全点击(带重试)
  async safeClick(selector, options = {}) {
    const maxAttempts = options.maxAttempts || 3;

    for (let i = 0; i < maxAttempts; i++) {
      try {
        await this.page.waitForSelector(selector, { state: 'visible', timeout: 5000 });
        await this.page.click(selector);
        return true;
      } catch (error) {
        if (i === maxAttempts - 1) throw error;
        await this.page.waitForTimeout(500);
      }
    }
    return false;
  }

  // 滚动到元素并可见
  async scrollIntoView(selector) {
    await this.page.evaluate((sel) => {
      const element = document.querySelector(sel);
      if (element) element.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }, selector);
    await this.page.waitForTimeout(300);
  }

  // 清空并输入文本
  async clearAndType(selector, text) {
    await this.page.click(selector, { clickCount: 3 });  // 全选
    await this.page.press('Backspace');  // 删除
    await this.page.type(selector, text);
  }

  // 等待元素消失
  async waitForElementHidden(selector, timeout = 5000) {
    await this.page.waitForSelector(selector, { state: 'hidden', timeout });
  }

  // 等待页面稳定(无网络活动)
  async waitForPageIdle(timeout = 3000) {
    await this.page.waitForLoadState('networkidle');
    await this.page.waitForTimeout(timeout);
  }
}

module.exports = { PageActions };

性能优化技巧

在处理大量页面时,性能优化尤为重要:

// 性能优化示例
// src/examples/performance-optimization.js

async function optimizedScraping() {
  const browser = new AgentBrowser({
    headless: true,
    // 禁用图片加载加速
    // 注意:这可能影响依赖图片的页面
  });

  await browser.launch();

  // 创建一个共享的上下文
  const context = await browser.newContext({
    // 禁用图片
    // 注意:现代网页可能依赖图片内容
    // viewport: { width: 1280, height: 720 }
  });

  // 预创建多个页面实例
  const pageCount = 5;
  const pages = await Promise.all(
    Array(pageCount).fill(null).map(() => context.newPage())
  );

  // 任务队列
  const urls = ['https://example.com/page1', 'https://example.com/page2', /* ... */];
  const results = [];

  // 并发处理
  const batchSize = 3;
  for (let i = 0; i < urls.length; i += batchSize) {
    const batch = urls.slice(i, i + batchSize);
    const batchPromises = batch.map((url, index) => processPage(pages[index], url));
    const batchResults = await Promise.all(batchPromises);
    results.push(...batchResults);
  }

  // 关闭页面释放资源
  await Promise.all(pages.map(page => page.close()));
  await browser.close();

  return results;
}

async function processPage(page, url) {
  await page.goto(url, { waitUntil: 'domcontentloaded' });  // 使用更快的加载策略
  return await page.evaluate(() => {
    // 提取数据
    return { title: document.title };
  });
}

module.exports = { optimizedScraping };

调试技巧

调试自动化脚本时,有效的调试技巧可以大大加快问题定位:

// 调试配置示例
// src/examples/debug-config.js

const { AgentBrowser } = require('@vercel/agent-browser');

// 使用有头模式便于调试
async function debugSession() {
  const browser = new AgentBrowser({
    headless: false,  // 显示浏览器窗口
    slowMo: 100,       // 操作延迟 100ms,便于观察
    devtools: true     // 打开开发者工具
  });

  await browser.launch();
  const page = await browser.newPage();

  // 开启详细日志
  page.on('console', msg => {
    console.log('[浏览器控制台]', msg.type(), msg.text());
  });

  page.on('pageerror', error => {
    console.error('[页面错误]', error.message);
  });

  page.on('request', request => {
    console.log('[请求]', request.url().substring(0, 100));
  });

  page.on('response', response => {
    if (response.status() >= 400) {
      console.log('[错误响应]', response.status(), response.url());
    }
  });

  // 你的测试代码...
  await page.goto('https://example.com');

  // 任意时刻截图
  await page.screenshot({ path: './debug-screenshot.png' });

  // 导出页面内容
  const content = await page.content();
  require('fs').writeFileSync('./debug-page.html', content);

  await browser.close();
}

module.exports = { debugSession };

与其他工具的集成

集成到 AI Agent 系统

Agent Browser 的核心价值在于让 AI Agent 能够与真实网页交互。以下是一个简化的 AI Agent 集成示例:

// src/examples/ai-agent-integration.js
// 将 Agent Browser 集成到 AI Agent 系统

const { AgentBrowser } = require('@vercel/agent-browser');

// 简化的 AI 指令生成器
// 实际项目中可以使用 GPT-4、Claude 等大模型
class AIAgent {
  constructor(browser) {
    this.browser = browser;
    this.page = null;
  }

  async init() {
    this.page = await this.browser.newPage();
  }

  // 根据自然语言指令执行操作
  async execute(command) {
    console.log(`执行指令: ${command}`);

    // 将指令转换为可执行的操作序列
    const actions = this.parseCommand(command);

    for (const action of actions) {
      await this.performAction(action);
    }

    // 返回执行结果
    return {
      success: true,
      screenshot: await this.takeSnapshot(),
      message: '任务完成'
    };
  }

  // 简化的命令解析(实际应用中应使用 LLM)
  parseCommand(command) {
    const lowerCommand = command.toLowerCase();

    if (lowerCommand.includes('打开') || lowerCommand.includes('goto')) {
      const url = this.extractUrl(command);
      return [{ type: 'goto', url }];
    }

    if (lowerCommand.includes('点击')) {
      const selector = this.extractSelector(command);
      return [{ type: 'click', selector }];
    }

    if (lowerCommand.includes('输入')) {
      const { selector, text } = this.extractInput(command);
      return [{ type: 'input', selector, text }];
    }

    if (lowerCommand.includes('截图')) {
      return [{ type: 'screenshot' }];
    }

    return [];
  }

  async performAction(action) {
    switch (action.type) {
      case 'goto':
        await this.page.goto(action.url, { waitUntil: 'networkidle' });
        console.log(`  已打开: ${action.url}`);
        break;

      case 'click':
        await this.page.click(action.selector);
        console.log(`  已点击: ${action.selector}`);
        break;

      case 'input':
        await this.page.fill(action.selector, action.text);
        console.log(`  已输入: ${action.selector} = "${action.text}"`);
        break;

      case 'screenshot':
        await this.takeSnapshot();
        break;
    }
  }

  async takeSnapshot() {
    const timestamp = Date.now();
    const path = `./screenshots/agent-${timestamp}.png`;
    await this.page.screenshot({ path, fullPage: true });
    console.log(`  截图已保存: ${path}`);
    return path;
  }

  // 辅助方法
  extractUrl(command) {
    const urlMatch = command.match(/https?:\/\/[^\s]+/);
    return urlMatch ? urlMatch[0] : 'https://example.com';
  }

  extractSelector(command) {
    // 简化的选择器提取
    const text = command.replace(/点击|click/g, '').trim();
    return `text=${text}`;
  }

  extractInput(command) {
    // 简化的输入提取
    return {
      selector: 'input',
      text: command.split('为')[1] || ''
    };
  }
}

// 使用示例
async function main() {
  const browser = new AgentBrowser({ headless: false });
  await browser.launch();

  const agent = new AIAgent(browser);
  await agent.init();

  // 执行一系列 AI 指令
  await agent.execute('打开 https://github.com');
  await agent.execute('点击 Sign in');
  await agent.execute('输入用户名为 test');
  await agent.execute('截图');

  console.log('\n演示完成');
  await browser.close();
}

main().catch(console.error);

集成到现有测试框架

如果你正在使用 Jest、Mocha 或其他测试框架,可以轻松集成 Agent Browser:

// src/test/examples.test.js
// Jest 测试集成示例

const { AgentBrowser } = require('@vercel/agent-browser');

// Jest 测试套件
describe('产品页面测试', () => {
  let browser;
  let page;

  // 测试前初始化
  beforeAll(async () => {
    browser = new AgentBrowser({ headless: true });
    await browser.launch();
  });

  // 测试后清理
  afterAll(async () => {
    if (browser) {
      await browser.close();
    }
  });

  // 每个测试前创建新页面
  beforeEach(async () => {
    page = await browser.newPage();
  });

  // 每个测试后关闭页面
  afterEach(async () => {
    if (page) {
      await page.close();
    }
  });

  test('页面标题正确', async () => {
    await page.goto('https://example.com');
    await expect(page).toHaveTitle(/Example/);
  });

  test('产品列表不为空', async () => {
    await page.goto('https://example.com/products');
    await page.waitForSelector('.product-item');
    const items = await page.$$('.product-item');
    expect(items.length).toBeGreaterThan(0);
  });

  test('可以添加产品到购物车', async () => {
    await page.goto('https://example.com/product/1');
    await page.click('#add-to-cart');
    await page.waitForSelector('.cart-badge');
    const badge = await page.textContent('.cart-badge');
    expect(badge).toBe('1');
  });
});

总结与展望

通过本文的详细讲解,你应该已经掌握了 Vercel Agent Browser 的核心概念和实战技巧。从基础的环境搭建,到页面导航和元素操作,再到复杂的表单处理和批量数据采集,Agent Browser 提供了一套完整且优雅的解决方案。

核心要点回顾

Agent Browser 的核心优势在于它提供了简洁直观的 API,将复杂的浏览器自动化操作封装成易于理解的方法调用。无论是导航、元素定位、交互操作还是截图,你都可以用最少的代码实现最强大的功能。

多上下文管理让你能够模拟多个独立的浏览器会话,适合需要并行处理的场景。而完善的异常处理机制和等待策略,则确保了你的自动化脚本能够稳定运行。

在实际应用中,Agent Browser 的价值不仅限于自动化测试和数据采集。它更大的潜力在于与 AI Agent 的结合——当 AI 能够像人类一样操控浏览器时,许多以前无法自动化的业务流程都将变得可能。

项目资源链接

以下是本文涉及的相关资源和深入学习的建议:

Vercel Agent Browser 官方仓库:https://github.com/vercel-labs/agent-browser

Playwright 官方文档(Agent Browser 基于此构建):https://playwright.dev/docs/intro

MDN Web Docs CSS 选择器参考:https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Selectors

如果你对 AI Agent 和浏览器自动化感兴趣,还可以关注以下相关项目:

Playwright(浏览器自动化基础框架)

Puppeteer(Google 开发的 Node.js 浏览器自动化库)

Selenium(跨浏览器的自动化测试框架)

AutoGPT(自主 AI Agent 实验项目)

LangChain(构建 LLM 应用的开发框架)

下一步学习建议

在掌握了本文内容后,你可以尝试以下进阶方向:深入学习 Playwright 的高级特性,如网络拦截和服务端请求模拟;探索 Agent Browser 与大语言模型的结合,构建真正智能的 AI Agent;学习设计模式在自动化项目中的应用,提高代码的可维护性和可扩展性;尝试将 Agent Browser 集成到你自己的项目中,解决实际问题。

希望本文能够帮助你在 AI Agent 和浏览器自动化的道路上迈出坚实的一步。技术的世界日新月异,保持学习的热情,你一定能在这个领域取得更大的成就!


本文档会持续更新,欢迎关注获取最新内容。如果有任何问题或建议,欢迎在评论区留言讨论。

如果内容对您有帮助,欢迎打赏

您的支持是我继续创作的动力

前往打赏页面

评论区

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注