docs: add RPA_HELPERS.md for same-process integration guide

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-11 18:34:56 +08:00
parent dadf89af3d
commit 6fc4f1af1d

218
references/RPA_HELPERS.md Normal file
View File

@@ -0,0 +1,218 @@
# rpa_helpers 库 — 业务 skill 同进程登录
`scripts/service/rpa_helpers/` 是 account-manager v2 提供给业务 skill 的 **同进程 Python 库**。业务代码调用 **`ensure_logged_in`**,库按账号记录的 **`auth_strategy`** 分发到对应登录流程(底层基于 Playwright
## 与 CLI 的关系
| 路径 | 职责 |
|---|---|
| **CLI subprocess** | 获取结构化账号 JSON、租约、`get-credential --reveal` 明文密码;进程隔离、易于跨语言 |
| **rpa_helpers** | 在已有 `BrowserContext`/`Page` 上执行登录编排与人类化输入 |
推荐组合:**CLI → pick / lease → get-credential → `launch_browser_with_profile` + `ensure_logged_in`**。
## 集成方式
### Import 路径
```python
import sys
AM_ROOT = r"D:\OpenClaw\client-commons\account-manager"
sys.path.insert(0, f"{AM_ROOT}/scripts")
from service.rpa_helpers import (
AuthStrategyError,
LoginResult,
close_browser,
ensure_logged_in,
launch_browser_with_profile,
)
```
**务必**`sys.path` 追加的是 **`…/account-manager/scripts`**,因此模块命名空间为 `service.rpa_helpers`**不要**写成 `from scripts.service.rpa_helpers …`)。
### 运行时依赖
- Python **3.10+**
- 已安装 **`playwright`** Python 包(宿主 runtime 提供)
- 本机已安装 **Chrome 或 Edge**(启动时使用 channel 模式,优先 Chrome失败回落 Edge——见 `service.browser_service.resolve_chromium_channel`
文档不提供 `pip install …` 指令;版本由匠厂宿主统一管理。
---
## 公开 API 一览
下列符号由 `service/rpa_helpers/__init__.py` **导出(共 19 个名字)**
| 分类 | 符号 |
|---|---|
| **dispatcher + 策略函数** | `ensure_logged_in`,以及 `ensure_logged_in_password_auto``ensure_logged_in_password_with_captcha``ensure_logged_in_password_plus_2fa``ensure_logged_in_qr_code_manual``ensure_logged_in_per_session_manual``ensure_logged_in_device_bound``ensure_logged_in_client_certificate``ensure_logged_in_api_token``ensure_logged_in_sso_redirect` |
| **结果 / 异常** | `LoginResult``AuthStrategyError` |
| **浏览器** | `launch_browser_with_profile``close_browser` |
| **人类化输入** | `pause``human_type``human_click``human_select``human_scroll_to` |
---
## `browser` 模块
### `launch_browser_with_profile(profile_dir, *, channel=None, headless=False, extra_args=None, locale="zh-CN") -> tuple[BrowserContext, Page]`
- 创建持久化上下文(`user_data_dir=profile_dir`),注入可选 stealth 脚本。
- `channel` 默认为宿主检测结果Chrome 优先)。
- **必须在 `finally` 中调用 `close_browser(context)`**,否则 playwright driver 可能泄漏。
### `close_browser(ctx) -> None`
关闭上下文并停止内部的 Playwright session manager。
---
## `human` 模块(人类化操作)
| 函数 | 行为 |
|---|---|
| `pause(min_ms=200, max_ms=800)` | 随机等待 |
| `human_type(locator, text, *, per_char_ms_range=(50, 200), pre_pause=True)` | 滚动进入视图 → 停顿 → `click` → 逐字符 `type` |
| `human_click(locator)` | 滚动 → 停顿 → `click` |
| `human_select(locator, value_or_label)` | 滚动 → 停顿 → `select_option`(依次尝试 value / label |
| `human_scroll_to(locator)` | 平滑滚动到元素 |
_locator_ 均为 Playwright `Locator`
---
## `auth_flows` 模块
### `ensure_logged_in(page, account, *, credentials=None, **kwargs) -> LoginResult`
1. 读取 `account["auth_strategy"]`。为空字符串 → 抛出 `AuthStrategyError(ERR_AUTH_STRATEGY_NOT_SUPPORTED)`
2. 若策略需要密码则要求 `credentials["plaintext"]`;缺失 → `ERR_CREDENTIAL_MISSING`
3. 分派到对应 `ensure_logged_in_*`
4. 未知策略 → `AuthStrategyError(ERR_AUTH_STRATEGY_NOT_SUPPORTED)`
附加 kwargs 会透传给具体策略(如 `captcha_value``wait_human_sec`)。
### `LoginResult`
| 字段 | 类型 | 说明 |
|---|---|---|
| `ok` | `bool` | 流程是否达到判定条件 |
| `needs_human` | `bool` | 是否需要人工协助(扫码 / OTP / SSO 等) |
| `message` | `str` | 描述信息(便于日志) |
### `AuthStrategyError`
属性:`code: str``message: str`。错误码与 CLI / [`ERRORS.md`](ERRORS.md) 对齐。
### 九个 `ensure_logged_in_*` 入口
业务代码通常只调用 `ensure_logged_in`。如需针对性 reuse可直接 import `auth_flows` 中的同名函数;签名见源码 `scripts/service/rpa_helpers/auth_flows.py`
---
## 已登录检测(`_is_already_logged_in`
核心逻辑:
1. 读取当前 `page.url`;空白页 → **False**
2. 仅在账号 `extra_json`(可为 dict 或 JSON 字符串)中出现以下 hook 之一才可能返回 True
- **`logged_in_url_pattern`**:正则,匹配当前 URL。
- **`logged_in_selector`**CSS/XPath selector`locator.first.is_visible(timeout=500)`
3. 若无任一 hook → **默认 False****除非已有持久化 session**,否则会完整跑登录流程。
### 接入建议
1. 首次上线:`auth_strategy=qr_code_manual` + 无 hook → 用户扫码登录。
2. 登录成功后记录典型 URL 片段或 DOM selector。
3. 写入账号 `extra_json`,例如:
```python
{
"logged_in_url_pattern": r"/user/",
"logged_in_selector": "div.avatar-wrapper"
}
```
4. 下次启动 → `_is_already_logged_in` 命中 → `needs_human` 可能降低。
### `per_session_manual` 的差异
该策略 **开场不会调用 `_is_already_logged_in`**,总是导航到业务 URL 并等待人工完成登录(适合强制会话)。
---
## 设备绑定策略(`device_bound`
如账号设置了 `device_fingerprint`,库会使用 `_current_machine_fingerprint()`hostname + MAC 派生)比对;不一致抛出 **`ERR_DEVICE_FINGERPRINT_MISMATCH`**。
---
## 客户端证书策略(`client_certificate`
直接抛出 **`ERR_AUTH_STRATEGY_NOT_AUTOMATABLE`**;请在宿主机手动装载证书后使用其它路径衔接。
---
## 完整示例CLI + rpa_helpers
```python
import json
import subprocess
import sys
AM = r"D:\OpenClaw\client-commons\account-manager"
PY = sys.executable
MAIN = f"{AM}/scripts/main.py"
sys.path.insert(0, f"{AM}/scripts")
from service.rpa_helpers import launch_browser_with_profile, close_browser, ensure_logged_in
def am_json(argv: list[str]) -> dict:
proc = subprocess.run([PY, MAIN, *argv], capture_output=True, text=True, check=False)
line = (proc.stdout or "").splitlines()[0]
return json.loads(line)
acc = am_json(
["account", "pick-web", "--platform", "icbc_sim", "--lease", "--holder", "monitor-rates"]
)
cred = am_json(
[
"account",
"get-credential",
str(acc["id"]),
"--reveal",
"--lease-token",
acc["lease_token"],
]
)
plaintext = cred.get("plaintext")
if plaintext in (None, ""):
raise RuntimeError("no plaintext returned; check secret_storage / lease")
ctx, page = launch_browser_with_profile(acc["profile_dir"])
try:
result = ensure_logged_in(page, acc, credentials={"plaintext": plaintext})
if not result.ok:
raise RuntimeError(result.message)
# …业务代码…
finally:
close_browser(ctx)
subprocess.run([PY, MAIN, "lease", "release", acc["lease_token"]], check=False)
```
---
## 测试建议
- account-manager 仓库内置 **107** 个 pytest截至 v2.0.0 文档修订时),覆盖 CLI / schema / rpa_helpers mock 路径。
- 业务 skill
- **单元测试**patch `ensure_logged_in` → 返回 `LoginResult(ok=True, …)`
- **端到端**:在 Windows + 真浏览器环境标记 `@pytest.mark.integration`,禁止在 CI 泄露明文密码。
更多 CLI 细节参见 [`CLI.md`](CLI.md)、[`INTEGRATION.md`](INTEGRATION.md)。