Install
openclaw skills install @fatmind/webclaw3-browser-automation浏览器自动化 / 网页数据采集 / 爬虫抓取——用你自己的 Chrome 和已登录状态干活,不用重新登录、不用交账号密码、不怕反爬。所有需要操作浏览器的活都走这个 skill。 场景一(日常上网干活):搜东西、看网页、查数据、抓列表、导表格、下载内容,或者在网站里动手操作——点按钮、填表单、发内容、和商家聊天;包括必须登录才能看的页面,如小红书、抖音、微博、推特(X)、知乎、B站、公众号、淘宝、拼多多、Shopify 后台等。 场景二(沉淀成 skill):任务跑通了,用户说"帮我提炼""以后每天自动跑",把这次探索固化成可反复运行、零 token 的本地脚本 skill,可配定时任务。 场景三(修复):网站改版导致之前生成的 skill 跑失败了,用户来找修,本地直接修好。 (EN) Browser automation and web scraping that drives your own logged-in Chrome — explore once, distill it into a free, deterministic skill you can rerun daily.
openclaw skills install @fatmind/webclaw3-browser-automation让 AI 用你已经登录的 Chrome 干活:搜索、抓数据、点按钮、发内容都行。跑通一次后说一句"帮我提炼",它就变成零 token、每天能自动跑的本地 skill。
能干什么(三个真实例子)
装起来要几步(先说清楚,别踩坑):三步、大约 5 分钟。① 装 Chrome 扩展(开发者模式加载一次);② 到 webclaw3.com 领 Access Key(免费 10 次生成额度);③ 对你的 Agent 说一句"帮我装 webclaw3 并检查环境"。装完就在对话框里说人话使唤它。
和其它浏览器工具差在哪:它不会另开一个干净浏览器让你重新登录,而是接管你自己那个已经登录好的 Chrome;另外自带站点经验库——别人探索过的站点结构你可以直接复用,生成更快、更稳。
以上是给人看的介绍。Agent 从下面这一节开始读。
webclaw3 覆盖「探索 → 提炼 → 日常跑 → 修复」完整动线,四个环节:
| 环节 | 什么时候进入 | 指引在哪 |
|---|---|---|
| ① 安装启动 | 首次使用,或前置检查不通过 | Read references/setup.md |
| ② 探索 | 用户提出联网任务(高频,日常主体) | 本文正文 |
| ③ 提炼 | 探索成功后,用户说"提炼成 skill / 以后自动跑" | Read references/brief.md |
| ④ 问题修复 | 之前生成的 skill 运行失败 | Read references/repair.md |
用户动线:装好通道(①)→ 自然语言探索、拿到用户确认满意的结果(②)→ 一句"帮我提炼"变成可反复跑的 skill(③)→ skill 日常运行,出问题先本地修(④)。
①③④ 低频,对应文件遇到场景时再 Read,不要提前加载。references/ 目录与本文件同级。
和用户说话,永远用大白话(贯穿全程,极重要) 把用户当成一个不懂技术的普通人,不是程序员。无论转达 doctor 的建议、报错、还是进度,都要翻译成简短、口语化的人话,告诉他"下一步点哪、做什么"就够了。不要把原始 JSON、日志路径、端口号、tarball / npm / PATH 这类术语甩给用户。一次只说重点,能一句话讲清就别写三句。(文末还有一条同样的提醒。)
本 skill 的 CLI 就在 skill 目录的 scripts/ 下,用 node 直接跑——不需要也不要做任何 PATH / npm link / 全局安装,也不要向用户提议。下文所有 webclaw3 <子命令> 写法均指:
node <本 skill 目录>/scripts/webclaw3.mjs <子命令>
skill 安装完成后、以及每次开始联网操作前,直接跑下面这条,不用询问用户要不要检查:
node <本 skill 目录>/scripts/webclaw3.mjs doctor
# → {"ok":true,...} 即可开始操作
doctor 会自动拉起 relay;ok:false 时输出里有 advice(Chrome 没开 / 扩展未装 / 被禁用等),需要引导用户人工处理的场景 Read references/setup.md。
本 skill 的脚本(relay、cdp-proxy、webclaw3 CLI)全部在 skill 目录的
scripts/下,自包含;Chrome 扩展是独立安装的 wc3-chrome(安装方式见references/setup.md)。
像人一样思考,兼顾高效与适应性的完成任务。
执行任务时不会过度依赖固有印象所规划的步骤,而是带着目标进入,边看边判断,遇到阻碍就解决,发现内容不够就深入——全程围绕「我要达成什么」做决策。这个 skill 的所有行为都应遵循这个逻辑。
① 拿到请求 — 先明确用户要做什么,定义成功标准:什么算完成了?需要获取什么信息、执行什么操作、达到什么结果?这是后续所有判断的锚点。
② 选择起点 — 根据任务性质、平台特征、达成条件,选一个最可能直达的方式作为第一步去验证。一次成功当然最好;不成功则在③中调整。比如,需要操作页面、需要登录态、已知静态方式不可达的平台(小红书、微信公众号等)→ 直接用浏览器中继
③ 过程校验 — 每一步的结果都是证据,不只是成功或失败的二元信号。用结果对照①的成功标准,更新你对目标的判断:路径在推进吗?结果的整体面貌(质量、相关度、量级)是否指向目标可达?发现方向错了立即调整,不在同一个方式上反复重试——搜索没命中不等于"还没找对方法",也可能是"目标不存在";API 报错、页面缺少预期元素、重试无改善,都是在告诉你该重新评估方向。遇到弹窗、登录墙等障碍,判断它是否真的挡住了目标:挡住了就处理,没挡住就绕过——内容可能已在页面 DOM 中,交互只是展示手段。
条件不可达时重新评估,不死磕: 当严格筛选条件下数据量确实不足(如"近一周+点赞>500"只有 3 条,目标要求 >=10),这不是技术问题而是数据现实。此时应主动向用户说明实际情况,并建议放宽条件(扩大时间范围、降低阈值),而不是在同一条件下反复翻页、换入口死磕。用户的验收标准是可以协商的,数据的客观存在量不是。
④ 完成判断 — 对照定义的任务成功标准,确认任务完成后才停止,但也不要过度操作,不为了"完整"而浪费代价。
| 场景 | 工具 |
|---|---|
| 搜索摘要或关键词结果,发现信息来源 | WebSearch |
| URL 已知,需要从页面定向提取特定信息 | WebFetch(拉取网页内容,由模型直接提取,返回结果) |
| URL 已知,需要原始 HTML 源码(meta、JSON-LD 等结构化字段) | curl |
| 非公开内容,或已知静态层无效的平台(小红书、微信公众号等公开内容也被反爬限制) | 浏览器(wc3-chrome 中继,跳过静态层) |
| 需要登录态、交互操作,或需要像人一样在浏览器内自由导航探索 | 浏览器(wc3-chrome 中继) |
浏览器中继不要求 URL 已知——可从任意入口出发,通过页面内搜索、点击、跳转等方式找到目标内容。WebSearch、WebFetch、curl 均不处理登录态。
WebSearch / WebFetch 指宿主自带的搜索、网页读取工具。
核实的目标是一手来源,而非更多的二手报道。多个媒体引用同一个错误会造成循环印证假象。
搜索引擎和聚合平台是定位信息的工具,不可用于直接证明真伪。找到来源后,直接访问读取原文。同一原则适用于工具能力/用法的调研——官方文档是一手来源,不确定时先查文档或源码,不猜测。
| 信息类型 | 一手来源 |
|---|---|
| 政策/法规 | 发布机构官网 |
| 企业公告 | 公司官方新闻页 |
| 学术声明 | 原始论文/机构官网 |
| 工具能力/用法 | 官方文档、源码 |
找不到官网时:权威媒体的原创报道(非转载)可作为次级依据,但需向用户说明:"未找到官方原文,以下核实来自[媒体名]报道,存在转述误差可能。"单一来源时同样向用户声明。
通过 Chrome 扩展走 WebSocket 中继到用户日常 Chrome,天然携带登录态,零授权弹窗,反检测隐身。
通过 HTTP REST API 与 Relay 服务器交互(Relay 是已运行的 WebSocket 服务器,监听 ws://127.0.0.1:3459,同时暴露 HTTP 端口):
# 检查状态
curl -s http://127.0.0.1:3459/api/status
# → {"extensionConnected":true,"wsPort":3459}
# 通用调用格式:POST /api/call,body 为 JSON { "op": "操作名", "params": {...} }
# 返回格式:{ "result": ... } 成功,{ "error": "..." } 失败
# --- Tab 操作 ---
curl -s -X POST http://127.0.0.1:3459/api/call -d '{"op":"tab.list"}'
curl -s -X POST http://127.0.0.1:3459/api/call -d '{"op":"tab.create","params":{"url":"https://example.com","active":false}}'
curl -s -X POST http://127.0.0.1:3459/api/call -d '{"op":"tab.setStatus","params":{"tabId":TAB_ID,"status":"running"}}'
curl -s -X POST http://127.0.0.1:3459/api/call -d '{"op":"tab.groupInfo"}'
curl -s -X POST http://127.0.0.1:3459/api/call -d '{"op":"tab.close","params":{"tabId":TAB_ID}}'
# --- 页面操作 ---
# 动态 eval(核心,任意 JS 字符串)
curl -s -X POST http://127.0.0.1:3459/api/call -d '{"op":"page.eval","params":{"tabId":TAB_ID,"code":"document.title"}}'
# Aria tree(DOM 遍历,ref_N 寻址)
curl -s -X POST http://127.0.0.1:3459/api/call -d '{"op":"page.ariaTree","params":{"tabId":TAB_ID,"filter":"interactive"}}'
# 点击/滚动/填表(通过 ref_N 定位元素,React 安全)
curl -s -X POST http://127.0.0.1:3459/api/call -d '{"op":"page.click","params":{"tabId":TAB_ID,"ref":"ref_3"}}'
curl -s -X POST http://127.0.0.1:3459/api/call -d '{"op":"page.scrollTo","params":{"tabId":TAB_ID,"ref":"ref_5"}}'
curl -s -X POST http://127.0.0.1:3459/api/call -d '{"op":"page.fillForm","params":{"tabId":TAB_ID,"ref":"ref_2","value":"搜索内容"}}'
# 关键词搜索页面元素
curl -s -X POST http://127.0.0.1:3459/api/call -d '{"op":"page.search","params":{"tabId":TAB_ID,"query":"关键词"}}'
# 等待元素出现
curl -s -X POST http://127.0.0.1:3459/api/call -d '{"op":"page.waitForElement","params":{"tabId":TAB_ID,"selector":".item-card"}}'
# 截图
curl -s -X POST http://127.0.0.1:3459/api/call -d '{"op":"page.screenshot","params":{"tabId":TAB_ID}}'
# 提取页面正文
curl -s -X POST http://127.0.0.1:3459/api/call -d '{"op":"page.getText","params":{"tabId":TAB_ID}}'
# 解除调试附加
curl -s -X POST http://127.0.0.1:3459/api/call -d '{"op":"debug.detach","params":{"tabId":TAB_ID}}'
Node.js 脚本中用 fetch 封装:
const RELAY_URL = 'http://127.0.0.1:3459';
async function relayCall(op, params = {}, timeout = 30000) {
const res = await fetch(`${RELAY_URL}/api/call`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ op, params, timeout }),
});
const data = await res.json();
if (data.error) throw new Error(data.error);
return data.result;
}
调用方可能在上文 ## 上下文变量 里声明以下变量,从声明取值;未声明则忽略。
"logFile":"<声明的值>"
curl -s -X POST http://127.0.0.1:3459/api/call -d '{"op":"tab.create","params":{"url":"https://example.com"},"logFile":"<声明的值>"}'
?logFile=<声明的值>(示例见 references/cdp-fallback.md)(站点知识由下文 ## 站点知识 节 + 声明的 WC3_SITE_KNOWLEDGE 覆盖,不在本节重复。)
/api/status 返回 extensionConnected: false,跑 webclaw3 doctor 按 advice 引导用户(详见 references/setup.md)chrome://extensions/page.eval 抛错 → 直接拿到 JS 异常信息(含 className、message、stack),修复脚本重试默认只用 Extension Relay。仅当 Extension 通道整体走不通(扩展未连、Service Worker 挂掉、eval 持续失败)时,回退 CDP HTTP 通道(:3456)继续操作——此时 Read references/cdp-fallback.md(启动、tab 映射、API、回退判定)。单次 SyntaxError、tab not found 属脚本问题,不回退。
ariaTree 和 page.eval 组合使用,是理解和提取页面的两个核心能力:
page.ariaTree 返回页面的压缩语义结构(role + name,~500 行 vs 原始 DOM ~10000 行)。进入新页面先看一次全貌——有哪些区域、多少数据项、分页结构、可交互元素,一次调用建立页面心智模型。用于理解页面结构和定位交互元素,不用于提取结构化数据。page.eval 执行任意 JS,是提取结构化数据的首选方式——直接从 DOM 返回 JSON(name/href/price/downloads 等),保留完整属性,不受文本布局影响。同时覆盖 Aria 树触达不了的一切——穿透 SPA 框架数据层、操控 DOM 元素、执行复杂交互逻辑。大多数任务都是 Aria 先看全貌、eval 再提取数据,两者交替推进。先了解页面结构,再决定下一步动作,不需要提前规划所有步骤。
ariaTree 返回格式:
[ref_1] heading "精选 TOP 50 AI Skills 榜单"
[ref_2] list (options=8)
[ref_3] listitem "Web Access ⭐ 1.4k ↓ 5.2k"
[ref_4] listitem "DeepSearch ⭐ 892 ↓ 3.1k"
[ref_13] button "下一页"
[ref_14] textbox "搜索"
每个节点有 [ref_N] 编号,可用于 page.click(tabId, 'ref_N') 和 page.fillForm(tabId, 'ref_N', value) 精确操作。
Aria 的边界——什么时候该切 eval:
page.eval 是结构化数据提取的首选方式。 扩展已通过 declarativeNetRequest 移除页面 CSP 头,page.eval 在绝大多数站点上可正常使用,正常情况下无需兜底到 CDP。直接从 DOM 提取结构化 JSON,不经过纯文本中间层——这样能保留 href、data-* 属性等 DOM 信息,且不受文本布局变化影响。(例外:若 Extension 通道整体不可用/持续失败,按 references/cdp-fallback.md 临时走 CDP 摸清页面结构,再切回 Relay。)
page.eval(code) 在用户真实 Chrome 中执行任意 JS,用前记牢它的关键约束:
unsafe-eval 禁用也能跑| 层级 | 方法 | 适用场景 |
|---|---|---|
| L1 | page.eval + CSS selector | 首选——直接从 DOM 提取结构化数据(name/href/price 等),返回 JSON |
| L2 | page.ariaTree 直读 | 只需看全貌、确认数据项数量/结构,或信息在可见文本中已足够 |
| L3 | page.eval + 穿透框架数据层 | SPA 没有语义化 class,需要从 React fiber、Vue data、全局 store 等拿原始数据对象 |
| L4 | 交互触发(page.click、翻页、滚动加载) | 数据需要交互才能出现 |
L3 补充:很多 React/Vue SPA 的 class 是哈希值,DOM 层面无法可靠定位。React 的 __reactFiber$ → memoizedProps 链路、Vue 的 __vue__.$data、以及 window.__NEXT_DATA__ / window.__INITIAL_STATE__ 等全局变量,都是穿透到原始数据的入口。具体怎么遍历需要根据目标页面的实际结构探索。
媒体资源:判断内容在图片里时,用 page.eval 从 DOM 直接拿图片 URL,再定向读取——比全页截图精准得多。
面对陌生页面,提取分四步推进——先侦察、再定位、后取值、最后才决定要不要加工:
getText 侦察 page.eval 定位 textContent 取值 后处理决策
──────────────── → ──────────────────── → ──────────────────── → ────────────────────
拿整页纯文本 根据侦察结论 拿 DOM 节点原值 判断原值够不够用
理解页面结构 用 eval + DOM 位置定位 保留原始 textContent ├─ 够用 → 直接输出
找到数据在哪里 锁定目标节点 不做提前格式化 ├─ 需要拆 → split
哪些字段可取 ├─ 需要匹配 → regex
└─ 需要理解 → LLM 总结
page.getText 拿整页纯文本是最自然的"看一眼"方式,用来理解页面结构、确认数据在哪、有哪些字段可取。但侦察 ≠ 提取:侦察清楚后,真正取数据必须切到 page.eval 从 DOM 拿结构化 JSON。绝不要用 getText 拿纯文本再正则解析来提取结构化数据——那样丢失了所有 DOM 属性(href、class、data-*),只能靠文本布局匹配,极脆弱。page.eval 里先拿到原始字符串(如 "99.7 万"、"43"),再决定要不要进一步处理。不要在提取时就预设格式做转换。split/trim;需要模式匹配才上 regex;需要"理解"内容(判断、归类、总结)才交给 LLM。不是所有场景都需要 regex。边界提醒:这套工作流描述的是人工浏览/交互过程的推进方式。getText 侦察只发生在浏览探查期;沉淀进可复用提取脚本的代码里只能有 page.eval,不要把侦察用的 getText 调用抄进脚本。
shadowRoot、iframe 的 contentDocument等)。eval 递归遍历可一次穿透所有层级,返回带标签的结构化内容,适合快速了解未知页面的完整结构。page.eval 执行滚动到底部会触发懒加载,使未进入视口的图片完成加载。提取图片 URL 前若未滚动,部分图片可能尚未加载。tab.create)可能触发网站的反爬风控。串行逐个处理是最安全的;如需并行,控制在 2-3 个 tab,避免短时间爆发。<textarea>,而是基于 contenteditable 的富文本编辑器(Twitter/X 的 Draft.js、Notion 的 ProseMirror、Google Docs 等)。这类编辑器有自己的事件系统和内部状态树,直接操作 DOM(如 textContent 赋值)会破坏框架状态,导致:输入不被识别、字数统计不更新、提交按钮不激活。面对 contenteditable 输入框,需要通过编辑器能感知的事件方式输入(如逐字符键盘事件),而非直接操作 DOM 文本。交互定位层级:
page.click + aria tree ref_N 语义定位,稳定抗改版page.eval + CSS selector 定位page.eval 内通过文本内容匹配目标元素后 click——特别适用于 aria tree 找不到的动态面板、模态框内按钮浏览器内操作页面有两种方式:
根据对目标平台的了解来判断。当程序化方式受阻时,GUI 交互是可靠的兜底。
站点内 URL 的可靠性:站点自己生成的链接(DOM 中的 href)天然携带平台所需的完整上下文(含会话相关参数如 token),而手动构造的 URL 可能缺失隐式必要参数,导致被拦截、返回错误页面、甚至触发反爬。提取 URL 时保留完整地址,不要裁剪或省略参数;当构造的 URL 出现异常时,应考虑是否是缺失参数所致。
页面内打开链接的两种方式:
page.click:在当前 tab 内直接点击,简单直接,串行处理。适合需要在同一页面内连续操作的场景,如点击展开、翻页、进入详情等。tab.create + 完整 URL:从 DOM 提取对象链接的完整地址(包含所有查询参数),在新 tab 中打开。适合需要同时访问多个页面的场景。el.value = ...:
const proto = HTMLInputElement.prototype;
const desc = Object.getOwnPropertyDescriptor(proto, 'value');
desc.set.call(el, text);
el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
el.dispatchEvent(new Event('change', { bubbles: true }));
page.screenshot 确认操作真正完成了。不要仅从 DOM 状态推断成功——页面上的圆形元素可能是字数统计器而非加载动画,按钮旁的指示器可能是静态 UI 而非操作反馈。同一个名字的按钮在不同上下文含义不同(如 Twitter 的 "Reply" 既是打开回复框的入口,也是提交回复的按钮)。当操作看起来"卡住"或"没反应"时,先截图看清页面真实状态再判断下一步,不要在错误假设上继续操作。window.__collected)累积,全部采集完再一次性 JSON.stringify 导出。避免通过 shell 变量中转大段 JSON。站点知识文件({site}.md)记录了某站点已验证的选择器、URL 模式、页面行为经验,是探索陌生站点时的重要参考。
来源:站点知识来自上文声明的 WC3_SITE_KNOWLEDGE(声明中已给出可用的站点知识文件清单,文件名即站点关键词,如 1688.md、reddit.md)。按当前目标 URL 的域名匹配对应文件后 Read;匹配不到就不读,不要凭任务描述猜站点。
使用原则:
&type=link&t=month)直接带进 URL,不要手动点 UI 设过滤器子 Agent 传递规则(极其重要): 子 Agent 运行在 skill 工作目录,该目录下没有站点知识文件。若要子 Agent 使用站点知识,必须在子 Agent 的 prompt 中原文嵌入 Read 指令,指向站点知识的绝对路径(不要改成工作目录、不要改路径):
开始前先 Read
{绝对路径}/{site}.md获取站点选择器、URL 模式和页面行为经验。
用户日常 Chrome 天然携带登录态,大多数常用网站已登录。
登录判断的核心问题只有一个:目标内容拿到了吗?
打开页面后先尝试获取目标内容。只有当确认目标内容无法获取且判断登录能解决时,才告知用户:
"当前页面在未登录状态下无法获取[具体内容],请在你的 Chrome 中登录 [网站名],完成后告诉我继续。"
登录完成后无需重启任何东西,直接刷新页面继续。
任务包含多个独立调研目标时(如同时调研 N 个项目、N 个来源),鼓励合理分治给子 Agent 并行执行,而非主 Agent 串行处理。
好处:
并行操作:每个子 Agent 在当前用户浏览器实例中,自行创建所需的后台 tab(tab.create),自行操作,任务结束自行关闭(tab.close)。所有子 Agent 共享一个 Chrome、一个 Relay,通过不同 tabId 操作不同 tab,无竞态风险。
子 Agent Prompt 写法:目标导向,而非步骤指令
必须加载 webclaw3 skill 并遵循指引 ,子 Agent 会自动加载 skill,无需在 prompt 中复制 skill 内容或指定路径。(注意:这里说的是 skill 本体无需路径;若子 Agent 要用站点知识文件,则需按下方 ## 站点知识 节用绝对路径传入,二者不冲突。)超时限制:本任务最多执行 10 分钟。如果某个操作连续失败 5 次,立即停止尝试,记录失败原因并返回已收集到的部分结果。不要死磕。
分治判断标准:
| 适合分治 | 不适合分治 |
|---|---|
| 目标相互独立,结果互不依赖 | 目标有依赖关系,下一个需要上一个的结果 |
| 每个子任务量足够大(多页抓取、多轮搜索) | 简单单页查询,分治开销大于收益 |
| 需要浏览器长时间运行的任务 | 几次 WebSearch / Jina 就能完成的轻量查询 |
用 tab.close 关闭自己创建的 tab,必须保留用户原有的 tab 不受影响。
Relay 持续运行,不建议主动停止——重启后需要等扩展重新连接。
最后再强调一遍:和用户说话用大白话。 用户是不懂技术的普通人。任何时候要把技术输出(doctor 结果、报错、日志、端口)翻译成简短的人话,只告诉他下一步该做什么;别把原始 JSON、日志路径、端口号、npm / PATH 这类术语丢给他。