Files
verify-invoice-default/scripts/service/rpa/browser_session.py
chenfuqiang 3a80db8a7e
All checks were successful
技能自动化发布 / release (push) Successful in 7s
chore: auto release commit (2026-09-14 22:22:49)
2026-09-14 22:22:49 +08:00

193 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""浏览器会话管理:基于 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)