"""浏览器会话管理:基于 skill-template 的 simulator_browser_rpa 示例。""" from __future__ import annotations import logging import os import random from typing import Any, List, Optional, Tuple from jiangchang_skill_core import config from jiangchang_skill_core.activity import interruptible_sleep logger = logging.getLogger(__name__) DEFAULT_PAGE_TIMEOUT_MS = 15_000 CHROME_LAUNCH_ARGS: List[str] = [ "--start-maximized", "--disable-blink-features=AutomationControlled", ] STEALTH_INIT_SCRIPT = """ (() => { try { Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); } catch (e) {} })(); """ def chrome_launch_args() -> List[str]: return list(CHROME_LAUNCH_ARGS) def _headless_from_env() -> bool: v = (os.getenv("OPENCLAW_BROWSER_HEADLESS") or "0").strip().lower() return v in ("1", "true", "yes", "on") def find_browser_executable() -> Optional[str]: """查找系统 Chrome 或 Edge 浏览器。""" try: from jiangchang_skill_core.runtime_env import find_chrome_executable as _find chrome = _find() if chrome and os.path.isfile(chrome): return chrome except ImportError: pass candidates = [ os.path.expandvars(r"%ProgramFiles%\Google\Chrome\Application\chrome.exe"), os.path.expandvars(r"%ProgramFiles(x86)%\Google\Chrome\Application\chrome.exe"), os.path.expandvars(r"%ProgramFiles%\Microsoft\Edge\Application\msedge.exe"), os.path.expandvars(r"%ProgramFiles(x86)%\Microsoft\Edge\Application\msedge.exe"), ] for path in candidates: if path and os.path.isfile(path): return path return None # 向后兼容别名 find_edge_executable = find_browser_executable def _clear_node_options() -> None: os.environ.pop("NODE_OPTIONS", None) async def start_browser_session( profile_dir: str, headless: bool = False, ) -> Tuple[Any, Any, Any]: """启动持久化 Chrome/Edge 浏览器会话。 URL 不在此文件打开;由 invoice_rpa.py 负责 page.goto。 Args: profile_dir: 浏览器配置目录 headless: 是否无头模式(生产必须为 False) Returns: (playwright, context, page) 三元组 Raises: RuntimeError: 浏览器启动失败 """ if not profile_dir: raise RuntimeError("ERROR:MISSING_BROWSER 缺少浏览器配置目录") # 确保有头模式(生产要求) if headless: logger.warning("生产环境强制使用有头模式") headless = False # 确保 profile 目录存在 os.makedirs(profile_dir, exist_ok=True) _clear_node_options() hl = headless if headless is not None else _headless_from_env() chrome = find_browser_executable() if not chrome: raise RuntimeError( "ERROR:MISSING_BROWSER 未检测到 Chrome 或 Edge 浏览器,请先安装系统浏览器后重试。" ) args = chrome_launch_args() logger.info( "browser_launch chrome=%s profile_dir=%s args=%s headless=%s", chrome, profile_dir, args, hl, ) try: from playwright.async_api import async_playwright pw = await async_playwright().start() context = None try: context = await pw.chromium.launch_persistent_context( user_data_dir=profile_dir, headless=hl, executable_path=chrome, locale="zh-CN", no_viewport=True, args=args, ignore_default_args=["--enable-automation"], ) await context.add_init_script(STEALTH_INIT_SCRIPT) page = context.pages[0] if context.pages else await context.new_page() page.set_default_timeout(DEFAULT_PAGE_TIMEOUT_MS) logger.info("browser_started profile_dir=%s", profile_dir) return pw, context, page except Exception: if context is not None: try: await context.close() except Exception: pass await pw.stop() raise except RuntimeError: raise except Exception as e: logger.error("browser_start_failed error=%s", str(e)) raise RuntimeError(f"ERROR:MISSING_BROWSER 浏览器启动失败: {str(e)}") async def close_browser_context(pw: Any, context: Any) -> None: """安全关闭浏览器上下文。""" try: if context is not None: await context.close() except Exception: pass try: if pw is not None: await pw.stop() except Exception: pass async def wait_for_page_ready(page: Any, timeout_sec: float = 30.0) -> bool: """等待页面加载完成。""" async def _check_ready(): try: ready_state = await page.evaluate("document.readyState") if ready_state == "complete": return True loading_selectors = [".loading", ".spinner", "[data-loading]"] for sel in loading_selectors: if await page.locator(sel).count() > 0: return False return True except Exception: return False try: from jiangchang_skill_core.rpa import poll_until result = await poll_until(_check_ready, timeout_sec=timeout_sec, interval_sec=0.5) return result.ok except ImportError: # fallback: 简单等待 import asyncio await asyncio.sleep(min(timeout_sec, 3.0)) return True async def random_delay(min_sec: float = 1.0, max_sec: float = 3.0) -> None: """随机延迟,模拟人类操作节奏。""" delay = random.uniform(min_sec, max_sec) await interruptible_sleep(delay)