flow2api / src /services /browser_captcha.py
genz27
refactor: 将有头浏览器打码从 patchright 改为 playwright
260483d
"""
基于 RT 的本地 reCAPTCHA 打码服务 (终极闭环版 - 无 fake_useragent 纯净版)
支持:自动刷新 Session Token、外部触发指纹切换、死磕重试
"""
import os
import sys
import subprocess
# 修复 Windows 上 playwright 的 asyncio 兼容性问题
os.environ.setdefault("PLAYWRIGHT_BROWSERS_PATH", "0")
import asyncio
import time
import re
import random
from pathlib import Path
from typing import Optional, Dict
from datetime import datetime
from urllib.parse import urlparse, unquote
from ..core.logger import debug_logger
# ==================== Docker 环境检测 ====================
def _is_running_in_docker() -> bool:
"""检测是否在 Docker 容器中运行"""
# 方法1: 检查 /.dockerenv 文件
if os.path.exists('/.dockerenv'):
return True
# 方法2: 检查 cgroup
try:
with open('/proc/1/cgroup', 'r') as f:
content = f.read()
if 'docker' in content or 'kubepods' in content or 'containerd' in content:
return True
except:
pass
# 方法3: 检查环境变量
if os.environ.get('DOCKER_CONTAINER') or os.environ.get('KUBERNETES_SERVICE_HOST'):
return True
return False
IS_DOCKER = _is_running_in_docker()
# ==================== playwright 自动安装 ====================
def _run_pip_install(package: str, use_mirror: bool = False) -> bool:
"""运行 pip install 命令"""
cmd = [sys.executable, '-m', 'pip', 'install', package]
if use_mirror:
cmd.extend(['-i', 'https://pypi.tuna.tsinghua.edu.cn/simple'])
try:
debug_logger.log_info(f"[BrowserCaptcha] 正在安装 {package}...")
print(f"[BrowserCaptcha] 正在安装 {package}...")
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
if result.returncode == 0:
debug_logger.log_info(f"[BrowserCaptcha] ✅ {package} 安装成功")
print(f"[BrowserCaptcha] ✅ {package} 安装成功")
return True
else:
debug_logger.log_warning(f"[BrowserCaptcha] {package} 安装失败: {result.stderr[:200]}")
return False
except Exception as e:
debug_logger.log_warning(f"[BrowserCaptcha] {package} 安装异常: {e}")
return False
def _run_playwright_install(use_mirror: bool = False) -> bool:
"""安装 playwright chromium 浏览器"""
cmd = [sys.executable, '-m', 'playwright', 'install', 'chromium']
env = os.environ.copy()
if use_mirror:
# 使用国内镜像
env['PLAYWRIGHT_DOWNLOAD_HOST'] = 'https://npmmirror.com/mirrors/playwright'
try:
debug_logger.log_info("[BrowserCaptcha] 正在安装 chromium 浏览器...")
print("[BrowserCaptcha] 正在安装 chromium 浏览器...")
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600, env=env)
if result.returncode == 0:
debug_logger.log_info("[BrowserCaptcha] ✅ chromium 浏览器安装成功")
print("[BrowserCaptcha] ✅ chromium 浏览器安装成功")
return True
else:
debug_logger.log_warning(f"[BrowserCaptcha] chromium 安装失败: {result.stderr[:200]}")
return False
except Exception as e:
debug_logger.log_warning(f"[BrowserCaptcha] chromium 安装异常: {e}")
return False
def _ensure_playwright_installed() -> bool:
"""确保 playwright 已安装"""
try:
import playwright
debug_logger.log_info("[BrowserCaptcha] playwright 已安装")
return True
except ImportError:
pass
debug_logger.log_info("[BrowserCaptcha] playwright 未安装,开始自动安装...")
print("[BrowserCaptcha] playwright 未安装,开始自动安装...")
# 先尝试官方源
if _run_pip_install('playwright', use_mirror=False):
return True
# 官方源失败,尝试国内镜像
debug_logger.log_info("[BrowserCaptcha] 官方源安装失败,尝试国内镜像...")
print("[BrowserCaptcha] 官方源安装失败,尝试国内镜像...")
if _run_pip_install('playwright', use_mirror=True):
return True
debug_logger.log_error("[BrowserCaptcha] ❌ playwright 自动安装失败,请手动安装: pip install playwright")
print("[BrowserCaptcha] ❌ playwright 自动安装失败,请手动安装: pip install playwright")
return False
def _ensure_browser_installed() -> bool:
"""确保 chromium 浏览器已安装"""
try:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
# 尝试获取浏览器路径,如果失败说明未安装
browser_path = p.chromium.executable_path
if browser_path and os.path.exists(browser_path):
debug_logger.log_info(f"[BrowserCaptcha] chromium 浏览器已安装: {browser_path}")
return True
except Exception as e:
debug_logger.log_info(f"[BrowserCaptcha] 检测浏览器时出错: {e}")
debug_logger.log_info("[BrowserCaptcha] chromium 浏览器未安装,开始自动安装...")
print("[BrowserCaptcha] chromium 浏览器未安装,开始自动安装...")
# 先尝试官方源
if _run_playwright_install(use_mirror=False):
return True
# 官方源失败,尝试国内镜像
debug_logger.log_info("[BrowserCaptcha] 官方源安装失败,尝试国内镜像...")
print("[BrowserCaptcha] 官方源安装失败,尝试国内镜像...")
if _run_playwright_install(use_mirror=True):
return True
debug_logger.log_error("[BrowserCaptcha] ❌ chromium 浏览器自动安装失败,请手动安装: python -m playwright install chromium")
print("[BrowserCaptcha] ❌ chromium 浏览器自动安装失败,请手动安装: python -m playwright install chromium")
return False
# 尝试导入 playwright
async_playwright = None
Route = None
BrowserContext = None
PLAYWRIGHT_AVAILABLE = False
if IS_DOCKER:
debug_logger.log_warning("[BrowserCaptcha] 检测到 Docker 环境,有头浏览器打码不可用,请使用第三方打码服务")
print("[BrowserCaptcha] ⚠️ 检测到 Docker 环境,有头浏览器打码不可用")
print("[BrowserCaptcha] 请使用第三方打码服务: yescaptcha, capmonster, ezcaptcha, capsolver")
else:
if _ensure_playwright_installed():
try:
from playwright.async_api import async_playwright, Route, BrowserContext
PLAYWRIGHT_AVAILABLE = True
# 检查并安装浏览器
_ensure_browser_installed()
except ImportError as e:
debug_logger.log_error(f"[BrowserCaptcha] playwright 导入失败: {e}")
print(f"[BrowserCaptcha] ❌ playwright 导入失败: {e}")
# 配置
LABS_URL = "https://labs.google/fx/tools/flow"
# ==========================================
# 代理解析工具函数
# ==========================================
def parse_proxy_url(proxy_url: str) -> Optional[Dict[str, str]]:
"""解析代理URL"""
if not proxy_url: return None
if not re.match(r'^(http|https|socks5)://', proxy_url): proxy_url = f"http://{proxy_url}"
match = re.match(r'^(socks5|http|https)://(?:([^:]+):([^@]+)@)?([^:]+):(\d+)$', proxy_url)
if match:
protocol, username, password, host, port = match.groups()
proxy_config = {'server': f'{protocol}://{host}:{port}'}
if username and password:
proxy_config['username'] = username
proxy_config['password'] = password
return proxy_config
return None
def validate_browser_proxy_url(proxy_url: str) -> tuple[bool, str]:
if not proxy_url: return True, None
parsed = parse_proxy_url(proxy_url)
if not parsed: return False, "代理格式错误"
return True, None
class TokenBrowser:
"""简化版浏览器:每次获取 token 时启动新浏览器,用完即关
每次都是新的随机 UA,避免长时间运行导致的各种问题
"""
# UA 池
UA_LIST = [
# Windows Chrome (120-132)
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
# Windows Chrome 完整版本号
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.83 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.139 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.117 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.6668.100 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.6613.138 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.6533.120 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.127 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.6422.141 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
# Windows Edge (120-132)
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/132.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 Edg/125.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Edg/123.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.83 Safari/537.36 Edg/132.0.2957.115",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.139 Safari/537.36 Edg/131.0.2903.99",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.117 Safari/537.36 Edg/130.0.2849.80",
# macOS Chrome (120-132)
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 13_6_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
# macOS Safari
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_3_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3.1 Safari/605.1.15",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_2_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 13_6_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Safari/605.1.15",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.2 Safari/605.1.15",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.1 Safari/605.1.15",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15",
# macOS Edge
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/132.0.0.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 Edg/132.0.0.0",
# Linux Chrome
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
# Linux Firefox
"Mozilla/5.0 (X11; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0",
"Mozilla/5.0 (X11; Linux x86_64; rv:133.0) Gecko/20100101 Firefox/133.0",
"Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0",
"Mozilla/5.0 (X11; Linux x86_64; rv:131.0) Gecko/20100101 Firefox/131.0",
"Mozilla/5.0 (X11; Linux x86_64; rv:130.0) Gecko/20100101 Firefox/130.0",
"Mozilla/5.0 (X11; Linux x86_64; rv:129.0) Gecko/20100101 Firefox/129.0",
"Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0",
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:133.0) Gecko/20100101 Firefox/133.0",
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0",
"Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:134.0) Gecko/20100101 Firefox/134.0",
# Windows Firefox
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:134.0) Gecko/20100101 Firefox/134.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:129.0) Gecko/20100101 Firefox/129.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
# macOS Firefox
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14.4; rv:134.0) Gecko/20100101 Firefox/134.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14.3; rv:133.0) Gecko/20100101 Firefox/133.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 14.2; rv:132.0) Gecko/20100101 Firefox/132.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:134.0) Gecko/20100101 Firefox/134.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:133.0) Gecko/20100101 Firefox/133.0",
# Opera
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 OPR/116.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 OPR/114.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 OPR/113.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 OPR/112.0.0.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 OPR/116.0.0.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 OPR/115.0.0.0",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 OPR/116.0.0.0",
# Brave
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Brave/131",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Brave/130",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Brave/131",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Brave/131",
# Vivaldi
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Vivaldi/6.9.3447.54",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Vivaldi/6.8.3381.55",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Vivaldi/6.9.3447.54",
]
# 分辨率池
RESOLUTIONS = [
(1920, 1080), (2560, 1440), (3840, 2160), (1366, 768), (1536, 864),
(1600, 900), (1280, 720), (1360, 768), (1920, 1200),
(1440, 900), (1680, 1050), (1280, 800), (2560, 1600),
(2880, 1800), (3024, 1890), (3456, 2160),
(1280, 1024), (1024, 768), (1400, 1050),
(1920, 1280), (2736, 1824), (2880, 1920), (3000, 2000),
(2256, 1504), (2496, 1664), (3240, 2160),
(3200, 1800), (2304, 1440), (1800, 1200),
]
def __init__(self, token_id: int, user_data_dir: str, db=None):
self.token_id = token_id
self.user_data_dir = user_data_dir
self.db = db
self._semaphore = asyncio.Semaphore(1) # 同时只能有一个任务
self._solve_count = 0
self._error_count = 0
async def _create_browser(self) -> tuple:
"""创建新浏览器实例(新 UA),返回 (playwright, browser, context)"""
import random
random_ua = random.choice(self.UA_LIST)
base_w, base_h = random.choice(self.RESOLUTIONS)
width, height = base_w, base_h - random.randint(0, 80)
viewport = {"width": width, "height": height}
playwright = await async_playwright().start()
Path(self.user_data_dir).mkdir(parents=True, exist_ok=True)
# 代理配置
proxy_option = None
try:
if self.db:
captcha_config = await self.db.get_captcha_config()
raw_url = captcha_config.browser_proxy_enabled and captcha_config.browser_proxy_url
if raw_url:
proxy_option = parse_proxy_url(raw_url.strip())
if proxy_option:
debug_logger.log_info(f"[BrowserCaptcha] Token-{self.token_id} 使用代理: {proxy_option['server']}")
except: pass
try:
browser = await playwright.chromium.launch(
headless=False,
proxy=proxy_option,
args=[
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-setuid-sandbox',
'--no-first-run',
'--no-zygote',
f'--window-size={width},{height}',
'--disable-infobars',
'--hide-scrollbars',
]
)
context = await browser.new_context(
user_agent=random_ua,
viewport=viewport,
)
return playwright, browser, context
except Exception as e:
debug_logger.log_error(f"[BrowserCaptcha] Token-{self.token_id} 启动浏览器失败: {type(e).__name__}: {str(e)[:200]}")
# 确保清理已创建的对象
try:
if playwright:
await playwright.stop()
except: pass
raise
async def _close_browser(self, playwright, browser, context):
"""关闭浏览器实例"""
try:
if context:
await context.close()
except: pass
try:
if browser:
await browser.close()
except: pass
try:
if playwright:
await playwright.stop()
except: pass
async def _execute_captcha(self, context, project_id: str, website_key: str, action: str) -> Optional[str]:
"""在给定 context 中执行打码逻辑"""
page = None
try:
page = await context.new_page()
await page.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined});")
page_url = f"https://labs.google/fx/tools/flow/project/{project_id}"
async def handle_route(route):
if route.request.url.rstrip('/') == page_url.rstrip('/'):
html = f"""<html><head><script src="https://www.google.com/recaptcha/enterprise.js?render={website_key}"></script></head><body></body></html>"""
await route.fulfill(status=200, content_type="text/html", body=html)
elif any(d in route.request.url for d in ["google.com", "gstatic.com", "recaptcha.net"]):
await route.continue_()
else:
await route.abort()
await page.route("**/*", handle_route)
try:
await page.goto(page_url, wait_until="load", timeout=30000)
except Exception as e:
debug_logger.log_warning(f"[BrowserCaptcha] Token-{self.token_id} page.goto 失败: {type(e).__name__}: {str(e)[:200]}")
return None
try:
await page.wait_for_function("typeof grecaptcha !== 'undefined'", timeout=15000)
except Exception as e:
debug_logger.log_warning(f"[BrowserCaptcha] Token-{self.token_id} grecaptcha 未就绪: {type(e).__name__}: {str(e)[:200]}")
return None
token = await asyncio.wait_for(
page.evaluate(f"""
(actionName) => {{
return new Promise((resolve, reject) => {{
const timeout = setTimeout(() => reject(new Error('timeout')), 25000);
grecaptcha.enterprise.execute('{website_key}', {{action: actionName}})
.then(t => {{ resolve(t); }})
.catch(e => {{ reject(e); }});
}});
}}
""", action),
timeout=30
)
return token
except Exception as e:
msg = f"{type(e).__name__}: {str(e)}"
debug_logger.log_warning(f"[BrowserCaptcha] Token-{self.token_id} 打码失败: {msg[:200]}")
return None
finally:
if page:
try: await page.close()
except: pass
async def get_token(self, project_id: str, website_key: str, action: str = "IMAGE_GENERATION") -> Optional[str]:
"""获取 Token:启动新浏览器 -> 打码 -> 关闭浏览器"""
async with self._semaphore:
MAX_RETRIES = 3
for attempt in range(MAX_RETRIES):
playwright = None
browser = None
context = None
try:
start_ts = time.time()
# 每次都启动新浏览器(新 UA)
playwright, browser, context = await self._create_browser()
# 执行打码
token = await self._execute_captcha(context, project_id, website_key, action)
if token:
self._solve_count += 1
debug_logger.log_info(f"[BrowserCaptcha] Token-{self.token_id} 获取成功 ({(time.time()-start_ts)*1000:.0f}ms)")
return token
self._error_count += 1
debug_logger.log_warning(f"[BrowserCaptcha] Token-{self.token_id} 尝试 {attempt+1}/{MAX_RETRIES} 失败")
except Exception as e:
self._error_count += 1
debug_logger.log_error(f"[BrowserCaptcha] Token-{self.token_id} 浏览器错误: {type(e).__name__}: {str(e)[:200]}")
finally:
# 无论成功失败都关闭浏览器
await self._close_browser(playwright, browser, context)
# 重试前等待
if attempt < MAX_RETRIES - 1:
await asyncio.sleep(1)
return None
class BrowserCaptchaService:
"""多浏览器轮询打码服务(单例模式)
支持配置浏览器数量,每个浏览器只开 1 个标签页,请求轮询分配
"""
_instance: Optional['BrowserCaptchaService'] = None
_lock = asyncio.Lock()
def __init__(self, db=None):
self.db = db
self.website_key = "6LdsFiUsAAAAAIjVDZcuLhaHiDn5nnHVXVRQGeMV"
self.base_user_data_dir = os.path.join(os.getcwd(), "browser_data_rt")
self._browsers: Dict[int, TokenBrowser] = {}
self._browsers_lock = asyncio.Lock()
# 浏览器数量配置
self._browser_count = 1 # 默认 1 个,会从数据库加载
self._round_robin_index = 0 # 轮询索引
# 统计指标
self._stats = {
"req_total": 0,
"gen_ok": 0,
"gen_fail": 0,
"api_403": 0
}
# 并发限制将在 _load_browser_count 中根据配置设置
self._token_semaphore = None
@classmethod
async def get_instance(cls, db=None) -> 'BrowserCaptchaService':
if cls._instance is None:
async with cls._lock:
if cls._instance is None:
cls._instance = cls(db)
# 从数据库加载 browser_count 配置
await cls._instance._load_browser_count()
return cls._instance
def _check_available(self):
"""检查服务是否可用"""
if IS_DOCKER:
raise RuntimeError(
"有头浏览器打码在 Docker 环境中不可用。"
"请使用第三方打码服务: yescaptcha, capmonster, ezcaptcha, capsolver"
)
if not PLAYWRIGHT_AVAILABLE or async_playwright is None:
raise RuntimeError(
"playwright 未安装或不可用。"
"请手动安装: pip install playwright && python -m playwright install chromium"
)
async def _load_browser_count(self):
"""从数据库加载浏览器数量配置"""
if self.db:
try:
captcha_config = await self.db.get_captcha_config()
self._browser_count = max(1, captcha_config.browser_count)
debug_logger.log_info(f"[BrowserCaptcha] 浏览器数量配置: {self._browser_count}")
except Exception as e:
debug_logger.log_warning(f"[BrowserCaptcha] 加载 browser_count 配置失败: {e},使用默认值 1")
self._browser_count = 1
# 并发限制 = 浏览器数量,不再硬编码限制
self._token_semaphore = asyncio.Semaphore(self._browser_count)
debug_logger.log_info(f"[BrowserCaptcha] 并发上限: {self._browser_count}")
async def reload_browser_count(self):
"""重新加载浏览器数量配置(用于配置更新后热重载)"""
old_count = self._browser_count
await self._load_browser_count()
# 如果数量减少,移除多余的浏览器实例
if self._browser_count < old_count:
async with self._browsers_lock:
for browser_id in list(self._browsers.keys()):
if browser_id >= self._browser_count:
self._browsers.pop(browser_id)
debug_logger.log_info(f"[BrowserCaptcha] 移除多余浏览器实例 {browser_id}")
def _log_stats(self):
total = self._stats["req_total"]
gen_fail = self._stats["gen_fail"]
api_403 = self._stats["api_403"]
gen_ok = self._stats["gen_ok"]
valid_success = gen_ok - api_403
if valid_success < 0: valid_success = 0
rate = (valid_success / total * 100) if total > 0 else 0.0
async def _get_or_create_browser(self, browser_id: int) -> TokenBrowser:
"""获取或创建指定 ID 的浏览器实例"""
async with self._browsers_lock:
if browser_id not in self._browsers:
user_data_dir = os.path.join(self.base_user_data_dir, f"browser_{browser_id}")
browser = TokenBrowser(browser_id, user_data_dir, db=self.db)
self._browsers[browser_id] = browser
debug_logger.log_info(f"[BrowserCaptcha] 创建浏览器实例 {browser_id}")
return self._browsers[browser_id]
def _get_next_browser_id(self) -> int:
"""轮询获取下一个浏览器 ID"""
browser_id = self._round_robin_index % self._browser_count
self._round_robin_index += 1
return browser_id
async def get_token(self, project_id: str, action: str = "IMAGE_GENERATION", token_id: int = None) -> tuple[Optional[str], int]:
"""获取 reCAPTCHA Token(轮询分配到不同浏览器)
Args:
project_id: 项目 ID
action: reCAPTCHA action
token_id: 忽略,使用轮询分配
Returns:
(token, browser_id) 元组,调用方失败时用 browser_id 调用 report_error
"""
# 检查服务是否可用
self._check_available()
self._stats["req_total"] += 1
# 全局并发限制(如果已配置)
if self._token_semaphore:
async with self._token_semaphore:
# 轮询选择浏览器
browser_id = self._get_next_browser_id()
browser = await self._get_or_create_browser(browser_id)
token = await browser.get_token(project_id, self.website_key, action)
if token:
self._stats["gen_ok"] += 1
else:
self._stats["gen_fail"] += 1
self._log_stats()
return token, browser_id
# 无并发限制时直接执行
browser_id = self._get_next_browser_id()
browser = await self._get_or_create_browser(browser_id)
token = await browser.get_token(project_id, self.website_key, action)
if token:
self._stats["gen_ok"] += 1
else:
self._stats["gen_fail"] += 1
self._log_stats()
return token, browser_id
async def report_error(self, browser_id: int = None):
"""上层举报:Token 无效(统计用)
Args:
browser_id: 浏览器 ID(当前架构下每次都是新浏览器,此参数仅用于日志)
"""
async with self._browsers_lock:
self._stats["api_403"] += 1
if browser_id is not None:
debug_logger.log_info(f"[BrowserCaptcha] 浏览器 {browser_id} 的 token 验证失败")
async def remove_browser(self, browser_id: int):
async with self._browsers_lock:
if browser_id in self._browsers:
self._browsers.pop(browser_id)
async def close(self):
async with self._browsers_lock:
self._browsers.clear()
async def open_login_browser(self): return {"success": False, "error": "Not implemented"}
async def create_browser_for_token(self, t, s=None): pass
def get_stats(self):
base_stats = {
"total_solve_count": self._stats["gen_ok"],
"total_error_count": self._stats["gen_fail"],
"risk_403_count": self._stats["api_403"],
"browser_count": len(self._browsers),
"configured_browser_count": self._browser_count,
"browsers": []
}
return base_stats