diff --git a/backend/browser_manager.py b/backend/browser_manager.py
index 327f91b5..2f7f149b 100644
--- a/backend/browser_manager.py
+++ b/backend/browser_manager.py
@@ -7,6 +7,7 @@
import logging
import os
import socket
+import sys
import time
from dataclasses import dataclass
from pathlib import Path
@@ -174,14 +175,9 @@ async def launch(self, profile: dict[str, Any]) -> RunningProfile:
self._launching.add(profile_id)
display, ws_port = await self.vnc.allocate()
+ vnc_available = self.vnc.available
- try:
- cdp_port = self._allocate_cdp_port()
- except ValueError:
- async with self._lock:
- self._launching.discard(profile_id)
- await self.vnc.stop_vnc(display)
- raise
+ cdp_port = self._allocate_cdp_port()
# Clean stale Chromium lock files (left by previous container crashes)
user_data_dir = Path(profile["user_data_dir"])
@@ -193,27 +189,32 @@ async def launch(self, profile: dict[str, Any]) -> RunningProfile:
_init_profile_defaults(user_data_dir)
try:
- # Start KasmVNC on the allocated display
- await self.vnc.start_vnc(
- display,
- ws_port,
- width=profile.get("screen_width", 1920),
- height=profile.get("screen_height", 1080),
- )
+ # Start KasmVNC on the allocated display (if available)
+ if vnc_available:
+ await self.vnc.start_vnc(
+ display,
+ ws_port,
+ width=profile.get("screen_width", 1920),
+ height=profile.get("screen_height", 1080),
+ )
# Build fingerprint args from profile settings
extra_args = self._build_fingerprint_args(profile)
extra_args += profile.get("launch_args") or []
extra_args.append(f"--remote-debugging-port={cdp_port}")
- # Normalize proxy format (host:port:user:pass → http://user:pass@host:port)
+ # Normalize proxy format
raw_proxy = profile.get("proxy") or None
proxy = _normalize_proxy(raw_proxy) if raw_proxy else None
if proxy:
_validate_proxy(proxy)
- # Launch CloakBrowser on that display
- # DISPLAY is passed via env kwarg to avoid process-wide os.environ mutation
+ # Build env: on VNC, set DISPLAY; on native desktop, don't
+ launch_env = {**os.environ}
+ if vnc_available:
+ launch_env["DISPLAY"] = f":{display}"
+
+ # Launch CloakBrowser
context = await launch_persistent_context_async(
user_data_dir=profile["user_data_dir"],
headless=bool(profile.get("headless", False)),
@@ -230,7 +231,7 @@ async def launch(self, profile: dict[str, Any]) -> RunningProfile:
"width": profile.get("screen_width", 1920),
"height": profile.get("screen_height", 1080) - 133,
},
- env={**os.environ, "DISPLAY": f":{display}"},
+ env=launch_env,
)
# Inject clipboard listener: captures copied text on every page
@@ -380,9 +381,12 @@ def _build_fingerprint_args(self, profile: dict[str, Any]) -> list[str]:
"""Build extra Chromium args from profile fingerprint settings."""
args: list[str] = [
"--disable-infobars",
- "--test-type", # suppress "unsupported flag: --no-sandbox" bad flags warning
- "--use-angle=swiftshader", # software GL for VNC (no GPU in container)
+ "--test-type",
]
+ # Only use software GL when running via VNC (no GPU in container).
+ # On native desktop (Windows/macOS), hardware GL is available.
+ if self.vnc.available:
+ args.append("--use-angle=swiftshader")
seed = profile.get("fingerprint_seed")
if seed is not None:
diff --git a/backend/database.py b/backend/database.py
index e4682564..4f54ef12 100644
--- a/backend/database.py
+++ b/backend/database.py
@@ -4,14 +4,28 @@
import datetime
import json
+import os
import random
import sqlite3
+import sys
import uuid
from contextlib import contextmanager
from pathlib import Path
from typing import Any
-DATA_DIR = Path("/data")
+
+
+def _get_data_dir() -> Path:
+ """Platform-appropriate data directory."""
+ if os.environ.get("CLOAKBROWSER_DATA_DIR"):
+ return Path(os.environ["CLOAKBROWSER_DATA_DIR"])
+ if sys.platform == "win32":
+ base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
+ return base / "cloakbrowser-manager"
+ return Path("/data")
+
+
+DATA_DIR = _get_data_dir()
DB_PATH = DATA_DIR / "profiles.db"
diff --git a/backend/main.py b/backend/main.py
index 92727a53..092c63fb 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -589,11 +589,20 @@ async def get_system_status():
@app.post("/api/profiles/{profile_id}/clipboard")
async def set_clipboard(profile_id: str, body: ClipboardRequest):
- """Push text into the VNC session's X clipboard via xclip."""
+ """Push text into the VNC session's X clipboard via xclip.
+
+ On Windows / native desktop, clipboard push is not supported
+ (xclip is Linux-only). Returns success anyway so the frontend
+ doesn't error.
+ """
running = browser_mgr.running.get(profile_id)
if not running:
raise HTTPException(status_code=404, detail="Profile not running")
+ if not browser_mgr.vnc.available or running.display == 0:
+ # Native desktop mode — clipboard is handled by the OS directly
+ return {"ok": True, "native": True}
+
import os
# Kill previous xclip for this display (it stays alive to serve paste)
@@ -647,7 +656,10 @@ async def get_clipboard(profile_id: str):
except Exception as exc:
logger.debug("Playwright clipboard read failed: %s", exc)
- # Fallback: xclip for non-Chrome clipboard owners
+ # Fallback: xclip for non-Chrome clipboard owners (Linux/VNC only)
+ if not browser_mgr.vnc.available or running.display == 0:
+ return {"text": ""}
+
import os
env = {**os.environ, "DISPLAY": f":{running.display}"}
@@ -676,7 +688,11 @@ async def get_clipboard(profile_id: str):
@app.websocket("/api/profiles/{profile_id}/vnc")
async def vnc_proxy(websocket: WebSocket, profile_id: str):
- """Proxy WebSocket frames between the frontend and a profile's KasmVNC."""
+ """Proxy WebSocket frames between the frontend and a profile's KasmVNC.
+
+ When VNC is unavailable (native desktop mode), reject the connection
+ so the frontend can show a "browser on desktop" view.
+ """
if not await _check_websocket_origin(websocket):
return
@@ -685,6 +701,10 @@ async def vnc_proxy(websocket: WebSocket, profile_id: str):
await websocket.close(code=4004, reason="Profile not running")
return
+ if not browser_mgr.vnc.available or running.ws_port == 0:
+ await websocket.close(code=4005, reason="VNC unavailable — browser running on desktop")
+ return
+
# Accept with client's requested subprotocol (if any) — RFC 6455 requires
# the server must not respond with a subprotocol the client didn't request.
requested = websocket.scope.get("subprotocols", [])
@@ -816,8 +836,8 @@ async def vnc_to_client():
)
# Dump Xvnc log on disconnect
- import os
- xvnc_log = f"/tmp/xvnc-{running.display}.log"
+ import tempfile
+ xvnc_log = os.path.join(tempfile.gettempdir(), f"xvnc-{running.display}.log")
if os.path.exists(xvnc_log):
with open(xvnc_log) as f:
log_content = f.read()
diff --git a/backend/vnc_manager.py b/backend/vnc_manager.py
index d43e5483..5a72b2da 100644
--- a/backend/vnc_manager.py
+++ b/backend/vnc_manager.py
@@ -1,12 +1,18 @@
-"""KasmVNC display allocation and lifecycle management."""
+"""KasmVNC display allocation and lifecycle management.
+
+On platforms without KasmVNC (Windows, macOS without XQuartz), VNC is
+unavailable — browsers open natively in their own window instead.
+"""
from __future__ import annotations
import asyncio
import logging
+import os
import shutil
import subprocess
-from dataclasses import dataclass, field
+import sys
+from dataclasses import dataclass
logger = logging.getLogger("cloakbrowser.manager.vnc")
@@ -18,6 +24,11 @@ class VNCInstance:
process: subprocess.Popen | None = None
+def _xvnc_available() -> bool:
+ """Check if KasmVNC (Xvnc) is installed and runnable."""
+ return shutil.which("Xvnc") is not None
+
+
class VNCManager:
BASE_DISPLAY = 100
BASE_WS_PORT = 6100
@@ -25,9 +36,24 @@ class VNCManager:
def __init__(self):
self._allocated: dict[int, VNCInstance] = {}
self._lock = asyncio.Lock()
+ self._available: bool | None = None # cached after first check
+
+ @property
+ def available(self) -> bool:
+ """Whether KasmVNC is available on this system."""
+ if self._available is None:
+ self._available = _xvnc_available()
+ return self._available
async def allocate(self) -> tuple[int, int]:
- """Returns (display_number, ws_port) for a new profile."""
+ """Returns (display_number, ws_port) for a new profile.
+
+ When VNC is unavailable, returns (0, 0) — the caller should
+ skip VNC setup and let the browser open natively.
+ """
+ if not self.available:
+ return 0, 0
+
async with self._lock:
display = self.BASE_DISPLAY
while display in self._allocated:
@@ -42,12 +68,18 @@ async def start_vnc(
ws_port: int,
width: int = 1920,
height: int = 1080,
- ) -> subprocess.Popen:
- """Start Xvnc (KasmVNC) on the given display."""
+ ) -> subprocess.Popen | None:
+ """Start Xvnc (KasmVNC) on the given display.
+
+ Returns None when VNC is unavailable (caller should skip VNC).
+ """
+ if not self.available or display == 0:
+ logger.info("VNC unavailable — browser will open natively on desktop")
+ return None
+
xvnc_bin = shutil.which("Xvnc") or "Xvnc"
# KasmVNC requires -httpd to enable the WebSocket handler on the websocket port.
- # Without it, the port accepts TCP but won't do WebSocket upgrade.
httpd_dir = "/usr/share/kasmvnc/www"
cmd = [
@@ -59,12 +91,13 @@ async def start_vnc(
"-depth", "24",
"-SecurityTypes", "None",
"-DisableBasicAuth",
- "-interface", "127.0.0.1", # internal only, proxied by FastAPI
+ "-interface", "127.0.0.1",
"-AlwaysShared",
"-httpd", httpd_dir,
]
- log_path = f"/tmp/xvnc-{display}.log"
+ import tempfile
+ log_path = os.path.join(tempfile.gettempdir(), f"xvnc-{display}.log")
logger.info("Starting Xvnc on :%d (ws_port=%d) log=%s", display, ws_port, log_path)
log_file = open(log_path, "w")
@@ -73,7 +106,7 @@ async def start_vnc(
stdout=log_file,
stderr=log_file,
)
- log_file.close() # Popen inherited the fd, parent doesn't need it
+ log_file.close()
# Wait a moment for Xvnc to initialize
await asyncio.sleep(0.5)
@@ -95,6 +128,9 @@ async def start_vnc(
async def stop_vnc(self, display: int):
"""Kill Xvnc for given display and release allocation."""
+ if display == 0:
+ return
+
async with self._lock:
instance = self._allocated.pop(display, None)
@@ -118,6 +154,20 @@ async def cleanup_all(self):
async def cleanup_stale(self):
"""Kill orphan Xvnc processes from previous runs."""
+ if not self.available:
+ return
+
+ if sys.platform == "win32":
+ # On Windows, use taskkill to clean up leftover Xvnc if somehow present
+ try:
+ subprocess.run(
+ ["taskkill", "/F", "/IM", "Xvnc.exe"],
+ capture_output=True,
+ )
+ except FileNotFoundError:
+ pass
+ return
+
try:
result = subprocess.run(
["pkill", "-f", r"Xvnc :[0-9]"],
diff --git a/frontend/src/components/ProfileViewer.tsx b/frontend/src/components/ProfileViewer.tsx
index 31543e2c..d17299f9 100644
--- a/frontend/src/components/ProfileViewer.tsx
+++ b/frontend/src/components/ProfileViewer.tsx
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react";
-import { ClipboardCopy, Code2, Maximize2, Minimize2 } from "lucide-react";
+import { ClipboardCopy, Code2, Maximize2, Minimize2, Monitor } from "lucide-react";
import { api } from "../lib/api";
interface ProfileViewerProps {
@@ -20,6 +20,7 @@ export function ProfileViewer({ profileId, cdpUrl, clipboardSync: initialClipboa
const [fullscreen, setFullscreen] = useState(false);
const [clipboardSync, setClipboardSync] = useState(initialClipboardSync);
const [cdpCopied, setCdpCopied] = useState(false);
+ const [nativeDesktop, setNativeDesktop] = useState(false);
useEffect(() => {
let rfb: any = null;
@@ -48,10 +49,15 @@ export function ProfileViewer({ profileId, cdpUrl, clipboardSync: initialClipboa
if (!cancelled) setConnected(true);
});
- rfb.addEventListener("disconnect", () => {
+ rfb.addEventListener("disconnect", (e: any) => {
if (!cancelled) {
setConnected(false);
- onDisconnect();
+ // Check if VNC is unavailable (native desktop mode)
+ if (e.detail?.code === 4005) {
+ setNativeDesktop(true);
+ } else {
+ onDisconnect();
+ }
}
});
@@ -228,6 +234,46 @@ export function ProfileViewer({ profileId, cdpUrl, clipboardSync: initialClipboa
return () => container.removeEventListener("wheel", handleWheel);
}, []);
+ if (nativeDesktop) {
+ return (
+
+
+
+
Browser running on your desktop
+
+ The browser window is open directly on your desktop — no VNC viewer needed.
+
+ {cdpUrl && (
+
+
+
+ )}
+
+
+
+ );
+ }
+
if (error) {
return (
diff --git a/run.bat b/run.bat
new file mode 100644
index 00000000..04935e38
--- /dev/null
+++ b/run.bat
@@ -0,0 +1,28 @@
+@echo off
+title CloakBrowser Manager
+echo Starting CloakBrowser Manager...
+echo.
+
+cd /d "%~dp0"
+
+REM Check for Python
+python --version >nul 2>&1
+if %errorlevel% neq 0 (
+ echo ERROR: Python is not installed or not in PATH.
+ echo Please install Python 3.10+ from https://www.python.org/downloads/
+ echo Make sure to check "Add Python to PATH" during installation.
+ pause
+ exit /b 1
+)
+
+REM Check for Node.js
+node --version >nul 2>&1
+if %errorlevel% neq 0 (
+ echo ERROR: Node.js is not installed or not in PATH.
+ echo Please install Node.js from https://nodejs.org/
+ pause
+ exit /b 1
+)
+
+python run.py
+pause
diff --git a/run.py b/run.py
new file mode 100644
index 00000000..381d594a
--- /dev/null
+++ b/run.py
@@ -0,0 +1,83 @@
+"""
+CloakBrowser Manager — Windows launcher.
+
+Double-click run.bat to start, or run:
+ python run.py
+
+This script:
+1. Installs Python dependencies (first run)
+2. Builds the React frontend (first run or when changed)
+3. Starts the FastAPI backend on http://localhost:8080
+"""
+
+import os
+import subprocess
+import sys
+import webbrowser
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent
+
+
+def step(description: str) -> None:
+ print(f"\n{'=' * 60}")
+ print(f" {description}")
+ print("=" * 60)
+
+
+def run(cmd: list[str], cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess:
+ print(f" $ {' '.join(cmd)}")
+ return subprocess.run(cmd, cwd=cwd, check=check)
+
+
+def main() -> None:
+ os.chdir(ROOT)
+
+ print(r"""
+ ____ _ _ ____ _
+ / ___| | ___ __ _| | __| __ ) _ __ _____ _____ _ __ ___ __| | ___ _ __
+ | | | |/ _ \ / _` | |/ /| _ \| '__/ _ \ \/ / _ \ '__/ _ \ / _` |/ _ \ '__|
+ | |___| | (_) | (_| | < | |_) | | | (_) > < __/ | | (_) | (_| | __/ |
+ \____|_|\___/ \__,_|_|\_\|____/|_| \___/_/\_\___|_| \___/ \__,_|\___|_|
+
+ Browser Profile Manager
+ """)
+
+ # --- 1. Install Python dependencies ---
+ step("Installing Python dependencies")
+ run([sys.executable, "-m", "pip", "install", "-r", "backend/requirements.txt"], cwd=ROOT)
+
+ # --- 2. Build frontend ---
+ step("Building React frontend")
+ frontend_dir = ROOT / "frontend"
+ dist_dir = frontend_dir / "dist"
+
+ if not (frontend_dir / "node_modules").exists():
+ print(" Installing npm packages...")
+ run(["npm", "install"], cwd=frontend_dir)
+
+ run(["npm", "run", "build"], cwd=frontend_dir)
+
+ # --- 3. Start backend ---
+ step("Starting CloakBrowser Manager")
+ print(f"\n Opening http://localhost:8080 in your browser...")
+ print(" Press Ctrl+C to stop.\n")
+
+ # Open browser after a short delay
+ webbrowser.open("http://localhost:8080")
+
+ run(
+ [sys.executable, "-m", "uvicorn", "backend.main:app", "--host", "127.0.0.1", "--port", "8080"],
+ cwd=ROOT,
+ )
+
+
+if __name__ == "__main__":
+ try:
+ main()
+ except KeyboardInterrupt:
+ print("\n\nShutting down...")
+ except subprocess.CalledProcessError as e:
+ print(f"\nError: Command failed with exit code {e.returncode}")
+ print("Make sure you have Node.js and Python installed.")
+ sys.exit(1)
diff --git "a/\346\225\231\347\250\213-\346\214\207\347\272\271\346\265\217\350\247\210\345\231\250\351\205\215\347\275\256\344\270\216\347\256\241\347\220\206.md" "b/\346\225\231\347\250\213-\346\214\207\347\272\271\346\265\217\350\247\210\345\231\250\351\205\215\347\275\256\344\270\216\347\256\241\347\220\206.md"
new file mode 100644
index 00000000..9b9a43f8
--- /dev/null
+++ "b/\346\225\231\347\250\213-\346\214\207\347\272\271\346\265\217\350\247\210\345\231\250\351\205\215\347\275\256\344\270\216\347\256\241\347\220\206.md"
@@ -0,0 +1,690 @@
+# CloakBrowser Manager 中文教程 — 指纹浏览器配置与管理完全指南
+
+> **适用版本**:CloakBrowser Manager v0.1.0
+> **适用平台**:Windows / Linux (Docker) / macOS
+> **前置条件**:Python 3.10+、Node.js(Windows 本地运行);Docker(容器运行)
+
+---
+
+## 目录
+
+1. [什么是 CloakBrowser Manager?](#1-什么是-cloakbrowser-manager)
+2. [核心概念:指纹浏览器 vs 普通浏览器](#2-核心概念指纹浏览器-vs-普通浏览器)
+3. [安装与启动](#3-安装与启动)
+4. [创建你的第一个指纹配置(Profile)](#4-创建你的第一个指纹配置profile)
+5. [Profile 配置详解](#5-profile-配置详解)
+ - [5.1 基本信息 (BASIC)](#51-基本信息-basic)
+ - [5.2 网络设置 (NETWORK)](#52-网络设置-network)
+ - [5.3 硬件指纹 (HARDWARE)](#53-硬件指纹-hardware)
+ - [5.4 行为模拟 (BEHAVIOR)](#54-行为模拟-behavior)
+ - [5.5 标签管理 (TAGS)](#55-标签管理-tags)
+ - [5.6 启动参数 (LAUNCH ARGS)](#56-启动参数-launch-args)
+6. [启动浏览器与实时查看](#6-启动浏览器与实时查看)
+7. [CDP 自动化 API — 对接 Playwright / Puppeteer](#7-cdp-自动化-api--对接-playwright--puppeteer)
+8. [实战场景](#8-实战场景)
+ - [场景一:多账号电商运营](#场景一多账号电商运营)
+ - [场景二:广告投放与验证](#场景二广告投放与验证)
+ - [场景三:数据采集与爬虫](#场景三数据采集与爬虫)
+ - [场景四:社交媒体多账号管理](#场景四社交媒体多账号管理)
+9. [常见问题 (FAQ)](#9-常见问题-faq)
+10. [进阶技巧](#10-进阶技巧)
+
+---
+
+## 1. 什么是 CloakBrowser Manager?
+
+**CloakBrowser Manager** 是一个**浏览器配置文件管理器**,用于创建、管理和启动带有独立指纹的浏览器实例。它是 Multilogin、GoLogin、AdsPower 等商业指纹浏览器的**免费、自托管**替代方案。
+
+它的底层浏览器引擎是 [**CloakBrowser**](https://github.com/CloakHQ/CloakBrowser)——一个经过 **59 处 C++ 源码级修改** 的 Chromium 浏览器,能够通过 Cloudflare Turnstile、reCAPTCHA v3(评分 0.9)、FingerprintJS 等 30+ 种反检测系统的验证。
+
+```
+┌─────────────────────────────────────────────┐
+│ CloakBrowser Manager │
+│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
+│ │Profile 1│ │Profile 2│ │Profile 3│ ... │
+│ │指纹 A │ │指纹 B │ │指纹 C │ │
+│ │代理 IP₁ │ │代理 IP₂ │ │代理 IP₃ │ │
+│ └────┬────┘ └────┬────┘ └────┬────┘ │
+│ │ │ │ │
+│ ┌────▼────────────▼────────────▼────┐ │
+│ │ CloakBrowser 引擎 │ │
+│ │ 59处C++源码级指纹修改 + CDP API │ │
+│ └───────────────────────────────────┘ │
+└─────────────────────────────────────────────┘
+```
+
+---
+
+## 2. 核心概念:指纹浏览器 vs 普通浏览器
+
+### 为什么 VPN + 无痕模式不够?
+
+网站不只通过 IP 和 Cookie 来识别你。它们使用 **50+ 种信号**来构建你的"浏览器指纹":
+
+| 检测维度 | 说明 |
+|----------|------|
+| Canvas 指纹 | 渲染同一张图片,不同 GPU/驱动产生细微差异 |
+| WebGL 指纹 | 报告 GPU 型号、驱动版本 |
+| AudioContext 指纹 | 音频处理器的硬件差异 |
+| 字体列表 | 系统安装的字体集合 |
+| 屏幕分辨率 | 屏幕尺寸和色深 |
+| 时区和语言 | `Intl.DateTimeFormat` 等 API |
+| WebRTC | 泄露真实内网 IP |
+| 硬件并发数 | `navigator.hardwareConcurrency` |
+| User Agent | 浏览器标识字符串 |
+
+### 各方案对比
+
+| 方案 | 改变了什么 | 账号会关联吗? |
+|------|-----------|--------------|
+| **VPN** | 仅 IP 地址 | ✅ 会 — 指纹相同 |
+| **无痕模式** | 清除 Cookie | ✅ 会 — 指纹相同 |
+| **Chrome 多用户** | 分离书签/Cookie | ✅ 会 — 硬件指纹相同 |
+| **CloakBrowser** | **全部 — 每个 Profile 模拟一台独立设备** | ❌ 不会 |
+
+**核心原理**:CloakBrowser 不是在 JavaScript 层面注入脚本来修改指纹(这很容易被检测),而是在 **Chromium 的 C++ 源码层面**修改了浏览器行为。这意味着即使是底层的二进制指纹检测也无法区分 CloakBrowser 和普通 Chrome。
+
+---
+
+## 3. 安装与启动
+
+### Windows 本地运行(推荐新手)
+
+```bash
+# 1. 克隆项目
+git clone https://github.com/CloakHQ/CloakBrowser-Manager.git
+cd CloakBrowser-Manager
+
+# 2. 双击 run.bat 即可启动!
+```
+
+首次运行会自动:
+1. 安装 Python 后端依赖(FastAPI、CloakBrowser 等)
+2. 安装前端依赖并构建 React 界面
+3. 自动下载 CloakBrowser 二进制文件(约 200MB)
+4. 启动服务器,浏览器自动打开 `http://localhost:8080`
+
+### Docker 运行(生产环境推荐)
+
+```bash
+# 直接拉取镜像运行
+docker run -p 8080:8080 -v cloakprofiles:/data cloakhq/cloakbrowser-manager
+
+# 或从源码构建
+docker compose up --build
+```
+
+### 开启密码保护
+
+```bash
+# 设置 AUTH_TOKEN 环境变量
+# Docker:
+docker run -p 8080:8080 -v cloakprofiles:/data -e AUTH_TOKEN=你的密码 cloakhq/cloakbrowser-manager
+
+# Windows:
+set AUTH_TOKEN=你的密码
+python run.py
+```
+
+---
+
+## 4. 创建你的第一个指纹配置(Profile)
+
+打开 `http://localhost:8080`,你会看到如下界面:
+
+> 📸 **初始界面**:左侧边栏有搜索框和"New Profile"按钮,右侧提示"Select a profile or create a new one"。
+
+点击左下角的 **"+ New Profile"** 按钮,进入创建界面:
+
+> 📸 **新建 Profile 表单**:包含 BASIC、NETWORK、HARDWARE、BEHAVIOR、TAGS、LAUNCH ARGS、NOTES 等配置区块。
+
+### 最小配置示例
+
+只需填写一个名称,其他保持默认,点击 **Create** 即可:
+
+| 字段 | 值 | 说明 |
+|------|-----|------|
+| Profile Name | `Amazon卖家-美国站` | 任意名称 |
+| 其他 | 默认 | 自动生成随机指纹 |
+
+创建成功后,Profile 会出现在左侧列表中:
+
+> 📸 **编辑界面**:左侧显示已创建的 Profile,右侧可编辑各项配置,顶部有 Launch/Stop 按钮。
+
+---
+
+## 5. Profile 配置详解
+
+### 5.1 基本信息 (BASIC)
+
+| 配置项 | 说明 | 推荐值 |
+|--------|------|--------|
+| **Profile Name** | Profile 的显示名称 | 按用途命名,如"FB-账号1" |
+| **Platform** | 操作系统指纹 | 根据代理 IP 所在地选择,默认 Windows(最常见) |
+| **Fingerprint Seed** | 指纹种子,决定整套指纹 | 默认"Auto (random)"每次随机;输入固定数字可保持指纹不变 |
+
+> ⚠️ **重要**:如果你需要"回头客"身份(即同一个用户多次访问),请使用**固定 Fingerprint Seed**。如果是批量注册新账号,使用随机种子即可。
+
+**Fingerprint Seed 的工作原理**:
+
+```
+Seed: 22680
+ ├── Canvas 指纹: 基于种子生成的唯一噪声
+ ├── WebGL 指纹: 基于种子选择的 GPU 型号
+ ├── Audio 指纹: 基于种子生成的振荡器偏移
+ ├── 字体列表: 基于种子过滤的系统字体
+ └── ... 等 50+ 个维度
+```
+
+同一个 Seed → 同一个"设备" → 网站看到的是同一个"回头客"
+
+### 5.2 网络设置 (NETWORK)
+
+这是**最关键的配置**,决定了网站看到的 IP 和地理位置。
+
+| 配置项 | 说明 | 示例 |
+|--------|------|------|
+| **Proxy** | 代理服务器地址 | `http://user:pass@residential.proxy:8080` |
+| **Timezone** | 浏览器时区 | `America/New_York`(应与代理 IP 一致) |
+| **Locale** | 浏览器语言 | `en-US`(应与代理 IP 所在地匹配) |
+| **GeoIP 自动检测** | 根据代理 IP 自动匹配时区和语言 | ✅ 勾选后无需手动设置 Timezone/Locale |
+
+**代理格式说明**:
+
+```
+# 标准格式
+http://用户名:密码@主机:端口
+
+# 也支持简化格式(自动转换)
+主机:端口:用户名:密码
+
+# 支持的协议
+http:// https:// socks5://
+```
+
+> ⚠️ **为什么需要代理?**
+> 指纹浏览器解决了"指纹"问题,但 IP 地址是另一个独立维度。你需要为每个 Profile 配置**不同的代理 IP**,否则所有 Profile 共享同一个 IP,网站仍然能关联你的账号。推荐使用**住宅代理 (Residential Proxy)**,因为它们看起来像真实用户的家庭宽带 IP。
+
+**Timezone/Locale 与代理 IP 的匹配**:
+
+```
+代理 IP: 纽约, 美国
+ ├── Timezone: America/New_York ✅
+ ├── Locale: en-US ✅
+ └── 不一致时 → 容易触发风控 ❌
+```
+
+勾选 **GeoIP 自动检测** 可以省去手动配置:
+
+> "Auto-detect timezone/locale from proxy IP" — CloakBrowser 会自动查询代理 IP 的地理位置并设置对应的时区和语言。
+
+### 5.3 硬件指纹 (HARDWARE)
+
+这部分配置决定了网站在你浏览器中读取到的"硬件信息"。
+
+| 配置项 | 说明 | 默认值 |
+|--------|------|--------|
+| **Screen Resolution** | 屏幕分辨率 | 1920×1080 (Full HD) |
+| **Hardware Concurrency** | CPU 核心数 | Auto (from seed) |
+| **GPU Preset** | GPU 预设(一键配置) | 无 |
+| **GPU Vendor** | GPU 厂商 | Auto (from seed) |
+| **GPU Renderer** | GPU 渲染器型号 | Auto (from seed) |
+
+**GPU 预设** 提供了常见配置的一键选择:
+
+- **NVIDIA RTX 3070** — 中高端游戏/工作站
+- **NVIDIA RTX 4070** — 新一代中高端
+- **AMD RX 6800 XT** — AMD 高端显卡
+- **Intel UHD 770** — 集成显卡(办公电脑)
+- **Apple M3 (macOS)** — Mac 用户
+
+> 💡 **提示**:对于电商运营等场景,推荐使用常见的硬件配置(如 Intel UHD 770 + 1920×1080),不要选择过于高端或稀有的配置,否则反而显得可疑。
+
+**屏幕分辨率选择建议**:
+
+| 分辨率 | 市场份额 | 适用场景 |
+|--------|---------|----------|
+| 1920×1080 | ~22% | 最常见,推荐首选 |
+| 1366×768 | ~14% | 低端笔记本,亚洲市场常见 |
+| 2560×1440 | ~10% | 高端显示器 |
+| 1440×900 | ~6% | 旧款 MacBook |
+| 1536×864 | ~5% | 中等分辨率 |
+
+### 5.4 行为模拟 (BEHAVIOR)
+
+| 配置项 | 说明 |
+|--------|------|
+| **Human-like behavior** | 启用后,所有鼠标、键盘、滚动操作都会模拟人类行为(贝塞尔曲线移动、随机打字速度、加速→减速滚动等) |
+| **Clipboard sync** | 启用剪贴板同步(在 Docker/VNC 环境下) |
+| **Auto launch** | 服务器启动时自动启动此 Profile |
+| **Color Scheme** | 浏览器配色方案(浅色/深色/系统默认) |
+| **User Agent** | 自定义 User Agent 字符串(留空使用默认) |
+
+> ⚠️ **Humanize 模式** 会让自动化脚本变慢,但能更好地通过行为检测。如果你对接的是 Playwright/Puppeteer 自动化脚本,启用 `humanize=True` 可以让脚本的操作看起来更像真人。
+
+### 5.5 标签管理 (TAGS)
+
+标签用于**组织和分类 Profile**。常见用法:
+
+```
+电商场景:
+ 🏷️ amazon 🏷️ ebay 🏷️ shopify
+ 🏷️ 美国站 🏷️ 日本站 🏷️ 欧洲站
+
+社媒场景:
+ 🏷️ facebook 🏷️ twitter 🏷️ instagram
+ 🏷️ 主号 🏷️ 小号 🏷️ 养号中
+
+运营场景:
+ 🏷️ 高优先级 🏷️ 待验证 🏷️ 已封禁
+```
+
+标签支持自定义颜色,方便视觉识别。左侧搜索框可以按标签过滤 Profile。
+
+### 5.6 启动参数 (LAUNCH ARGS)
+
+高级用户可以通过启动参数传递自定义 Chromium 命令行标志:
+
+```bash
+# 加载扩展
+--load-extension=/path/to/extension
+
+# 禁用特定功能
+--disable-features=WebRTC
+
+# 窗口位置
+--window-position=0,0
+--window-size=1280,720
+```
+
+---
+
+## 6. 启动浏览器与实时查看
+
+### Windows 本地模式
+
+在 Windows 上运行 CloakBrowser Manager 时,点击 **Launch** 按钮后:
+
+1. CloakBrowser 浏览器窗口会在你的桌面上**直接打开**(原生窗口)
+2. Web 界面会显示 "Browser running on your desktop" 提示
+3. 你可以直接在浏览器窗口中操作
+
+### Docker / VNC 模式
+
+在 Docker 中运行时(Linux 容器无桌面环境),浏览器通过 **VNC(虚拟网络计算)** 在 Web 界面内显示:
+
+1. 点击 Launch → Web 界面内嵌的 VNC 查看器连接浏览器
+2. 你可以直接在网页中操作浏览器(就像操作本地浏览器一样)
+3. 支持全屏模式、剪贴板同步
+
+> 📸 **VNC 查看器**:工具栏显示连接状态,支持全屏、复制 CDP URL、剪贴板同步。
+
+---
+
+## 7. CDP 自动化 API — 对接 Playwright / Puppeteer
+
+每个启动的 Profile 都会暴露一个 **CDP (Chrome DevTools Protocol)** 端点,你可以用 Playwright 或 Puppeteer 连接并自动化操作浏览器。
+
+### 获取 CDP URL
+
+Profile 启动后,在 VNC 查看器工具栏点击 **代码图标 (< >)** 即可复制 CDP URL:
+
+```
+/api/profiles/
/cdp
+```
+
+### Python (Playwright) 示例
+
+```python
+from playwright.async_api import async_playwright
+
+async def auto_browse():
+ async with async_playwright() as pw:
+ # 连接到 CloakBrowser Manager 中的某个 Profile
+ browser = await pw.chromium.connect_over_cdp(
+ "http://localhost:8080/api/profiles/你的profile-id/cdp"
+ )
+ # 获取已打开的页面
+ page = browser.contexts[0].pages[0]
+
+ # 执行自动化操作
+ await page.goto("https://www.amazon.com")
+ await page.fill("#twotabsearchtextbox", "laptop")
+ await page.click("#nav-search-submit-button")
+
+ # 结果页面
+ results = await page.text_content(".s-main-slot")
+ print(results)
+
+asyncio.run(auto_browse())
+```
+
+### JavaScript (Puppeteer) 示例
+
+```javascript
+const puppeteer = require('puppeteer-core');
+
+async function autoBrowse() {
+ const browser = await puppeteer.connect({
+ browserURL: 'http://localhost:8080/api/profiles/你的profile-id/cdp'
+ });
+
+ const pages = await browser.pages();
+ const page = pages[0];
+
+ await page.goto('https://www.amazon.com');
+ await page.type('#twotabsearchtextbox', 'laptop');
+ await page.click('#nav-search-submit-button');
+
+ await browser.disconnect();
+}
+
+autoBrowse();
+```
+
+### 自动化流程示意
+
+```
+你的 Python/JS 脚本
+ │
+ │ connect_over_cdp()
+ ▼
+┌───────────────────────┐
+│ CloakBrowser Manager │
+│ /api/profiles/xxx/cdp│
+└──────────┬────────────┘
+ │ CDP WebSocket
+ ▼
+┌───────────────────────┐
+│ CloakBrowser │
+│ (独立指纹 + 代理IP) │
+└───────────────────────┘
+```
+
+---
+
+## 8. 实战场景
+
+### 场景一:多账号电商运营
+
+**需求**:运营 5 个 Amazon 卖家账号,每个账号需要看起来来自不同的设备和网络。
+
+**配置方案**:
+
+| Profile | 代理 IP | 时区 | 指纹种子 | 备注 |
+|---------|---------|------|---------|------|
+| Amazon-卖家A | 美国东部住宅代理 | America/New_York | 12345 | 固定种子,保持回头客身份 |
+| Amazon-卖家B | 美国西部住宅代理 | America/Los_Angeles | 23456 | 固定种子 |
+| Amazon-卖家C | 英国住宅代理 | Europe/London | 34567 | en-GB 语言 |
+| Amazon-卖家D | 德国住宅代理 | Europe/Berlin | 45678 | de-DE 语言 |
+| Amazon-卖家E | 日本住宅代理 | Asia/Tokyo | 56789 | ja-JP 语言 |
+
+**操作流程**:
+
+```
+1. 为每个账号创建一个 Profile
+2. 配置不同的代理 IP(来自不同地区)
+3. 使用固定 Fingerprint Seed(保持登录状态不被风控)
+4. 每日轮换启动 → 检查订单 → 回复客户 → 关闭
+5. 使用 CDP API 批量获取销售数据
+```
+
+### 场景二:广告投放与验证
+
+**需求**:验证广告在不同地区、不同设备上的展示效果。
+
+**配置方案**:
+
+```python
+# 批量启动多个 Profile 并截图
+import asyncio
+from playwright.async_api import async_playwright
+
+PROFILES = {
+ "US-Windows": "profile-id-1",
+ "UK-Mac": "profile-id-2",
+ "JP-Windows": "profile-id-3",
+}
+
+async def check_ad(profile_name, profile_id, url):
+ async with async_playwright() as pw:
+ browser = await pw.chromium.connect_over_cdp(
+ f"http://localhost:8080/api/profiles/{profile_id}/cdp"
+ )
+ page = browser.contexts[0].pages[0]
+ await page.goto(url)
+ await page.screenshot(path=f"ad-check-{profile_name}.png")
+ print(f"✅ {profile_name} 截图完成")
+ await browser.close()
+
+async def main():
+ tasks = [
+ check_ad(name, pid, "https://你的广告落地页")
+ for name, pid in PROFILES.items()
+ ]
+ await asyncio.gather(*tasks)
+
+asyncio.run(main())
+```
+
+### 场景三:数据采集与爬虫
+
+**需求**:爬取目标网站的数据,避免被反爬虫系统封禁。
+
+**关键配置**:
+
+```
+✅ 启用 humanize=True(模拟人类操作行为)
+✅ 每个爬取任务使用不同的 Fingerprint Seed
+✅ 配置住宅代理 IP(不要用机房 IP)
+✅ 添加随机延迟,不要固定频率访问
+✅ 使用 GeoIP 自动匹配时区和语言
+```
+
+**爬虫脚本模板**:
+
+```python
+from cloakbrowser import launch_persistent_context_async
+import asyncio
+
+async def scrape_with_profile(profile_config):
+ context = await launch_persistent_context_async(
+ user_data_dir=profile_config["user_data_dir"],
+ proxy=profile_config["proxy"],
+ geoip=True, # 自动匹配时区/语言
+ humanize=True, # 人类行为模拟
+ headless=False, # 有头模式更难检测
+ )
+
+ page = await context.new_page()
+ await page.goto("https://目标网站.com")
+
+ # 模拟人类滚动
+ for _ in range(3):
+ await page.mouse.wheel(0, 300)
+ await asyncio.sleep(1.5) # 随机延迟
+
+ # 提取数据
+ data = await page.evaluate("""() => {
+ return Array.from(document.querySelectorAll('.product'))
+ .map(el => ({
+ title: el.querySelector('.title')?.innerText,
+ price: el.querySelector('.price')?.innerText,
+ }));
+ }""")
+
+ await context.close()
+ return data
+```
+
+### 场景四:社交媒体多账号管理
+
+**需求**:管理多个 Facebook/Twitter/Instagram 账号,每个账号独立隔离。
+
+**Profile 组织**:
+
+```
+🏷️ facebook
+ ├── FB-主号 (固定种子 11111, 美国住宅代理)
+ ├── FB-小号1 (随机种子, 美国住宅代理)
+ └── FB-小号2 (随机种子, 美国住宅代理)
+
+🏷️ twitter
+ ├── TW-品牌号 (固定种子 22222, 日本住宅代理)
+ └── TW-个人号 (固定种子 33333, 新加坡代理)
+```
+
+**注意事项**:
+
+> ⚠️ 社媒平台的风控比电商平台更严格。建议:
+> - 新号前 7-14 天只做浏览操作("养号")
+> - 不要在同一天内大量操作
+> - 使用 humanize=True 模式
+> - 每次操作间隔随机时间(2-10 秒)
+
+---
+
+## 9. 常见问题 (FAQ)
+
+### Q: Fingerprint Seed 应该随机还是固定?
+
+| 场景 | 建议 | 原因 |
+|------|------|------|
+| 首次注册新账号 | **随机** | 每个新号看起来来自不同设备 |
+| 长期运营已有账号 | **固定** | 保持"回头客"身份,避免触发风控 |
+| 批量爬取数据 | **随机** | 每次访问看起来是不同用户 |
+| 广告验证 | **固定** | 模拟特定地区/设备的真实用户 |
+
+### Q: 免费版 CloakBrowser 和 Pro 版有什么区别?
+
+| 功能 | 免费版 | Pro 版 |
+|------|--------|--------|
+| C++ 源码级指纹修改 | ✅ 59 处 | ✅ 更多 |
+| Cloudflare Turnstile 通过 | ✅ | ✅ |
+| reCAPTCHA v3 评分 | 0.9 | 0.9 |
+| 二进制版本 | v146 | v148 (最新) |
+| 平台支持 | Windows/Linux/Mac | 全部 |
+| Playwright/Puppeteer API | ✅ | ✅ |
+| 商业支持 | 社区 | 邮件支持 |
+
+**对于大多数用户,免费版完全够用。**
+
+### Q: 如何检查指纹是否真的不同?
+
+每个 Profile 启动时会**自动创建**一组书签,位于"Detection Tests"文件夹中:
+
+- **Rebrowser Bot Detector** — 综合检测
+- **BrowserScan Bot** — 机器人检测
+- **Pixelscan** — 指纹一致性检查
+- **CreepJS** — 深度指纹分析
+- **BrowserLeaks (Canvas/WebGL/Fonts)** — 各维度独立检测
+
+启动 Profile 后,打开这些检测网站验证你的指纹是否真实、是否与其他 Profile 不同。
+
+### Q: 一个 Profile 占用多少资源?
+
+- **内存**:约 512 MB / Profile
+- **磁盘**:首次下载浏览器约 200 MB;每个 Profile 的会话数据约 50-200 MB
+- **CPU**:空闲时几乎为 0,活跃浏览时约 5-15%
+
+### Q: 数据存储在哪里?
+
+| 平台 | 数据路径 |
+|------|---------|
+| Windows | `%LOCALAPPDATA%\cloakbrowser-manager\` |
+| Docker | `/data/`(挂载到宿主机 volume) |
+| 自定义 | 设置 `CLOAKBROWSER_DATA_DIR` 环境变量 |
+
+每个 Profile 的数据存储在 `profiles//` 目录下,包含完整的 Chrome 用户数据(Cookie、LocalStorage、扩展等)。
+
+---
+
+## 10. 进阶技巧
+
+### 技巧一:批量创建 Profile
+
+```python
+import requests
+
+API = "http://localhost:8080/api/profiles"
+
+profiles = [
+ {"name": f"FB-账号{i}", "proxy": f"http://user:pass@proxy-{i}:8080"}
+ for i in range(1, 11)
+]
+
+for p in profiles:
+ resp = requests.post(API, json=p)
+ print(f"✅ 创建: {resp.json()['name']} (ID: {resp.json()['id']})")
+```
+
+### 技巧二:通过 CDP 注入自定义脚本
+
+```python
+# 连接到已有 Profile
+browser = await pw.chromium.connect_over_cdp(cdp_url)
+page = browser.contexts[0].pages[0]
+
+# 注入反检测脚本
+await page.add_init_script("""
+ // 覆盖 navigator.webdriver
+ Object.defineProperty(navigator, 'webdriver', { get: () => false });
+""")
+
+# 创建新页面时自动注入
+page = await browser.contexts[0].new_page()
+```
+
+### 技巧三:使用 Auto Launch 实现"开机自启"
+
+在 Profile 设置中勾选 **"Launch automatically when container starts"**:
+
+- Docker 容器启动时 → 自动启动该 Profile
+- 服务器重启后 → 自动恢复所有浏览器实例
+- 配合 Cron job → 定时启动/停止 Profile
+
+### 技巧四:健康检查与监控
+
+```bash
+# 检查 Manager 状态
+curl http://localhost:8080/api/status
+# 返回: {"running_count": 3, "binary_version": "146.0.7680.177.5", "profiles_total": 10}
+
+# 检查单个 Profile 状态
+curl http://localhost:8080/api/profiles//status
+# 返回: {"status": "running", "display": ":100", "cdp_url": "/api/profiles/xxx/cdp"}
+```
+
+### 技巧五:自定义 Chrome 扩展
+
+在 Profile 的 **LAUNCH ARGS** 中添加:
+
+```
+--load-extension=C:\extensions\my-extension
+```
+
+扩展会在 Profile 启动时自动加载,可用于:
+- 广告拦截(uBlock Origin)
+- 自动化辅助(如自动填表)
+- 代理管理(SwitchyOmega)
+
+---
+
+## 总结
+
+CloakBrowser Manager + CloakBrowser 提供了一个**完整的指纹浏览器解决方案**:
+
+| 特性 | 说明 |
+|------|------|
+| 🔒 **真正的指纹隔离** | C++ 源码级修改,非 JS 注入 |
+| 🌐 **代理集成** | 支持 HTTP/HTTPS/SOCKS5 代理 |
+| 🤖 **自动化 API** | 通过 CDP 对接 Playwright/Puppeteer |
+| 📦 **自托管** | 数据完全在你自己的机器上 |
+| 🆓 **免费** | 核心功能完全免费 |
+| 🖥️ **跨平台** | Windows 原生运行 + Docker 部署 |
+
+---
+
+> 📧 问题反馈:[GitHub Issues](https://github.com/CloakHQ/CloakBrowser-Manager/issues)
+> 🌐 官方网站:[cloakbrowser.dev](https://cloakbrowser.dev)
+> 📖 CloakBrowser 文档:[github.com/CloakHQ/CloakBrowser](https://github.com/CloakHQ/CloakBrowser)