浏览器自动化:让AI替你上网 ——CogitoAgent开发实战(九)
浏览器自动化:让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。
原因:
- 跨浏览器:支持Chromium、Firefox、WebKit
- API设计现代:基于Promise,天然支持async/await
- 自动等待:元素出现前自动等待,减少手动sleep
- 截图、下载、网络监控开箱即用
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();
// ...
}
查找策略的优先级:
- CSS选择器(最精确)
- XPath(精确但复杂)
- 文本匹配(最自然)
- 属性匹配(兜底)
3.3 为什么AI能使用自然语言查找?
当用户说“点击登录按钮”,AI调用工具时传的是 clickElement("登录按钮")。
程序会尝试:
page.$("登录按钮")→ 找不到(不是有效的CSS选择器)- 文本匹配 →
page.locator("text=登录按钮")→ 找到包含“登录按钮”文本的元素
所以,AI不需要记住CSS选择器的语法,只需要描述元素的文本内容即可。
四、页面状态捕获与变化对比
4.1 为什么需要状态快照?
用户执行一系列操作后,会问:“页面发生了什么变化?”
程序需要知道:
- 操作前的页面状态
- 操作后的页面状态
- 两者之间的差异
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 → 文本 → 属性(多策略) |
| 状态对比 | 操作前后快照对比 |
| 核心工具 | 点击、填表、截图、搜索、抓取 |
核心设计原则:
- 让AI能用自然语言描述元素(文本匹配)
- 每次操作后展示页面变化
- 失败时给出清晰错误信息
下一篇预告:动态工具注册与插件化架构
我们将深入 registry.js,看看:
- 如何用注册表替代switch-case
- 工具的声明式配置
- 如何让用户自定义工具
如果这篇文章对你有帮助,欢迎 ⭐Star 支持一下开源项目!
更多推荐




所有评论(0)