Skip to content

Commit a12c02c

Browse files
committed
Optimize captcha browser slot pooling
1 parent 671a40e commit a12c02c

10 files changed

Lines changed: 1217 additions & 667 deletions

File tree

config/setting_example.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ base_url = "" # 缓存文件访问的基础URL, 留空则使用服务器地址
5656
[captcha]
5757
captcha_method = "browser" # 打码方式: yescaptcha/browser/personal/remote_browser
5858
browser_recaptcha_settle_seconds = 3.0 # reload/clr 就绪后的额外稳态等待
59+
browser_count = 1 # browser 模式的有头浏览器实例数量
60+
personal_project_pool_size = 4 # personal 模式下单个 Token 默认维护的项目池数量(仅影响项目轮换,不决定打码标签页数量)
61+
personal_max_resident_tabs = 5 # personal 模式共享打码标签页上限,所有 Token/project 共用这组 tab
62+
personal_idle_tab_ttl_seconds = 600 # personal 模式标签页空闲回收时间(秒)
5963
yescaptcha_api_key = "" # YesCaptcha API密钥
6064
yescaptcha_base_url = "https://api.yescaptcha.com"
6165
remote_browser_base_url = "" # 远程有头打码服务地址

src/api/admin.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,6 +1528,7 @@ async def update_captcha_config(
15281528
browser_proxy_enabled = request.get("browser_proxy_enabled", False)
15291529
browser_proxy_url = request.get("browser_proxy_url", "")
15301530
browser_count = request.get("browser_count", 1)
1531+
personal_project_pool_size = request.get("personal_project_pool_size")
15311532
personal_max_resident_tabs = request.get("personal_max_resident_tabs")
15321533
personal_idle_tab_ttl_seconds = request.get("personal_idle_tab_ttl_seconds")
15331534

@@ -1570,6 +1571,7 @@ async def update_captcha_config(
15701571
browser_proxy_enabled=browser_proxy_enabled,
15711572
browser_proxy_url=browser_proxy_url if browser_proxy_enabled else None,
15721573
browser_count=max(1, int(browser_count)) if browser_count else 1,
1574+
personal_project_pool_size=personal_project_pool_size,
15731575
personal_max_resident_tabs=personal_max_resident_tabs,
15741576
personal_idle_tab_ttl_seconds=personal_idle_tab_ttl_seconds
15751577
)
@@ -1618,6 +1620,7 @@ async def get_captcha_config(token: str = Depends(verify_admin_token)):
16181620
"browser_proxy_enabled": captcha_config.browser_proxy_enabled,
16191621
"browser_proxy_url": captcha_config.browser_proxy_url or "",
16201622
"browser_count": captcha_config.browser_count,
1623+
"personal_project_pool_size": captcha_config.personal_project_pool_size,
16211624
"personal_max_resident_tabs": captcha_config.personal_max_resident_tabs,
16221625
"personal_idle_tab_ttl_seconds": captcha_config.personal_idle_tab_ttl_seconds
16231626
}

src/core/config.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,13 +391,22 @@ def browser_idle_ttl_seconds(self) -> int:
391391

392392
@property
393393
def personal_max_resident_tabs(self) -> int:
394-
"""内置浏览器打码最大常驻标签页数量"""
394+
"""内置浏览器打码的共享标签页上限"""
395395
value = self._config.get("captcha", {}).get("personal_max_resident_tabs", 5)
396396
try:
397397
return max(1, min(50, int(value))) # 限制在1-50之间
398398
except Exception:
399399
return 5
400400

401+
@property
402+
def personal_project_pool_size(self) -> int:
403+
"""单个 Token 默认维护的项目池数量,仅影响项目轮换。"""
404+
value = self._config.get("captcha", {}).get("personal_project_pool_size", 4)
405+
try:
406+
return max(1, min(50, int(value)))
407+
except Exception:
408+
return 4
409+
401410
@property
402411
def personal_idle_tab_ttl_seconds(self) -> int:
403412
"""内置浏览器打码标签页空闲超时(秒)"""
@@ -408,11 +417,17 @@ def personal_idle_tab_ttl_seconds(self) -> int:
408417
return 600
409418

410419
def set_personal_max_resident_tabs(self, value: int):
411-
"""设置内置浏览器打码最大常驻标签页数量"""
420+
"""设置内置浏览器打码的共享标签页上限"""
412421
if "captcha" not in self._config:
413422
self._config["captcha"] = {}
414423
self._config["captcha"]["personal_max_resident_tabs"] = max(1, min(50, int(value)))
415424

425+
def set_personal_project_pool_size(self, value: int):
426+
"""设置单个 Token 默认维护的项目池数量,仅影响项目轮换"""
427+
if "captcha" not in self._config:
428+
self._config["captcha"] = {}
429+
self._config["captcha"]["personal_project_pool_size"] = max(1, min(50, int(value)))
430+
416431
def set_personal_idle_tab_ttl_seconds(self, value: int):
417432
"""设置内置浏览器打码标签页空闲超时(秒)"""
418433
if "captcha" not in self._config:

src/core/models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,8 @@ class CaptchaConfig(BaseModel):
193193
browser_proxy_enabled: bool = False # 浏览器打码是否启用代理
194194
browser_proxy_url: Optional[str] = None # 浏览器打码代理URL
195195
browser_count: int = 1 # 浏览器打码实例数量
196-
personal_max_resident_tabs: int = 5 # 内置浏览器最大常驻标签页数量
196+
personal_project_pool_size: int = 4 # 单个 Token 默认维护的项目池数量(仅影响项目轮换)
197+
personal_max_resident_tabs: int = 5 # 内置浏览器共享打码标签页数量上限
197198
personal_idle_tab_ttl_seconds: int = 600 # 内置浏览器标签页空闲超时(秒)
198199
created_at: Optional[datetime] = None
199200
updated_at: Optional[datetime] = None

src/main.py

Lines changed: 25 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -44,76 +44,48 @@ async def lifespan(app: FastAPI):
4444
await db.check_and_migrate_db(config_dict)
4545
print("✓ Database migration check completed.")
4646

47-
# Load admin config from database
48-
admin_config = await db.get_admin_config()
49-
if admin_config:
50-
config.set_admin_username_from_db(admin_config.username)
51-
config.set_admin_password_from_db(admin_config.password)
52-
config.api_key = admin_config.api_key
53-
54-
# Load cache configuration from database
55-
cache_config = await db.get_cache_config()
56-
config.set_cache_enabled(cache_config.cache_enabled)
57-
config.set_cache_timeout(cache_config.cache_timeout)
58-
config.set_cache_base_url(cache_config.cache_base_url or "")
59-
60-
# Load generation configuration from database
61-
generation_config = await db.get_generation_config()
62-
config.set_image_timeout(generation_config.image_timeout)
63-
config.set_video_timeout(generation_config.video_timeout)
64-
65-
# Load debug configuration from database
66-
debug_config = await db.get_debug_config()
67-
config.set_debug_enabled(debug_config.enabled)
68-
69-
# Load captcha configuration from database
47+
# 启动时统一把数据库配置同步到内存,避免 personal/brower 相关运行时配置遗漏。
48+
await db.reload_config_to_memory()
7049
captcha_config = await db.get_captcha_config()
71-
72-
config.set_captcha_method(captcha_config.captcha_method)
73-
config.set_yescaptcha_api_key(captcha_config.yescaptcha_api_key)
74-
config.set_yescaptcha_base_url(captcha_config.yescaptcha_base_url)
75-
config.set_capmonster_api_key(captcha_config.capmonster_api_key)
76-
config.set_capmonster_base_url(captcha_config.capmonster_base_url)
77-
config.set_ezcaptcha_api_key(captcha_config.ezcaptcha_api_key)
78-
config.set_ezcaptcha_base_url(captcha_config.ezcaptcha_base_url)
79-
config.set_capsolver_api_key(captcha_config.capsolver_api_key)
80-
config.set_capsolver_base_url(captcha_config.capsolver_base_url)
81-
config.set_remote_browser_base_url(captcha_config.remote_browser_base_url)
82-
config.set_remote_browser_api_key(captcha_config.remote_browser_api_key)
83-
config.set_remote_browser_timeout(captcha_config.remote_browser_timeout)
50+
51+
# 尽量在浏览器服务启动前就拿到 token 快照,后续并发管理和预热共用。
52+
tokens = await token_manager.get_all_tokens()
8453

8554
# Initialize browser captcha service if needed
8655
browser_service = None
8756
if captcha_config.captcha_method == "personal":
8857
from .services.browser_captcha_personal import BrowserCaptchaService
8958
browser_service = await BrowserCaptchaService.get_instance(db)
9059
print("✓ Browser captcha service initialized (nodriver mode)")
91-
92-
# 启动常驻模式:从第一个可用token获取project_id
93-
tokens = await token_manager.get_all_tokens()
94-
resident_project_id = None
95-
for t in tokens:
96-
if t.current_project_id and t.is_active:
97-
resident_project_id = t.current_project_id
98-
break
99-
100-
if resident_project_id:
101-
# 直接启动常驻模式(会自动导航到项目页面,cookie已持久化)
102-
await browser_service.start_resident_mode(resident_project_id)
103-
print(f"✓ Browser captcha resident mode started (project: {resident_project_id[:8]}...)")
60+
61+
warmup_limit = max(1, int(config.personal_max_resident_tabs or 1))
62+
warmup_project_ids = await token_manager.get_personal_warmup_project_ids(
63+
tokens=tokens,
64+
limit=warmup_limit,
65+
)
66+
67+
warmed_slots = await browser_service.warmup_resident_tabs(
68+
warmup_project_ids,
69+
limit=warmup_limit,
70+
)
71+
if warmed_slots:
72+
print(
73+
f"✓ Browser captcha shared resident tabs warmed "
74+
f"({len(warmed_slots)} slot(s), limit={warmup_limit})"
75+
)
76+
elif tokens:
77+
print("⚠ Browser captcha resident warmup skipped: no tab warmed successfully")
10478
else:
105-
# 没有可用的project_id时,打开登录窗口供用户手动操作
79+
# 没有任何可用 token 时,打开登录窗口供用户手动操作
10680
await browser_service.open_login_window()
107-
print("⚠ No active token with project_id found, opened login window for manual setup")
81+
print("⚠ No active token found, opened login window for manual setup")
10882
elif captcha_config.captcha_method == "browser":
10983
from .services.browser_captcha import BrowserCaptchaService
11084
browser_service = await BrowserCaptchaService.get_instance(db)
11185
await browser_service.warmup_browser_slots()
11286
print("? Browser captcha service initialized (headed mode)")
11387

11488
# Initialize concurrency manager
115-
tokens = await token_manager.get_all_tokens()
116-
11789
await concurrency_manager.initialize(tokens)
11890

11991
if config.captcha_method == "remote_browser":

0 commit comments

Comments
 (0)