feat: warm remote browser pool and tighten timeouts

This commit is contained in:
genz27
2026-03-30 01:50:02 +08:00
parent f3eb69308d
commit 5d8c0d41e2
4 changed files with 95 additions and 21 deletions

View File

@@ -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 # 远程有头打码请求超时(秒)

View File

@@ -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()

View File

@@ -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,

View File

@@ -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()