chore: auto release commit (2026-09-14 22:22:49)
All checks were successful
技能自动化发布 / release (push) Successful in 7s

This commit is contained in:
chenfuqiang
2026-09-14 22:22:49 +08:00
parent 6998fb3bd7
commit 3a80db8a7e
155 changed files with 1365 additions and 21023 deletions

View File

@@ -1,67 +1,147 @@
"""浏览器会话管理:基于skill-template的real_browser_rpa示例。"""
"""浏览器会话管理:基于 skill-template 的 simulator_browser_rpa 示例。"""
from __future__ import annotations
import logging
import os
import random
from typing import Any, Optional, Tuple
from typing import Any, List, Optional, Tuple
from jiangchang_skill_core import config
from jiangchang_skill_core.activity import interruptible_sleep
from jiangchang_skill_core.rpa import launch_persistent_browser, poll_until
logger = logging.getLogger(__name__)
# 目标网站配置
TARGET_URL = "https://sandbox.jc2009.com/industries/campus/smartcampus/app/finance/invoice-precheck"
LOGIN_URL = "https://sandbox.jc2009.com/login"
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)
headless: 是否无头模式(生产必须为 False)
Returns:
(playwright, context, page) 三元组
Raises:
RuntimeError: 浏览器启动失败
"""
if not profile_dir:
raise RuntimeError("ERROR:MISSING_BROWSER 缺少浏览器配置目录")
# 确保有头模式(生产要求)
if headless:
logger.warning("生产环境强制使用有头模式")
headless = False
try:
# 启动持久化浏览器上下文
pw, context = await launch_persistent_browser(
user_data_dir=profile_dir,
headless=headless,
args=[
"--start-maximized",
"--disable-blink-features=AutomationControlled",
],
# 确保 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 浏览器,请先安装系统浏览器后重试。"
)
# 创建新页面
page = await context.new_page()
# 导航到目标页面
await page.goto(TARGET_URL, wait_until="domcontentloaded", timeout=60000)
logger.info("browser_started url=%s profile_dir=%s", page.url, profile_dir)
return pw, context, page
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)}")
@@ -70,22 +150,24 @@ async def start_browser_session(
async def close_browser_context(pw: Any, context: Any) -> None:
"""安全关闭浏览器上下文。"""
try:
if context:
if context is not None:
await context.close()
if pw:
except Exception:
pass
try:
if pw is not None:
await pw.stop()
except Exception as e:
logger.warning("browser_close_error error=%s", str(e))
except Exception:
pass
async def wait_for_page_ready(page: Any, timeout_sec: float = 30.0) -> bool:
"""等待页面加载完成。"""
async def _check_ready():
try:
# 检查页面是否加载完成
if await page.evaluate("document.readyState") == "complete":
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:
@@ -93,9 +175,16 @@ async def wait_for_page_ready(page: Any, timeout_sec: float = 30.0) -> bool:
return True
except Exception:
return False
result = await poll_until(_check_ready, timeout_sec=timeout_sec, interval_sec=0.5)
return result.ok
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: