diff --git a/config/setting.toml b/config/setting.toml index f0b6156..ca60e6f 100644 --- a/config/setting.toml +++ b/config/setting.toml @@ -54,9 +54,21 @@ timeout = 7200 # 缓存超时时间(秒), 默认2小时 base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址 [captcha] -captcha_method = "remote_browser" # 打码方式: yescaptcha/browser/personal/remote_browser +captcha_method = "personal" # 打码方式: yescaptcha/browser/personal/remote_browser browser_recaptcha_settle_seconds = 1.0 # 打码完成额外稳态等待,速度优先可设 1.0 browser_launch_background = true # browser 打码默认后台启动(最小化/避免抢占前台) + +# 内置浏览器打码 (personal) 配置 +# 每个项目(project_id)对应一个常驻标签页,标签页会被复用以提高性能 +# 推荐配置(每个标签约占用200-300MB内存): +# - 2GB内存: 3个标签 +# - 4GB内存: 5个标签 +# - 8GB内存: 10个标签 +# - 16GB内存: 20个标签 +# 注意:标签数量取决于不同项目的数量,单个项目的并发请求会复用同一个标签页 +personal_max_resident_tabs = 5 # 最大常驻标签页数量 +personal_idle_tab_ttl_seconds = 600 # 标签页空闲超时(秒),超时后自动回收 + yescaptcha_api_key = "" # YesCaptcha API密钥 yescaptcha_base_url = "https://api.yescaptcha.com" remote_browser_base_url = "http://127.0.0.1:8060" # 本地 token 池服务地址 diff --git a/src/api/admin.py b/src/api/admin.py index c4a6d4a..344c05c 100644 --- a/src/api/admin.py +++ b/src/api/admin.py @@ -1528,6 +1528,8 @@ async def update_captcha_config( browser_proxy_enabled = request.get("browser_proxy_enabled", False) browser_proxy_url = request.get("browser_proxy_url", "") browser_count = request.get("browser_count", 1) + personal_max_resident_tabs = request.get("personal_max_resident_tabs") + personal_idle_tab_ttl_seconds = request.get("personal_idle_tab_ttl_seconds") # 验证浏览器代理URL格式 if browser_proxy_enabled and browser_proxy_url: @@ -1567,9 +1569,14 @@ async def update_captcha_config( remote_browser_timeout=remote_browser_timeout, browser_proxy_enabled=browser_proxy_enabled, browser_proxy_url=browser_proxy_url if browser_proxy_enabled else None, - browser_count=max(1, int(browser_count)) if browser_count else 1 + browser_count=max(1, int(browser_count)) if browser_count else 1, + personal_max_resident_tabs=personal_max_resident_tabs, + personal_idle_tab_ttl_seconds=personal_idle_tab_ttl_seconds ) + # 🔥 Hot reload: sync database config to memory + await db.reload_config_to_memory() + # 如果使用 browser 打码,热重载浏览器数量配置 if captcha_method == "browser": try: @@ -1579,8 +1586,14 @@ async def update_captcha_config( except Exception: pass - # 🔥 Hot reload: sync database config to memory - await db.reload_config_to_memory() + # 如果使用 personal 打码,热重载配置 + if captcha_method == "personal": + try: + from ..services.browser_captcha_personal import BrowserCaptchaService + service = await BrowserCaptchaService.get_instance(db) + await service.reload_config() + except Exception as e: + print(f"[Admin] Personal 配置热更新失败: {e}") return {"success": True, "message": "验证码配置更新成功"} @@ -1604,7 +1617,9 @@ async def get_captcha_config(token: str = Depends(verify_admin_token)): "remote_browser_timeout": captcha_config.remote_browser_timeout, "browser_proxy_enabled": captcha_config.browser_proxy_enabled, "browser_proxy_url": captcha_config.browser_proxy_url or "", - "browser_count": captcha_config.browser_count + "browser_count": captcha_config.browser_count, + "personal_max_resident_tabs": captcha_config.personal_max_resident_tabs, + "personal_idle_tab_ttl_seconds": captcha_config.personal_idle_tab_ttl_seconds } diff --git a/src/core/config.py b/src/core/config.py index fc1c4d7..0ce2560 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -389,6 +389,36 @@ class Config: except Exception: return 600 + @property + def personal_max_resident_tabs(self) -> int: + """内置浏览器打码最大常驻标签页数量""" + value = self._config.get("captcha", {}).get("personal_max_resident_tabs", 5) + try: + return max(1, min(50, int(value))) # 限制在1-50之间 + except Exception: + return 5 + + @property + def personal_idle_tab_ttl_seconds(self) -> int: + """内置浏览器打码标签页空闲超时(秒)""" + value = self._config.get("captcha", {}).get("personal_idle_tab_ttl_seconds", 600) + try: + return max(60, int(value)) + except Exception: + return 600 + + def set_personal_max_resident_tabs(self, value: int): + """设置内置浏览器打码最大常驻标签页数量""" + if "captcha" not in self._config: + self._config["captcha"] = {} + self._config["captcha"]["personal_max_resident_tabs"] = max(1, min(50, int(value))) + + def set_personal_idle_tab_ttl_seconds(self, value: int): + """设置内置浏览器打码标签页空闲超时(秒)""" + if "captcha" not in self._config: + self._config["captcha"] = {} + self._config["captcha"]["personal_idle_tab_ttl_seconds"] = max(60, int(value)) + @property def yescaptcha_api_key(self) -> str: """Get YesCaptcha API key""" diff --git a/src/core/database.py b/src/core/database.py index a39b1ca..31ea250 100644 --- a/src/core/database.py +++ b/src/core/database.py @@ -459,6 +459,21 @@ class Database: except Exception as e: print(f" ✗ Failed to add column '{col_name}': {e}") + # Check and add missing columns to captcha_config table + if await self._table_exists(db, "captcha_config"): + captcha_columns_to_add = [ + ("personal_max_resident_tabs", "INTEGER DEFAULT 5"), + ("personal_idle_tab_ttl_seconds", "INTEGER DEFAULT 600"), + ] + + for col_name, col_type in captcha_columns_to_add: + if not await self._column_exists(db, "captcha_config", col_name): + try: + await db.execute(f"ALTER TABLE captcha_config ADD COLUMN {col_name} {col_type}") + print(f" ✓ Added column '{col_name}' to captcha_config table") + except Exception as e: + print(f" ✗ Failed to add column '{col_name}': {e}") + # ========== Step 3: Ensure all config tables have default rows ========== # Note: This will NOT overwrite existing config rows # It only ensures missing rows are created with default values from setting.toml @@ -662,6 +677,8 @@ class Database: browser_proxy_enabled BOOLEAN DEFAULT 0, browser_proxy_url TEXT, browser_count INTEGER DEFAULT 1, + personal_max_resident_tabs INTEGER DEFAULT 5, + personal_idle_tab_ttl_seconds INTEGER DEFAULT 600, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) @@ -1506,6 +1523,8 @@ class Database: config.set_remote_browser_base_url(captcha_config.remote_browser_base_url) config.set_remote_browser_api_key(captcha_config.remote_browser_api_key) config.set_remote_browser_timeout(captcha_config.remote_browser_timeout) + config.set_personal_max_resident_tabs(captcha_config.personal_max_resident_tabs) + config.set_personal_idle_tab_ttl_seconds(captcha_config.personal_idle_tab_ttl_seconds) # Cache config operations async def get_cache_config(self) -> CacheConfig: @@ -1637,7 +1656,9 @@ class Database: remote_browser_timeout: int = None, browser_proxy_enabled: bool = None, browser_proxy_url: str = None, - browser_count: int = None + browser_count: int = None, + personal_max_resident_tabs: int = None, + personal_idle_tab_ttl_seconds: int = None ): """Update captcha configuration""" async with self._connect(write=True) as db: @@ -1662,7 +1683,11 @@ class Database: new_proxy_enabled = browser_proxy_enabled if browser_proxy_enabled is not None else current.get("browser_proxy_enabled", False) new_proxy_url = browser_proxy_url if browser_proxy_url is not None else current.get("browser_proxy_url") new_browser_count = browser_count if browser_count is not None else current.get("browser_count", 1) + new_personal_max_tabs = personal_max_resident_tabs if personal_max_resident_tabs is not None else current.get("personal_max_resident_tabs", 5) + new_personal_idle_ttl = personal_idle_tab_ttl_seconds if personal_idle_tab_ttl_seconds is not None else current.get("personal_idle_tab_ttl_seconds", 600) new_remote_timeout = max(5, int(new_remote_timeout)) if new_remote_timeout is not None else 60 + new_personal_max_tabs = max(1, min(50, int(new_personal_max_tabs))) # 限制1-50 + new_personal_idle_ttl = max(60, int(new_personal_idle_ttl)) # 最少60秒 await db.execute(""" UPDATE captcha_config @@ -1671,12 +1696,15 @@ class Database: ezcaptcha_api_key = ?, ezcaptcha_base_url = ?, capsolver_api_key = ?, capsolver_base_url = ?, remote_browser_base_url = ?, remote_browser_api_key = ?, remote_browser_timeout = ?, - browser_proxy_enabled = ?, browser_proxy_url = ?, browser_count = ?, updated_at = CURRENT_TIMESTAMP + browser_proxy_enabled = ?, browser_proxy_url = ?, browser_count = ?, + personal_max_resident_tabs = ?, personal_idle_tab_ttl_seconds = ?, + updated_at = CURRENT_TIMESTAMP WHERE id = 1 """, (new_method, new_yes_key, new_yes_url, new_cap_key, new_cap_url, new_ez_key, new_ez_url, new_cs_key, new_cs_url, (new_remote_base_url or "").strip(), (new_remote_api_key or "").strip(), new_remote_timeout, - new_proxy_enabled, new_proxy_url, new_browser_count)) + new_proxy_enabled, new_proxy_url, new_browser_count, + new_personal_max_tabs, new_personal_idle_ttl)) else: new_method = captcha_method if captcha_method is not None else "yescaptcha" new_yes_key = yescaptcha_api_key if yescaptcha_api_key is not None else "" @@ -1693,19 +1721,25 @@ class Database: new_proxy_enabled = browser_proxy_enabled if browser_proxy_enabled is not None else False new_proxy_url = browser_proxy_url new_browser_count = browser_count if browser_count is not None else 1 + new_personal_max_tabs = personal_max_resident_tabs if personal_max_resident_tabs is not None else 5 + new_personal_idle_ttl = personal_idle_tab_ttl_seconds if personal_idle_tab_ttl_seconds is not None else 600 new_remote_timeout = max(5, int(new_remote_timeout)) + new_personal_max_tabs = max(1, min(50, int(new_personal_max_tabs))) + new_personal_idle_ttl = max(60, int(new_personal_idle_ttl)) await db.execute(""" INSERT INTO captcha_config (id, captcha_method, yescaptcha_api_key, yescaptcha_base_url, capmonster_api_key, capmonster_base_url, ezcaptcha_api_key, ezcaptcha_base_url, capsolver_api_key, capsolver_base_url, remote_browser_base_url, remote_browser_api_key, remote_browser_timeout, - browser_proxy_enabled, browser_proxy_url, browser_count) - VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + browser_proxy_enabled, browser_proxy_url, browser_count, + personal_max_resident_tabs, personal_idle_tab_ttl_seconds) + VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, (new_method, new_yes_key, new_yes_url, new_cap_key, new_cap_url, new_ez_key, new_ez_url, new_cs_key, new_cs_url, (new_remote_base_url or "").strip(), (new_remote_api_key or "").strip(), new_remote_timeout, - new_proxy_enabled, new_proxy_url, new_browser_count)) + new_proxy_enabled, new_proxy_url, new_browser_count, + new_personal_max_tabs, new_personal_idle_ttl)) await db.commit() diff --git a/src/core/models.py b/src/core/models.py index 71a42a8..78473cc 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -193,6 +193,8 @@ class CaptchaConfig(BaseModel): browser_proxy_enabled: bool = False # 浏览器打码是否启用代理 browser_proxy_url: Optional[str] = None # 浏览器打码代理URL browser_count: int = 1 # 浏览器打码实例数量 + personal_max_resident_tabs: int = 5 # 内置浏览器最大常驻标签页数量 + personal_idle_tab_ttl_seconds: int = 600 # 内置浏览器标签页空闲超时(秒) created_at: Optional[datetime] = None updated_at: Optional[datetime] = None diff --git a/src/services/browser_captcha.py b/src/services/browser_captcha.py index 8b15a29..43b4b20 100644 --- a/src/services/browser_captcha.py +++ b/src/services/browser_captcha.py @@ -1104,8 +1104,9 @@ class TokenBrowser: 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}" + + # 使用更简单的 API 地址,避免加载复杂页面 + page_url = "https://labs.google/fx/api/auth/providers" primary_host = "https://www.recaptcha.net" if self._browser_proxy_active else "https://www.google.com" secondary_host = "https://www.google.com" if primary_host == "https://www.recaptcha.net" else "https://www.recaptcha.net" debug_logger.log_info( @@ -1175,13 +1176,13 @@ class TokenBrowser: page.on("response", handle_response) try: - await page.goto(page_url, wait_until="load", timeout=30000) + await page.goto(page_url, wait_until="load", timeout=15000) # 减少到15秒 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) + await page.wait_for_function("typeof grecaptcha !== 'undefined'", timeout=10000) # 减少到10秒 except Exception as e: debug_logger.log_warning(f"[BrowserCaptcha] Token-{self.token_id} grecaptcha 未就绪: {type(e).__name__}: {str(e)[:200]}") return None @@ -1205,7 +1206,7 @@ class TokenBrowser: # 按要求:等待 enterprise/reload 与 enterprise/clr 均出现并返回 200 try: - await asyncio.wait_for(reload_ok_event.wait(), timeout=12) + await asyncio.wait_for(reload_ok_event.wait(), timeout=8) # 减少到8秒 except asyncio.TimeoutError: debug_logger.log_warning( f"[BrowserCaptcha] Token-{self.token_id} 等待 recaptcha enterprise/reload 200 超时" @@ -1213,7 +1214,7 @@ class TokenBrowser: return None try: - await asyncio.wait_for(clr_ok_event.wait(), timeout=12) + await asyncio.wait_for(clr_ok_event.wait(), timeout=8) # 减少到8秒 except asyncio.TimeoutError: debug_logger.log_warning( f"[BrowserCaptcha] Token-{self.token_id} 等待 recaptcha enterprise/clr 200 超时" diff --git a/src/services/browser_captcha_personal.py b/src/services/browser_captcha_personal.py index 0ee3d79..6b41ea3 100644 --- a/src/services/browser_captcha_personal.py +++ b/src/services/browser_captcha_personal.py @@ -200,7 +200,22 @@ class BrowserCaptchaService: cls._instance._idle_tab_reaper_loop() ) return cls._instance - + + async def reload_config(self): + """热更新配置(从数据库重新加载)""" + from ..core.config import config + old_max_tabs = self._max_resident_tabs + old_idle_ttl = self._idle_tab_ttl_seconds + + self._max_resident_tabs = config.personal_max_resident_tabs + self._idle_tab_ttl_seconds = config.personal_idle_tab_ttl_seconds + + debug_logger.log_info( + f"[BrowserCaptcha] Personal 配置已热更新: " + f"max_tabs {old_max_tabs}->{self._max_resident_tabs}, " + f"idle_ttl {old_idle_ttl}s->{self._idle_tab_ttl_seconds}s" + ) + def _check_available(self): """检查服务是否可用""" if DOCKER_HEADED_BLOCKED: @@ -305,7 +320,7 @@ class BrowserCaptchaService: f"[BrowserCaptcha] 使用指定浏览器可执行文件: {browser_executable_path}" ) - # 启动 nodriver 浏览器 + # 启动 nodriver 浏览器(后台启动,不占用前台) config = uc.Config( headless=self.headless, user_data_dir=self.user_data_dir, @@ -316,7 +331,15 @@ class BrowserCaptchaService: '--disable-setuid-sandbox', '--disable-gpu', '--window-size=1280,720', + '--window-position=3000,3000', # 窗口位置移到屏幕外 '--profile-directory=Default', + '--disable-extensions', + '--disable-background-networking', + '--disable-sync', + '--disable-translate', + '--disable-default-apps', + '--no-first-run', + '--no-default-browser-check', ] ) self.browser = await uc.start(config) @@ -1059,7 +1082,7 @@ class BrowserCaptchaService: Returns: reCAPTCHA token字符串,如果获取失败返回None """ - debug_logger.log_info(f"[BrowserCaptcha] get_token 开始: project_id={project_id}, action={action}") + debug_logger.log_info(f"[BrowserCaptcha] get_token 开始: project_id={project_id}, action={action}, 当前标签页数={len(self._resident_tabs)}/{self._max_resident_tabs}") # 确保浏览器已初始化 await self.initialize() @@ -1074,7 +1097,7 @@ class BrowserCaptchaService: # 双重检查,避免并发创建 resident_info = self._resident_tabs.get(project_id) if resident_info is None: - debug_logger.log_info(f"[BrowserCaptcha] 开始创建标签页 (project: {project_id})") + debug_logger.log_info(f"[BrowserCaptcha] 开始创建标签页 (project: {project_id}, 当前: {len(self._resident_tabs)}/{self._max_resident_tabs})") # 先检查是否需要淘汰旧标签页 await self._evict_lru_tab_if_needed() @@ -1083,9 +1106,11 @@ class BrowserCaptchaService: debug_logger.log_warning(f"[BrowserCaptcha] 创建标签页失败,fallback 到传统模式 (project: {project_id})") return await self._get_token_legacy(project_id, action) self._resident_tabs[project_id] = resident_info - debug_logger.log_info(f"[BrowserCaptcha] ✅ 标签页创建成功 (project: {project_id}, 当前共 {len(self._resident_tabs)} 个)") + debug_logger.log_info(f"[BrowserCaptcha] ✅ 标签页创建成功 (project: {project_id}, 当前共 {len(self._resident_tabs)}/{self._max_resident_tabs} 个)") + else: + debug_logger.log_info(f"[BrowserCaptcha] 标签页已存在,复用 (project: {project_id})") - debug_logger.log_info(f"[BrowserCaptcha] 准备执行打码 (project: {project_id})") + debug_logger.log_info(f"[BrowserCaptcha] 准备执行打码 (project: {project_id}, 标签页使用次数: {resident_info.use_count if resident_info else 0})") # 使用常驻标签页生成 token(在锁外执行,避免阻塞) if resident_info and resident_info.recaptcha_ready and resident_info.tab: diff --git a/src/services/flow_client.py b/src/services/flow_client.py index 5b79d2e..18b65c1 100644 --- a/src/services/flow_client.py +++ b/src/services/flow_client.py @@ -2269,13 +2269,18 @@ class FlowClient: - 其他模式: browser_id 为 None """ captcha_method = config.captcha_method + debug_logger.log_info(f"[reCAPTCHA] 开始获取 token: method={captcha_method}, project_id={project_id}, action={action}") # 内置浏览器打码 (nodriver) if captcha_method == "personal": + debug_logger.log_info(f"[reCAPTCHA] 使用 personal 模式") try: from .browser_captcha_personal import BrowserCaptchaService + debug_logger.log_info(f"[reCAPTCHA] 导入 BrowserCaptchaService 成功") service = await BrowserCaptchaService.get_instance(self.db) + debug_logger.log_info(f"[reCAPTCHA] 获取服务实例成功,准备调用 get_token") token = await service.get_token(project_id, action) + debug_logger.log_info(f"[reCAPTCHA] get_token 返回: {token[:50] if token else None}...") fingerprint = service.get_last_fingerprint() if token else None self._set_request_fingerprint(fingerprint if token else None) return token, None diff --git a/static/manage.html b/static/manage.html index 3953990..809a2d1 100644 --- a/static/manage.html +++ b/static/manage.html @@ -451,6 +451,37 @@ + + +