Freeman Browser — Complete Browser Automation Playbook
Don't use plain Playwright. Don't use fetch. Don't use curl. Use this.
Quick Start
CODEBLOCK0
launchFreeman() — options
CODEBLOCK1
Default fingerprint (what sites see)
- - Device: iPhone 15 Pro, iOS 17.4.1, Safari
- Viewport: 393×852, deviceScaleFactor=3
- Timezone: America/New_York (configurable via
browser.json) - Touch: 5 points, real touch events
- webdriver: INLINECODE1
- Mouse: Bezier curve paths, not straight lines
- Typing: 60–220ms/char + random pauses
You can customize the timezone, locale, and geolocation by creating a browser.json file in your working directory:
CODEBLOCK2
Freeman-like interaction helpers
CODEBLOCK3
Shadow DOM — forms inside web components
Reddit, Shopify, many modern React apps use Shadow DOM for forms. Standard page.$() and page.fill() won't find these inputs.
Detect if Shadow DOM is the issue
CODEBLOCK4
Universal shadow DOM traversal
CODEBLOCK5
Playwright's built-in shadow DOM piercing
Playwright can pierce shadow DOM natively in some cases:
// Works for single shadow root (not nested)
await page.locator('input[name="username"]').fill('value'); // auto-pierces 1 level
Rich text editors (Lexical, ProseMirror, Quill, Draft.js)
Standard page.fill() and page.type() don't work on contenteditable editors.
Clipboard paste — most reliable method
CODEBLOCK7
Common editor selectors
'[data-lexical-editor]' // Reddit, Meta, many modern apps
'.public-DraftEditor-content' // Draft.js (Twitter, Quora)
'.ql-editor' // Quill (many SaaS apps)
'.ProseMirror' // ProseMirror (Linear, Confluence)
'[contenteditable="true"]' // Generic — pick the right one if multiple
'.tox-edit-area__iframe' // TinyMCE — need to switch into iframe
Login patterns
Reddit (shadow DOM + Enter key submission)
CODEBLOCK9
Key insights for Reddit:
- - Mobile launchFreeman() shows app redirect page — always use INLINECODE7
- Button click on "Log In" unreliable —
keyboard.press('Enter') works - INLINECODE9 pierces Reddit's shadow DOM automatically
- reCAPTCHA v3 scores the session — human-like typing delays improve score
- After login, URL stays at
/login/ — check via /api/me.json, not URL
CAPTCHA solving (2captcha integration)
Use when a site's login or form requires CAPTCHA.
2captcha API key: INLINECODE12
CODEBLOCK10
Network interception (intercept/modify/mock requests)
CODEBLOCK11
Common debugging techniques
Take screenshot when something fails
CODEBLOCK12
Dump all visible form elements
CODEBLOCK13
Check if login actually worked (don't trust URL)
CODEBLOCK14
Verify stealth fingerprint
const fp = await page.evaluate(() => ({
webdriver: navigator.webdriver,
platform: navigator.platform,
touchPoints: navigator.maxTouchPoints,
languages: navigator.languages,
vendor: navigator.vendor,
}));
console.log(fp);
// webdriver: false ✅, platform: 'iPhone' ✅, touchPoints: 5 ✅
Cloudflare bypass patterns
Cloudflare checks these signals (in order of importance):
- 1. IP reputation
- TLS fingerprint (JA4)
- navigator.webdriver —
true = instant block - Mouse entropy — no mouse events = bot
- Canvas fingerprint — static across sessions = flagged
- HTTP/2 fingerprint
CODEBLOCK16
If still blocked:
- - Try desktop mode:
launchFreeman({ mobile: false }) — some CF rules target mobile UAs - Add longer wait:
await sleep(5000) after navigation before interacting
Session persistence (save/restore cookies)
CODEBLOCK17
Freeman Browser — 完整浏览器自动化操作手册
不要使用普通的 Playwright。不要使用 fetch。不要使用 curl。用这个。
快速开始
js
const { launchFreeman } = require(./.agents/skills/freeman-browser/scripts/browser-freeman);
const { page, humanType, humanScroll, sleep } = await launchFreeman();
await page.goto(https://any-protected-site.com);
// Cloudflare、DataDome、Instagram — 全部通过。
launchFreeman() — 选项
js
// 移动端(默认):iPhone 15 Pro,触控事件
const { browser, page, humanType, humanClick, humanScroll, humanRead, sleep } = await launchFreeman();
// 桌面端:Chrome — 用于拒绝移动端的网站
const { browser, page } = await launchFreeman({ mobile: false });
默认指纹(网站看到的内容)
- - 设备: iPhone 15 Pro,iOS 17.4.1,Safari
- 视口: 393×852,deviceScaleFactor=3
- 时区: America/New_York(可通过 browser.json 配置)
- 触控: 5点,真实触控事件
- webdriver: false
- 鼠标: 贝塞尔曲线路径,非直线
- 打字: 60–220ms/字符 + 随机停顿
您可以通过在工作目录中创建 browser.json 文件来自定义时区、语言环境和地理位置:
json
{
locale: en-US,
timezoneId: America/New_York,
geolocation: {
latitude: 40.7128,
longitude: -74.006,
accuracy: 50
}
}
Freeman 式交互辅助函数
js
// 输入 — 触发所有原生输入事件(React、Angular、Vue、Web Components)
await humanType(page, input[name=email], user@example.com);
// 点击 — 点击前使用贝塞尔鼠标移动
await humanClick(page, x, y);
// 滚动 — 平滑、分步、带抖动
await humanScroll(page, down); // 或 up
// 阅读 — 模拟阅读时间的随机停顿
await humanRead(page); // 等待 1.5–4 秒
// 睡眠
await sleep(1500);
Shadow DOM — Web 组件内的表单
Reddit、Shopify 以及许多现代 React 应用在表单中使用 Shadow DOM。标准的 page.$() 和 page.fill() 无法找到这些输入框。
检测是否为 Shadow DOM 问题
js
// 如果返回 0 但输入框在屏幕上可见 — 说明是 Shadow DOM
const inputs = await page.$$(input);
console.log(inputs.length); // 0 = shadow DOM
通用 Shadow DOM 遍历
js
// 深度查询 — 查找任意深度 shadow root 内的元素
async function shadowQuery(page, selector) { ... }
// 在 Shadow DOM 中填充输入框
async function shadowFill(page, selector, value) { ... }
// 根据文本在 Shadow DOM 中点击按钮
async function shadowClickButton(page, buttonText) { ... }
// 转储所有输入框(包括 Shadow DOM)— 用于调试
async function dumpInteractiveElements(page) { ... }
Playwright 内置的 Shadow DOM 穿透
Playwright 在某些情况下可以原生穿透 Shadow DOM:
js
// 适用于单层 shadow root(非嵌套)
await page.locator(input[name=username]).fill(value); // 自动穿透 1 层
富文本编辑器(Lexical、ProseMirror、Quill、Draft.js)
标准的 page.fill() 和 page.type() 不适用于 contenteditable 编辑器。
剪贴板粘贴 — 最可靠的方法
js
// 适用于所有富文本编辑器(Reddit、Notion、Linear 等)
async function pasteIntoEditor(page, editorSelector, text) { ... }
常见编辑器选择器
js
[data-lexical-editor] // Reddit、Meta、许多现代应用
.public-DraftEditor-content // Draft.js(Twitter、Quora)
.ql-editor // Quill(许多 SaaS 应用)
.ProseMirror // ProseMirror(Linear、Confluence)
[contenteditable=true] // 通用 — 如有多个请选择正确的
.tox-edit-area
iframe // TinyMCE — 需要切换到 iframe 内
登录模式
Reddit(Shadow DOM + Enter 键提交)
js
// Reddit 使用 Shadow DOM 表单 AND reCAPTCHA — 必须使用桌面模式 + Enter
const { browser, page, sleep } = await launchFreeman({ mobile: false }); // 需要桌面端
await page.goto(https://www.reddit.com/login/, { waitUntil: domcontentloaded });
await sleep(3000);
// 自然输入 — 触发 React 状态 + reCAPTCHA 评分
await page.locator(input[name=username]).click();
await sleep(500);
await page.keyboard.type(USERNAME, { delay: 120 });
// ... 使用 Enter 键,而非按钮点击 — Enter 触发正确的表单提交
await page.keyboard.press(Enter);
Reddit 的关键见解:
- - 移动端 launchFreeman() 显示应用重定向页面 — 始终使用 { mobile: false }
- 点击登录按钮不可靠 — keyboard.press(Enter) 有效
- page.locator(input[name=username]) 自动穿透 Reddit 的 Shadow DOM
- reCAPTCHA v3 对会话进行评分 — 类人打字延迟可提高评分
- 登录后,URL 仍停留在 /login/ — 通过 /api/me.json 检查,而非 URL
CAPTCHA 解决(2captcha 集成)
当网站的登录或表单需要 CAPTCHA 时使用。
2captcha API 密钥: 14cbfeed64fea439d5c055111d6760e5
js
const { solveCaptcha } = require(./.agents/skills/freeman-browser/scripts/browser-freeman);
// 用法:解决,然后在提交前注入表单
const { token, type } = await solveCaptcha(page, { apiKey: 14cbfeed64fea439d5c055111d6760e5 });
await page.click(button[type=submit]);
网络拦截(拦截/修改/模拟请求)
js
// 拦截并记录所有请求
page.on(request, req => { ... });
// 拦截响应体
page.on(response, async res => { ... });
// 修改请求(例如,注入令牌)
await page.route(/api/submit, async route => { ... });
// 阻止跟踪器以加快页面加载速度
await page.route(/(analytics|tracking|ads)/, route => route.abort());
常见调试技巧
失败时截图
js
await page.screenshot({ path: /tmp/debug.png });
转储所有可见表单元素
js
const els = await dumpInteractiveElements(page);
console.log(els);
检查登录是否实际成功(不要相信 URL)
js
// 通过 API/cookie 检查 — 登录后 URL 通常保持不变
const me = await page.evaluate(async () => {
const r = await fetch(/api/me.json, { credentials: include });
return (await r.json())?.data?.name;
});
验证隐身指纹
js
const fp = await page.evaluate(() => ({
webdriver: navigator.webdriver,
platform: navigator.platform,
touchPoints: navigator.maxTouchPoints,
languages: navigator.languages,
vendor: navigator.vendor,
}));
console.log(fp);
// webdriver: false ✅, platform: iPhone ✅, touchPoints: 5 ✅
Cloudflare 绕过模式
Cloudflare 检查以下信号(按重要性排序):
- 1. IP 信誉
- TLS 指纹(JA4)
- navigator.webdriver — true = 立即拦截
- 鼠标熵 — 无鼠标事件 = 机器人
- Canvas 指纹 — 跨会话静态 = 被标记
- HTTP/2 指纹
js
// 针对 Cloudflare 保护网站的最佳实践
const { page, humanScroll, sleep } = await launchFreeman();
await page.goto(https://cf-protected.com, { waitUntil: networkidle, timeout: 30000 });
await sleep(2000); // 让 CF