| | """ |
| | 基于 RT 的本地 reCAPTCHA 打码服务 (终极闭环版 - 无 fake_useragent 纯净版) |
| | 支持:自动刷新 Session Token、外部触发指纹切换、死磕重试 |
| | """ |
| | import os |
| | import sys |
| | import subprocess |
| | |
| | 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 |
| |
|
| |
|
| | |
| | def _is_running_in_docker() -> bool: |
| | """检测是否在 Docker 容器中运行""" |
| | |
| | if os.path.exists('/.dockerenv'): |
| | return True |
| | |
| | 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 |
| | |
| | if os.environ.get('DOCKER_CONTAINER') or os.environ.get('KUBERNETES_SERVICE_HOST'): |
| | return True |
| | return False |
| |
|
| |
|
| | IS_DOCKER = _is_running_in_docker() |
| |
|
| |
|
| | |
| | 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 |
| |
|
| |
|
| | |
| | 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_LIST = [ |
| | |
| | "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", |
| | |
| | "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", |
| | |
| | "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", |
| | |
| | "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", |
| | |
| | "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", |
| | |
| | "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", |
| | |
| | "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", |
| | |
| | "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", |
| | |
| | "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", |
| | |
| | "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", |
| | |
| | "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", |
| | |
| | "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", |
| | |
| | "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() |
| | |
| | |
| | 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 |
| | self._round_robin_index = 0 |
| | |
| | |
| | self._stats = { |
| | "req_total": 0, |
| | "gen_ok": 0, |
| | "gen_fail": 0, |
| | "api_403": 0 |
| | } |
| | |
| | |
| | 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) |
| | |
| | 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 |
| |
|