@@ -26,6 +26,16 @@ class PlaywrightError(Exception): # type: ignore[no-redef]
2626 pass
2727
2828
29+ try :
30+ from greenlet import error as GreenletError
31+ except ImportError :
32+
33+ class GreenletError (Exception ): # type: ignore[no-redef]
34+ """Fallback greenlet error used when greenlet is not installed."""
35+
36+ pass
37+
38+
2939class SandboxToolSet (CommonToolSet ):
3040 """沙箱工具集基类
3141
@@ -727,24 +737,47 @@ def __init__(
727737 polar_fs_config = polar_fs_config ,
728738 )
729739 self ._playwright_sync : Optional ["BrowserPlaywrightSync" ] = None
740+ self ._playwright_thread : Optional [threading .Thread ] = None
730741
731742 def _get_playwright (self , sb : BrowserSandbox ) -> "BrowserPlaywrightSync" :
732743 """获取或创建 Playwright 连接 / Get or create Playwright connection
733744
734745 复用已有连接以减少连接建立开销和瞬态错误。
735746 使用双重检查锁定避免并发调用时创建多个连接导致资源泄漏。
747+ 当创建连接的线程已退出时,自动重建连接(Playwright greenlet 绑定到创建它的线程)。
748+
736749 Reuses existing connection to reduce connection overhead and transient errors.
737750 Uses double-checked locking to avoid leaking connections under concurrent calls.
751+ Automatically recreates the connection when the thread that created it has exited,
752+ because Playwright's internal greenlet is bound to the thread that created it.
738753 """
739- if self ._playwright_sync is not None :
740- return self ._playwright_sync
754+ if self ._playwright_sync is not None and self ._playwright_thread is not None :
755+ current_thread = threading .current_thread ()
756+ creator_thread = self ._playwright_thread
757+ if not creator_thread .is_alive () or current_thread is not creator_thread :
758+ if not creator_thread .is_alive ():
759+ logger .debug (
760+ "Playwright creating thread (id=%s) has exited, recreating"
761+ " connection" ,
762+ creator_thread .ident ,
763+ )
764+ else :
765+ logger .debug (
766+ "Playwright creating thread (id=%s) differs from current"
767+ " thread (id=%s), recreating connection" ,
768+ creator_thread .ident ,
769+ current_thread .ident ,
770+ )
771+ self ._reset_playwright ()
741772
742- with self .lock :
743- if self ._playwright_sync is None :
744- playwright_sync = sb .sync_playwright ()
745- playwright_sync .open ()
746- self ._playwright_sync = playwright_sync
747- return self ._playwright_sync
773+ if self ._playwright_sync is None :
774+ with self .lock :
775+ if self ._playwright_sync is None :
776+ playwright_sync = sb .sync_playwright ()
777+ playwright_sync .open ()
778+ self ._playwright_sync = playwright_sync
779+ self ._playwright_thread = threading .current_thread ()
780+ return self ._playwright_sync
748781
749782 def _reset_playwright (self ) -> None :
750783 """重置 Playwright 连接 / Reset Playwright connection
@@ -763,6 +796,7 @@ def _reset_playwright(self) -> None:
763796 exc_info = True ,
764797 )
765798 self ._playwright_sync = None
799+ self ._playwright_thread = None
766800
767801 def _run_in_sandbox (self , callback : Callable [[Sandbox ], Any ]) -> Any :
768802 """在沙箱中执行操作,智能区分错误类型 / Execute in sandbox with smart error handling
@@ -812,6 +846,22 @@ def _run_in_sandbox(self, callback: Callable[[Sandbox], Any]) -> Any:
812846 "Browser tool-level error (no sandbox rebuild): %s" , e
813847 )
814848 return {"error" : f"{ e !s} " }
849+ except GreenletError as e :
850+ logger .debug (
851+ "Greenlet thread-binding error, resetting Playwright: %s" ,
852+ e ,
853+ )
854+ # Keep the existing sandbox (it is still healthy); only the
855+ # Playwright connection needs to be recreated on this thread.
856+ try :
857+ self ._reset_playwright ()
858+ return callback (sb )
859+ except Exception as e2 :
860+ logger .debug (
861+ "Retry after Playwright reset failed: %s" ,
862+ e2 ,
863+ )
864+ return {"error" : f"{ e !s} " }
815865 except Exception as e :
816866 logger .debug ("Unexpected error in browser sandbox: %s" , e )
817867 return {"error" : f"{ e !s} " }
@@ -881,7 +931,7 @@ def inner(sb: Sandbox):
881931 def browser_navigate (
882932 self ,
883933 url : str ,
884- wait_until : str = "load " ,
934+ wait_until : str = "domcontentloaded " ,
885935 timeout : Optional [float ] = None ,
886936 ) -> Dict [str , Any ]:
887937 """导航到 URL / Navigate to URL"""
0 commit comments