From 97bc4560b6710a9baa3281d252ac7d25b8104423 Mon Sep 17 00:00:00 2001 From: liuwei Date: Fri, 17 Apr 2026 10:14:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E8=BD=AC=E5=9B=BE=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=E4=BF=9D=E6=B4=BB=EF=BC=9A=E6=88=AA=E5=9B=BE?= =?UTF-8?q?=E5=90=8E=E6=8E=A2=E6=B4=BB=E5=B9=B6=E8=87=AA=E5=8A=A8=E9=87=8D?= =?UTF-8?q?=E5=BB=BA=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=90=8E=E5=8F=B0=E5=BF=83?= =?UTF-8?q?=E8=B7=B3=E5=B7=A1=E6=A3=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- utils/markdown_to_image.py | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/utils/markdown_to_image.py b/utils/markdown_to_image.py index 39c51be..8dadfcb 100644 --- a/utils/markdown_to_image.py +++ b/utils/markdown_to_image.py @@ -521,6 +521,8 @@ class _PersistentBrowser: self._last_launch_source = "unknown" # 记录当前常驻浏览器所属事件循环,避免跨 loop 复用导致的句柄异常。 self._owner_loop_id: Optional[int] = None + # 保活心跳任务:定期探测浏览器连通性,异常时自动重建。 + self._heartbeat_task: Optional[asyncio.Task] = None async def _launch_browser(self): if self._playwright is None: @@ -576,6 +578,7 @@ class _PersistentBrowser: f"[md2img] 常驻浏览器就绪: source={self._last_launch_source}, " f"loop={self._owner_loop_id}, pid={browser_pid}" ) + self._ensure_heartbeat_task() return self._browser async def restart_browser(self): @@ -593,8 +596,41 @@ class _PersistentBrowser: f"[md2img] 常驻浏览器已重建: source={self._last_launch_source}, " f"loop={self._owner_loop_id}, pid={browser_pid}" ) + self._ensure_heartbeat_task() return self._browser + async def _is_browser_alive(self, browser, timeout_seconds: float = 3.0) -> bool: + """探测浏览器是否仍可用。""" + if not browser or not browser.is_connected(): + return False + try: + await asyncio.wait_for(browser.version(), timeout=timeout_seconds) + return True + except Exception: + return False + + async def _heartbeat_loop(self): + """周期性探测浏览器可用性,断连后自动重建。""" + while True: + try: + await asyncio.sleep(10) + # 没有浏览器实例时只保持心跳存活,不主动创建,避免空闲时不必要消耗。 + if not self._browser: + continue + if not await self._is_browser_alive(self._browser, timeout_seconds=2.0): + logger.warning("[md2img] 心跳探测发现浏览器已断连,准备自动重建") + await self.restart_browser() + except asyncio.CancelledError: + raise + except Exception as e: + logger.warning(f"[md2img] 心跳探测异常: {e}") + + def _ensure_heartbeat_task(self): + """确保保活任务已启动(幂等)。""" + if self._heartbeat_task and not self._heartbeat_task.done(): + return + self._heartbeat_task = asyncio.create_task(self._heartbeat_loop(), name="md2img:heartbeat") + async def screenshot(self, html_content: str, output_image: str): browser = await self.ensure_browser() @@ -619,6 +655,11 @@ class _PersistentBrowser: try: await _capture_with_browser(browser) + # 截图完成后立刻做一次可用性探测。 + # 在部分系统环境中,浏览器可能在任务完成后迅速断连,这里主动重建保证“常驻”语义。 + if not await self._is_browser_alive(browser, timeout_seconds=2.0): + logger.warning("[md2img] 截图后浏览器已断连,立即执行自动重建") + await self.restart_browser() except Exception as e: # 首次失败后重建一次浏览器再重试,提升抗偶发故障能力。 logger.warning(f"[md2img] 常驻浏览器截图失败,准备重建后重试: {e}")