1919from ..core .logger import debug_logger
2020from ..core .config import config
2121
22+ # 复用 browser 模式的浏览器缓存目录约定,避免容器内每次换位置。
23+ os .environ .setdefault ("PLAYWRIGHT_BROWSERS_PATH" , "0" )
24+
2225
2326# ==================== Docker 环境检测 ====================
2427def _is_running_in_docker () -> bool :
@@ -118,6 +121,111 @@ def _ensure_nodriver_installed() -> bool:
118121 return False
119122
120123
124+ def _run_playwright_install (use_mirror : bool = False ) -> bool :
125+ """安装 playwright chromium 浏览器,复用 browser 模式的安装方式。"""
126+ cmd = [sys .executable , '-m' , 'playwright' , 'install' , 'chromium' ]
127+ env = os .environ .copy ()
128+
129+ if use_mirror :
130+ env ['PLAYWRIGHT_DOWNLOAD_HOST' ] = 'https://npmmirror.com/mirrors/playwright'
131+
132+ try :
133+ debug_logger .log_info ("[BrowserCaptcha] 正在安装 chromium 浏览器..." )
134+ print ("[BrowserCaptcha] 正在安装 chromium 浏览器..." )
135+ result = subprocess .run (cmd , capture_output = True , text = True , timeout = 600 , env = env )
136+ if result .returncode == 0 :
137+ debug_logger .log_info ("[BrowserCaptcha] ✅ chromium 浏览器安装成功" )
138+ print ("[BrowserCaptcha] ✅ chromium 浏览器安装成功" )
139+ return True
140+
141+ debug_logger .log_warning (f"[BrowserCaptcha] chromium 安装失败: { result .stderr [:200 ]} " )
142+ return False
143+ except Exception as e :
144+ debug_logger .log_warning (f"[BrowserCaptcha] chromium 安装异常: { e } " )
145+ return False
146+
147+
148+ def _ensure_playwright_installed () -> bool :
149+ """确保 playwright 可用,便于复用其 chromium 二进制。"""
150+ try :
151+ import playwright # noqa: F401
152+ debug_logger .log_info ("[BrowserCaptcha] playwright 已安装" )
153+ return True
154+ except ImportError :
155+ pass
156+
157+ debug_logger .log_info ("[BrowserCaptcha] playwright 未安装,开始自动安装..." )
158+ print ("[BrowserCaptcha] playwright 未安装,开始自动安装..." )
159+
160+ if _run_pip_install ('playwright' , use_mirror = False ):
161+ return True
162+
163+ debug_logger .log_info ("[BrowserCaptcha] 官方源安装失败,尝试国内镜像..." )
164+ print ("[BrowserCaptcha] 官方源安装失败,尝试国内镜像..." )
165+ if _run_pip_install ('playwright' , use_mirror = True ):
166+ return True
167+
168+ debug_logger .log_error ("[BrowserCaptcha] ❌ playwright 自动安装失败,请手动安装: pip install playwright" )
169+ print ("[BrowserCaptcha] ❌ playwright 自动安装失败,请手动安装: pip install playwright" )
170+ return False
171+
172+
173+ def _detect_playwright_browser_path () -> Optional [str ]:
174+ """读取 playwright 管理的 chromium 可执行文件路径。"""
175+ detect_script = (
176+ "from playwright.sync_api import sync_playwright\n "
177+ "with sync_playwright() as p:\n "
178+ " print(p.chromium.executable_path or '')\n "
179+ )
180+ env = os .environ .copy ()
181+ env .setdefault ("PLAYWRIGHT_BROWSERS_PATH" , os .environ .get ("PLAYWRIGHT_BROWSERS_PATH" , "0" ) or "0" )
182+
183+ try :
184+ result = subprocess .run (
185+ [sys .executable , "-c" , detect_script ],
186+ capture_output = True ,
187+ text = True ,
188+ timeout = 60 ,
189+ env = env ,
190+ )
191+ browser_path_lines = (result .stdout or "" ).strip ().splitlines ()
192+ browser_path = browser_path_lines [- 1 ].strip () if browser_path_lines else ""
193+ if result .returncode == 0 and browser_path and os .path .exists (browser_path ):
194+ debug_logger .log_info (f"[BrowserCaptcha] 检测到 playwright chromium: { browser_path } " )
195+ return browser_path
196+
197+ stderr_text = (result .stderr or "" ).strip ()
198+ if stderr_text :
199+ debug_logger .log_warning (f"[BrowserCaptcha] 检测 playwright chromium 失败: { stderr_text [:200 ]} " )
200+ except Exception as e :
201+ debug_logger .log_info (f"[BrowserCaptcha] 检测 playwright chromium 时出错: { e } " )
202+
203+ return None
204+
205+
206+ def _ensure_playwright_browser_path () -> Optional [str ]:
207+ """确保存在可复用的 chromium 二进制,并返回路径。"""
208+ browser_path = _detect_playwright_browser_path ()
209+ if browser_path :
210+ return browser_path
211+
212+ if not _ensure_playwright_installed ():
213+ return None
214+
215+ debug_logger .log_info ("[BrowserCaptcha] playwright chromium 未安装,开始自动安装..." )
216+ print ("[BrowserCaptcha] playwright chromium 未安装,开始自动安装..." )
217+
218+ if not _run_playwright_install (use_mirror = False ):
219+ debug_logger .log_info ("[BrowserCaptcha] 官方源安装失败,尝试国内镜像..." )
220+ print ("[BrowserCaptcha] 官方源安装失败,尝试国内镜像..." )
221+ if not _run_playwright_install (use_mirror = True ):
222+ debug_logger .log_error ("[BrowserCaptcha] ❌ chromium 浏览器自动安装失败,请手动安装: python -m playwright install chromium" )
223+ print ("[BrowserCaptcha] ❌ chromium 浏览器自动安装失败,请手动安装: python -m playwright install chromium" )
224+ return None
225+
226+ return _detect_playwright_browser_path ()
227+
228+
121229# 尝试导入 nodriver
122230uc = None
123231NODRIVER_AVAILABLE = False
@@ -126,14 +234,15 @@ def _ensure_nodriver_installed() -> bool:
126234if DOCKER_HEADED_BLOCKED :
127235 debug_logger .log_warning (
128236 "[BrowserCaptcha] 检测到 Docker 环境,默认禁用内置浏览器打码。"
129- "如需启用请设置 ALLOW_DOCKER_HEADED_CAPTCHA=true,并提供 DISPLAY/Xvfb。"
237+ "如需启用请设置 ALLOW_DOCKER_HEADED_CAPTCHA=true。"
238+ "personal 模式默认支持无头,不强制依赖 DISPLAY/Xvfb。"
130239 )
131240 print ("[BrowserCaptcha] ⚠️ 检测到 Docker 环境,默认禁用内置浏览器打码" )
132- print ("[BrowserCaptcha] 如需启用请设置 ALLOW_DOCKER_HEADED_CAPTCHA=true,并提供 DISPLAY/Xvfb " )
241+ print ("[BrowserCaptcha] 如需启用请设置 ALLOW_DOCKER_HEADED_CAPTCHA=true" )
133242else :
134243 if IS_DOCKER and ALLOW_DOCKER_HEADED :
135244 debug_logger .log_warning (
136- "[BrowserCaptcha] Docker 内置浏览器打码白名单已启用,请确保 DISPLAY/Xvfb 可用 "
245+ "[BrowserCaptcha] Docker 内置浏览器打码白名单已启用,personal 模式将按 headless 配置决定是否需要 DISPLAY/Xvfb"
137246 )
138247 print ("[BrowserCaptcha] ✅ Docker 内置浏览器打码白名单已启用" )
139248 if _ensure_nodriver_installed ():
@@ -549,14 +658,18 @@ def _refresh_runtime_tunables(self):
549658 except Exception :
550659 self ._fingerprint_cache_ttl_seconds = 3600.0
551660
661+ def _requires_virtual_display (self ) -> bool :
662+ """仅在显式有头模式下要求 Docker/Linux 提供 DISPLAY/Xvfb。"""
663+ return bool (IS_DOCKER and os .name == "posix" and not self .headless )
664+
552665 def _check_available (self ):
553666 """检查服务是否可用"""
554667 if DOCKER_HEADED_BLOCKED :
555668 raise RuntimeError (
556669 "检测到 Docker 环境,默认禁用内置浏览器打码。"
557- "如需启用请设置环境变量 ALLOW_DOCKER_HEADED_CAPTCHA=true,并提供 DISPLAY/Xvfb 。"
670+ "如需启用请设置环境变量 ALLOW_DOCKER_HEADED_CAPTCHA=true。"
558671 )
559- if IS_DOCKER and not os .environ .get ("DISPLAY" ):
672+ if self . _requires_virtual_display () and not os .environ .get ("DISPLAY" ):
560673 raise RuntimeError (
561674 "Docker 内置浏览器打码已启用,但 DISPLAY 未设置。"
562675 "请设置 DISPLAY(例如 :99)并启动 Xvfb。"
@@ -1294,6 +1407,13 @@ async def initialize(self):
12941407 f"[BrowserCaptcha] 指定浏览器不存在,改为自动发现: { browser_executable_path } "
12951408 )
12961409 browser_executable_path = None
1410+ if not browser_executable_path :
1411+ playwright_browser_path = _ensure_playwright_browser_path ()
1412+ if playwright_browser_path :
1413+ browser_executable_path = playwright_browser_path
1414+ debug_logger .log_info (
1415+ f"[BrowserCaptcha] 复用 playwright chromium 作为 nodriver 浏览器: { browser_executable_path } "
1416+ )
12971417 if browser_executable_path :
12981418 debug_logger .log_info (
12991419 f"[BrowserCaptcha] 使用指定浏览器可执行文件: { browser_executable_path } "
@@ -1361,7 +1481,8 @@ async def initialize(self):
13611481 browser_args .append ('--disable-extensions' )
13621482
13631483 effective_launch_args = list (browser_args )
1364- await self ._wait_for_display_ready (display_value )
1484+ if self ._requires_virtual_display ():
1485+ await self ._wait_for_display_ready (display_value )
13651486
13661487 effective_uid = "n/a"
13671488 if hasattr (os , "geteuid" ):
0 commit comments