feat(browser): migrate to CloakBrowser for browser emulation and streamline dependency management

- Replace Playwright-based browser emulation with CloakBrowser as default
- Update config to support CloakBrowser options and humanization presets
- Refactor browser helper to use CloakBrowser context and remove cf_clearance dependency
- Update Dockerfile, entrypoint, and update scripts to install CloakBrowser runtime
- Ensure CloakBrowser kernel is pre-installed during local setup and dependency updates
- Add tests for CloakBrowser integration and legacy compatibility
This commit is contained in:
jxxghp
2026-05-16 20:51:38 +08:00
parent 9069dccb2a
commit 0ee9fec1d2
10 changed files with 293 additions and 140 deletions

View File

@@ -198,68 +198,62 @@ class BrowseWebpageTool(MoviePilotTool):
cookies: Optional[str],
user_agent: Optional[str],
) -> str:
"""在同步上下文中执行 Playwright 浏览器操作"""
from playwright.sync_api import sync_playwright
"""在同步上下文中执行 CloakBrowser 浏览器操作"""
from cloakbrowser import launch_context
try:
with sync_playwright() as playwright:
browser = None
context = None
page = None
try:
# 启动浏览器
browser_type = settings.PLAYWRIGHT_BROWSER_TYPE or "chromium"
browser = playwright[browser_type].launch(headless=True)
# 创建上下文
context_kwargs = {}
if user_agent:
context_kwargs["user_agent"] = user_agent
# 设置视口大小
context_kwargs["viewport"] = {
context = None
page = None
try:
context_kwargs = {
"viewport": {
"width": SCREENSHOT_MAX_WIDTH,
"height": SCREENSHOT_MAX_HEIGHT,
}
}
if user_agent:
context_kwargs["user_agent"] = user_agent
context = browser.new_context(**context_kwargs)
page = context.new_page()
page.set_default_timeout(timeout * 1000)
context = launch_context(
headless=True,
humanize=settings.CLOAKBROWSER_HUMANIZE,
human_preset=settings.CLOAKBROWSER_HUMAN_PRESET,
**context_kwargs,
)
page = context.new_page()
page.set_default_timeout(timeout * 1000)
# 设置 cookies
if cookies:
page.set_extra_http_headers({"cookie": cookies})
# 设置 cookies
if cookies:
page.set_extra_http_headers({"cookie": cookies})
# 对于非 goto 操作,如果提供了 url 先导航
if url and browser_action != BrowserAction.GOTO:
page.goto(
url, wait_until="domcontentloaded", timeout=timeout * 1000
)
page.wait_for_load_state("networkidle", timeout=timeout * 1000)
# 对于非 goto 操作,如果提供了 url 先导航
if url and browser_action != BrowserAction.GOTO:
page.goto(url, wait_until="domcontentloaded", timeout=timeout * 1000)
page.wait_for_load_state("networkidle", timeout=timeout * 1000)
# 执行具体操作
result = self._do_action(
page,
browser_action,
url,
selector,
value,
script,
content_type,
timeout,
)
return result
# 执行具体操作
result = self._do_action(
page,
browser_action,
url,
selector,
value,
script,
content_type,
timeout,
)
return result
finally:
if page:
page.close()
if context:
context.close()
if browser:
browser.close()
finally:
if page:
page.close()
if context:
context.close()
except Exception as e:
logger.error(f"Playwright 执行失败: {e}", exc_info=True)
return f"Playwright 执行失败: {str(e)}"
logger.error(f"CloakBrowser 执行失败: {e}", exc_info=True)
return f"CloakBrowser 执行失败: {str(e)}"
def _do_action(
self,

View File

@@ -331,8 +331,12 @@ class ConfigModel(BaseModel):
NO_CACHE_SITE_KEY: str = "m-team"
# OCR服务器地址用于识别站点验证码
OCR_HOST: str = "https://movie-pilot.org"
# 仿真类型:playwright 或 flaresolverr
BROWSER_EMULATION: str = "playwright"
# 仿真类型:cloakbrowser 或 flaresolverr,其他值按 cloakbrowser 处理
BROWSER_EMULATION: str = "cloakbrowser"
# CloakBrowser 是否启用拟人化输入
CLOAKBROWSER_HUMANIZE: bool = True
# CloakBrowser 拟人化输入预设default 或 careful
CLOAKBROWSER_HUMAN_PRESET: str = "default"
# FlareSolverr 服务地址,例如 http://127.0.0.1:8191
FLARESOLVERR_URL: Optional[str] = None
@@ -526,7 +530,7 @@ class ConfigModel(BaseModel):
# ==================== Docker配置 ====================
# Docker Client API地址
DOCKER_CLIENT_API: Optional[str] = "tcp://127.0.0.1:38379"
# Playwright浏览器类型chromium/firefox
# Playwright浏览器类型供智能体浏览器工具和插件直接使用 Playwright 时读取
PLAYWRIGHT_BROWSER_TYPE: str = "chromium"
# ==================== AI智能体配置 ====================

View File

@@ -1,8 +1,7 @@
import uuid
from typing import Callable, Any, Optional
from cf_clearance import sync_cf_retry, sync_stealth
from playwright.sync_api import sync_playwright, Page
from playwright.sync_api import BrowserContext, Page
from app.core.config import settings
from app.log import logger
@@ -10,17 +9,33 @@ from app.utils.http import RequestUtils, cookie_parse
class PlaywrightHelper:
def __init__(self, browser_type=settings.PLAYWRIGHT_BROWSER_TYPE):
self.browser_type = browser_type
def __init__(self, browser_type: Optional[str] = None, *args, **kwargs):
"""
兼容旧的 PlaywrightHelper(browser_type=...) 构造方式。
"""
self.browser_type = browser_type or settings.PLAYWRIGHT_BROWSER_TYPE
@staticmethod
def __pass_cloudflare(url: str, page: Page) -> bool:
def __browser_emulation() -> str:
"""
尝试跳过cloudfare验证
当前浏览器仿真类型。
"""
sync_stealth(page, pure=True)
page.goto(url)
return sync_cf_retry(page)[0]
return (settings.BROWSER_EMULATION or "cloakbrowser").lower()
@staticmethod
def __launch_cloakbrowser_context(headless: bool,
user_agent: Optional[str] = None,
proxies: Optional[dict] = None) -> BrowserContext:
"""
启动 CloakBrowser 上下文。
"""
from cloakbrowser import launch_context
return launch_context(headless=headless,
proxy=proxies,
user_agent=user_agent,
humanize=settings.CLOAKBROWSER_HUMANIZE,
human_preset=settings.CLOAKBROWSER_HUMAN_PRESET)
@staticmethod
def __fs_cookie_str(cookies: list) -> str:
@@ -148,51 +163,44 @@ class PlaywrightHelper:
"""
result = None
try:
with sync_playwright() as playwright:
browser = None
context = None
page = None
try:
# 如果配置使用 FlareSolverr先通过其获取清除后的 cookies 与 UA
fs_cookie_header = None
fs_ua = None
if settings.BROWSER_EMULATION == "flaresolverr":
solution = self.__flaresolverr_request(url=url, cookies=cookies,
proxy_config=proxies, timeout=timeout)
if solution:
fs_cookie_header = self.__fs_cookie_str(solution.get("cookies", []))
fs_ua = solution.get("userAgent")
context = None
page = None
try:
# 如果配置使用 FlareSolverr先通过其获取清除后的 cookies 与 UA
fs_cookie_header = None
fs_ua = None
if self.__browser_emulation() == "flaresolverr":
solution = self.__flaresolverr_request(url=url, cookies=cookies,
proxy_config=proxies, timeout=timeout)
if solution:
fs_cookie_header = self.__fs_cookie_str(solution.get("cookies", []))
fs_ua = solution.get("userAgent")
browser = playwright[self.browser_type].launch(headless=headless)
context = browser.new_context(user_agent=fs_ua or ua, proxy=proxies)
page = context.new_page()
context = self.__launch_cloakbrowser_context(headless=headless,
user_agent=fs_ua or ua,
proxies=proxies)
page = context.new_page()
# 优先使用 FlareSolverr 返回,其次使用入参
merged_cookie = fs_cookie_header or cookies
if merged_cookie:
page.set_extra_http_headers({"cookie": merged_cookie})
# 优先使用 FlareSolverr 返回,其次使用入参
merged_cookie = fs_cookie_header or cookies
if merged_cookie:
page.set_extra_http_headers({"cookie": merged_cookie})
if settings.BROWSER_EMULATION == "playwright":
if not self.__pass_cloudflare(url, page):
logger.warn("cloudflare challenge fail")
else:
page.goto(url)
page.wait_for_load_state("networkidle", timeout=timeout * 1000)
page.goto(url)
page.wait_for_load_state("networkidle", timeout=timeout * 1000)
# 回调函数
result = callback(page)
# 回调函数
result = callback(page)
except Exception as e:
logger.error(f"网页操作失败: {str(e)}")
finally:
if page:
page.close()
if context:
context.close()
if browser:
browser.close()
except Exception as e:
logger.error(f"网页操作失败: {str(e)}")
finally:
if page:
page.close()
if context:
context.close()
except Exception as e:
logger.error(f"Playwright初始化失败: {str(e)}")
logger.error(f"CloakBrowser初始化失败: {str(e)}")
return result
@@ -213,7 +221,7 @@ class PlaywrightHelper:
"""
source = None
# 如果配置为 FlareSolverr则直接调用获取页面源码
if settings.BROWSER_EMULATION == "flaresolverr":
if self.__browser_emulation() == "flaresolverr":
try:
solution = self.__flaresolverr_request(url=url, cookies=cookies,
proxy_config=proxies, timeout=timeout)
@@ -222,36 +230,32 @@ class PlaywrightHelper:
except Exception as e:
logger.error(f"FlareSolverr 获取源码失败: {str(e)}")
try:
with sync_playwright() as playwright:
browser = None
context = None
page = None
try:
browser = playwright[self.browser_type].launch(headless=headless)
context = browser.new_context(user_agent=ua, proxy=proxies)
page = context.new_page()
context = None
page = None
try:
context = self.__launch_cloakbrowser_context(headless=headless,
user_agent=ua,
proxies=proxies)
page = context.new_page()
if cookies:
page.set_extra_http_headers({"cookie": cookies})
if cookies:
page.set_extra_http_headers({"cookie": cookies})
if not self.__pass_cloudflare(url, page):
logger.warn("cloudflare challenge fail")
page.wait_for_load_state("networkidle", timeout=timeout * 1000)
page.goto(url)
page.wait_for_load_state("networkidle", timeout=timeout * 1000)
source = page.content()
source = page.content()
except Exception as e:
logger.error(f"获取网页源码失败: {str(e)}")
source = None
finally:
# 确保资源被正确清理
if page:
page.close()
if context:
context.close()
if browser:
browser.close()
except Exception as e:
logger.error(f"获取网页源码失败: {str(e)}")
source = None
finally:
# 确保资源被正确清理
if page:
page.close()
if context:
context.close()
except Exception as e:
logger.error(f"Playwright初始化失败: {str(e)}")
logger.error(f"CloakBrowser初始化失败: {str(e)}")
return source