浏览器自动化:让AI替你上网

——CogitoAgent开发实战(第9篇)

📖 本文是专栏的第九篇。前八篇我们让AI学会了思考、动手、管理文件、联网、说话、记忆、写代码、操作Git。但还有一个重要的能力没有覆盖——让AI像人一样操作浏览器。点击按钮、填写表单、搜索信息、截图保存……这一篇,我们深入浏览器自动化模块,看看AI如何通过Playwright控制真实的浏览器。

在这里插入图片描述


📌 从一个场景开始

你想让AI帮你做一件事:“帮我查一下今天的新闻,然后截图保存。”

如果AI只能调用API,它做不到。因为新闻网站的数据不是通过API提供的,而是通过HTML页面展示的。

但如果你让AI打开浏览器,像人一样访问网页、读取内容、截图保存,它就能做到。

这就是浏览器自动化的价值:让AI能够操作真实浏览器,处理那些没有提供API的网站。


一、为什么选择Playwright?

1.1 几种浏览器自动化方案

方案 优点 缺点
Puppeteer Chrome官方,生态成熟 只支持Chromium
Playwright 跨浏览器,API更现代 稍大(但可接受)
Selenium 老牌,支持多语言 慢,配置复杂

CogitoAgent选择了Playwright

原因

  1. 跨浏览器:支持Chromium、Firefox、WebKit
  2. API设计现代:基于Promise,天然支持async/await
  3. 自动等待:元素出现前自动等待,减少手动sleep
  4. 截图、下载、网络监控开箱即用

1.2 延迟加载策略

Playwright 是一个重型依赖(安装时会下载浏览器二进制文件)。CogitoAgent 采用延迟加载:只在用户首次调用浏览器工具时才加载 Playwright。

// browser.js
let chromium = null;

async function checkPlaywright() {
  if (chromium) return true;
  
  try {
    const { chromium: chromiumModule } = await import('playwright');
    chromium = chromiumModule;
    return true;
  } catch (err) {
    return false;
  }
}

async function initBrowser(url) {
  if (!await checkPlaywright()) {
    return { 
      success: false, 
      error: 'playwright 未安装,请先安装:npm install playwright' 
    };
  }
  // ... 初始化浏览器
}

好处

  • 不使用浏览器功能的用户无需安装Playwright
  • 启动速度更快(不加载未使用的模块)
  • 依赖失败时给出清晰提示

二、浏览器实例管理

2.1 全局状态

let browser = null;
let page = null;
let pageStateSnapshot = null;

这三个全局变量管理浏览器的生命周期:

  • browser:浏览器实例(一个进程)
  • page:当前页面(可以多个,但本项目只用一个)
  • pageStateSnapshot:页面状态快照(用于对比变化)

2.2 初始化浏览器

async function initBrowser(url) {
  // 1. 关闭旧实例(如果有)
  if (browser) {
    try { await browser.close(); } catch (e) {}
    browser = null;
    page = null;
  }
  
  // 2. 启动新浏览器
  browser = await chromium.launch({
    headless: false,              // 显示窗口(用户能看到)
    args: ['--start-maximized']   // 最大化窗口
  });
  
  // 3. 创建上下文(隔离的浏览器环境)
  const context = await browser.newContext({
    viewport: { width: 1920, height: 1080 },
    acceptDownloads: true,
    downloadsPath: './downloads'
  });
  page = await context.newPage();
  
  // 4. 导航到目标URL
  await page.goto(url, { waitUntil: 'networkidle', timeout: 30000 });
  
  // 5. 捕获初始状态
  pageStateSnapshot = await capturePageState();
  
  return { success: true, data: `已成功打开网页:${url}` };
}

关键参数

  • headless: false:显示浏览器窗口,用户能看到AI在做什么
  • waitUntil: 'networkidle':等待网络请求完成后再继续
  • timeout: 30000:30秒超时

三、智能元素查找

3.1 问题:如何让AI“找到”页面上的元素?

用户说“点击登录按钮”,AI需要找到页面上那个叫“登录”的按钮。

但页面上的元素有无数种表示方式:

  • CSS选择器:#login-btn
  • XPath://button[text()='登录']
  • 文本内容:登录
  • 属性:name="login"

AI不应该记住这些技术细节,它只需要说“我想点击登录按钮”,程序自动找到对应的元素。

3.2 多策略查找

async function clickElement(selector, description = '') {
  // 策略1:CSS选择器
  try {
    element = await page.$(selector);
  } catch (e) { /* 不是有效的CSS选择器 */ }
  
  // 策略2:XPath
  if (!element && (selector.startsWith('//') || selector.startsWith('id('))) {
    element = await page.$(`xpath=${selector}`);
  }
  
  // 策略3:文本匹配
  if (!element) {
    element = await page.locator(`text=${selector.trim()}`).first();
  }
  
  // 策略4:属性匹配
  if (!element) {
    const selectors = [
      `[id="${selector}"]`,
      `[name="${selector}"]`,
      `[placeholder="${selector}"]`,
      `button:has-text("${selector}")`,
      `a:has-text("${selector}")`,
    ];
    for (const sel of selectors) {
      element = await page.$(sel);
      if (element) break;
    }
  }
  
  // 如果还是找不到,报错
  if (!element) {
    return { success: false, error: `未找到元素:${selector}` };
  }
  
  // 执行点击
  await element.click();
  // ...
}

查找策略的优先级

  1. CSS选择器(最精确)
  2. XPath(精确但复杂)
  3. 文本匹配(最自然)
  4. 属性匹配(兜底)

3.3 为什么AI能使用自然语言查找?

当用户说“点击登录按钮”,AI调用工具时传的是 clickElement("登录按钮")

程序会尝试:

  1. page.$("登录按钮") → 找不到(不是有效的CSS选择器)
  2. 文本匹配 → page.locator("text=登录按钮") → 找到包含“登录按钮”文本的元素

所以,AI不需要记住CSS选择器的语法,只需要描述元素的文本内容即可。


四、页面状态捕获与变化对比

4.1 为什么需要状态快照?

用户执行一系列操作后,会问:“页面发生了什么变化?”

程序需要知道:

  1. 操作前的页面状态
  2. 操作后的页面状态
  3. 两者之间的差异

4.2 捕获页面状态

async function capturePageState() {
  const state = await page.evaluate(() => {
    const forms = [];
    document.querySelectorAll('form, input, select, textarea, button').forEach((el) => {
      forms.push({
        tag: el.tagName.toLowerCase(),
        id: el.id || null,
        name: el.name || null,
        type: el.type || null,
        value: el.value || null,
        placeholder: el.placeholder || null,
        xpath: getXPath(el)
      });
    });
    
    return {
      url: window.location.href,
      title: document.title,
      forms: forms.slice(0, 50),
      bodyText: document.body.innerText.slice(0, 2000)
    };
  });
  
  return state;
}

捕获的内容

  • URL(页面地址变化)
  • 标题(页面标题变化)
  • 表单元素(输入框的值变化)
  • 正文文本(内容变化)

4.3 对比变化

function comparePageState(oldState, newState) {
  const changes = [];
  
  // URL变化
  if (oldState.url !== newState.url) {
    changes.push(`URL变化:${oldState.url}${newState.url}`);
  }
  
  // 标题变化
  if (oldState.title !== newState.title) {
    changes.push(`标题变化:${oldState.title}${newState.title}`);
  }
  
  // 表单值变化
  const oldValues = new Map(oldState.forms.map(f => [f.xpath, f.value]));
  const newValues = new Map(newState.forms.map(f => [f.xpath, f.value]));
  
  for (const [xpath, newValue] of newValues) {
    const oldValue = oldValues.get(xpath);
    if (oldValue !== newValue) {
      changes.push(`表单变化:${xpath} 值从 "${oldValue}" 变为 "${newValue}"`);
    }
  }
  
  return changes.length > 0 ? changes.join('\n') : '无明显变化';
}

五、核心浏览器工具

5.1 点击元素

async function clickElement(selector, description = '') {
  // 1. 查找元素
  const element = await findElement(selector);
  if (!element) {
    return { success: false, error: `未找到元素:${selector}` };
  }
  
  // 2. 点击
  await element.click();
  await page.waitForLoadState('networkidle', { timeout: 5000 });
  
  // 3. 捕获变化
  const newState = await capturePageState();
  const changes = comparePageState(pageStateSnapshot, newState);
  pageStateSnapshot = newState;
  
  return { 
    success: true, 
    data: `成功点击元素:${selector}\n${changes}` 
  };
}

5.2 填写表单

async function fillField(selector, value, description = '') {
  const element = await findElement(selector);
  if (!element) {
    return { success: false, error: `未找到输入框:${selector}` };
  }
  
  await element.fill(value);
  
  const newState = await capturePageState();
  const changes = comparePageState(pageStateSnapshot, newState);
  pageStateSnapshot = newState;
  
  return { 
    success: true, 
    data: `成功填写字段:${selector} = "${value}"\n${changes}` 
  };
}

5.3 选择下拉框

async function selectOption(selector, value) {
  await page.selectOption(selector, value);
  
  const newState = await capturePageState();
  const changes = comparePageState(pageStateSnapshot, newState);
  pageStateSnapshot = newState;
  
  return { 
    success: true, 
    data: `成功选择选项:${selector} = "${value}"\n${changes}` 
  };
}

5.4 截图

async function takeScreenshot(name = 'screenshot') {
  const screenshotPath = `${name}-${Date.now()}.png`;
  await page.screenshot({ path: screenshotPath, fullPage: true });
  return { 
    success: true, 
    data: `截图已保存:${screenshotPath}` 
  };
}

5.5 获取页面内容

async function getPageContent() {
  const content = await page.evaluate(() => {
    return {
      url: window.location.href,
      title: document.title,
      bodyText: document.body.innerText.slice(0, 3000),
      forms: Array.from(document.querySelectorAll('input, select, textarea')).map(el => ({
        name: el.name,
        id: el.id,
        type: el.type,
        value: el.value,
        placeholder: el.placeholder
      })),
      tables: Array.from(document.querySelectorAll('table')).map(table => {
        // 提取表格数据...
      })
    };
  });
  
  return { success: true, data: content };
}

5.6 搜索引擎搜索

async function searchOnEngine(query, engine = 'bing') {
  // 1. 打开搜索引擎
  const searchUrl = engines[engine.toLowerCase()] || engines['baidu'];
  await initBrowser(searchUrl);
  
  // 2. 查找搜索框(多种选择器策略)
  const searchInputSelectors = [
    'input[name="wd"]',  // 百度
    '#kw',               // 百度
    'input[name="q"]',   // Google/Bing
    'input[type="search"]',
    'input[placeholder*="搜索"]',
    // ... 更多
  ];
  
  let searchInput = null;
  for (const selector of searchInputSelectors) {
    searchInput = await page.$(selector);
    if (searchInput) break;
  }
  
  // 3. 填写搜索词
  await searchInput.fill(query);
  
  // 4. 提交搜索
  const submitButton = await page.$('input[type="submit"]');
  if (submitButton) {
    await submitButton.click();
  } else {
    await searchInput.press('Enter');
  }
  
  // 5. 等待结果加载
  await page.waitForLoadState('networkidle');
  
  // 6. 提取搜索结果
  const searchResults = await page.evaluate(() => {
    const results = [];
    document.querySelectorAll('a[href]').forEach(link => {
      const text = link.innerText.trim();
      if (text && text.length > 5 && text.length < 200) {
        results.push({ title: text, url: link.href });
      }
    });
    return results.slice(0, 15);
  });
  
  return { success: true, data: searchResults };
}

六、AI如何使用浏览器工具?

6.1 系统提示词中的说明

## 浏览器自动化工具
- initBrowser(url) - 启动浏览器并打开指定网址
- clickElement(selector, description) - 点击网页元素(支持CSS选择器、XPath、文本)
- fillField(selector, value, description) - 填写表单字段
- selectOption(selector, value) - 选择下拉框选项
- viewChanges() - 查看页面变化
- getPageContent() - 获取页面内容
- takeScreenshot(name) - 截图当前页面
- closeBrowser() - 关闭浏览器
- searchOnPage(text, description) - 在页面内搜索文本
- findElements(selector, description) - 查找页面元素
- searchOnEngine(query, engine) - 在搜索引擎中自动搜索
- downloadFile(urlOrSelector, description, options) - 下载文件

6.2 典型工作流

场景:搜索信息并截图

用户:帮我搜索一下今天的新闻,然后截图保存。

AI:[TOOL] searchOnEngine("2025年6月 新闻", "bing") [/TOOL]
AI:已搜索,找到15条结果。

AI:[TOOL] clickElement("第一条结果") [/TOOL]
AI:已点击第一条结果。

AI:[TOOL] getPageContent() [/TOOL]
AI:页面标题是“2025年6月19日 新闻摘要”,正文包含...

AI:[TOOL] takeScreenshot("新闻截图") [/TOOL]
AI:截图已保存为 新闻截图-1710508800000.png

七、设计决策回顾

决策 原因
延迟加载Playwright 不使用浏览器功能的用户无需安装
显示浏览器窗口 用户能看到AI在做什么,增加信任
多策略元素查找 AI不需要记住选择器语法
状态快照对比 用户能知道页面发生了什么变化
统一错误信息 AI能理解错误并尝试其他方法

八、小结

这一篇讲了浏览器自动化的实现:

功能 实现
浏览器管理 Playwright 延迟加载 + 全局实例
元素查找 CSS → XPath → 文本 → 属性(多策略)
状态对比 操作前后快照对比
核心工具 点击、填表、截图、搜索、抓取

核心设计原则

  1. 让AI能用自然语言描述元素(文本匹配)
  2. 每次操作后展示页面变化
  3. 失败时给出清晰错误信息

下一篇预告:动态工具注册与插件化架构

我们将深入 registry.js,看看:

  • 如何用注册表替代switch-case
  • 工具的声明式配置
  • 如何让用户自定义工具

如果这篇文章对你有帮助,欢迎 ⭐Star 支持一下开源项目!

👉 https://gitee.com/cnt-code/cogito-agent 👈

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐