Skip to content

Commit 5d8c0d4

Browse files
committed
feat: warm remote browser pool and tighten timeouts
1 parent f3eb693 commit 5d8c0d4

4 files changed

Lines changed: 95 additions & 21 deletions

File tree

‎config/setting.toml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@ timeout = 7200 # 缓存超时时间(秒), 默认2小时
5454
base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址
5555

5656
[captcha]
57-
captcha_method = "browser" # 打码方式: yescaptcha/browser/personal/remote_browser
57+
captcha_method = "remote_browser" # 打码方式: yescaptcha/browser/personal/remote_browser
5858
browser_recaptcha_settle_seconds = 1.0 # 打码完成额外稳态等待,速度优先可设 1.0
5959
browser_launch_background = true # browser 打码默认后台启动(最小化/避免抢占前台)
6060
yescaptcha_api_key = "" # YesCaptcha API密钥
6161
yescaptcha_base_url = "https://api.yescaptcha.com"
62-
remote_browser_base_url = "" # 远程有头打码服务地址
63-
remote_browser_api_key = "" # 远程有头打码服务 API Key
64-
remote_browser_timeout = 60 # 远程有头打码请求超时(秒)
62+
remote_browser_base_url = "http://127.0.0.1:8060" # 本地 token 池服务地址
63+
remote_browser_api_key = "" # 本地 token 池服务 API Key
64+
remote_browser_timeout = 35 # 远程有头打码请求超时(秒)

‎src/main.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,13 @@ async def lifespan(app: FastAPI):
116116

117117
await concurrency_manager.initialize(tokens)
118118

119+
if config.captcha_method == "remote_browser":
120+
try:
121+
warmed_projects = await flow_client.prefill_remote_browser_for_tokens(tokens, action="IMAGE_GENERATION")
122+
print(f"✓ Remote browser pool prefill started for {warmed_projects} project(s)")
123+
except Exception as e:
124+
print(f"⚠ Remote browser pool prefill failed: {e}")
125+
119126
# Start file cache cleanup task
120127
await generation_handler.file_cache.start_cleanup_task()
121128

‎src/services/flow_client.py‎

Lines changed: 78 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ def __init__(self, proxy_manager, db=None):
3333
"flow_request_fingerprint",
3434
default=None
3535
)
36+
self._remote_browser_prefill_last_sent: Dict[str, float] = {}
3637

3738
# Default "real browser" headers (Android Chrome style) to reduce upstream 4xx/5xx instability.
3839
# These will be applied as defaults (won't override caller-provided headers).
@@ -1980,6 +1981,7 @@ async def _notify_browser_captcha_error(
19801981
method="POST",
19811982
path=f"/api/v1/sessions/{session_id}/error",
19821983
json_data={"error_reason": error_reason or error_message or "upstream_error"},
1984+
timeout_override=2,
19831985
)
19841986
except Exception as e:
19851987
debug_logger.log_warning(f"[reCAPTCHA RemoteBrowser] 上报 error 失败: {e}")
@@ -2000,6 +2002,7 @@ async def _notify_browser_captcha_request_finished(self, browser_id: Optional[Un
20002002
method="POST",
20012003
path=f"/api/v1/sessions/{session_id}/finish",
20022004
json_data={"status": "success"},
2005+
timeout_override=2,
20032006
)
20042007
except Exception as e:
20052008
debug_logger.log_warning(f"[reCAPTCHA RemoteBrowser] 上报 finish 失败: {e}")
@@ -2027,6 +2030,17 @@ def _get_remote_browser_service_config(self) -> tuple[str, str, int]:
20272030

20282031
return base_url, api_key, timeout
20292032

2033+
@staticmethod
2034+
def _build_remote_browser_http_timeout(read_timeout: float) -> httpx.Timeout:
2035+
read_value = max(3.0, float(read_timeout))
2036+
write_value = min(10.0, max(3.0, read_value))
2037+
return httpx.Timeout(
2038+
connect=2.5,
2039+
read=read_value,
2040+
write=write_value,
2041+
pool=2.5,
2042+
)
2043+
20302044
@staticmethod
20312045
async def _sync_json_http_request(
20322046
method: str,
@@ -2040,7 +2054,7 @@ async def _sync_json_http_request(
20402054
request_method = (method or "GET").upper()
20412055
request_kwargs: Dict[str, Any] = {
20422056
"headers": req_headers,
2043-
"timeout": timeout,
2057+
"timeout": FlowClient._build_remote_browser_http_timeout(timeout),
20442058
}
20452059

20462060
if payload is not None:
@@ -2051,7 +2065,7 @@ async def _sync_json_http_request(
20512065
try:
20522066
# remote_browser 控制面只需要稳定传输 JSON,不需要浏览器指纹伪装。
20532067
# 使用 httpx 可以避免 curl_cffi 在当前环境下 POST body 被吞掉。
2054-
async with httpx.AsyncClient(follow_redirects=True) as session:
2068+
async with httpx.AsyncClient(follow_redirects=False, trust_env=False) as session:
20552069
response = await session.request(
20562070
method=request_method,
20572071
url=url,
@@ -2103,25 +2117,72 @@ async def _call_remote_browser_service(
21032117

21042118
return payload
21052119

2120+
async def prefill_remote_browser_pool(
2121+
self,
2122+
project_id: str,
2123+
action: str = "IMAGE_GENERATION",
2124+
token_id: Optional[int] = None,
2125+
*,
2126+
cooldown_seconds: float = 8.0,
2127+
) -> bool:
2128+
"""让本地 remote_browser 服务提前开始补池,尽量把取 token 等待搬到前面。"""
2129+
if config.captcha_method != "remote_browser":
2130+
return False
2131+
2132+
normalized_project = str(project_id or "").strip()
2133+
normalized_action = str(action or "IMAGE_GENERATION").strip() or "IMAGE_GENERATION"
2134+
if not normalized_project:
2135+
return False
2136+
2137+
cache_key = f"{normalized_project}|{normalized_action}|{int(token_id or 0)}"
2138+
now_value = time.monotonic()
2139+
last_sent = float(self._remote_browser_prefill_last_sent.get(cache_key, 0.0) or 0.0)
2140+
if (now_value - last_sent) < max(0.5, float(cooldown_seconds)):
2141+
return False
2142+
2143+
try:
2144+
await self._call_remote_browser_service(
2145+
method="POST",
2146+
path="/api/v1/prefill",
2147+
json_data={
2148+
"project_id": normalized_project,
2149+
"action": normalized_action,
2150+
"token_id": token_id,
2151+
},
2152+
timeout_override=3,
2153+
)
2154+
self._remote_browser_prefill_last_sent[cache_key] = now_value
2155+
return True
2156+
except Exception as e:
2157+
debug_logger.log_warning(f"[reCAPTCHA RemoteBrowser] prefill 失败: {e}")
2158+
return False
2159+
2160+
async def prefill_remote_browser_for_tokens(self, tokens: List[Any], action: str = "IMAGE_GENERATION") -> int:
2161+
if config.captcha_method != "remote_browser":
2162+
return 0
2163+
2164+
unique_projects: List[str] = []
2165+
seen_projects = set()
2166+
for token in tokens or []:
2167+
project_id = str(getattr(token, "current_project_id", "") or "").strip()
2168+
if not project_id or project_id in seen_projects:
2169+
continue
2170+
seen_projects.add(project_id)
2171+
unique_projects.append(project_id)
2172+
2173+
warmed = 0
2174+
for project_id in unique_projects:
2175+
if await self.prefill_remote_browser_pool(project_id, action=action):
2176+
warmed += 1
2177+
return warmed
2178+
21062179
def _resolve_remote_browser_solve_timeout(self, action: str) -> int:
21072180
base_timeout = max(5, int(config.remote_browser_timeout or 60))
21082181
action_name = str(action or "").strip().upper()
21092182

2110-
if action_name == "VIDEO_GENERATION":
2111-
expected = max(
2112-
int(getattr(config, "video_timeout", 1500) or 1500) + 180,
2113-
int(getattr(config, "flow_timeout", 120) or 120) + 240,
2114-
900,
2115-
)
2116-
else:
2117-
expected = max(
2118-
int(getattr(config, "image_timeout", 300) or 300) + 180,
2119-
int(getattr(config, "upsample_timeout", 300) or 300) + 120,
2120-
int(getattr(config, "flow_timeout", 120) or 120) + 180,
2121-
300,
2122-
)
2123-
2124-
return max(base_timeout, min(expected, 3600))
2183+
# 这里只是拿 reCAPTCHA token,不应该跟整条生成链路共用数百秒级超时。
2184+
target_timeout = 45 if action_name == "VIDEO_GENERATION" else 35
2185+
return max(12, min(base_timeout, target_timeout))
21252186

21262187
async def _get_recaptcha_token(
21272188
self,

‎src/services/generation_handler.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,12 @@ async def handle_generation(
927927
progress=22,
928928
response_extra={"project_id": project_id},
929929
)
930+
prefill_action = "IMAGE_GENERATION" if generation_type == "image" else "VIDEO_GENERATION"
931+
await self.flow_client.prefill_remote_browser_pool(
932+
project_id=project_id,
933+
action=prefill_action,
934+
token_id=token.id,
935+
)
930936

931937
# 5. 根据类型处理
932938
generation_pipeline_started_at = time.time()

0 commit comments

Comments
 (0)