chore: sync RpaVideoSession standard and default OPENCLAW_RECORD_VIDEO=1
All checks were successful
技能自动化发布 / release (push) Successful in 4s
All checks were successful
技能自动化发布 / release (push) Successful in 4s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,7 +12,7 @@ OPENCLAW_BROWSER_HEADLESS=0 # 0=有头(默认) 1=无头(CI)
|
|||||||
OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启反检测指纹(默认)
|
OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启反检测指纹(默认)
|
||||||
|
|
||||||
# ── 存证 ──
|
# ── 存证 ──
|
||||||
OPENCLAW_RECORD_VIDEO=0 # 1=全程录屏(合规场景建议开)
|
OPENCLAW_RECORD_VIDEO=1
|
||||||
OPENCLAW_ARTIFACTS_ON_FAILURE=1 # 1=失败截图(默认)
|
OPENCLAW_ARTIFACTS_ON_FAILURE=1 # 1=失败截图(默认)
|
||||||
|
|
||||||
# ── 节流 / 超时 ──
|
# ── 节流 / 超时 ──
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ OPENCLAW_BROWSER_HEADLESS=0 # 0=有头(默认) 1=无头(CI)
|
|||||||
OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启反检测指纹(默认)
|
OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启反检测指纹(默认)
|
||||||
|
|
||||||
# ── 存证 ──
|
# ── 存证 ──
|
||||||
OPENCLAW_RECORD_VIDEO=0 # 1=全程录屏(合规场景建议开)
|
OPENCLAW_RECORD_VIDEO=1 # 1=录屏+字幕+背景音+MP4成片(RPA技能默认开,见 RPA.md)
|
||||||
OPENCLAW_ARTIFACTS_ON_FAILURE=1 # 1=失败截图(默认)
|
OPENCLAW_ARTIFACTS_ON_FAILURE=1 # 1=失败截图(默认)
|
||||||
|
|
||||||
# ── 节流 / 超时 ──
|
# ── 节流 / 超时 ──
|
||||||
|
|||||||
@@ -159,7 +159,10 @@ skill 退出/抛错统一用 `ERROR:` 前缀 + 稳定码,方便宿主与上层
|
|||||||
|
|
||||||
- **路径**:`{CLAW_DATA_ROOT}/{user}/{slug}/rpa-artifacts/{batch_id}/{tag}_{timestamp}.png`
|
- **路径**:`{CLAW_DATA_ROOT}/{user}/{slug}/rpa-artifacts/{batch_id}/{tag}_{timestamp}.png`
|
||||||
- **失败必截图**:受 `OPENCLAW_ARTIFACTS_ON_FAILURE`(默认开)控制。
|
- **失败必截图**:受 `OPENCLAW_ARTIFACTS_ON_FAILURE`(默认开)控制。
|
||||||
- **全程录屏**:受 `OPENCLAW_RECORD_VIDEO`(默认关)控制;合规场景(如代发工资)建议开。浏览器用 Playwright `record_video_dir`。
|
- **运行录屏成片**:RPA 型技能默认 `OPENCLAW_RECORD_VIDEO=1`,表示录屏、中文字幕、背景音乐、最终 MP4 **一体开启**。由 service 核心中的 `RpaVideoSession` 统一触发(见 `jiangchang_skill_core.rpa.video_session`),不在 CLI/E2E 入口单独实现。
|
||||||
|
- **最终视频**:`{skill_data_dir}/videos/{skill_slug}_{yyyyMMdd_HHmmss}_{batch_id}.mp4`。
|
||||||
|
- **中间产物**:`{skill_data_dir}/rpa-artifacts/{batch_id}/`(raw-video、字幕、失败截图等),**不放最终 MP4**。
|
||||||
|
- Playwright 使用 `record_video_dir` 指向 `rpa-artifacts/{batch_id}/raw-video/`。
|
||||||
- **常见 tag**:`before_submit` / `after_submit` / `captcha` / `login_fail` / `error`。
|
- **常见 tag**:`before_submit` / `after_submit` / `captcha` / `login_fail` / `error`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ try:
|
|||||||
except ImportError:
|
except ImportError:
|
||||||
launch_persistent_browser = None # type: ignore[misc, assignment]
|
launch_persistent_browser = None # type: ignore[misc, assignment]
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .video_session import RpaVideoSession
|
||||||
|
except ImportError:
|
||||||
|
RpaVideoSession = None # type: ignore[misc, assignment]
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"anti_detect",
|
"anti_detect",
|
||||||
"artifacts",
|
"artifacts",
|
||||||
@@ -17,6 +22,7 @@ __all__ = [
|
|||||||
"human_login",
|
"human_login",
|
||||||
"stealth",
|
"stealth",
|
||||||
"launch_persistent_browser",
|
"launch_persistent_browser",
|
||||||
|
"RpaVideoSession",
|
||||||
# re-export common anti_detect helpers
|
# re-export common anti_detect helpers
|
||||||
"random_delay",
|
"random_delay",
|
||||||
"human_delay_short",
|
"human_delay_short",
|
||||||
|
|||||||
289
scripts/jiangchang_skill_core/rpa/video_session.py
Normal file
289
scripts/jiangchang_skill_core/rpa/video_session.py
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
"""RPA 运行录屏会话:Playwright raw video → 字幕 + 背景音乐 → 最终 MP4。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
from .. import config
|
||||||
|
|
||||||
|
# 与 screencast/composer.py 保持一致的字幕样式
|
||||||
|
_JIANGCHANG_SUBTITLE_STYLE = (
|
||||||
|
"FontName=Arial,FontSize=14,"
|
||||||
|
"PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000,"
|
||||||
|
"Outline=2,Shadow=1,Alignment=2"
|
||||||
|
)
|
||||||
|
|
||||||
|
_DEFAULT_MUSIC_ROOT = os.environ.get(
|
||||||
|
"MEDIA_ASSETS_ROOT",
|
||||||
|
r"D:\OpenClaw\client-commons\media-assets",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _record_video_enabled() -> bool:
|
||||||
|
return config.get_bool("OPENCLAW_RECORD_VIDEO", default=False)
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_music_file() -> Optional[Path]:
|
||||||
|
music_dir = Path(_DEFAULT_MUSIC_ROOT) / "music"
|
||||||
|
if not music_dir.is_dir():
|
||||||
|
return None
|
||||||
|
files = list(music_dir.rglob("*.mp3"))
|
||||||
|
if not files:
|
||||||
|
return None
|
||||||
|
return random.choice(files)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _StepEntry:
|
||||||
|
start: float
|
||||||
|
text: str
|
||||||
|
duration: float = 4.0
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_srt(entries: List[_StepEntry], output_path: str) -> None:
|
||||||
|
"""根据步骤时间线生成 SRT(防重叠)。"""
|
||||||
|
MIN_DURATION = 1.0
|
||||||
|
GAP = 0.1
|
||||||
|
steps = sorted(entries, key=lambda e: e.start)
|
||||||
|
if not steps:
|
||||||
|
steps = [_StepEntry(0.0, "任务执行中", 4.0)]
|
||||||
|
|
||||||
|
for i in range(len(steps) - 1):
|
||||||
|
cur, nxt = steps[i], steps[i + 1]
|
||||||
|
max_end = nxt.start - GAP
|
||||||
|
new_dur = max_end - cur.start
|
||||||
|
if new_dur < MIN_DURATION:
|
||||||
|
cur.duration = MIN_DURATION
|
||||||
|
nxt.start = cur.start + MIN_DURATION + GAP
|
||||||
|
else:
|
||||||
|
cur.duration = min(cur.duration, new_dur)
|
||||||
|
|
||||||
|
def _fmt(s: float) -> str:
|
||||||
|
h = int(s // 3600)
|
||||||
|
m = int((s % 3600) // 60)
|
||||||
|
sec = int(s % 60)
|
||||||
|
ms = int((s % 1) * 1000)
|
||||||
|
return f"{h:02d}:{m:02d}:{sec:02d},{ms:03d}"
|
||||||
|
|
||||||
|
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(output_path, "w", encoding="utf-8") as f:
|
||||||
|
for i, e in enumerate(steps, 1):
|
||||||
|
f.write(f"{i}\n")
|
||||||
|
f.write(f"{_fmt(e.start)} --> {_fmt(e.start + e.duration)}\n")
|
||||||
|
f.write(f"{e.text}\n\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _escape_srt_path(path: str) -> str:
|
||||||
|
return path.replace("\\", "/").replace(":", "\\:")
|
||||||
|
|
||||||
|
|
||||||
|
def _find_raw_webm(raw_dir: str) -> Optional[Path]:
|
||||||
|
root = Path(raw_dir)
|
||||||
|
if not root.is_dir():
|
||||||
|
return None
|
||||||
|
webms = list(root.rglob("*.webm"))
|
||||||
|
if not webms:
|
||||||
|
return None
|
||||||
|
return max(webms, key=lambda p: p.stat().st_size)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_ffmpeg(cmd: List[str]) -> tuple[bool, str]:
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
errors="replace",
|
||||||
|
)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False, "ffmpeg not found"
|
||||||
|
if result.returncode != 0:
|
||||||
|
err = (result.stderr or result.stdout or "").strip()
|
||||||
|
return False, err[:500] if err else f"exit {result.returncode}"
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
|
def _compose_webm_mp4(
|
||||||
|
webm_path: Path,
|
||||||
|
srt_path: Path,
|
||||||
|
output_path: Path,
|
||||||
|
*,
|
||||||
|
music_file: Optional[Path] = None,
|
||||||
|
music_volume: float = 0.15,
|
||||||
|
) -> tuple[bool, str]:
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
srt_esc = _escape_srt_path(str(srt_path.resolve()))
|
||||||
|
subtitle_filter = (
|
||||||
|
f"subtitles='{srt_esc}'"
|
||||||
|
f":force_style='{_JIANGCHANG_SUBTITLE_STYLE}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
if music_file and music_file.is_file():
|
||||||
|
filter_complex = (
|
||||||
|
f"[0:v]{subtitle_filter}[vout];"
|
||||||
|
f"[1:a]volume={music_volume},apad[aout]"
|
||||||
|
)
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg", "-y",
|
||||||
|
"-i", str(webm_path),
|
||||||
|
"-i", str(music_file),
|
||||||
|
"-filter_complex", filter_complex,
|
||||||
|
"-map", "[vout]",
|
||||||
|
"-map", "[aout]",
|
||||||
|
"-c:v", "libx264",
|
||||||
|
"-preset", "fast",
|
||||||
|
"-crf", "23",
|
||||||
|
"-c:a", "aac",
|
||||||
|
"-b:a", "128k",
|
||||||
|
"-shortest",
|
||||||
|
"-pix_fmt", "yuv420p",
|
||||||
|
str(output_path),
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg", "-y",
|
||||||
|
"-i", str(webm_path),
|
||||||
|
"-vf", subtitle_filter,
|
||||||
|
"-c:v", "libx264",
|
||||||
|
"-preset", "fast",
|
||||||
|
"-crf", "23",
|
||||||
|
"-c:a", "aac",
|
||||||
|
"-b:a", "128k",
|
||||||
|
"-pix_fmt", "yuv420p",
|
||||||
|
str(output_path),
|
||||||
|
]
|
||||||
|
return _run_ffmpeg(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
class RpaVideoSession:
|
||||||
|
"""Service/RPA 层统一录屏会话;OPENCLAW_RECORD_VIDEO=1 时启用完整成片流程。"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
skill_slug: str,
|
||||||
|
skill_data_dir: str,
|
||||||
|
batch_id: str,
|
||||||
|
title: str = "",
|
||||||
|
) -> None:
|
||||||
|
self.skill_slug = skill_slug
|
||||||
|
self.skill_data_dir = os.path.abspath(skill_data_dir)
|
||||||
|
self.batch_id = batch_id
|
||||||
|
self.title = title
|
||||||
|
self.enabled = _record_video_enabled()
|
||||||
|
self.warnings: List[str] = []
|
||||||
|
self.output_video_path: Optional[str] = None
|
||||||
|
self.record_video_dir: Optional[str] = None
|
||||||
|
self.artifacts: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
safe_batch = "".join(c if c.isalnum() or c in "-_" else "_" for c in batch_id)
|
||||||
|
self._video_stem = f"{skill_slug}_{ts}_{safe_batch}"
|
||||||
|
|
||||||
|
self._videos_dir = os.path.join(self.skill_data_dir, "videos")
|
||||||
|
self._artifact_root = os.path.join(
|
||||||
|
self.skill_data_dir, "rpa-artifacts", batch_id
|
||||||
|
)
|
||||||
|
self._raw_video_dir = os.path.join(self._artifact_root, "raw-video")
|
||||||
|
self._subtitles_dir = os.path.join(self._artifact_root, "subtitles")
|
||||||
|
self._logs_dir = os.path.join(self._artifact_root, "logs")
|
||||||
|
self._srt_path = os.path.join(self._subtitles_dir, f"{self._video_stem}.srt")
|
||||||
|
self._planned_output = os.path.join(self._videos_dir, f"{self._video_stem}.mp4")
|
||||||
|
|
||||||
|
self._steps: List[_StepEntry] = []
|
||||||
|
self._t0: Optional[float] = None
|
||||||
|
|
||||||
|
if self.enabled:
|
||||||
|
os.makedirs(self._videos_dir, exist_ok=True)
|
||||||
|
os.makedirs(self._raw_video_dir, exist_ok=True)
|
||||||
|
os.makedirs(self._subtitles_dir, exist_ok=True)
|
||||||
|
os.makedirs(self._logs_dir, exist_ok=True)
|
||||||
|
self.record_video_dir = self._raw_video_dir
|
||||||
|
|
||||||
|
def step(self, text: str, *, duration: float = 4.0) -> None:
|
||||||
|
"""记录一步骤字幕(中文说明)。"""
|
||||||
|
if not self.enabled:
|
||||||
|
return
|
||||||
|
if self._t0 is None:
|
||||||
|
self._t0 = time.monotonic()
|
||||||
|
elapsed = time.monotonic() - self._t0
|
||||||
|
self._steps.append(_StepEntry(start=elapsed, text=text.strip(), duration=duration))
|
||||||
|
|
||||||
|
async def __aenter__(self) -> "RpaVideoSession":
|
||||||
|
if self.enabled:
|
||||||
|
self._t0 = time.monotonic()
|
||||||
|
if self.title:
|
||||||
|
self.step(self.title, duration=3.0)
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb) -> None:
|
||||||
|
if self.enabled:
|
||||||
|
await self.finalize()
|
||||||
|
|
||||||
|
async def finalize(self) -> None:
|
||||||
|
"""在 Playwright context 已 close 后合成最终 MP4;失败只记 warning。"""
|
||||||
|
if not self.enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self._steps and self.title:
|
||||||
|
self.step(self.title)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
webm = _find_raw_webm(self._raw_video_dir)
|
||||||
|
if webm is None:
|
||||||
|
self.warnings.append("no_playwright_raw_video")
|
||||||
|
self.artifacts = self.to_artifact_dict()
|
||||||
|
return
|
||||||
|
|
||||||
|
entries = list(self._steps)
|
||||||
|
if entries:
|
||||||
|
entries[-1].duration = max(entries[-1].duration, 5.0)
|
||||||
|
_generate_srt(entries, self._srt_path)
|
||||||
|
|
||||||
|
music = _resolve_music_file()
|
||||||
|
if music is None:
|
||||||
|
self.warnings.append("background_music_missing")
|
||||||
|
|
||||||
|
ok, err = _compose_webm_mp4(
|
||||||
|
webm,
|
||||||
|
Path(self._srt_path),
|
||||||
|
Path(self._planned_output),
|
||||||
|
music_file=music,
|
||||||
|
)
|
||||||
|
if not ok and music is not None:
|
||||||
|
self.warnings.append(f"ffmpeg_with_music_failed: {err}")
|
||||||
|
ok, err = _compose_webm_mp4(
|
||||||
|
webm,
|
||||||
|
Path(self._srt_path),
|
||||||
|
Path(self._planned_output),
|
||||||
|
music_file=None,
|
||||||
|
)
|
||||||
|
if not ok:
|
||||||
|
self.warnings.append(f"ffmpeg_compose_failed: {err}")
|
||||||
|
self.artifacts = self.to_artifact_dict()
|
||||||
|
return
|
||||||
|
|
||||||
|
self.output_video_path = str(Path(self._planned_output).resolve())
|
||||||
|
self.artifacts = self.to_artifact_dict()
|
||||||
|
except Exception as exc:
|
||||||
|
self.warnings.append(f"video_finalize_error: {exc}")
|
||||||
|
self.artifacts = self.to_artifact_dict()
|
||||||
|
|
||||||
|
def to_artifact_dict(self) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"enabled": self.enabled,
|
||||||
|
"path": self.output_video_path,
|
||||||
|
"warnings": list(self.warnings),
|
||||||
|
"batch_id": self.batch_id,
|
||||||
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
"""仿真 RPA 档位:演示共享库 browser / anti_detect 用法(需浏览器,默认开发档)。"""
|
"""仿真 RPA 档位:演示 RpaVideoSession + launch_persistent_browser(需浏览器,默认开发档)。"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from jiangchang_skill_core import config
|
from jiangchang_skill_core import config
|
||||||
from jiangchang_skill_core.rpa import anti_detect, artifacts, errors, launch_persistent_browser
|
from jiangchang_skill_core.rpa import anti_detect, artifacts, errors, launch_persistent_browser
|
||||||
from jiangchang_skill_core.rpa.human_login import wait_for_captcha_pass
|
from jiangchang_skill_core.rpa.human_login import wait_for_captcha_pass
|
||||||
|
from jiangchang_skill_core.rpa.video_session import RpaVideoSession
|
||||||
|
|
||||||
from util.constants import SKILL_SLUG
|
from util.constants import SKILL_SLUG
|
||||||
from util.runtime_paths import get_skill_data_dir, get_skill_root
|
from util.runtime_paths import get_skill_data_dir, get_skill_root
|
||||||
@@ -26,16 +28,11 @@ class SimRpaAdapter(AdapterBase):
|
|||||||
config.ensure_env_file(SKILL_SLUG, example_path)
|
config.ensure_env_file(SKILL_SLUG, example_path)
|
||||||
|
|
||||||
url = config.get("TARGET_BASE_URL") or target or "https://example.com"
|
url = config.get("TARGET_BASE_URL") or target or "https://example.com"
|
||||||
batch_id = "sim-demo"
|
batch_id = f"sim_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||||||
data_dir = get_skill_data_dir()
|
data_dir = get_skill_data_dir()
|
||||||
profile_dir = os.path.join(data_dir, "browser-profile")
|
profile_dir = os.path.join(data_dir, "browser-profile")
|
||||||
os.makedirs(profile_dir, exist_ok=True)
|
os.makedirs(profile_dir, exist_ok=True)
|
||||||
|
|
||||||
record_video = config.get_bool("OPENCLAW_RECORD_VIDEO")
|
|
||||||
video_dir = os.path.join(data_dir, "rpa-artifacts", batch_id, "video") if record_video else None
|
|
||||||
if video_dir:
|
|
||||||
os.makedirs(video_dir, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from playwright.async_api import async_playwright
|
from playwright.async_api import async_playwright
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -47,51 +44,66 @@ class SimRpaAdapter(AdapterBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
artifact_paths: dict = {}
|
artifact_paths: dict = {}
|
||||||
try:
|
async with RpaVideoSession(
|
||||||
async with async_playwright() as pw:
|
skill_slug=SKILL_SLUG,
|
||||||
context = await launch_persistent_browser(
|
skill_data_dir=data_dir,
|
||||||
pw,
|
batch_id=batch_id,
|
||||||
profile_dir=profile_dir,
|
title="技能仿真演示",
|
||||||
headless=config.get_bool("OPENCLAW_BROWSER_HEADLESS"),
|
) as video:
|
||||||
record_video_dir=video_dir,
|
video.step("正在打开目标页面")
|
||||||
)
|
try:
|
||||||
page = context.pages[0] if context.pages else await context.new_page()
|
async with async_playwright() as pw:
|
||||||
await page.goto(url, wait_until="domcontentloaded", timeout=60_000)
|
context = await launch_persistent_browser(
|
||||||
await anti_detect.human_mouse_wiggle(page)
|
pw,
|
||||||
|
profile_dir=profile_dir,
|
||||||
# 演示拟人输入:若页面有 search 输入框则尝试输入
|
headless=config.get_bool("OPENCLAW_BROWSER_HEADLESS"),
|
||||||
search = page.locator('input[type="search"], input[name="q"]').first
|
record_video_dir=video.record_video_dir,
|
||||||
if await search.count() > 0:
|
|
||||||
demo_text = items[0].label if items else "openclaw-demo"
|
|
||||||
await anti_detect.human_type(page, search, demo_text)
|
|
||||||
await anti_detect.human_delay_short()
|
|
||||||
|
|
||||||
if not await wait_for_captcha_pass(
|
|
||||||
page,
|
|
||||||
timeout_sec=config.get_int("HUMAN_WAIT_TIMEOUT", 180),
|
|
||||||
):
|
|
||||||
shot = await artifacts.capture_failure(page, data_dir, batch_id, "captcha")
|
|
||||||
if shot:
|
|
||||||
artifact_paths["captcha"] = shot
|
|
||||||
return ExampleResult(
|
|
||||||
ok=False,
|
|
||||||
serial_no=None,
|
|
||||||
error_msg=errors.ERR_CAPTCHA_NEED_HUMAN,
|
|
||||||
artifacts=artifact_paths,
|
|
||||||
)
|
)
|
||||||
|
page = context.pages[0] if context.pages else await context.new_page()
|
||||||
|
await page.goto(url, wait_until="domcontentloaded", timeout=60_000)
|
||||||
|
await anti_detect.human_mouse_wiggle(page)
|
||||||
|
|
||||||
await context.close()
|
search = page.locator('input[type="search"], input[name="q"]').first
|
||||||
except Exception as exc:
|
if await search.count() > 0:
|
||||||
return ExampleResult(
|
demo_text = items[0].label if items else "openclaw-demo"
|
||||||
ok=False,
|
video.step("正在填写演示内容")
|
||||||
serial_no=None,
|
await anti_detect.human_type(page, search, demo_text)
|
||||||
error_msg=str(exc),
|
await anti_detect.human_delay_short()
|
||||||
artifacts=artifact_paths,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
video.step("正在等待人工验证(如有)")
|
||||||
|
if not await wait_for_captcha_pass(
|
||||||
|
page,
|
||||||
|
timeout_sec=config.get_int("HUMAN_WAIT_TIMEOUT", 180),
|
||||||
|
):
|
||||||
|
shot = await artifacts.capture_failure(
|
||||||
|
page, data_dir, batch_id, "captcha"
|
||||||
|
)
|
||||||
|
if shot:
|
||||||
|
artifact_paths["captcha"] = shot
|
||||||
|
return ExampleResult(
|
||||||
|
ok=False,
|
||||||
|
serial_no=None,
|
||||||
|
error_msg=errors.ERR_CAPTCHA_NEED_HUMAN,
|
||||||
|
artifacts={**artifact_paths, **video.to_artifact_dict()},
|
||||||
|
)
|
||||||
|
|
||||||
|
video.step("演示任务完成")
|
||||||
|
await context.close()
|
||||||
|
except Exception as exc:
|
||||||
|
video.step("演示异常结束")
|
||||||
|
return ExampleResult(
|
||||||
|
ok=False,
|
||||||
|
serial_no=None,
|
||||||
|
error_msg=str(exc),
|
||||||
|
artifacts={**artifact_paths, **video.to_artifact_dict()},
|
||||||
|
)
|
||||||
|
|
||||||
|
arts = {"mode": "sim_rpa", "url": url, **artifact_paths, **video.to_artifact_dict()}
|
||||||
|
if video.output_video_path:
|
||||||
|
arts["video_path"] = video.output_video_path
|
||||||
return ExampleResult(
|
return ExampleResult(
|
||||||
ok=True,
|
ok=True,
|
||||||
serial_no=f"SIM-{len(items)}",
|
serial_no=f"SIM-{len(items)}",
|
||||||
error_msg=None,
|
error_msg=None,
|
||||||
artifacts={"mode": "sim_rpa", "url": url, **artifact_paths},
|
artifacts=arts,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user