Remotely control a real Chrome browser on a Windows machine over LAN HTTP — from a NAS, Docker container, or any Linux/Mac client. No Playwright required on the client side.
NAS / Docker (client) Windows Machine
scripts/client.py ──HTTP POST /start──► controller.js (Node.js + Playwright)
──HTTP POST /goto ──► └─ launches Chrome (CDP 127.0.0.1:9222)
──HTTP POST /eval ──► └─ executes JS inside page
──HTTP GET /screenshot► └─ returns PNG screenshot
── all ops via HTTP ──►
Typical use-cases: login-state reuse, anti-bot site automation (Douyin, Xiaohongshu, 1688, Taobao), remote page scraping in a real Windows browser environment.
| File | Runs on | Description |
|---|---|---|
controller/controller.js |
Windows | Node.js HTTP server + Playwright; launches Chrome and exposes a full page-operation API |
scripts/client.py |
NAS / Docker | Lightweight HTTP client; pure Python stdlib, zero extra dependencies |
references/api.md |
— | Full API specification and response format reference |
- Node.js 16+
playwright-core:npm install playwright-core- Google Chrome installed
- Firewall rules allowing inbound TCP on port 17373 (controller) and 9223 (CDP proxy)
git clone https://github.com/LazyLinchen/openclaw-broswe.git
npm init -y
npm install playwright-core
node controller/controller.js| Variable | Default | Description |
|---|---|---|
WIN_BROWSER_PORT |
17373 |
Controller HTTP listen port |
WIN_BROWSER_TOKEN |
— | Auth token (sent as X-Token header) |
CHROME_PATH |
auto-detected | Path to chrome.exe |
CDP_PORT |
9222 |
Chrome local CDP debug port |
CDP_PROXY_PORT |
9223 |
TCP proxy external port (for direct CDP access) |
CHROME_USER_DATA_DIR |
%TEMP%\chrome-cdp-profile |
Chrome profile directory (preserves login state) |
⚠️ Security: The controller listens on0.0.0.0. Always setWIN_BROWSER_TOKENto restrict access.
netsh advfirewall firewall add rule name="win-browser controller" dir=in action=allow protocol=TCP localport=17373
netsh advfirewall firewall add rule name="win-browser CDP proxy" dir=in action=allow protocol=TCP localport=9223export WIN_BROWSER_HOST=192.168.1.100 # LAN IP of the Windows machine
export WIN_BROWSER_PORT=17373
export WIN_BROWSER_TOKEN=mysecret # must match controllerpython3 scripts/client.py statusWB="python3 scripts/client.py"
# Lifecycle
$WB start
$WB status
$WB stop
# Navigation
$WB goto https://www.example.com
$WB goto https://www.example.com --wait-until networkidle
# Screenshot (saved to /tmp/wb-shot.png by default)
$WB shot
$WB shot --path /tmp/shot.png
# Page content
$WB content
# JavaScript evaluation
$WB eval "document.title"
$WB eval '() => [...document.querySelectorAll("h2")].map(e => e.innerText)'
# Data extraction helpers
$WB text "h1.title"
$WB text ".product-name" --all
$WB attr "a" href --all
$WB cookies
# Interaction
$WB click "button.submit"
$WB fill "input#search" "keyword"
$WB press Enter
$WB scroll down 700
$WB scroll up 500
# Wait
$WB wait ".product-list" --state visible --timeout 5000
# Tab management
$WB tabs
$WB switch-tab # switch to latest tab
$WB switch-tab 0 # switch to tab by index
$WB close-tab
$WB contexts
# Batch mode (one command per line, # for comments)
$WB batch commands.txt
echo -e "start\ngoto https://example.com\nshot\nstop" | $WB batchAll requests require the X-Token header. See references/api.md for the full specification.
| Method | Path | Body | Description |
|---|---|---|---|
| POST | /start |
— | Launch Chrome |
| GET | /status |
— | Query controller & browser state |
| POST | /stop |
— | Stop Chrome and clean up |
| POST | /goto |
{url, waitUntil?} |
Navigate to URL |
| GET | /screenshot |
— | Capture viewport (returns PNG binary) |
| GET | /content |
— | Get full page HTML |
| POST | /eval |
{script} |
Execute JavaScript |
| POST | /click |
{selector, timeout?} |
Click element |
| POST | /fill |
{selector, value} |
Fill input |
| POST | /press |
{key} |
Press keyboard key |
| POST | /scroll |
{direction, amount?} |
Mouse-wheel scroll |
| POST | /wait |
{selector?, state?, timeout?} |
Wait for element or time |
| GET | /tabs |
— | List all open tabs |
| POST | /switch-tab |
{index?} |
Switch active tab |
| POST | /close-tab |
— | Close current tab |
| GET | /contexts |
— | List browser contexts |
- controller.js runs on Windows, maintains a persistent Playwright + CDP connection to Chrome, and exposes all page operations over HTTP (~50 ms overhead per call).
- client.py uses only Python stdlib — no Playwright, no WebSocket, just
urllib. - Chrome's CDP is bound only to
127.0.0.1:9222;controller.jsalso starts a TCP proxy on0.0.0.0:9223for optional direct CDP access from external tools.