chore: auto release commit (2026-06-17 09:21:19)
All checks were successful
技能自动化发布 / release (push) Successful in 6s

This commit is contained in:
2026-06-17 09:21:21 +08:00
parent b267ca3266
commit 79098dad5c
20 changed files with 472 additions and 359 deletions

View File

@@ -1,26 +0,0 @@
"""示例 adapter 四档 dispatch。
由 ``OPENCLAW_TEST_TARGET``(或 ``config.get``)决定档位。
复制本目录后把 ``example_adapter`` 改名为 ``<domain>_adapter``。
"""
from __future__ import annotations
import os
from .mock import MockAdapter
from .real_api import RealApiAdapter
from .real_rpa import RealRpaAdapter
from .sim_rpa import SimRpaAdapter
def get_adapter():
"""返回当前档位 adapter 实例。"""
target = (os.environ.get("OPENCLAW_TEST_TARGET") or "simulator_rpa").strip().lower()
if target in ("unit", "mock"):
return MockAdapter()
if target == "real_api":
return RealApiAdapter()
if target == "real_rpa":
return RealRpaAdapter()
return SimRpaAdapter()

View File

@@ -1,37 +0,0 @@
"""示例适配器数据契约与基类。
复制本目录为 ``<domain>_adapter/``,按 ADAPTER.md 实现各档位。
业务逻辑只依赖 ``base`` 里的接口,不感知 mock / sim_rpa / real_* 差异。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
@dataclass
class ExampleItem:
"""单条业务输入(示例)。"""
id: str
label: str
@dataclass
class ExampleResult:
"""批量操作结果(以批为单位返回)。"""
ok: bool
serial_no: Optional[str]
error_msg: Optional[str]
artifacts: Dict[str, Any] = field(default_factory=dict)
class AdapterBase:
"""四档 adapter 统一接口;子类实现 ``do_batch``。"""
name = "base"
def do_batch(self, target: str, items: List[ExampleItem]) -> ExampleResult:
raise NotImplementedError

View File

@@ -1,24 +0,0 @@
"""离线 mock 档位纯内存仿真CI / 单测必跑。"""
from __future__ import annotations
from .base import AdapterBase, ExampleItem, ExampleResult
class MockAdapter(AdapterBase):
name = "mock"
def do_batch(self, target: str, items: list[ExampleItem]) -> ExampleResult:
if not items:
return ExampleResult(
ok=False,
serial_no=None,
error_msg="empty batch",
artifacts={},
)
return ExampleResult(
ok=True,
serial_no=f"MOCK-{len(items)}",
error_msg=None,
artifacts={"mode": "mock", "target": target, "count": len(items)},
)

View File

@@ -1,14 +0,0 @@
"""真实 API 档位占位:有官方接口时在此实现。"""
from __future__ import annotations
from .base import AdapterBase, ExampleItem, ExampleResult
class RealApiAdapter(AdapterBase):
name = "real_api"
def do_batch(self, target: str, items: list[ExampleItem]) -> ExampleResult:
raise NotImplementedError(
"RealApiAdapter: 复制 example_adapter 后在此对接真实 API"
)

View File

@@ -1,14 +0,0 @@
"""真实 RPA 档位占位:无 API 时最后手段,谨慎实现。"""
from __future__ import annotations
from .base import AdapterBase, ExampleItem, ExampleResult
class RealRpaAdapter(AdapterBase):
name = "real_rpa"
def do_batch(self, target: str, items: list[ExampleItem]) -> ExampleResult:
raise NotImplementedError(
"RealRpaAdapter: 复制 example_adapter 后在此实现生产界面 RPA"
)

View File

@@ -1,108 +0,0 @@
"""仿真 RPA 档位:演示 RpaVideoSession + launch_persistent_browser需浏览器默认开发档"""
from __future__ import annotations
import asyncio
import os
from datetime import datetime
from jiangchang_skill_core import config
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.video_session import RpaVideoSession
from util.constants import SKILL_SLUG
from util.runtime_paths import get_skill_data_dir, get_skill_root
from .base import AdapterBase, ExampleItem, ExampleResult
class SimRpaAdapter(AdapterBase):
name = "sim_rpa"
def do_batch(self, target: str, items: list[ExampleItem]) -> ExampleResult:
return asyncio.run(self._do_batch_async(target, items))
async def _do_batch_async(self, target: str, items: list[ExampleItem]) -> ExampleResult:
example_path = os.path.join(get_skill_root(), ".env.example")
config.ensure_env_file(SKILL_SLUG, example_path)
url = config.get("TARGET_BASE_URL") or target or "https://example.com"
batch_id = f"sim_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
data_dir = get_skill_data_dir()
profile_dir = os.path.join(data_dir, "browser-profile")
os.makedirs(profile_dir, exist_ok=True)
try:
from playwright.async_api import async_playwright
except ImportError:
return ExampleResult(
ok=False,
serial_no=None,
error_msg="playwright not installed",
artifacts={},
)
artifact_paths: dict = {}
async with RpaVideoSession(
skill_slug=SKILL_SLUG,
skill_data_dir=data_dir,
batch_id=batch_id,
title="技能仿真演示",
) as video:
video.add_step("正在打开目标页面")
try:
async with async_playwright() as pw:
context = await launch_persistent_browser(
pw,
profile_dir=profile_dir,
headless=config.get_bool("OPENCLAW_BROWSER_HEADLESS"),
)
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)
search = page.locator('input[type="search"], input[name="q"]').first
if await search.count() > 0:
demo_text = items[0].label if items else "openclaw-demo"
video.step("正在填写演示内容")
await anti_detect.human_type(page, search, demo_text)
await anti_detect.human_delay_short()
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(
ok=True,
serial_no=f"SIM-{len(items)}",
error_msg=None,
artifacts=arts,
)

View File

@@ -1,88 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""浏览器 RPA 最小用法示例(复制到新 skill 后按需改)。
演示标准流程:
1. config.ensure_env_file 首次落盘 .env
2. launch_persistent_browser 启动 stealth persistent context
3. 拟人操作mouse_wiggle / human_type / human_click
4. 验证码 HITL 等待
5. 失败 capture_failure 截图
运行(需系统 Chrome/Edge + playwright 包):
python scripts/service/example_browser_rpa.py
"""
from __future__ import annotations
import asyncio
import os
import sys
_scripts_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _scripts_dir not in sys.path:
sys.path.insert(0, _scripts_dir)
from jiangchang_skill_core import config
from jiangchang_skill_core.rpa import (
anti_detect,
artifacts,
errors,
launch_persistent_browser,
wait_for_captcha_pass,
)
from util.constants import SKILL_SLUG
from util.runtime_paths import get_skill_data_dir, get_skill_root
async def demo() -> int:
example_path = os.path.join(get_skill_root(), ".env.example")
config.ensure_env_file(SKILL_SLUG, example_path)
url = config.get("TARGET_BASE_URL") or "https://example.com"
data_dir = get_skill_data_dir()
profile_dir = os.path.join(data_dir, "browser-profile")
os.makedirs(profile_dir, exist_ok=True)
batch_id = "demo"
try:
from playwright.async_api import async_playwright
except ImportError:
print("ERROR: playwright not installed")
return 1
async with async_playwright() as pw:
context = await launch_persistent_browser(
pw,
profile_dir=profile_dir,
headless=config.get_bool("OPENCLAW_BROWSER_HEADLESS"),
)
page = context.pages[0] if context.pages else await context.new_page()
try:
await page.goto(url, wait_until="domcontentloaded", timeout=60_000)
await anti_detect.human_mouse_wiggle(page)
search = page.locator('input[type="search"], input[name="q"]').first
if await search.count() > 0:
await anti_detect.human_type(page, search, "openclaw-rpa-demo")
await anti_detect.human_click(page, selector='input[type="search"]')
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")
print(errors.ERR_CAPTCHA_NEED_HUMAN, shot or "")
return 1
print("OK browser RPA demo finished")
return 0
except Exception as exc:
shot = await artifacts.capture_failure(page, data_dir, batch_id, "error")
print(f"ERROR: {exc}", shot or "")
return 1
finally:
await context.close()
if __name__ == "__main__":
raise SystemExit(asyncio.run(demo()))