Skip to main content

patchright-browser

反检测浏览器操作技能(Patchright 驱动)。当普通抓取遇到 Cloudflare/DataDome 等风控墙、强反爬站点(小红书/闲鱼等敏感操作)、需要隐身身份隔离于日常浏览器,或需要渲染/点击/登录态时使用。本技能通过常驻控制服务暴露精简 CLI,token 消耗远低于 MCP 方案。日常搜索、读静态页优先 WebFetch/curl,不要用本技能。

Source facts

Repository
XuePengJu/agentest-skillhub
Last source activity
October 16, 2026 at 01:56
Detected SKILL.md language
Chinese
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
7 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
patchright-browser
description
反检测浏览器操作技能(Patchright 驱动)。当普通抓取遇到 Cloudflare/DataDome 等风控墙、强反爬站点(小红书/闲鱼等敏感操作)、需要隐身身份隔离于日常浏览器,或需要渲染/点击/登录态时使用。本技能通过常驻控制服务暴露精简 CLI,token 消耗远低于 MCP 方案。日常搜索、读静态页优先 WebFetch/curl,不要用本技能。
version
1.0.0
license
Apache-2.0
display_name
Patchright 隐身浏览器
display_name_en
Patchright Stealth Browser
# Patchright 隐身浏览器技能 Patchright = 打过反检测补丁的 Playwright(去 Runtime.enable 泄漏、去自动化标记)。本技能用常驻 HTTP 控制服务(默认 `127.0.0.1:9333`)+ curl 一行命令操作,输出精简省 token。 **约定**:下文 `$SKILL_DIR` 是**文档占位符、不是环境变量** —— 它指本 SKILL.md 所在目录(即技能根目录),执行命令时请替换为实际绝对路径或先 `export SKILL_DIR=<实际技能目录>`。随附脚本一律用自身路径定位,不依赖安装位置与当前工作目录。 **平台**:核心链路(Patchright + 控制服务)跨平台;脚本示例用 `bash`/`curl`,macOS 与 Linux 均可。文中「macOS 启动优化」一节仅涉 macOS。 ## 路由边界(先读) - 日常搜索、读内容、静态页 → **WebSearch / WebFetch / curl,禁止启动本技能** - 需要渲染、点击、登录态、多步交互的普通操作 → **本技能(`/eval` + `/extract` 定向取值)** - 遇到风控墙(Cloudflare 挑战页、403 反爬)、强风控站点操作、需要身份隔离 → **本技能(首选,唯一带隐身补丁)** ## 前置检查 ```bash PY="${PR_PY:-python3}" # 用装了 patchright 的解释器,可用 PR_PY 指定 "$PY" -c "import patchright" 2>/dev/null || "$PY" -m pip install --no-cache-dir patchright ``` 若未装系统 Chrome 内核且无 `channel="chrome"`,脚本自动回退 patchright 自带 chromium(隐身稍弱);需要时执行 `"$PY" -m patchright install chromium`。 ## 启动 / 停止 ```bash PY="${PR_PY:-python3}" # 启动(常驻服务,须以后台任务方式跑,勿用 nohup 再包一层) "$PY" "$SKILL_DIR/scripts/pr.py" serve # 存活检查(GET) curl -s 127.0.0.1:9333/status # 停止 curl -s -X POST 127.0.0.1:9333/close ``` - 单例:重复启动自动让位(裸 socket 探端口,不走代理) - profile 默认落在 `~/.workbuddy/skills/patchright-browser/profile/`,登录态跨会话保留;用 `PR_PROFILE` 可指定到别处,删除即重置身份 - 默认**有头**模式(过检率最高);无头加环境变量 `PR_HEADLESS=1` - 换端口:`PR_PORT=9334` - 启动策略:在受限进程树内 Chrome 原生沙箱可能初始化失败,故默认 `--no-sandbox` + `--test-type` 压掉"不受支持的命令行标记"提示栏(该提示栏会占 ~40px 视口,属可检测特征;页面 JS 读不到命令行本身)。在普通终端直接跑可用 `PR_CLEAN=1` 获得剔除 no-sandbox 的完全干净启动 ### macOS 启动优化(进阶,可选) 若把浏览器封装成「双击即用」的 `.app`,注意一个 macOS 调度陷阱:`.app` 若为 `LSUIElement`(后台型)应用,**由 LaunchServices 直接启动时系统会把整棵进程树按"后台应用"降级调度 → `nice=5`**,在多核机器上只能挤到能效核,**Chrome 加载多资源页面慢 20~60 倍**(实测同一首页 26.2s/15.0s vs 0.52s/0.45s)。 解法是把启动器改为经 `launchctl bootstrap` 由 launchd 拉起(plist 放 `~/Library/LaunchAgents/`),即可拿到正常优先级 `nice=0`。 - 自检:查 `pr.py` 与 Chrome 主进程的 `nice`,**必须是 0**;若为 5 就是又被降级了 - 该 `.app` 启动器为作者本机自建,未随本技能发布;按上述原理自行封装即可 ## 操作命令(POST JSON,一律 `curl -s`,建议带 `-m` 超时) | 命令 | 示例 | |---|---| | 导航 | `curl -s -m 60 -X POST 127.0.0.1:9333/goto -d '{"url":"https://example.com","wait":"#main"}'` | | 点击 | `curl -s -X POST 127.0.0.1:9333/click -d '{"selector":"button.submit"}'` | | 填表 | `curl -s -X POST 127.0.0.1:9333/fill -d '{"selector":"#kw","text":"关键词"}'` | | 执行 JS | `curl -s -X POST 127.0.0.1:9333/eval -d '{"expr":"document.title"}'` | | 批量提取 | `curl -s -X POST 127.0.0.1:9333/extract -d '{"css":"h2 a","attr":"href","limit":50}'` | | 截图 | `curl -s -X POST 127.0.0.1:9333/shot -d '{"full":false}'` → 返回图片路径 | | 标签页 | `.../tabs -d '{"op":"list"}'` / `new` / `switch` / `close` | 完整参数说明见 `references/api_reference.md`。 ## 省 token 约定 - 优先 `/extract`(带 `limit`、`attr`)拿结构化结果,`/eval` 只用于 extract 覆盖不了的逻辑 - 截图只回路径,需要看图再用 Read 工具打开 - 所有响应超 4000 字符自动截断(`PR_MAX_OUT` 可调),先靠 limit/attr 收窄而不是调大阈值 ## 已知坑 - **打开网页极慢(nice=5 后台降级)**:见上文「macOS 启动优化」。诊断顺序:① 查 `pr.py`/Chrome 的 nice 是不是 5;② 若是 5 → 改用 launchd 拉起;③ 排除法(同 profile 副本 + 同参数另起一个实例,若秒开则证明是"实例被降级/状态异常",而非 profile/参数/代理问题) - **窗口一闪即关 / 反复开关**:旧版单例检测用 `urllib` 探测 `127.0.0.1:9333`,在有全局 `http_proxy` 的环境会被代理劫持 → 永远误判"没在跑" → 真的另起一个浏览器,随后端口绑定失败又退出。已改为**裸 socket 探端口**(不走代理)+ 绑定失败时关闭刚起的浏览器。若再现,先查日志里是否有连续多次 `ui mode:` 且缺失 `already running` - **浏览器被强杀或彻底退出后,旧版服务不会自愈、会永久卡死**(2026-10-16 实测并已修) **症状**:端口仍在监听,`/status` 甚至仍回 `{"running": true, "pages": [...]}`(读到的是陈旧页面列表),但任何真实操作(`goto` 等)会**永久挂起** —— 单线程服务被这一个请求彻底堵死,表现为「9333 在监听但完全不通」。 **根因**(做过双向验证:确认 kill 真的生效、Chrome 进程确已消失):浏览器一消失,patchright 的**三个官方信号全都不可信** —— - `ctx.pages` 返回**陈旧**的页面列表(浏览器已死仍照报 1 页) - `ctx.browser.is_connected()` 仍返回 `True` - `context.on("close")` / `browser.on("disconnected")` **都不触发** 于是旧版 `--ui` 的生命周期判定 `bool(ctx.pages)` 恒为真 → 服务永不退出。 **修法(已内置于 `pr.py` 的 `browser_alive()` / `_serving()`,`/status` 与 `--ui` 都改用它)**:判活改为**分层降级**,不再信 Playwright: | 序 | 判据 | 结论 | |---|---|---| | 1 | `launch()` 时记录下的 Chrome 主进程 PID,`os.kill(pid, 0)` | 进程没了 => 死(强杀/崩溃/优雅退出全覆盖)| | 2 | `profile/SingletonLock` → `<hostname>-<pid>` + 验活 | 锁在且活 => 活;锁在但 pid 已死 => 死(悬空链接)| | 3 | 启动时锁存在过、现在没了 | => 死(Chrome 优雅退出会**自己删掉**锁文件)| | 4 | 以上都不适用 | 返回 `None`,退回旧 `ctx.pages` 判定 | **为什么必须记录 PID,而不能只看锁**(2026-10-16 补测):用户**优雅退出**(Cmd+Q)时 Chrome 会把 `SingletonLock` **删掉**,于是判据 2 失效、`browser_alive()` 只能返回 `None`,一路退回 `ctx.pages` —— 而那也是**陈旧**的(明明浏览器没了照报 1 页)。结果 `/status` 依旧假阳性。判据 1 就是为了堵这个洞:**别依赖"锁存在"这个前提**。 **排查已在跑旧版服务的实例**(旧版指未含上述判据 1/3 的 `pr.py`): ```bash lsof -nP -iTCP:9333 -sTCP:LISTEN # 有监听 curl -s -m 5 127.0.0.1:9333/status # 旧版可能恒回 running:true,不可信 ls -la <profile>/SingletonLock # 悬空链接 => 被强杀;文件不存在 => 优雅退出过 lsof -nP 2>/dev/null | grep -F "<profile>" # 无输出 => 浏览器真的不在了(最可靠) kill <服务PID> # 处置:只能 kill ``` ⚠️ **别用 `lsof -c Chrome` 数 Chrome 进程**:macOS 上进程名是 `Google Chrome`,`lsof` 的 `-c` 按前 9 字符匹配,`-c Chrome` **恒返回 0**,会让人误判"浏览器已退出"(我踩过)。要数就用 `lsof -c "Google Ch"`,或直接查 profile 占用(更准)。 **连带症状**:此时双击 `LSUIElement` 的 `.app` 会**毫无反应** —— 启动器单例检测误判「已在运行」直接退出,或(探测超时)交给 `pr.py`,而 `pr.py` 的端口探测发现端口被占同样直接退出。所以「双击没反应」先查这个,别急着怀疑启动器坏了。 - **单线程阻塞**:服务是单线程 `HTTPServer`,一个慢请求(如 goto 等慢站)会堵住后续所有请求。curl 侧务必带 `-m`(超时),长导航用 `{"timeout":毫秒}` 收窄 Playwright 侧等待 - **选择器过时**:不要凭记忆写 `#kw`/`#su` 这类旧版选择器(某搜索引擎新版首页输入框是 `#chat-textarea`,`#kw` 隐藏不可见导致 fill/click 超时)。先 `/eval` 用 `offsetParent!==null` 过滤出**可见**输入框/按钮,拿到真实选择器再 fill/click - **截图偶发空响应**:结果页大图首次 `/shot` 可能无响应(服务被上一请求余波卡住),带 `-m 20` 重试一次即可成功 - **所有网站图标变成灰色地球(favicon 被吞)**:patchright 默认开启全局请求拦截,驱动 `coreBundle.js` 的 `requestStarted()` 里有 `if (request._isFavicon) { route?.abort("aborted"); return; }` —— 上游 Playwright 因默认不开拦截(`route2` 为 null),这行是死代码;patchright 开了拦截,于是**每个 `/favicon.ico` 都在发出网络请求前被真的掐断**(表现为 netlog 里 `CORS_REQUEST` 只到 phase=1 就没了下文)。**只有 `.ico` 受影响**,PNG/SVG/`<link rel=icon>` 图标正常 → 典型症状是"少数站点有图标、大多数没有"。修复:把 `abort("aborted")` 改成 `continue({})`(等价于不拦截该请求)。升级 patchright 会覆盖驱动文件,需重跑补丁: ```bash PY="${PR_PY:-python3}" "$PY" "$SKILL_DIR/scripts/reapply_favicon_fix.py" # 打补丁(自动备份 .bak-faviconfix) "$PY" "$SKILL_DIR/scripts/reapply_favicon_fix.py" --check # 只体检,退出码 0=在位 curl -s --noproxy '*' -X POST 127.0.0.1:9333/close # 重启生效 ``` 验证:打开目标站点静置 ~20s,`<profile>/Default/Favicons` 里应新增该站 `favicon.ico`,且 `icon_mapping` 出现该站点。诊断口径:查 `icon_mapping`(页面→图标映射)而不是只看 `favicons`(去重后的图标 URL)。 ## 隐身纪律 - 保持 `launch_persistent_context` + 独立 profile,不要改成 `launch + new_context` - **不要设置自定义 user_agent**(默认即最优) - 遇到挑战页:`goto` 后 sleep 3~5 秒再检查,不要高频刷新 - 操作节奏放慢:风控站点每步之间可 `sleep 1~3` ## 自检 ```bash PY="${PR_PY:-python3}" "$PY" "$SKILL_DIR/scripts/selftest.py" # 依赖 + 内核 "$PY" "$SKILL_DIR/scripts/selftest.py" --with-server # 含端到端(会临时拉起服务) ```
View on GitHub