mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-07-21 22:27:13 +08:00
fix(browser): drive system Chrome via spawn+CDP
This commit is contained in:
@@ -321,6 +321,14 @@ class BrowserService:
|
||||
self._context = None
|
||||
self._page = None
|
||||
|
||||
# When we drive a system Chrome/Edge, we spawn it ourselves with a
|
||||
# debugging port and attach over CDP (see chrome_launcher). This avoids
|
||||
# the macOS Automation prompt + multi-second stall that
|
||||
# chromium.launch(channel=...) incurs. Holds the child process owner.
|
||||
self._chrome_launcher = None
|
||||
# Path to the system browser executable when using system-chrome mode.
|
||||
self._system_exe: Optional[str] = None
|
||||
|
||||
# Launch mode: one of "fresh" | "persistent" | "cdp".
|
||||
# - cdp: connect to an externally launched Chrome via CDP endpoint.
|
||||
# - persistent: launch with launch_persistent_context using a user_data_dir
|
||||
@@ -352,13 +360,29 @@ class BrowserService:
|
||||
|
||||
# Resolve which browser engine to drive (system Chrome vs downloaded
|
||||
# Chromium). Deferred detection failures are surfaced at launch time.
|
||||
#
|
||||
# For a system Chrome/Edge we DON'T use chromium.launch(channel=...):
|
||||
# that "takes over" another app and triggers the macOS Automation
|
||||
# prompt + a long stall. Instead we spawn the browser ourselves with a
|
||||
# debugging port and attach over CDP (self._launch_mode = "system-cdp").
|
||||
# `self._system_exe` is the browser executable; the persistent
|
||||
# user_data_dir keeps login state across sessions.
|
||||
if self._launch_mode != "cdp":
|
||||
try:
|
||||
from agent.tools.browser.browser_env import resolve_engine
|
||||
engine = resolve_engine(self._config)
|
||||
if engine["mode"] == "system-chrome":
|
||||
self._channel = engine["channel"]
|
||||
logger.info(f"[Browser] Engine resolved: {engine['reason']}")
|
||||
self._system_exe = engine.get("path")
|
||||
# Only switch to spawn+CDP when we actually know the exe
|
||||
# path (macOS/Windows/Linux detection returns it). Persist
|
||||
# login state in a dedicated profile dir.
|
||||
if self._system_exe:
|
||||
self._launch_mode = "system-cdp"
|
||||
if not self._user_data_dir:
|
||||
self._user_data_dir = expand_path(_DEFAULT_USER_DATA_DIR)
|
||||
logger.info(f"[Browser] Engine resolved: {engine['reason']} "
|
||||
f"(spawn+CDP={bool(self._system_exe)})")
|
||||
elif engine["mode"] == "playwright-chromium":
|
||||
logger.info(f"[Browser] Engine resolved: {engine['reason']}")
|
||||
else:
|
||||
@@ -509,6 +533,8 @@ class BrowserService:
|
||||
|
||||
if self._launch_mode == "cdp":
|
||||
self._connect_cdp(viewport)
|
||||
elif self._launch_mode == "system-cdp":
|
||||
self._launch_system_cdp(launch_args, viewport)
|
||||
elif self._launch_mode == "persistent":
|
||||
self._launch_persistent(launch_args, viewport, user_agent)
|
||||
else:
|
||||
@@ -577,6 +603,41 @@ class BrowserService:
|
||||
self._page = pages[0] if pages else self._context.new_page()
|
||||
self._wire_close_listeners()
|
||||
|
||||
def _launch_system_cdp(self, launch_args: List[str], viewport: Dict[str, int]):
|
||||
"""Spawn the user's system Chrome/Edge with a debugging port, attach via CDP.
|
||||
|
||||
This is the default for system browsers. Unlike launch(channel=...), it
|
||||
does not "take over" the browser app, so it avoids the macOS Automation
|
||||
prompt / long stall. Login state persists in the isolated user_data_dir.
|
||||
"""
|
||||
from agent.tools.browser.chrome_launcher import ChromeLauncher
|
||||
|
||||
os.makedirs(self._user_data_dir, exist_ok=True)
|
||||
logger.info(
|
||||
f"[Browser] Launching system:{self._channel} via spawn+CDP "
|
||||
f"(headless={self._headless}, profile={self._user_data_dir})"
|
||||
)
|
||||
self._chrome_launcher = ChromeLauncher(
|
||||
executable=self._system_exe,
|
||||
user_data_dir=self._user_data_dir,
|
||||
extra_args=launch_args,
|
||||
headless=self._headless,
|
||||
)
|
||||
endpoint = self._chrome_launcher.launch()
|
||||
|
||||
self._browser = self._playwright.chromium.connect_over_cdp(endpoint)
|
||||
# The spawned Chrome opens its own default context (backed by
|
||||
# user_data_dir); reuse it so cookies / logins persist.
|
||||
contexts = self._browser.contexts
|
||||
self._context = contexts[0] if contexts else self._browser.new_context(viewport=viewport)
|
||||
pages = self._context.pages
|
||||
self._page = pages[0] if pages else self._context.new_page()
|
||||
try:
|
||||
self._page.set_viewport_size(viewport)
|
||||
except Exception:
|
||||
pass
|
||||
self._wire_close_listeners()
|
||||
|
||||
def _connect_cdp(self, viewport: Dict[str, int]):
|
||||
"""Attach to an existing Chrome started with --remote-debugging-port."""
|
||||
endpoint = self._cdp_endpoint
|
||||
@@ -631,13 +692,27 @@ class BrowserService:
|
||||
self._cancel_idle_timer()
|
||||
|
||||
if self._launch_mode == "cdp":
|
||||
# For CDP, browser.close() only detaches the Playwright client;
|
||||
# the user's Chrome process and its tabs stay alive.
|
||||
# For external CDP, browser.close() only detaches the Playwright
|
||||
# client; the user's Chrome process and its tabs stay alive.
|
||||
try:
|
||||
if self._browser:
|
||||
self._browser.close()
|
||||
except Exception as e:
|
||||
logger.debug(f"[Browser] cdp disconnect error: {e}")
|
||||
elif self._launch_mode == "system-cdp":
|
||||
# We own the spawned Chrome: detach the CDP client, then kill the
|
||||
# process we started so it doesn't linger.
|
||||
try:
|
||||
if self._browser:
|
||||
self._browser.close()
|
||||
except Exception as e:
|
||||
logger.debug(f"[Browser] system-cdp disconnect error: {e}")
|
||||
try:
|
||||
if self._chrome_launcher:
|
||||
self._chrome_launcher.close()
|
||||
except Exception as e:
|
||||
logger.debug(f"[Browser] chrome launcher close error: {e}")
|
||||
self._chrome_launcher = None
|
||||
else:
|
||||
for obj, label in [
|
||||
(self._context, "context"),
|
||||
|
||||
Reference in New Issue
Block a user