Dev Browser Skill
浏览器自动化,能够跨脚本执行保持页面状态。编写小而专注的脚本逐步完成任务。一旦您验证了某个工作流的部分内容并且有重复工作需要完成,您可以编写一个脚本以单次执行完成重复工作。
选择方法
- 本地/源代码可访问的站点:先阅读源代码以直接编写选择器
- 未知页面布局:使用
getAISnapshot()发现元素,然后使用selectSnapshotRef()与之交互 - 视觉反馈:截图以查看用户所见内容
设置
提供两种模式。如果不确定使用哪种,请询问用户。
独立模式(默认)
启动一个新的 Chromium 浏览器以进行全新的自动化会话。
./skills/dev-browser/server.sh &
如果用户请求,添加 --headless 标志。在运行脚本之前等待 Ready 消息。
扩展模式
连接到用户现有的 Chrome 浏览器。在以下情况下使用:
- 用户已登录某些网站,希望您在其认证体验下执行操作(非本地开发环境)。
- 用户要求您使用扩展。
重要:核心流程仍然相同。您在他们的浏览器中创建命名页面。
启动中继服务器:
cd skills/dev-browser && npm i && npm run start-extension &
等待控制台中出现 Waiting for extension to connect... 后跟 Extension connected。这表明客户端已连接,浏览器已准备好被控制。
工作流程:
- 脚本调用
client.page("name"),就像普通模式一样创建新页面/连接到现有页面。 - 自动化在用户的实际浏览器会话上运行
如果扩展尚未连接,请告知用户启动并激活它。下载链接:https://github.com/SawyerHood/dev-browser/releases
编写脚本
所有脚本从
skills/dev-browser/目录运行。@/导入别名要求此目录的配置。
使用 heredoc 内联执行脚本:
cd skills/dev-browser && npx tsx <<'EOF'
import { connect, waitForPageLoad } from "@/client.js";
const client = await connect();
// 创建具有自定义视口大小的页面(可选)
const page = await client.page("example", { viewport: { width: 1920, height: 1080 } });
await page.goto("https://example.com");
await waitForPageLoad(page);
console.log({ title: await page.title(), url: page.url() });
await client.disconnect();
EOF
仅当脚本需要重用、复杂或用户明确请求时才写入 tmp/ 文件。
关键原则
- 小脚本:每个脚本只做一件事(导航、点击、填写、检查)
- 评估状态:在脚本末尾记录/返回状态,以决定下一步操作
- 描述性页面名称:使用
"checkout"、"login",而不是"main" - 断开连接以退出:
await client.disconnect()- 页面在服务器上持久化 - 在 evaluate 中使用纯 JS:
page.evaluate()在浏览器中运行 - 不支持 TypeScript 语法
工作流循环
对于复杂任务,遵循以下模式:
- 编写一个脚本执行一个操作
- 运行它并观察输出
- 评估 - 它起作用了吗?当前状态是什么?
- 决定 - 任务是否完成,还是需要另一个脚本?
- 重复直到任务完成
浏览器上下文中无 TypeScript
传递给 page.evaluate() 的代码在浏览器中运行,它不理解 TypeScript:
// ✅ 正确:纯 JavaScript
const text = await page.evaluate(() => {
return document.body.innerText;
});
// ❌ 错误:TypeScript 语法将在运行时失败
const text = await page.evaluate(() => {
const el: HTMLElement = document.body; // 类型注解在浏览器中会出错!
return el.innerText;
});
抓取数据
对于抓取大型数据集,请拦截并重放网络请求,而不是滚动 DOM。请参阅 references/scraping.md 获取完整指南,涵盖请求捕获、模式发现和分页 API 重放。
客户端 API
const client = await connect();
// 获取或创建命名页面(视口仅适用于新页面)
const page = await client.page("name");
const pageWithSize = await client.page("name", { viewport: { width: 1920, height: 1080 } });
const pages = await client.list(); // 列出所有页面名称
await client.close("name"); // 关闭一个页面
await client.disconnect(); // 断开连接(页面保留)
// ARIA 快照方法
const snapshot = await client.getAISnapshot("name"); // 获取无障碍树
const element = await client.selectSnapshotRef("name", "e5"); // 通过引用获取元素
page 对象是标准的 Playwright Page。
等待
import { waitForPageLoad } from "@/client.js";
await waitForPageLoad(page); // 导航后
await page.waitForSelector(".results"); // 等待特定元素
await page.waitForURL("**/success"); // 等待特定 URL
检查页面状态
截图
await page.screenshot({ path: "tmp/screenshot.png" });
await page.screenshot({ path: "tmp/full.png", fullPage: true });
ARIA 快照(元素发现)
使用 getAISnapshot() 发现页面元素。返回 YAML 格式的无障碍树:
- banner:
- link "Hacker News" [ref=e1]
- navigation:
- link "new" [ref=e2]
- main:
- list:
- listitem:
- link "Article Title" [ref=e8]
- link "328 comments" [ref=e9]
- contentinfo:
- textbox [ref=e10]
- /placeholder: "Search"
解释引用:
[ref=eN]- 用于交互的元素引用(仅限可见、可点击的元素)[checked]、[disabled]、[expanded]- 元素状态[level=N]- 标题级别/url:、/placeholder:- 元素属性
与引用交互:
const snapshot = await client.getAISnapshot("hackernews");
console.log(snapshot); // 找到你需要的引用
const element = await client.selectSnapshotRef("hackernews", "e2");
await element.click();
错误恢复
页面状态在失败后保持。使用以下方法调试:
cd skills/dev-browser && npx tsx <<'EOF'
import { connect } from "@/client.js";
const client = await connect();
const page = await client.page("hackernews");
await page.screenshot({ path: "tmp/debug.png" });
console.log({
url: page.url(),
title: await page.title(),
bodyText: await page.textContent("body").then((t) => t?.slice(0, 200)),
});
await client.disconnect();
EOF