diff --git a/config/setting.toml b/config/setting.toml index 27afd99..f0b6156 100644 --- a/config/setting.toml +++ b/config/setting.toml @@ -54,11 +54,11 @@ timeout = 7200 # 缓存超时时间(秒), 默认2小时 base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址 [captcha] -captcha_method = "browser" # 打码方式: yescaptcha/browser/personal/remote_browser +captcha_method = "remote_browser" # 打码方式: yescaptcha/browser/personal/remote_browser browser_recaptcha_settle_seconds = 1.0 # 打码完成额外稳态等待,速度优先可设 1.0 browser_launch_background = true # browser 打码默认后台启动(最小化/避免抢占前台) yescaptcha_api_key = "" # YesCaptcha API密钥 yescaptcha_base_url = "https://api.yescaptcha.com" -remote_browser_base_url = "" # 远程有头打码服务地址 -remote_browser_api_key = "" # 远程有头打码服务 API Key -remote_browser_timeout = 60 # 远程有头打码请求超时(秒) +remote_browser_base_url = "http://127.0.0.1:8060" # 本地 token 池服务地址 +remote_browser_api_key = "" # 本地 token 池服务 API Key +remote_browser_timeout = 35 # 远程有头打码请求超时(秒) diff --git a/src/main.py b/src/main.py index e301237..54cfbc2 100644 --- a/src/main.py +++ b/src/main.py @@ -116,6 +116,13 @@ async def lifespan(app: FastAPI): await concurrency_manager.initialize(tokens) + if config.captcha_method == "remote_browser": + try: + warmed_projects = await flow_client.prefill_remote_browser_for_tokens(tokens, action="IMAGE_GENERATION") + print(f"✓ Remote browser pool prefill started for {warmed_projects} project(s)") + except Exception as e: + print(f"⚠ Remote browser pool prefill failed: {e}") + # Start file cache cleanup task await generation_handler.file_cache.start_cleanup_task() diff --git a/src/services/flow_client.py b/src/services/flow_client.py index f75a6f3..c738067 100644 --- a/src/services/flow_client.py +++ b/src/services/flow_client.py @@ -33,6 +33,7 @@ class FlowClient: "flow_request_fingerprint", default=None ) + self._remote_browser_prefill_last_sent: Dict[str, float] = {} # Default "real browser" headers (Android Chrome style) to reduce upstream 4xx/5xx instability. # These will be applied as defaults (won't override caller-provided headers). @@ -1980,6 +1981,7 @@ class FlowClient: method="POST", path=f"/api/v1/sessions/{session_id}/error", json_data={"error_reason": error_reason or error_message or "upstream_error"}, + timeout_override=2, ) except Exception as e: debug_logger.log_warning(f"[reCAPTCHA RemoteBrowser] 上报 error 失败: {e}") @@ -2000,6 +2002,7 @@ class FlowClient: method="POST", path=f"/api/v1/sessions/{session_id}/finish", json_data={"status": "success"}, + timeout_override=2, ) except Exception as e: debug_logger.log_warning(f"[reCAPTCHA RemoteBrowser] 上报 finish 失败: {e}") @@ -2027,6 +2030,17 @@ class FlowClient: return base_url, api_key, timeout + @staticmethod + def _build_remote_browser_http_timeout(read_timeout: float) -> httpx.Timeout: + read_value = max(3.0, float(read_timeout)) + write_value = min(10.0, max(3.0, read_value)) + return httpx.Timeout( + connect=2.5, + read=read_value, + write=write_value, + pool=2.5, + ) + @staticmethod async def _sync_json_http_request( method: str, @@ -2040,7 +2054,7 @@ class FlowClient: request_method = (method or "GET").upper() request_kwargs: Dict[str, Any] = { "headers": req_headers, - "timeout": timeout, + "timeout": FlowClient._build_remote_browser_http_timeout(timeout), } if payload is not None: @@ -2051,7 +2065,7 @@ class FlowClient: try: # remote_browser 控制面只需要稳定传输 JSON,不需要浏览器指纹伪装。 # 使用 httpx 可以避免 curl_cffi 在当前环境下 POST body 被吞掉。 - async with httpx.AsyncClient(follow_redirects=True) as session: + async with httpx.AsyncClient(follow_redirects=False, trust_env=False) as session: response = await session.request( method=request_method, url=url, @@ -2103,25 +2117,72 @@ class FlowClient: return payload + async def prefill_remote_browser_pool( + self, + project_id: str, + action: str = "IMAGE_GENERATION", + token_id: Optional[int] = None, + *, + cooldown_seconds: float = 8.0, + ) -> bool: + """让本地 remote_browser 服务提前开始补池,尽量把取 token 等待搬到前面。""" + if config.captcha_method != "remote_browser": + return False + + normalized_project = str(project_id or "").strip() + normalized_action = str(action or "IMAGE_GENERATION").strip() or "IMAGE_GENERATION" + if not normalized_project: + return False + + cache_key = f"{normalized_project}|{normalized_action}|{int(token_id or 0)}" + now_value = time.monotonic() + last_sent = float(self._remote_browser_prefill_last_sent.get(cache_key, 0.0) or 0.0) + if (now_value - last_sent) < max(0.5, float(cooldown_seconds)): + return False + + try: + await self._call_remote_browser_service( + method="POST", + path="/api/v1/prefill", + json_data={ + "project_id": normalized_project, + "action": normalized_action, + "token_id": token_id, + }, + timeout_override=3, + ) + self._remote_browser_prefill_last_sent[cache_key] = now_value + return True + except Exception as e: + debug_logger.log_warning(f"[reCAPTCHA RemoteBrowser] prefill 失败: {e}") + return False + + async def prefill_remote_browser_for_tokens(self, tokens: List[Any], action: str = "IMAGE_GENERATION") -> int: + if config.captcha_method != "remote_browser": + return 0 + + unique_projects: List[str] = [] + seen_projects = set() + for token in tokens or []: + project_id = str(getattr(token, "current_project_id", "") or "").strip() + if not project_id or project_id in seen_projects: + continue + seen_projects.add(project_id) + unique_projects.append(project_id) + + warmed = 0 + for project_id in unique_projects: + if await self.prefill_remote_browser_pool(project_id, action=action): + warmed += 1 + return warmed + def _resolve_remote_browser_solve_timeout(self, action: str) -> int: base_timeout = max(5, int(config.remote_browser_timeout or 60)) action_name = str(action or "").strip().upper() - if action_name == "VIDEO_GENERATION": - expected = max( - int(getattr(config, "video_timeout", 1500) or 1500) + 180, - int(getattr(config, "flow_timeout", 120) or 120) + 240, - 900, - ) - else: - expected = max( - int(getattr(config, "image_timeout", 300) or 300) + 180, - int(getattr(config, "upsample_timeout", 300) or 300) + 120, - int(getattr(config, "flow_timeout", 120) or 120) + 180, - 300, - ) - - return max(base_timeout, min(expected, 3600)) + # 这里只是拿 reCAPTCHA token,不应该跟整条生成链路共用数百秒级超时。 + target_timeout = 45 if action_name == "VIDEO_GENERATION" else 35 + return max(12, min(base_timeout, target_timeout)) async def _get_recaptcha_token( self, diff --git a/src/services/generation_handler.py b/src/services/generation_handler.py index 34ca2b4..132caa2 100644 --- a/src/services/generation_handler.py +++ b/src/services/generation_handler.py @@ -927,6 +927,12 @@ class GenerationHandler: progress=22, response_extra={"project_id": project_id}, ) + prefill_action = "IMAGE_GENERATION" if generation_type == "image" else "VIDEO_GENERATION" + await self.flow_client.prefill_remote_browser_pool( + project_id=project_id, + action=prefill_action, + token_id=token.id, + ) # 5. 根据类型处理 generation_pipeline_started_at = time.time()