本文由 莫潇羽@无忆研习社(www.wuyi.wiki) 原创整理,转载请注明出处。
快速摘要
Browser Harness 是 Browser Use 团队最新开源的一个极简浏览器自动化项目,全部核心代码仅约 592 行 Python。它不走 Selenium、Playwright 那条"预置 API、层层封装"的老路,而是直接通过 Chrome DevTools Protocol(CDP)用一根 WebSocket 接入用户正在运行的浏览器,然后把绝大部分控制权交给大模型自己。最颠覆的地方在于两点:一是 AI 在执行任务过程中可以实时改写 helpers.py 把缺失的能力补上(所谓"自愈"),二是完成任务后会把踩过的坑、摸清的选择器、私有 API 自动沉淀成 domain-skills/ 里的技能文件,下次遇到同类网站直接复用。下文会一层层拆开:它到底是什么、底层怎么跑、怎么安装、踩坑点在哪、适合什么场景,建议读完全文再下手动手。
一、先说背景:为什么需要一种"新的"浏览器自动化
做过网页自动化的朋友都知道,这条路走到今天其实相当拧巴。
十年前我们用 Selenium,后来换成 Puppeteer、Playwright,再到近两年流行的 Browser Use、各种浏览器 Agent 框架,表面看工具一代比一代强,但底层的思路几乎没怎么变——开发者提前写好一堆"点击、输入、滚动、截图"这样的原子操作,把能做的动作封装成 API,然后让脚本或者 AI 去按顺序调用。
这种做法在页面结构稳定的年代非常好用。可问题是,现在的网页早已不是十年前那副"静态 DOM + 表单提交"的样子。单页应用(SPA)、Shadow DOM、跨域 iframe、虚拟滚动、懒加载组件、React/Vue 的受控输入、各种异步状态机……几乎每一个现代前端特性都在给自动化工具挖坑。框架作者疲于奔命加适配器,使用者疲于奔命查文档,而 AI Agent 被夹在中间,只能调用那些"被允许"的 API,一旦预置能力覆盖不到真实场景,就只能报错或者绕路。
莫潇羽@无忆研习社 在写这篇拆解之前,自己也用过不下五种浏览器自动化方案,最强烈的感受就是:互联网是活的,但框架是死的。框架的作者永远走在网页变化之后,Agent 的行为永远被框架作者的想象力所限。
Browser Use 团队这次开源的 Browser Harness,本质上是在正面反思这个问题。他们在项目的配套博文《苦涩教训:Agent 框架的幻觉》里写得很直白:Agent 框架失败,往往不是模型不够聪明,而是我们给模型定义的"动作空间"太窄了。大模型经过大量代码、浏览器交互、CLI 工具的训练,本身就具备相当完整的"网页操作直觉",我们真正要做的,不是教它怎么点按钮,而是把浏览器这个"沙盒"原原本本交给它。
二、Browser Harness 是什么
回到主角。Browser Harness 这个名字里,harness 原意是"马的笼头"或者"线束",套在英文语境里通常翻译为"把某样能力驯化成可用的形态"。官方的自我描述是一句话:
The simplest, thinnest, self-healing harness that gives LLM complete freedom to complete any browser task. Built directly on CDP.
翻成人话:它是一层极薄的壳,把一个真实 Chrome 浏览器的控制权直接交到大模型手里,让模型自己想办法完成任何网页任务;而且这层壳具备"自愈"能力——任务过程中缺什么能力,模型自己顺手写上去。
从工程角度看,整个项目只有以下几个文件:
install.md:首次安装和浏览器 bootstrap 指引SKILL.md:日常使用说明,会被 Claude Code/Codex 读取run.py:约 36 行,负责预加载 helpers 并执行用户的 Python 片段helpers.py:约 195 行,初始的工具函数集合,AI 会动态编辑这个文件admin.py+daemon.py:合计约 361 行,负责后台守护进程、CDP WebSocket 桥接、Unix Socket 通信domain-skills/:面向具体网站的技能目录,例如github/、linkedin/、amazon/、tiktok/等interaction-skills/:面向交互机制的技能目录,例如iframes.md、shadow-dom.md、dropdowns.md、uploads.md等
算下来 Python 代码总共大约 592 行,这还是包括注释和空行的口径。对比动辄几万行的传统自动化框架,这个体量几乎可以用"小到不像话"来形容。
项目采用 MIT 协议开源,目前在 GitHub 上位于 https://github.com/browser-use/browser-harness,开源后不久就收获了数千 Star,讨论热度相当高。
三、三大核心理念:极简、自愈、沉淀
读完源码和文档之后,个人总结 Browser Harness 的设计哲学其实就是三件事:做减法、给自由、留痕迹。
做减法:用一根 WebSocket 连通一切
Browser Harness 没有自研抽象层、没有场景 DSL、没有所谓的"Agent Runtime"。它直接使用 Chrome DevTools Protocol(CDP),这是 Chrome 官方提供的远程调试协议,任何一个开启了 --remote-debugging-port 的 Chrome 实例都可以被它接管。通信方式是最经典的 WebSocket + JSON,每次一行请求一行响应,简单到几乎无法再简化。
因为底层就是 CDP,所以理论上 Chrome 能做的一切(打开页面、执行 JS、截图、下载、设置 cookie、监听网络、操作 Service Worker、控制扩展等等),Browser Harness 都能做到,不会被框架作者的能力边界锁死。
给自由:让模型自己写缺失的能力
这是它最不同寻常的一点。传统自动化里,如果某个功能没实现,流程就会卡住,要等开发者去写新代码。而 Browser Harness 默认假设"helpers.py 里没写的,模型可以自己写上去"。
官方 README 里给了一个非常形象的示意:
● agent: wants to upload a file
│
● helpers.py → upload_file() missing
│
● agent edits the harness and writes it helpers.py 192 → 199 lines
│ + upload_file()
✓ file uploaded
模型发现自己需要上传文件,但 helpers.py 里没有 upload_file() 这个函数,于是它就直接打开这个文件,添加几行代码把功能补齐,然后继续执行任务。整个过程不需要开发者介入。这就是"Self-healing"(自愈)这个词的由来。
留痕迹:把经验沉淀成可复用的技能
模型每一次摸清一个网站的"套路"——比如某个私有 API、某个稳定的 data-* 选择器、某个 React 组件的奇怪行为——都可以写进 domain-skills/<site>/ 目录。下次再碰到同一个网站,模型第一件事就是读这个目录,直接复用之前的经验。
官方非常强调一点:这些技能文件应该由 Agent 自己来写,而不是人手工维护。因为手写的"教程"往往是臆想出来的稳定路径,而 Agent 实际跑过一遍后留下的笔记,才反映真正在浏览器里能跑通的路径。
项目目前已经内置了若干示例技能,包括 GitHub、LinkedIn、Amazon、TikTok 等常见站点的片段,感兴趣的朋友可以去仓库里看看它们是什么形状。
四、技术架构拆解:从 Chrome 到 Python 的完整链路
数据链路示意
先把整个系统的数据链路画出来(莫潇羽@无忆研习社 根据 SKILL.md 里的架构图重绘):
┌─────────────────────────────┐
│ Chrome / Browser Use 云端 │
└──────────────┬──────────────┘
│ CDP over WebSocket
▼
┌─────────────────────────────┐
│ daemon.py │ ← 常驻守护进程
└──────────────┬──────────────┘
│ Unix Domain Socket (/tmp/bu-<name>.sock)
▼
┌─────────────────────────────┐
│ run.py (+ helpers.py) │ ← 每次任务短连接
└─────────────────────────────┘
整条链路的关键点有三层:
- 上层 Chrome 通过 CDP 的 WebSocket 和
daemon.py对话,daemon.py是唯一一个需要常驻的进程 daemon.py对下暴露一个 Unix Domain Socket,路径按BU_NAME命名(默认为default)- 每次用户调用
uv run browser-harness,run.py就起一个短暂的 Python 环境,预加载好helpers.py,通过那个 socket 把指令转给 daemon
这种设计有两个好处:一是 CDP 的长连接只需要建立一次,后续命令几乎零启动开销;二是用户的 Python 代码是"一次性"的,跑完就退出,不会污染全局状态。
通信协议
Browser Harness 的通信协议极其克制,每条消息都是一行 JSON,明确区分三类:
- 如果是 CDP 调用,格式是
{method, params, session_id} - 如果是 daemon 自己的控制指令,格式是
{meta: ...} - 响应是
{result}/{error}/{events}/{session_id}之一
没有 gRPC、没有 protobuf、没有 Thrift,甚至连框架级的序列化都懒得上。好处是任何人读十分钟源码就能搞明白协议长什么样,坏处是极少——因为 CDP 本身就是 Chrome 在用的协议,稳定性不用担心。
为什么坚持用 CDP 而不是 Playwright
这是理解 Browser Harness 设计哲学的一把钥匙。Playwright 是一个优秀的浏览器自动化库,但它本质上也是在 CDP 之上做了一层面向"人类开发者"的抽象:链式 API、自动等待、selector engine、locator 概念等等。这些抽象对于写脚本的人类来说非常舒服,但对大模型来说反而是"多余的中间层"——模型训练时见过的浏览器控制示例,主要是原生 JS、原生 CDP 以及零碎的 Python 脚本,让它使用一个风格强烈的封装库,反而会降低它的准确率。
CDP 的另一个杀手锏是"合成器级别的点击"(Compositor-level click)。官方 SKILL.md 里反复强调:优先用坐标点击(Input.dispatchMouseEvent),因为这种点击事件会直接经过 Chrome 的合成器层,穿透 iframe、跨域、Shadow DOM,几乎不会被前端那些花里胡哨的封装拦截。而 Playwright 的 locator.click() 有时会因为 DOM 变化而失败。
用句大白话总结:给人类用的库求"声明式",给大模型用的库求"回归裸机"。这是 Browser Harness 最聪明的判断。
CDP 到底是什么:顺便补一点背景
CDP 全称 Chrome DevTools Protocol,是 Chrome 浏览器在 2013 年前后就开放出来的远程控制接口。你平时按 F12 打开的"开发者工具",底下其实就是一个 CDP 客户端在和浏览器对话。所有 DevTools 能看到、能操作的东西,CDP 都能通过 API 暴露出来。
它的模型大概长这样:协议里有若干"Domain",比如 Page、DOM、Runtime、Network、Input、Target、Emulation 等等,每个 Domain 下有一组方法(比如 Page.navigate、Runtime.evaluate、Input.dispatchMouseEvent)和一组事件(比如 Page.loadEventFired、Network.responseReceived)。客户端通过 WebSocket 发方法、收事件,就能拿到完整的浏览器控制能力。
Browser Harness 里的 cdp(method, **params) 就是把这套协议原封不动地暴露给大模型。对于训练中见过海量 Chrome 源码、DevTools 文档、Puppeteer 源码的大模型来说,这个接口几乎不需要学习成本——它知道 Page.captureScreenshot 怎么用,知道 DOM.getDocument 会返回什么结构,知道 Network.setExtraHTTPHeaders 能干什么。
换句话说,CDP 对大模型来说是"母语级"的 API,封装得越薄,模型发挥越稳。这是 Browser Harness 把所有高层 API 都当成可选项、把 CDP 当成必选项的根本原因。
合成器级点击的技术细节
前面提到"合成器级别的点击"这个术语,这里再稍微展开讲一下,因为它是整个项目稳定性的基石。
浏览器渲染一个页面,大致要经过几层:DOM 树 → Render 树 → Layout → Paint → Compositor(合成器)→ 显示。合成器位于最底层,负责把各种图层合并成最终画面。用户手指在屏幕上点一下,操作系统把坐标传给浏览器,浏览器的合成器根据坐标找到对应图层,再反向派发事件给上层 DOM。
CDP 的 Input.dispatchMouseEvent 模拟的正是合成器这一层的事件——不是"在某个 DOM 元素上触发 click",而是"在屏幕的 (x, y) 坐标上模拟一次真实点击"。这种做法有两个无可替代的优势:
- 能穿透任何层级的封装,包括跨域 iframe、Shadow DOM、iframe 嵌套 iframe 这种地狱结构
- 不依赖元素的 ID、class、aria 属性,不会因为前端改版而失效
代价也有:需要先用截图确认坐标。但在有截图能力的前提下,这其实反而更像人类操作——先看见再点击。这也是为什么 SKILL.md 里把 "screenshot first" 列为最重要的一条实践原则。
五、自愈机制具体是怎么工作的
这部分很多人最好奇,我单独拎出来细说。
helpers.py 是一份"活的"工具箱
初始版本的 helpers.py 只有大约 195 行,预置了一些最基础的能力:打开页面(goto)、等待加载(wait_for_load)、截图(screenshot)、坐标点击(click)、执行 JavaScript(js)、读取页面信息(page_info)、批量 HTTP 请求(http_get)、以及兜底的原生 CDP 调用(cdp)。
这些函数覆盖了 90% 的日常场景,但面对某个具体任务总会有漏洞。比如:
- 需要把文件拖拽到某个 drop zone 上
- 需要批量下载资源并重命名
- 需要在特定时刻截获一个 XHR 响应
- 需要绕过 beforeunload 对话框
- 需要处理一个奇怪的 Web Component
这时候 Agent(Claude Code 或 Codex)会做一件事:直接编辑 helpers.py,把这个能力写成一个新的函数,然后马上在下一条命令里调用。项目作者特意把 helpers.py 设计成一个"鼓励被改写"的文件,注释里明确写着 the agent edits these。
自愈不是玄学,而是工程选择的结果
为什么这个机制能跑起来?因为现代大模型(尤其是 Claude、GPT 系列)在"写 Python + 调用 CDP"这件事上几乎是天才选手。让模型自己读文档、自己写适配代码,比让人类开发者去写预置 API 更快、更准、更契合当下这次任务。
再叠加一个关键细节:daemon.py 是常驻的,helpers.py 是被 run.py 每次重新加载的。这意味着 Agent 改完 helpers.py 后,下一次执行任务时新函数就会自动生效,完全不用重启任何东西。工程上非常优雅。
一个写出来的真实例子
以官方 domain-skills/tiktok/upload.md 为例(这是项目自带的)。一开始,helpers 里根本没有"上传视频到 TikTok"这种函数。但当一个 Agent 跑完第一次上传流程后,它会把这一趟发现的坑整理成一份 Markdown:TikTok 的上传表单其实是一个挂在 Shadow DOM 里的 input[type=file],需要用 CDP 的 DOM.setFileInputFiles 注入,而不能用坐标点击;发布按钮挂在 React 组件的 portal 里;草稿自动保存有一个 10 秒延时,必须等它保存完才能跳转……
下一个 Agent 再拿到"上传 TikTok"任务时,会先 rg --files domain-skills 看一眼有没有现成技能,看到 tiktok/upload.md 直接读完,省掉自己从零摸索的时间。如此一来,这个开源库在每一次使用中都会"变得更聪明",这是传统自动化框架完全做不到的。
一段完整的自愈流程模拟
为了让大家直观感受"自愈"到底怎么发生的,莫潇羽@无忆研习社 把一次典型会话的过程还原一下。假设用户给 Claude Code 的指令是:"帮我在某个文件共享网站上传一份本地的 PDF"。
整个过程大致是:
用户 → Claude Code:
"打开 xxx.com,上传 /Users/moxiaoyu/files/report.pdf"
Claude Code 读 SKILL.md、helpers.py,决定先尝试:
> uv run bh <<'PY'
> goto("https://xxx.com")
> wait_for_load()
> screenshot("step1.png")
> PY
截图显示:首页右上角有个 "Upload" 按钮。Claude 继续:
> uv run bh <<'PY'
> click(1240, 88) # 基于截图的坐标
> wait_for_load()
> screenshot("step2.png")
> PY
截图显示:弹出了一个带 input[type=file] 的表单。Claude 发现 helpers.py
里没有 `upload_file` 函数,它不报错、不停下,而是直接:
> 读取 helpers.py
> 在文件末尾追加:
def upload_file(selector, path):
"""把本地文件注入到指定 input[type=file] 上。"""
backend_id = cdp("DOM.getDocument", depth=-1)["root"]["backendNodeId"]
node_id = cdp("DOM.querySelector", nodeId=backend_id, selector=selector)["nodeId"]
cdp("DOM.setFileInputFiles", nodeId=node_id, files=[path])
> uv run bh <<'PY'
> upload_file("input[type=file]", "/Users/moxiaoyu/files/report.pdf")
> screenshot("step3.png")
> PY
截图显示:文件已经成功上传,文件名出现在页面。任务完成。
Claude 最后把这次新增的逻辑,总结成 domain-skills/xxx/upload.md,
说明"这个站点的上传是通过直接 setFileInputFiles 完成的,
不要尝试用坐标点击",留给下一次任务复用。
整个过程没有任何一行代码是用户手写的,也没有任何"适配器"被专门为这个站点写。Agent 利用自己对 CDP 协议的了解,临场写了一个新函数,补齐了 helpers 的短板,并把经验留在仓库里。这就是 Browser Harness 的工作流。
为什么 helpers.py 最终不会臃肿
有人可能担心:这样一直加函数下去,helpers.py 是不是会越来越胖,最后变成一个怪物?
作者对这个问题其实也给出了答复:helpers.py 应该保持"短而通用",Agent 加的函数如果只对某个站点有意义,应该以"站点技能"的形式写在 domain-skills/ 里,而不是塞进 helpers。真正被加到 helpers 的函数,必须是"在多数网站上都有用"的基础原语。
换句话说,helpers.py 是基础设施,domain-skills/ 是业务逻辑,两者的边界要守住。这就是为什么项目运行一段时间后,helpers.py 也许会从 195 行慢慢涨到 300 行左右,但不会失控——多出来的能力应该流向技能目录而不是核心代码。
六、592 行代码都干了什么:逐文件解析
run.py(约 36 行)
这是用户真正会"敲"的入口。它的职责极简:
- 读取环境变量(
BU_NAME、BU_CDP_WS等) - 调用
admin.ensure_daemon()确保守护进程在线 - 把用户传进来的 Python 代码片段,在一个已经预加载好
from helpers import *的命名空间里exec掉
整个文件不到 40 行,作者在 SKILL.md 里甚至特意叮嘱:"run.py 保持小巧,不要加 argparse、不要加子命令、不要加控制层"。
helpers.py(约 195 行)
这就是我们前面说的"活工具箱"。初始版本提供的函数,按用途大概分几类:
- 页面生命周期:
goto(url)、wait_for_load()、ensure_real_tab() - 视觉交互:
screenshot()、click(x, y)、type_text(s)、press_key(key) - DOM 读写:
js(code)、page_info() - 批量抓取:
http_get(url)(配合ThreadPoolExecutor,官方举例 249 个 Netflix 页面 2.8 秒跑完) - 兜底:
cdp("Domain.method", **params)
这些函数绝大多数都是 CDP 原语的薄封装,读源码时你会发现它几乎是"把 CDP 手册翻译成 Python 名字"。这正是作者的审美:给模型留最大空间,封装层越薄越好。
admin.py + daemon.py(合计约 361 行)
这两个文件承担了最脏最累的活:
- 发现本地正在运行的 Chrome(通过读
DevToolsActivePort文件,而不是/json/version,因为 Chrome 144+ 后者已经停止服务) - 建立并维护和 Chrome 之间的 CDP WebSocket 长连接
- 开一个 Unix Domain Socket,监听来自
run.py的请求 - 做消息转发、事件分发、session 管理
- 支持通过
BU_BROWSER_ID+BROWSER_USE_API_KEY接入 Browser Use 提供的远程浏览器
值得一提的是,daemon.py 不做任何"管理员"式的工作——没有重试框架、没有日志框架、没有配置中心、没有 session manager。作者在 SKILL.md 里专门写了一条设计约束:Don't add a manager layer。这种极端的克制其实很难得。
多说几句 daemon 的设计动机。Chrome 的 CDP 连接虽然强大,但建立一次连接的耗时不容忽视——要发现端口、要走 WebSocket 握手、要枚举 target、要 attach session。如果每次用户敲一段代码都重新连一次,整体体验会糟糕到无法忍受。常驻 daemon 的方案把这些开销摊销到一次启动里,后续每条命令只走 Unix Socket 的本地通信,延迟几乎可以忽略。
另一方面,daemon 也负责"会话归一化"。CDP 的 target(可以简单理解为标签页、iframe 这类可附着的对象)会随着用户打开/关闭标签而变化,daemon 会自动维护一个"当前活动 target"的概念,这样上层代码不用每次都显式指定 session_id。对应的 helper 函数 ensure_real_tab() 就是在这里发挥作用:当你发现自己不小心操作到一个内部 target(比如 devtools 自己的面板、omnibox 弹窗),调一下这个函数就能重新指向一个真实页面。
pyproject.toml 与 .env.example
项目根目录还有两个小文件常被忽略:
pyproject.toml:定义了少量 Python 依赖和uv run bh、uv run browser-harness两个脚本入口。依赖极少,基本只有websockets、cdp-use等几个核心包,这也是它能做到 592 行总代码的前提——能用标准库就用标准库.env.example:示意了BROWSER_USE_API_KEY、BU_NAME、BU_CDP_WS、BU_BROWSER_ID等环境变量,方便用户一键配置
整个项目读源码的时候给人的感觉非常清爽:没有一层套一层的目录,没有到处飞的 import,没有难以追踪的动态派发。这一点哪怕不打算用它,也值得借鉴。
domain-skills/ 与 interaction-skills/
这两个目录不是代码,而是由 Agent 写出来的 Markdown 笔记:
domain-skills/:按网站分目录,比如github/、linkedin/、amazon/、tiktok/,记录每个站点的 URL 规律、私有 API、稳定选择器、框架坑interaction-skills/:按交互机制分文件,比如dialogs.md(对话框)、downloads.md(下载)、drag-and-drop.md(拖拽)、iframes.md(内嵌框架)、shadow-dom.md(影子 DOM)、uploads.md(上传)、cross-origin-iframes.md(跨域框架)等
一个新 Agent 接到任务后,会先 rg --files domain-skills 搜一下有没有现成资料,再决定怎么干。这个"先翻笔记再动手"的习惯,是整个项目智能涌现的核心。
七、怎么上手:从零到跑通一个任务
基本前提
动手之前,机器上需要准备这几样:
- 一个可以在终端用的 AI 编码助手,官方主推 Claude Code 或 Codex(这两者都有 CLI 版本,可以通过订阅或 API Key 接入)
- 安装了最新版的 Google Chrome(不是 Chromium,也不是 Edge)
- Python 环境,推荐用
uv做包管理器(项目的运行入口就是uv run ...)
一句话安装
最快的方式是官方那段 setup prompt,直接粘到 Claude Code 或 Codex 里:
Set up https://github.com/browser-use/browser-harness for me.
Read `install.md` first to install and connect this repo to my real browser.
Then read `SKILL.md` for normal usage. Always read `helpers.py` because that is
where the functions are.
翻译成中文意思是:帮我把这个仓库装上,先读 install.md 把它和我真实浏览器连通,然后读 SKILL.md 看怎么用,每次都要读 helpers.py 因为函数都在那。
看到这段 prompt 你应该就能领悟到:作者默认用户是让 AI 来帮自己装,而不是人手动折腾。这也是为什么整个项目没有 setup.py、没有 install.sh、没有花哨的安装脚本——因为 AI 读 markdown 比读脚本更可靠。
连接真实浏览器
安装过程中,Claude Code 或 Codex 会指导你做一件事:让 Chrome 进入可被调试的模式。具体做法是打开一个特殊的 chrome:// 内部页面,勾选一个允许远程调试的复选框。首次出现 Chrome 的"Allow"对话框时必须手动点允许,之后就不用再操作了。
如果你以前折腾过 Puppeteer / Playwright,你会知道它们默认会自己启动一个独立的浏览器实例。Browser Harness 反其道而行之——它要求接入你自己平时在用的那个 Chrome。原因也很明确:你自己浏览器里的登录状态、cookie、扩展、书签、历史记录,全都能被 Agent 直接利用,不用再为每个网站重新登录。
跑第一个 Hello World
装完之后,最小可运行片段长这样(直接在终端敲):
uv run bh <<'PY'
goto("https://www.wuyi.wiki")
wait_for_load()
print(page_info())
PY
上面这段代码做了三件事:打开无忆研习社的首页,等页面加载完毕,然后打印页面信息。page_info() 会返回当前页面的标题、URL、视口大小等基础信息。
这里用到的 goto、wait_for_load、page_info 都是 helpers.py 预置的函数,uv run bh 就是前面说的短连接调度入口。
让 AI 来驱动
真实使用中,你通常不会自己写 Python 片段,而是直接把任务用自然语言告诉 Claude Code,让它代写。比如:
帮我打开 GitHub,搜索 "browser-harness",把第一页的前 10 条仓库名、描述和 Star 数导出成 CSV。
Claude Code 读完 SKILL.md 和 domain-skills/github/ 后,会自己组织一段类似下面的 Python:
goto("https://github.com/search?q=browser-harness&type=repositories")
wait_for_load()
repos = js("""
Array.from(document.querySelectorAll('[data-testid="results-list"] > div'))
.slice(0, 10).map(el => ({
name: el.querySelector('a[href]').innerText,
desc: (el.querySelector('p')||{}).innerText || '',
stars: (el.querySelector('a[href$="/stargazers"]')||{}).innerText || '0'
}))
""")
import csv, sys
w = csv.writer(sys.stdout)
w.writerow(["name", "desc", "stars"])
for r in repos:
w.writerow([r["name"], r["desc"], r["stars"]])
它会当场跑起来,拿到结果再整理给你。如果过程中某个选择器失效、某个按钮拦不住,Agent 会自己改方案,甚至在 helpers.py 里加新函数。
Claude Code 与 Codex 的细微差别
虽然官方 setup prompt 对两者一视同仁,但在实际使用中 莫潇羽@无忆研习社 发现它们表现略有差异,给大家提前打个预防针:
- Claude Code 在"读长文档"这件事上更耐心,会把
install.md、SKILL.md、helpers.py都仔细过一遍再动手,所以首次安装往往一次就能跑通;但它会问不少确认问题,适合喜欢"边聊边做"的用户 - Codex 更倾向"直接上手试",有时会跳过部分文档先开始执行,失败了再回过头来读。这种风格在大多数情况下速度更快,但偶尔会在一些 Chrome 权限细节上栽跟头
- 两者处理截图反馈的能力都不错,但 Claude Code 对截图的理解通常更细,能指出页面上一些 Codex 容易忽略的视觉线索
建议新手先用 Claude Code 完整跑通一次安装流程,熟练之后可以按任务类型切换使用。
把任务拆成"粗颗粒指令"更省心
这是一个 prompt 工程的小技巧。很多人第一次用 Browser Harness 时,会把指令写成"打开 A,点 B,填 C,再点 D"——这种把步骤都写死的方式其实会限制 Agent 自主决策的空间,反而更容易因为一次选择器失效就全盘卡壳。
更好的方式是交代目标:
帮我去 https://example.com 的商品页,找出所有满足条件的商品,
按某个维度排序,把前 20 条导出成 Markdown 表格。
这样 Agent 会自己规划路径——先看截图判断页面结构,再决定是走前端排序还是调私有 API,中间哪一步卡住它会自己换思路。这才是极简自由式框架的正确打开方式。
一份 domain skill 应该怎么写
前面说到技能文件是 Agent 自己写出来的,那它到底应该长什么样?官方 SKILL.md 在这块给了相当细致的指引,我摘出几条关键点,顺便用 莫潇羽@无忆研习社 的理解解释一下。
一份好的 domain skill 应该捕捉的是网站的"持久形态"——也就是未来几个月乃至一年内基本不会变的那部分事实。值得写进去的内容包括:
- URL 模式和关键查询参数,比如"列表页永远是
/search?q=...&page=...,跳过 loader 的技巧是加?th=1" - 站点私有的 XHR/Fetch 接口:请求路径、方法、请求体结构、鉴权字段。这一条最值钱——很多时候调一个私有 API 比在 DOM 里一层层点快 10 倍
- 稳定的选择器,比如
data-testid、aria-label、语义化的 class 名;以及要避免使用的选择器,比如被 CSS Module 混淆过的哈希类名 - 站点的框架特性,比如"这个下拉框是 React Combobox,只有按 Escape 才会提交值""这个列表是 Vue 的虚拟滚动,行级
scrollIntoView会失效,必须滚动它自己的容器" - 必要的等待与原因:
wait_for_load不够用的地方和背后的机制 - 陷阱:过期草稿、旧版 ID 现在返回 null、Unicode 特殊字符、
beforeunload对话框、验证码出现的触发条件等
而不应该写进去的内容则更明确:
- 绝对不要写原始像素坐标。坐标会随视口、缩放、布局变化而失效,要写的是"如何定位目标",不是"我上次点的位置在哪"
- 不要写这一次任务的流水账。技能是给下一个 Agent 看的地图,不是这一次的日记
- 绝对不要写任何 cookie、session token、账号密码或和特定用户绑定的状态,因为
domain-skills/是公共目录,会进 PR
这几条规则其实放在任何技术文档写作上都是金科玉律,但在 Agent 协作的语境下尤其重要——你没法指望一个只读了笔记就来干活的 Agent 能够自己判断哪些内容是"昨天对今天不对"的。
人能手改这些技能文件吗
理论上可以。但官方非常不鼓励这样做。原因有三:
- 人手写的技能经常带着"理想化假设",比如"这个按钮的 class 就叫 submit-btn",而实际上今天它叫
_3Xf91aB - Agent 自己写的技能,是在真实 CDP 调用跑通后才写下的,每一条都是"实测可行"的事实
- 如果大家都手写,仓库很快就会被低质量的"看起来是对的但其实不对"的 markdown 淹没,整个技能系统就失效了
所以建议的贡献方式非常独特:跑你自己的任务,让 Agent 发现新规律,Agent 自己把技能文件写好,你直接发 PR 把这个 domain-skills/<site>/ 目录提交上去。这种"让使用者同时成为维护者"的模式,在开源世界里也算一股清流。
八、典型使用场景盘点
替代浏览器扩展做浏览器内工作流
一些原本需要写 Tampermonkey 脚本或浏览器插件才能完成的重复动作,现在可以一次性用自然语言交给 Agent。莫潇羽@无忆研习社 试过一个典型场景:把某个资讯站点的订阅列表批量导出到本地 Markdown,整个过程从说"想要"到拿到结果不到 3 分钟,全程没敲一行代码。
表单填充与数据录入
凡是涉及"把一份已有数据搬到某个 Web 系统里"的活,都是 Browser Harness 的菜。它可以对着你自己的 Chrome 登录状态干活,不用处理任何 OAuth、SSO、双因素认证。
爬虫 + 静态资源混抓
helpers.py 里内置了 http_get,官方还特意提示:静态页不要用浏览器去抓,用线程池直接 HTTP 拉比浏览器快一个数量级。这是一个非常务实的建议——Browser Harness 不是只有浏览器自动化,它是一个"以浏览器为中心的网页操作瑞士军刀"。
网页测试与回归
虽然官方没把测试作为主打场景,但把一个测试用例写成一段 Python 放进 uv run bh,再加上截图验证,其实已经是一套极简的 E2E 测试框架。对于小团队来说,比引入完整的 Playwright 测试栈轻得多。
子 Agent 分发
官方支持通过 BROWSER_USE_API_KEY 启动远程浏览器,免费层提供 3 个并发浏览器。这让"一个主 Agent 派发若干子 Agent 去不同网站并行干活"变得很自然,相关做法可以在 cloud.browser-use.com 查看,但本文不深入商业侧,只提工程上的可行性。
研究型工作流:抓取 + 分析 + 产出报告
这是 莫潇羽@无忆研习社 个人觉得最有想象力的一个方向。传统做研究要自己写爬虫、自己清洗数据、自己写报告模板,流程割裂。用 Browser Harness 配合 Claude Code 后,可以把"打开若干目标站点 → 结构化抽取内容 → 本地做文本聚类 → 生成 Markdown 报告"写成一条自然语言指令,全程 AI 自动走完。过程中如果某个网站需要特殊处理,Agent 顺手更新 domain-skills/ 就行,下次还能复用。
浏览器内的私有工作台
不少团队都会有一些"只在浏览器里能做"的私有操作:ERP、CRM、某些 SaaS 后台、内部工单系统。这些系统往往没有像样的 API,传统自动化要么反复模拟登录要么写浏览器插件。Browser Harness 天然对接真实 Chrome,登录一次就够了,非常适合把这些"后台苦力活"打包成一个个 Agent 可调用的能力。
三个实战小例子
光讲原理容易让人云里雾里,莫潇羽@无忆研习社 挑三段自己实际跑过的代码片段,给大家直观的感觉。
例一:批量截取一组页面的首屏做素材整理
urls = [
"https://example.com/a",
"https://example.com/b",
"https://example.com/c",
]
for i, u in enumerate(urls):
goto(u)
wait_for_load()
screenshot(f"shot_{i}.png")
print(f"saved shot_{i}.png, title = {p