基于 Camoufox 内核的反爬浏览器,C++ 层面伪造硬件指纹(WebGL/AudioContext/屏幕几何等),REST API 封装,专为 AI 编程 Agent 的网页自动化任务设计。
站在 Camoufox 这位巨人的肩膀上——Camoufox 是一个在 C++ 层面进行指纹伪造的 Firefox 分支。
由 jo 团队打造,jo 是一个个人 AI 智能体,一半运行在你的 Mac 上,一半运行在专用的云端机器上——无需任何维护。支持 macOS、Telegram、WhatsApp 和电子邮件。免费试用 beta 版 ->
git clone https://github.com/jo-inc/camofox-browser && cd camofox-browser
npm install && npm start
# -> http://localhost:9377
AI 智能体需要浏览真实的网页。Playwright 会被拦截。无头 Chrome 会被指纹识别。隐身插件反而成了指纹。
Camoufox 在 C++ 实现层面修补 Firefox——navigator.hardwareConcurrency、WebGL 渲染器、AudioContext、屏幕几何、WebRTC——全都在 JavaScript 看到之前就伪造好了。没有垫片、没有包装、没有破绽。
本项目将这个引擎包装成专为智能体设计的 REST API:可访问性快照代替臃肿的 HTML、稳定的元素引用用于点击,以及针对常见站点的搜索宏。
C++ 级反检测 - 绕过 Google、Cloudflare 和大多数机器人检测
元素引用 - 稳定的 e1、e2、e3 标识符,保证交互可靠
Token 高效 - 可访问性快照比原始 HTML 小约 90%
任意环境运行 - 懒加载浏览器 + 空闲关闭,空闲时内存保持在约 40MB。设计为与你的技术栈其他部分共用同一台机器——Raspberry Pi、5 美元的 VPS、共享基础设施。
会话隔离 - 每个用户独立的 cookie/存储
Cookie 导入 - 注入 Netscape 格式的 cookie 文件,实现认证浏览
文件上传 - 从配置的上传目录附加文件,无需原生 OS 对话框
代理 + GeoIP - 通过住宅代理路由流量,自动设置地区/时区
结构化日志 - JSON 日志行,带请求 ID,方便生产环境可观测
YouTube 字幕 - 通过 yt-dlp 提取任意 YouTube 视频字幕,无需 API key
搜索宏 - @google_search、@youtube_search、@amazon_search、@reddit_subreddit,以及另外 10 多种
快照截图 - 在可访问性快照旁包含 base64 PNG 截图
大页面处理 - 自动快照截断,基于偏移量的分页
下载捕获 - 捕获浏览器下载,通过 API 获取(可选内联 base64)
DOM 图片提取 - 列出 <img> src/alt,可选返回内联 data URL
任意部署 - Docker、Fly.io、Railway
VNC 交互登录 - 通过 noVNC 可视化登录站点,导出存储状态供智能体复用
OpenAPI 文档 - /openapi.json 自动生成规范,/docs 提供交互式文档
结构化提取 - POST /tabs/:tabId/extract,JSON Schema 将属性映射到快照引用 via x-ref
会话追踪 - 可选按会话开启 Playwright 追踪捕获(截图 + DOM 快照 + 网络),提供 API 端点列出、获取和删除追踪 zip 文件
遥测 - 通过 GitHub Issues 自动发送匿名崩溃/挂起遥测。识别哪些站点导致失败以及常见失败模式。私有域名经 HMAC 哈希处理,路径/参数剥离,token/IP 脱敏。通过 CAMOFOX_CRASH_REPORT_ENABLED=false 退出。
可选依赖
Docker 镜像包含 yt-dlp。对于本地开发,安装它以使用 /youtube/transcript 端点。不安装时,端点会回退到较慢的基于浏览器的方法。
openclaw plugins install @askjo/camofox-browser
工具:camofox_create_tab | camofox_snapshot | camofox_click | camofox_type | camofox_navigate | camofox_scroll | camofox_screenshot | camofox_close_tab | camofox_list_tabs | camofox_import_cookies
npx @askjo/camofox-browser
git clone https://github.com/jo-inc/camofox-browser
cd camofox-browser
npm install
npm start # 首次运行时下载 Camoufox(约 300MB)
默认端口为 9377。参见环境变量了解所有选项。
注意:postinstall 脚本会在获取 Camoufox 二进制文件之前取消设置 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD。没有这个覆盖,导出的 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1(常见于 Playwright 配置使用系统 Chrome 时)会静默跳过二进制文件下载,导致服务器运行时崩溃。
外部 Camoufox 可执行文件:在 npm install 之前和启动服务器时设置 CAMOUFOX_EXECUTABLE=/path/to/camoufox-bin,跳过捆绑下载并启动该可执行文件。兼容别名包括 CAMOUFOX_EXECUTABLE_PATH 和 CAMOFOX_EXECUTABLE_PATH。这对于 NixOS 路径(如 /nix/store/.../camoufox-bin)很有用;可执行文件必须来自包含 properties.json、version.json 和 fontconfig/ 的 Camoufox 捆绑包。
气隙或自定义二进制管理:当已有 Camoufox 捆绑包时优先使用 CAMOUFOX_EXECUTABLE。否则通过 npm install --ignore-scripts 禁用自动获取(跳过每个依赖的生命周期脚本——最粗暴的选项),或者更精细地使用 npm install --omit=optional 加上手动 npx camoufox-js fetch 步骤指向你的镜像。注意 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm install 不再跳过 Camoufox 下载(postinstall 在本地清理了环境);使用 --ignore-scripts 或 CAMOUFOX_EXECUTABLE 来实现。
包含的 Makefile 自动检测 CPU 架构,在 Docker 构建之外预下载 Camoufox + yt-dlp 二进制文件,因此重建很快(约 30 秒 vs 约 3 分钟)。
# 构建并启动(自动检测架构:M1/M2 为 aarch64,Intel 为 x86_64)
make up
# 停止并移除容器
make down
# 强制全新构建(例如升级 VERSION/RELEASE 后)
make reset
# 仅下载二进制文件(不构建)
make fetch
# 显式覆盖架构或版本
make up ARCH=x86_64
make up VERSION=135.0.1 RELEASE=beta.24
Windows 上没有 make。使用包含的 build.ps1 PowerShell 脚本代替:
# 构建并启动
.\build.ps1 up
# 停止并移除容器
.\build.ps1 down
# 仅构建镜像
.\build.ps1 build
# 强制全新构建
.\build.ps1 reset
# 仅下载二进制文件(不构建)
.\build.ps1 fetch
# 覆盖架构
.\build.ps1 up -Arch x86_64
.\build.ps1 up -Arch aarch64
注意:推荐 PowerShell 7+ (pwsh),但 powershell.exe (Windows PowerShell 5.1) 也可以工作。脚本需要启用 WSL2 后端的 Docker Desktop for Windows。
行尾符:本项目包含 .gitattributes 文件,强制 .sh 文件使用 Unix (LF) 行尾符。如果你已经克隆了仓库,在 docker 构建过程中遇到 sh: not found 或 set: Illegal option - 错误,运行:
Get-ChildItem -Recurse *.sh | ForEach-Object { (Get-Content $_) -join "`n" + "`n" | Set-Content $_ -NoNewline }
这会将 shell 脚本转换为 LF 行尾符。以后的克隆会通过 .gitattributes 自动处理。
警告:不要直接运行 docker build。Dockerfile 使用绑定挂载从 dist/ 拉取预下载的二进制文件。始终使用 make up(或 make fetch 然后 make build)——它会先下载二进制文件。
对于 Fly.io 或其他远程 CI,需要一个 Dockerfile,在构建时下载二进制文件而不是使用绑定挂载。
包含 railway.toml。它使用 Dockerfile.ci(在构建时下载二进制文件)并将 Railway 的 PORT 环境变量自动映射到 CAMOFOX_PORT。
# 安装 Railway CLI,然后:
railway link
railway up
通过 Railway 仪表板或 CLI 设置密钥:
railway variables set CAMOFOX_API_KEY="your-generated-key"
将浏览器的 cookie 导入 Camoufox,跳过 LinkedIn、Amazon 等站点的交互式登录。
# macOS / Linux
openssl rand -hex 32
export CAMOFOX_API_KEY="your-generated-key"
openclaw start
同一密钥同时被插件(用于认证请求)和服务器(用于验证请求)使用。两者运行在同一环境中——设置一次即可。
为什么要用环境变量?密钥是秘密。openclaw.json 中的插件配置以明文存储,所以密钥不能放在那里。在 shell 配置文件、systemd 单元、Docker 环境变量或 Fly.io secrets 中设置 CAMOFOX_API_KEY。
Cookie 导入默认禁用。如果未设置 CAMOFOX_API_KEY,服务器会拒绝所有 cookie 请求并返回 403。
安装一个能导出 Netscape 格式 cookie 文件的浏览器扩展(例如 Chrome/Firefox 的 "cookies.txt")。将你需要认证的站点的 cookies 导出。
mkdir -p ~/.camofox/cookies
cp ~/Downloads/linkedin_cookies.txt ~/.camofox/cookies/linkedin.txt
默认目录是 ~/.camofox/cookies/。可用 CAMOFOX_COOKIES_DIR 覆盖。
Import my LinkedIn cookies from linkedin.txt
AI 智能体调用 camofox_import_cookies -> 读取文件 -> 用 Bearer token 发起 POST 请求 -> cookies 被注入浏览器会话。随后调用 camofox_create_tab 访问 linkedin.com 时已处于认证状态。
~/.camofox/cookies/linkedin.txt (Netscape 格式,磁盘上)
|
v
camofox_import_cookies 工具 (解析文件,按域名过滤)
|
v POST /sessions/:userId/cookies
| Authorization: Bearer <CAMOFOX_API_KEY>
| Body: { cookies: [Playwright cookie 对象] }
v
camofox 服务器 (验证、清理、注入)
|
v context.addCookies(...)
|
Camoufox 浏览器会话 (认证状态浏览)
cookiesPath 相对于 cookies 目录解析——路径遍历被阻止,无法访问目录外的文件
每次请求最多 500 个 cookies,文件大小限制 5MB
Cookie 对象经过白名单清理,只保留 Playwright 允许的字段
默认情况下,camofox 会将每个用户的 cookies 和 localStorage 持久化到 ~/.camofox/profiles/。会话在浏览器重启后依然保留——只需登录一次(通过 cookies 或 VNC),后续会话会自动恢复认证状态。
~/.camofox/
|-- cookies/ # 引导 cookie 文件(Netscape 格式)
\-- profiles/ # 持久化的会话状态(自动管理)
\-- <hashed-userId>/
\-- storage_state.json
可用 CAMOFOX_PROFILE_DIR 或在 persistence 插件配置中设置 "profileDir" 覆盖目录。如需禁用持久化,在 camofox.config.json 中设置 "persistence": { "enabled": false }。
默认情况下,存储状态只包含 cookies 和 localStorage。如还需持久化 IndexedDB,在 persistence 插件配置中设置 "indexedDB": true。这会捕获所有可序列化的 IndexedDB 记录——不仅是认证数据——可能导致快照显著变大、检查点变慢。
捕获每个会话中每个操作的 Playwright trace:页面截图、DOM 快照、网络请求和控制台输出。输出是一个单独的 .zip 文件,你可以在 Playwright 内置的 Trace Viewer 中打开。
在打开第一个标签页时传入 trace: true 来选择加入每个会话的追踪:
curl -X POST http://localhost:9377/tabs \
-H 'Content-Type: application/json' \
-d '{"userId":"agent1","sessionKey":"task1","url":"https://example.com","trace":true}'
Trace 在会话关闭时写入。关闭会话以刷新 trace,然后列出、获取和查看:
# 关闭会话以刷新 trace
curl -X DELETE http://localhost:9377/sessions/agent1
# 列出 trace 文件
curl http://localhost:9377/sessions/agent1/traces
# {"traces":[{"filename":"trace-2026-04-18T04-05-00-...zip","sizeBytes":42810,"createdAt":...}]}
# 下载 (Content-Type: application/zip)
curl http://localhost:9377/sessions/agent1/traces/trace-2026-04-18T04-05-00-abc.zip > session.zip
# 在 Playwright Trace Viewer 中查看
npx playwright show-trace session.zip
# 删除
curl -X DELETE http://localhost:9377/sessions/agent1/traces/trace-2026-04-18T04-05-00-abc.zip
为什么要 trace 而不是视频:Camoufox 基于 Firefox,而 Playwright 的 recordVideo 仅支持 Chromium。Trace 在 Firefox 上可用,且提供的信息比视频更多(网络 + DOM + 控制台 + 截图)。
追踪无法在现有会话上切换。如需更改标志,先 DELETE /sessions/:userId。
存储默认位置为 ~/.camofox/traces/<hashed-userId>/,并在服务器启动时清理:
CAMOFOX_TRACES_DIR - 基础目录(默认:~/.camofox/traces)
CAMOFOX_TRACES_MAX_BYTES - 每个 trace 的最大大小,超出后在下一次启动时删除(默认:50MB)
CAMOFOX_TRACES_TTL_HOURS - 早于此时间的 trace 在下一次启动时删除(默认:24)
curl -X POST http://localhost:9377/sessions/agent1/cookies \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_CAMOFOX_API_KEY' \
-d '{"cookies":[{"name":"foo","value":"bar","domain":"example.com","path":"/","expires":-1,"httpOnly":false,"secure":false}]}'
docker run -p 9377:9377 \
-e CAMOFOX_API_KEY="your-generated-key" \
-v ~/.camofox/cookies:/home/node/.camofox/cookies:ro \
camofox-browser
fly secrets set CAMOFOX_API_KEY="your-generated-key"
railway variables set CAMOFOX_API_KEY="your-generated-key"
通过代理路由所有浏览器流量,并利用 Camoufox 内置的 GeoIP 根据代理的 IP 地址自动设置 locale、时区和地理位置。
简单代理(单一端点):
export PROXY_HOST=166.88.179.132
export PROXY_PORT=46040
export PROXY_USERNAME=myuser
export PROXY_PASSWORD=mypass
npm start
回连代理(轮换 sticky 会话):
适用于 Decodo、Bright Data 或 Oxylabs 等提供单一网关端点并支持基于会话的 sticky IP 的提供商:
export PROXY_STRATEGY=backconnect
export PROXY_BACKCONNECT_HOST=gate.provider.com
export PROXY_BACKCONNECT_PORT=7000
export PROXY_USERNAME=myuser
export PROXY_PASSWORD=mypass
npm start
每个浏览器上下文获得一个唯一的 sticky 会话,因此不同用户获得不同的 IP 地址。在代理错误或 Google 屏蔽时会话会自动轮换。
docker run -p 9377:9377 \
-e PROXY_HOST=166.88.179.132 \
-e PROXY_PORT=46040 \
-e PROXY_USERNAME=myuser \
-e PROXY_PASSWORD=mypass \
camofox-browser
配置代理后:
所有流量通过代理路由
Camoufox 的 GeoIP 自动根据代理的出口 IP 设置 locale、时区和地理位置
浏览器指纹(语言、时区、坐标)与代理位置一致
未配置代理时,默认为 en-US、America/Los_Angeles、旧金山坐标
浏览器自动化以难以预测的方式失败——Cloudflare 挑战、站点重设计破坏选择器、重定向循环、弹窗风暴、渲染器崩溃等。问题范围广,失败模式多样。没有遥测数据时,唯一的信号就是"它不工作"。
遥测数据为我们提供结构化数据,了解哪些站点失败、如何失败、失败频率,从而可以优先修复实际影响用户的模式。它会在以下情况自动提交 GitHub Issues:
未捕获的异常导致进程崩溃
事件循环停滞超过 5 秒(看门狗检测)
挫折模式——同一标签页上连续 3 次以上失败(超时、死上下文、导航中止)
每份报告包含失败类型、堆栈跟踪、标签健康计数器(HTTP 状态直方图、控制台错误、请求失败、重定向深度)以及目标 URL——全部匿名化。
遥测数据发送到轻量级 Cloudflare Worker 端点 https://camofox-telemetry.askjo.workers.dev。该端点在环境 secrets 中持有 GitHub App 凭证——没有 secrets 被打包进这个包。
lib/reporter.js (客户端,无 secrets)
| anonymize -> POST https://camofox-telemetry.askjo.workers.dev/report
v
Cloudflare Worker (持有 GitHub App 密钥)
| validate -> rate-limit -> dedup -> create GitHub Issue
v
GitHub Issue 创建
端点源代码在仓库的 workers/crash-reporter/index.ts 中。
你不必信任我们——验证一下线上运行的是什么代码:
# 1. 询问端点它正在运行什么代码
curl https://camofox-telemetry.askjo.workers.dev/source
# -> { "commit": "abc1234", "sha256": "e3b0c44...", "source": "https://github.com/..." }
# 2. 对比这个仓库中源代码的 sha256
sha256sum workers/crash-reporter/index.ts
# 3. 检查 commit 是否与 CI 部署的匹配
# https://github.com/jo-inc/camofox-browser/actions/workflows/telemetry-deploy.yml
git log --oneline workers/crash-reporter/index.ts | head -1
如果哈希不匹配,说明端点运行的代码与仓库中的不同。部署工作流 (.github/workflows/telemetry-deploy.yml) 在部署时注入 commit 和 source 哈希——每次部署都是可在 GitHub Actions 中审计的。
或者完全跳过验证:CAMOFOX_CRASH_REPORT_ENABLED=false 禁用所有遥测数据,或用 CAMOFOX_CRASH_REPORT_URL 指向你自己的端点。
所有报告的数据在离开进程前都会经过偏执级别的匿名化处理(lib/reporter.js L28-290):
URLs -- 已知公共域名(Google、Amazon、Reddit、Cloudflare 等)原样显示,以便我们识别哪些站点导致问题。私有/未知域名替换为稳定的 HMAC 哈希值(site-a1b2c3d4)—— 在不同报告中哈希值相同以供关联,但不可逆推回原始域名。路径段变为 */*/*(仅保留深度)。查询参数变为 ?[3](仅保留数量)。永远不会包含任何键名、键值或路径内容。
文件路径 -> 仅保留文件名(<path>/server.js)
Token、密钥、API 密钥 -> <token>
IP、电子邮件、环境变量 -> 脱敏处理
Docker/Fly 机器 ID -> <id>
Tab 健康状态 -- 纯计数器(崩溃次数、错误次数、状态码直方图)。无页面内容、无 URL、无用户数据。
重复问题按堆栈签名检测,遇到重复问题会在已有 issue 上追加 +1 评论,而不是创建新 issue。
# 禁用遥测
export CAMOFOX_CRASH_REPORT_ENABLED=false
# 指向你自己的端点(见下文)
export CAMOFOX_CRASH_REPORT_URL=https://your-endpoint.example.com/report
# 调整速率限制(默认:每小时 10 次)
export CAMOFOX_CRASH_REPORT_RATE_LIMIT=5
将遥测报告提交到你自己的 GitHub 仓库而非 jo-inc/camofox-browser:
创建 GitHub App -- Settings -> Developer settings -> GitHub Apps -> New Permissions: Repository -> Issues -> Read & Write 取消勾选 Webhook -> Active(不需要) 点击 Generate a key -- 下载一个 .pem 文件 在目标仓库安装该 App(Install App -> 选择仓库) 记录你的 App ID(应用 General 页面上的数字)和 Installation ID(安装后从 URL 中获取:github.com/settings/installations/{id})
创建 GitHub App -- Settings -> Developer settings -> GitHub Apps -> New
Permissions: Repository -> Issues -> Read & Write
取消勾选 Webhook -> Active(不需要)
点击 Generate a key -- 下载一个 .pem 文件
在目标仓库安装该 App(Install App -> 选择仓库)
记录你的 App ID(应用 General 页面上的数字)和 Installation ID(安装后从 URL 中获取:github.com/settings/installations/{id})
部署端点 -- 克隆此仓库并部署 worker:
cd workers/crash-reporter
# 编辑 wrangler.toml:设置 account_id 为你的 Cloudflare 账户 ID
npx wrangler deploy
该 worker 是一个单文件 TypeScript,零 npm 依赖。它也可以在 Deno、Bun 或任何支持 Web Crypto API 的运行时上运行。
设置 worker 密钥:
cd workers/crash-reporter
echo "YOUR_APP_ID" | npx wrangler secret put GH_APP_ID
echo "YOUR_INSTALL_ID" | npx wrangler secret put GH_INSTALL_ID
# 密钥必须是 PKCS#8 DER base64 格式(不是原始 PEM)
openssl pkcs8 -topk8 -inform PEM -outform DER -nocrypt -in your-app.pem | \
base64 | tr -d '\n' | npx wrangler secret put GH_PRIVATE_KEY
# 在你的仓库中创建 issue
echo "your-org/your-repo" | npx wrangler secret put GH_REPO
cd workers/crash-reporter
echo "YOUR_APP_ID" | npx wrangler secret put GH_APP_ID
echo "YOUR_INSTALL_ID" | npx wrangler secret put GH_INSTALL_ID
# 密钥必须是 PKCS#8 DER base64 格式(不是原始 PEM)
openssl pkcs8 -topk8 -inform PEM -outform DER -nocrypt -in your-app.pem | \
base64 | tr -d '\n' | npx wrangler secret put GH_PRIVATE_KEY
# 在你的仓库中创建 issue
echo "your-org/your-repo" | npx wrangler secret put GH_REPO
将 camofox-browser 指向你的端点:
export CAMOFOX_CRASH_REPORT_URL=https://your-worker.your-subdomain.workers.dev/report
export CAMOFOX_CRASH_REPORT_URL=https://your-worker.your-subdomain.workers.dev/report
验证:
curl https://your-worker.your-subdomain.workers.dev/health
# -> {"status":"ok"}
curl https://your-worker.your-subdomain.workers.dev/health
# -> {"status":"ok"}
所有日志输出均为 JSON(每行一个对象),便于日志聚合器解析:
{"ts":"2026-02-11T23:45:01.234Z","level":"info","msg":"req","reqId":"a1b2c3d4","method":"POST","path":"/tabs","userId":"agent1"}
{"ts":"2026-02-11T23:45:01.567Z","level":"info","msg":"res","reqId":"a1b2c3d4","status":200,"ms":333}
健康检查请求(/health)不计入请求日志,以减少噪音。
# 创建一个 tab
curl -X POST http://localhost:9377/tabs \
-H 'Content-Type: application/json' \
-d '{"userId": "agent1", "sessionKey": "task1", "url": "https://example.com"}'
# 获取带元素引用的无障碍快照
curl "http://localhost:9377/tabs/TAB_ID/snapshot?userId=agent1"
# -> { "snapshot": "[button e1] Submit [link e2] Learn more", ... }
# 通过引用点击
curl -X POST http://localhost:9377/tabs/TAB_ID/click \
-H 'Content-Type: application/json' \
-d '{"userId": "agent1", "ref": "e1"}'
# 向元素中输入文本
curl -X POST http://localhost:9377/tabs/TAB_ID/type \
-H 'Content-Type: application/json' \
-d '{"userId": "agent1", "ref": "e2", "text": "hello", "pressEnter": true}'
# 使用搜索宏导航
curl -X POST http://localhost:9377/tabs/TAB_ID/navigate \
-H 'Content-Type: application/json' \
-d '{"userId": "agent1", "macro": "@google_search", "query": "best coffee beans"}'
curl -X POST http://localhost:9377/youtube/transcript \
-H 'Content-Type: application/json' \
-d '{"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "languages": ["en"]}'
# -> { "status": "ok", "transcript": "[00:18] [music] We're no strangers to love [music]\n...", "video_title": "...", "total_words": 548 }
优先使用 yt-dlp(快速,无需浏览器)。如果未安装 yt-dlp,则回退到基于浏览器的拦截方式——这种方式较慢且不太可靠,因为 YouTube 有广告前贴片。
@google_search | @youtube_search | @amazon_search | @reddit_search | @reddit_subreddit | @wikipedia_search | @twitter_search | @yelp_search | @spotify_search | @netflix_search | @linkedin_search | @instagram_search | @tiktok_search | @twitch_search
Reddit 宏直接返回 JSON(无需 HTML 解析):
@reddit_search - 搜索整个 Reddit,返回包含 25 条结果的 JSON