feat: add three-surface RPA locator standards and examples
All checks were successful
技能自动化发布 / release (push) Successful in 5s
All checks were successful
技能自动化发布 / release (push) Successful in 5s
This commit is contained in:
@@ -9,6 +9,13 @@
|
||||
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
|
||||
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
|
||||
|
||||
## 1.0.59
|
||||
|
||||
- 新增浏览器、Windows 桌面、Android/iOS 三端元素拾取与定位标准,要求先真实拾取、填写 element map、验证唯一性和业务后置状态
|
||||
- 从已跑通的企业微信添加客户技能提炼真实 Windows UIA example,提供窗口唯一性、作用域定位、probe 和 dry-run/assisted 范式
|
||||
- 手机 RPA 增加设备、Appium Inspector、Native/WebView、控件树和失败留证标准;在成功案例补齐前明确标为规格骨架
|
||||
- 需求模板和交活 Checklist 增加禁止 AI 猜 selector、禁止 first/index 掩盖歧义、结构变化停止留证等门禁
|
||||
|
||||
## 1.0.58
|
||||
|
||||
- 新增 account-manager 接入标准,说明一账号一 Profile、多账号矩阵、登录状态持久化与完整运行生命周期
|
||||
|
||||
4
SKILL.md
4
SKILL.md
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: 技能开发模板(通用业务版)
|
||||
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
|
||||
version: 1.0.58
|
||||
version: 1.0.59
|
||||
author: 深圳匠厂科技有限公司
|
||||
metadata:
|
||||
openclaw:
|
||||
@@ -27,7 +27,7 @@ allowed-tools:
|
||||
- **Agent 是入口与编排者**:理解用户意图、补齐参数、判断风险、选择 Action,并用业务语言解释结果。
|
||||
- **RPA 是确定性执行器**:只按已验证步骤操作授权范围内的浏览器、桌面软件或手机;不得破解、逆向、调用未授权私有协议或绕过验证码、安全验证和平台限制。
|
||||
- 遇到验证码、人脸、U盾、频控、账号风险或权限不足时,转人工或安全停止,禁止自动硬闯、换号或无限重试。
|
||||
- 开发者与 AI 编程工具先读 `development/AI_RPA.md`;浏览器账号、Profile、多账号和租约再读 `development/ACCOUNT_MANAGER.md`;三端执行细则读 `development/RPA.md`。
|
||||
- 开发者与 AI 编程工具先读 `development/AI_RPA.md`;三端界面代码必须先读 `development/RPA_ELEMENT_LOCATING.md` 并完成真实拾取与 element map,再按浏览器/桌面/手机选读 `RPA_BROWSER.md` / `RPA_DESKTOP.md` / `RPA_MOBILE.md`;浏览器账号、Profile、多账号和租约另读 `development/ACCOUNT_MANAGER.md`。
|
||||
|
||||
### 架构铁律(先读)
|
||||
|
||||
|
||||
@@ -147,6 +147,12 @@ checklist_version: 1.0
|
||||
- [ ] A8-8 **P1** 只对明确可重试错误做有限重试与递增退避;风险、封禁、权限和结果不明确时不盲目重试
|
||||
- [ ] A8-9 **P1** 流程结束先核验业务结果并完成日志 / 截图 / 可选视频,再保留合理结束缓冲后关闭会话
|
||||
- [ ] A8-10 **P1** Agent 能区分成功、部分成功、失败、等待人工、等待确认和主动停止,并用业务语言解释结果
|
||||
- [ ] A8-11 **P0** 关键元素均有真实拾取证据和 element map;没有真实 DOM/UIA/XML 时未由 AI 猜 selector
|
||||
- [ ] A8-12 **P0** 不可逆动作定位器在目标作用域内唯一;没有用 `.first` / `hwnds[0]` / `instance=0` 掩盖歧义
|
||||
- [ ] A8-13 **P1** 页面、窗口、弹窗、iframe、Native/WebView 等作用域明确;列表先定位业务行再定位动作元素
|
||||
- [ ] A8-14 **P1** 重启浏览器/软件/App 后关键定位复测通过;动态列表顺序变化不会误点
|
||||
- [ ] A8-15 **P1** 定位失败保存截图和 DOM/UIA/XML、候选数量、作用域及目标版本;结构变化时停止而非在线猜新定位器
|
||||
- [ ] A8-16 **P1** 所有 fallback 均经过人工验证;图像/OCR/坐标仅在控件树确认不可用后使用
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -134,6 +134,8 @@ scripts/
|
||||
若 skill 需要浏览器/桌面/手机自动化,按以下顺序落地:
|
||||
|
||||
1. **先读总纲与四份标准**:`AI_RPA.md`(职责、合规、风控与完整闭环)、`ACCOUNT_MANAGER.md`(账号 / Profile / 租约 / 多账号 / 查看排障)、`RPA.md`(含 §0.2:Profile 强制 / 有头 / 登录分平台)、`CONFIG.md`(`.env` 落盘与读取)、`ADAPTER.md`(四档 adapter)。
|
||||
- 写任何界面操作代码前再读 `RPA_ELEMENT_LOCATING.md`,先完成真实拾取、element map、唯一性和后置状态。
|
||||
- 浏览器读 `RPA_BROWSER.md`;Windows 桌面读 `RPA_DESKTOP.md`;Android/iOS 读 `RPA_MOBILE.md`。
|
||||
2. **从 examples 选择性复制**(不要整包照搬 `examples/<mode>/`,见各 example README 的 copy map):
|
||||
- 若是 **真实浏览器 RPA**(真实网站、登录态、验证码、滚动采集),**必须先读** `examples/real_browser_rpa/README.md`,再按需复制到 `scripts/service/`:
|
||||
- `examples/real_browser_rpa/scripts/service/browser_session.py`
|
||||
@@ -156,7 +158,8 @@ scripts/
|
||||
```
|
||||
上述 import 来自宿主共享 runtime 安装的 `jiangchang-platform-kit`,不是技能目录副本。
|
||||
4. **单测/CI 须 mock 离线可过**;业务 `.env` 默认 `real_rpa`,须真实跑通。**不得只 mock 通就交活**;须 release 到匠厂宿主验收(见 §15,含用户体验)。多账号或矩阵能力还须按 `ACCOUNT_MANAGER.md` 覆盖至少两个账号、租约冲突、关闭重开保持登录和异常释放,不能以单账号一两次成功代替规模验证。
|
||||
5. **桌面/手机**:本期标准见 RPA.md 第 2/3 节,复用 `jiangchang_desktop_sdk` / `screencast`,**不要在新 skill 里重复造包**(尚待实战验证)。
|
||||
5. **Windows 桌面**:技术路线已由 `add-friend-wecom` 验证。先读 `examples/real_desktop_rpa/README.md`,复用窗口唯一性、UIA作用域、probe、dry-run/assisted 和失败留证范式;目标软件 selector 必须重新拾取,禁止照抄企业微信控件名。
|
||||
6. **手机**:先按 `RPA_MOBILE.md` 完成设备、context、element map 和验收设计。当前 `examples/mobile_rpa/` 只是完成规格,尚无真实成功案例;不得把骨架宣传为已跑通实现。
|
||||
|
||||
**技术路线红线:**正常界面或获得授权的官方 API 无法完成闭环时,停止并向负责人说明;禁止 AI 或开发者擅自改用破解、反编译、逆向、私有协议、Hook / 注入、验证码破解、设备指纹伪造、自动换号或无限重试。
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
| 步骤 | 做什么 | 详见 |
|
||||
|------|--------|------|
|
||||
| 1 | **拿到仓库**:① 项目经理在 [自建 Gitea](https://git.jc2009.com/)(`git.jc2009.com`,非 Gitea 官网)开仓并授权后 `git clone`;或 ② 本地已有 `skill-template` 时用 scaffold / 复制出新目录(遵守 Git 红线) | [`DEVELOPMENT.md`](DEVELOPMENT.md) §1、§4 |
|
||||
| 2 | **先读 AI + RPA 总纲**;浏览器技能再读账号 / Profile / 租约标准,理解真实多账号场景 | [`AI_RPA.md`](AI_RPA.md)、[`ACCOUNT_MANAGER.md`](ACCOUNT_MANAGER.md) |
|
||||
| 2 | **先读 AI + RPA 总纲**;界面技能先完成元素拾取/element map,再按浏览器、桌面或手机读分端手册 | [`AI_RPA.md`](AI_RPA.md)、[`RPA_ELEMENT_LOCATING.md`](RPA_ELEMENT_LOCATING.md) |
|
||||
| 3 | 按 [`NAMING.md`](NAMING.md) 确认 slug;**先填** [`REQUIREMENTS.md`](REQUIREMENTS.md),再写业务代码 | [`REQUIREMENTS.md`](REQUIREMENTS.md)、[`NAMING.md`](NAMING.md) |
|
||||
| 4 | 替换标识与占位,按四象限 / examples 实现 `scripts/service/` | [`DEVELOPMENT.md`](DEVELOPMENT.md) §4~§14 |
|
||||
| 5 | **配置 `developer_ids`(开发自测必做)**:开发期技能在匠厂默认不公开;不加则开发者自己也装测不了。从宿主设置复制用户 ID 写入 `SKILL.md` | [`DEVELOPMENT.md`](DEVELOPMENT.md) §6「关于 developer_ids」 |
|
||||
@@ -34,12 +34,14 @@
|
||||
7. [`TESTING.md`](TESTING.md) — 测试分层、隔离数据根与档位开关
|
||||
8. [`LOGGING.md`](LOGGING.md) — 日志分层、必打节点、敏感信息红线(**涉及长任务、RPA、外部系统时必读**)
|
||||
9. [`ADAPTER.md`](ADAPTER.md) — 涉及外部系统对接时
|
||||
10. [`RPA.md`](RPA.md) — 涉及浏览器 / 桌面 / 手机自动化时
|
||||
11. [`CONFIG.md`](CONFIG.md) — `.env` 规范与 bootstrap 机制
|
||||
12. [`DATA_PATHS.md`](DATA_PATHS.md) — 下载/导入/导出等本地文件路径标准(涉及文件读写时必读)
|
||||
13. [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) — Skill Action sync/async、任务中心、Agent 禁令、队列 pick(涉及 RPA / 长任务 / 数据管理按钮 / Cron / Agent 时必读)
|
||||
14. [`RUNTIME.md`](RUNTIME.md) — 共享 runtime、数据路径、发布打包与编码约定
|
||||
14. [`CHECKLIST.md`](CHECKLIST.md) — **交活验收清单(第三方视角)**;报告输出到 [`../acceptance-reports/`](../acceptance-reports/)
|
||||
10. [`RPA.md`](RPA.md) — 涉及浏览器 / 桌面 / 手机自动化时的会话、风控和工程总纲
|
||||
11. [`RPA_ELEMENT_LOCATING.md`](RPA_ELEMENT_LOCATING.md) — **所有界面技能必读**:真实拾取、element map、唯一性、降级和 AI 编程约束
|
||||
12. 按执行端选读:[`RPA_BROWSER.md`](RPA_BROWSER.md) / [`RPA_DESKTOP.md`](RPA_DESKTOP.md) / [`RPA_MOBILE.md`](RPA_MOBILE.md)
|
||||
13. [`CONFIG.md`](CONFIG.md) — `.env` 规范与 bootstrap 机制
|
||||
14. [`DATA_PATHS.md`](DATA_PATHS.md) — 下载/导入/导出等本地文件路径标准(涉及文件读写时必读)
|
||||
15. [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) — Skill Action sync/async、任务中心、Agent 禁令、队列 pick(涉及 RPA / 长任务 / 数据管理按钮 / Cron / Agent 时必读)
|
||||
16. [`RUNTIME.md`](RUNTIME.md) — 共享 runtime、数据路径、发布打包与编码约定
|
||||
17. [`CHECKLIST.md`](CHECKLIST.md) — **交活验收清单(第三方视角)**;报告输出到 [`../acceptance-reports/`](../acceptance-reports/)
|
||||
|
||||
Agent 调用契约见 [`../references/CLI.md`](../references/CLI.md)、[`../references/SCHEMA.md`](../references/SCHEMA.md)、[`../references/ACTIONS.md`](../references/ACTIONS.md)。
|
||||
用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.md),不要写进本目录。
|
||||
|
||||
@@ -54,6 +54,10 @@
|
||||
### RPA 风控设计
|
||||
|
||||
- 执行端:浏览器 / 桌面 / Android / iOS:
|
||||
- 真实界面版本、拾取工具和拾取日期:
|
||||
- element map 文件路径;关键元素是否全部登记:
|
||||
- 每个关键元素的作用域、主定位器、匹配数量和已验证 fallback:
|
||||
- 页面 / iframe / 窗口 / 弹窗 / Native / WebView 如何选择:
|
||||
- 会话、Profile、窗口或设备如何准备与互斥:
|
||||
- 单批上限、单位时间操作量和最长运行时间:
|
||||
- 普通动作节奏(真实环境默认 `1~3` 秒):
|
||||
@@ -63,6 +67,7 @@
|
||||
- 哪些步骤进入人工处理(验证码、人脸、指纹、短信、U盾等):
|
||||
- 如何防止重复提交并支持必要的断点续跑:
|
||||
- 需要保存哪些日志、截图、视频和业务结果:
|
||||
- 定位失败时如何保存 DOM/UIA/XML、候选数量和环境版本:
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
@@ -212,12 +217,15 @@
|
||||
- 安装后按本技能 `actions.json` 声明完成宿主多入口验收(见 `development/DEVELOPMENT.md` §15);**须看用户体验**(进度可见、失败可读、RPA 有头、文案与真实行为一致等),后台通了不算完
|
||||
- 已人工核验授权与技术路线:无破解、逆向、私有协议、验证码绕过、Hook / 注入、自动换号或其他规避平台限制的实现
|
||||
- RPA 真实主路径已验证状态等待、`1~3` 秒正常操作节奏、有限重试、账号 / 设备串行、人工介入、结果核验和结束存证
|
||||
- RPA 关键元素已在真实界面拾取并登记 element map;关键动作唯一定位,重启会话后复测通过
|
||||
- 页面/窗口/设备结构变化时停止并保存截图与 DOM/UIA/XML,不运行时猜 selector
|
||||
|
||||
## 10. 开发注意事项
|
||||
|
||||
- 只修改当前 skill 仓库,不要改动无关兄弟项目
|
||||
- 正常界面或获得授权的官方 API 无法完成闭环时,停止并上报;不得擅自改走破解、逆向或绕过安全机制的路线
|
||||
- 先判断四象限类型(`real_browser_rpa` / `real_api` / `simulator_browser_rpa` / `simulator_api`),再读对应 `examples/*/README.md`
|
||||
- 涉及界面时再按端阅读 `RPA_ELEMENT_LOCATING.md` 和 `RPA_BROWSER.md` / `RPA_DESKTOP.md` / `RPA_MOBILE.md`
|
||||
- `cli` 只做参数解析;核心逻辑在 `service`;兄弟 skill 调用集中封装(见 `ADAPTER.md`)
|
||||
- 发布前完成本地验证、工作流验证和正式环境安装验证(含 `developer_ids` 与宿主多入口)
|
||||
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
|
||||
我们开发的各类 skill,本质上都是在替人操作三类界面:**浏览器、桌面软件、手机软件**。三类的底层技术不同,但**工程范式相同**:可控会话(Profile)、有头运行、拟人操作、失败存证、人工兜底。
|
||||
|
||||
**开始写界面操作代码前,必须先读 [`RPA_ELEMENT_LOCATING.md`](RPA_ELEMENT_LOCATING.md),完成真实拾取、元素清单、唯一性和后置状态设计。** 分端实操:
|
||||
|
||||
- 浏览器:[`RPA_BROWSER.md`](RPA_BROWSER.md)
|
||||
- Windows 桌面:[`RPA_DESKTOP.md`](RPA_DESKTOP.md)
|
||||
- Android / iOS:[`RPA_MOBILE.md`](RPA_MOBILE.md)
|
||||
|
||||
---
|
||||
|
||||
## 0. 三端通用范式(先看这个)
|
||||
@@ -22,7 +28,7 @@
|
||||
| **步骤间随机等待** | 每两个正常业务动作之间 `random_delay(min,max)`,真实环境默认 **1~3 秒**;状态等待另算,不得用固定睡眠代替 |
|
||||
| **人工兜底(HITL)** | 滑块 / 短信验证码 / 人脸 / U盾 / 动态口令 → **停下来轮询等人工**,超时报 `ERROR:XXX_NEED_HUMAN`,绝不自动硬闯 |
|
||||
| **失败存证** | 失败必截图,合规场景全程录屏,统一存 `{数据目录}/rpa-artifacts/{batch_id}/{tag}_{ts}.png` |
|
||||
| **选择器纪律** | 语义选择器优先(id/name/text/aria);**F12 确认后再写,严禁凭记忆猜 DOM** |
|
||||
| **选择器纪律** | 先真实拾取并填写 element map;语义/可访问性属性优先;关键动作唯一匹配;**严禁凭记忆猜 selector 或用 first/index 掩盖歧义** |
|
||||
| **统一错误码** | `ERROR:REQUIRE_LOGIN` / `ERROR:CAPTCHA_NEED_HUMAN` / `ERROR:RATE_LIMITED` / `ERROR:LOGIN_TIMEOUT` 等,见下方错误码表 |
|
||||
| **步骤可控制** | 每条 RPA 指令 = 一个 `@rpa_step` 或 `video.add_step`;等待用 `interruptible_sleep`;宿主通过 control.json 暂停/继续/停止(SRCP,见 [`LOGGING.md`](LOGGING.md) §2.5) |
|
||||
| **幂等 / 断点续跑** | 批量操作记录"已处理到第几条",崩溃后能续跑、不重复提交 |
|
||||
@@ -127,6 +133,8 @@ async def open_login(page):
|
||||
|
||||
**选型:Playwright + 系统 Chrome/Edge。** 这是团队验证最充分的一条线(标杆技能 `1688-scrape-contacts`、`receive-order` 已落地)。
|
||||
|
||||
元素拾取、定位优先级、iframe/弹窗/Shadow DOM、动态列表和唯一性标准见 [`RPA_BROWSER.md`](RPA_BROWSER.md)。本节继续定义会话、启动、HITL 和工程边界。
|
||||
|
||||
### 1.0 生产路径(优先)
|
||||
|
||||
| 项 | 标准 |
|
||||
@@ -275,13 +283,14 @@ from jiangchang_skill_core.rpa.stealth import stealth_enabled, STEALTH_INIT_SCRI
|
||||
|
||||
## 2. 桌面软件(Windows 原生程序)
|
||||
|
||||
**选型:pywinauto(UIA backend)为主 + 图像识别兜底。**
|
||||
**选型:Windows UI Automation(UIA)为主 + 图像识别兜底。** 当前已验证成功案例 `add-friend-wecom` 使用 Python `uiautomation`;`pywinauto(backend="uia")` 是兼容方案。完整拾取、定位和降级标准见 [`RPA_DESKTOP.md`](RPA_DESKTOP.md)。
|
||||
|
||||
桌面端常见于 ERP 客户端、网银控件、银企直连等本地程序。优先走可访问性树(控件 ID/名字),坐标点击只做最后兜底。
|
||||
|
||||
| 技术 | 优先级 | 适用 / 说明 |
|
||||
|------|--------|------|
|
||||
| **pywinauto(`backend="uia"`)** | ✅ 首选 | 基于微软 UI Automation 树,拿控件 AutomationId/Name/ControlType,**稳定、不依赖屏幕坐标**,纯 Python |
|
||||
| **uiautomation** | ✅ 已验证首选 | `add-friend-wecom` 真实成功案例使用;直接读取 UIA Window/Control 树 |
|
||||
| **pywinauto(`backend="uia"`)** | ✅ 兼容 | 基于微软 UI Automation 树,拿控件 AutomationId/Name/ControlType,**稳定、不依赖屏幕坐标**,纯 Python |
|
||||
| **FlaUI**(经 pythonnet 调 .NET) | 备选 | UIA 拿不到的复杂/自绘控件时更完整,但需引入 .NET 运行时 |
|
||||
| **Playwright** | 特例 | 目标是 **Electron 套壳应用**(很多新 SaaS 客户端)时,可当浏览器驱动 |
|
||||
| **PyAutoGUI / SikuliX(图像识别)** | ⚠️ 兜底 | 控件树完全拿不到时(Flash/远程桌面/纯自绘 UI);**靠截图找图+坐标,分辨率/缩放一变就崩**,仅最后手段 |
|
||||
@@ -295,11 +304,12 @@ from jiangchang_skill_core.rpa.stealth import stealth_enabled, STEALTH_INIT_SCRI
|
||||
- **禁止侵入**:不注入或 Hook 第三方进程,不修改客户端文件,不反编译或调用未授权私有协议。
|
||||
|
||||
```bash
|
||||
pip install pywinauto # UIA 自动化
|
||||
pip install uiautomation # 当前真实成功案例
|
||||
# 或:pip install pywinauto # UIA backend
|
||||
# 图像兜底:pip install pyautogui opencv-python
|
||||
```
|
||||
|
||||
> 状态:桌面端选型为推荐标准,**尚待真实项目实战验证**,落地时回补踩坑记录到本文。
|
||||
> 状态:UIA 技术路线已经 `add-friend-wecom` 真实项目验证。脱敏、收紧后的参考 example 见 `examples/real_desktop_rpa/`;目标软件 selector 仍必须重新拾取。
|
||||
|
||||
---
|
||||
|
||||
@@ -307,6 +317,8 @@ pip install pywinauto # UIA 自动化
|
||||
|
||||
**选型:Android 用 uiautomator2(或 Appium);iOS 用 Appium + WebDriverAgent(需 Mac)。** 底层都是经 USB 的 ADB / WDA。
|
||||
|
||||
设备准备、Appium Inspector/UIAutoDev 拾取、Native/WebView、定位优先级和失败 XML 留证见 [`RPA_MOBILE.md`](RPA_MOBILE.md)。
|
||||
|
||||
| 平台 | 技术 | 优先级 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| **Android** | **uiautomator2**(python 原生) | ✅ 首选 | ADB over USB,直接拿控件树点击/输入,比 Appium 轻快,纯 Python |
|
||||
@@ -330,7 +342,7 @@ python -m uiautomator2 init # 初始化设备端 agent
|
||||
# 或统一走 Appium:pip install Appium-Python-Client(另需 Appium Server)
|
||||
```
|
||||
|
||||
> 状态:手机端选型为推荐标准,**尚待真实项目实战验证**,落地时回补踩坑记录到本文。
|
||||
> 状态:手机端标准已定义,但**尚无团队真实成功案例**。`examples/mobile_rpa/` 仅是 example 完成规格,不能作为已跑通代码照抄;收到成功案例后再升级状态。
|
||||
|
||||
---
|
||||
|
||||
@@ -404,6 +416,10 @@ skill 退出/抛错统一用 `ERROR:` 前缀 + 稳定码,方便宿主与上层
|
||||
|
||||
## 相关文档
|
||||
|
||||
- `RPA_ELEMENT_LOCATING.md` — 三端共用的拾取、element map、唯一性、降级和 AI 编程约束
|
||||
- `RPA_BROWSER.md` — 浏览器 Pick Locator、DOM 作用域和定位质量
|
||||
- `RPA_DESKTOP.md` — Windows UIA 拾取与已验证桌面路线
|
||||
- `RPA_MOBILE.md` — Android/iOS 拾取、context、设备与 XML 留证
|
||||
- `ADAPTER.md` — 真实/仿真 × API/RPA 的四档适配器模式
|
||||
- `CONFIG.md` — `.env` 配置规范与首次落盘机制
|
||||
- `RUNTIME.md` — 运行时目录与环境变量约定
|
||||
|
||||
123
development/RPA_BROWSER.md
Normal file
123
development/RPA_BROWSER.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# 浏览器 RPA 元素拾取与定位标准
|
||||
|
||||
> 先读 [`RPA_ELEMENT_LOCATING.md`](RPA_ELEMENT_LOCATING.md)。本文只定义浏览器端的拾取、定位、验证和常见陷阱;账号、Profile、HITL 和录屏仍以 [`RPA.md`](RPA.md) 为准。
|
||||
|
||||
## 目录
|
||||
|
||||
1. 拾取工具
|
||||
2. 拾取步骤
|
||||
3. 定位优先级
|
||||
4. 特殊上下文
|
||||
5. 验证标准
|
||||
6. 禁止模式
|
||||
7. example 路由
|
||||
|
||||
## 1. 拾取工具
|
||||
|
||||
优先使用 Playwright Inspector / Codegen 的 **Pick Locator**,辅以 Chrome/Edge DevTools:
|
||||
|
||||
- Pick Locator:点选页面元素,获取 role/label/text/testid 等候选定位器。
|
||||
- DevTools Elements:确认真实 DOM、属性、父容器、iframe 和 Shadow DOM。
|
||||
- Playwright trace/screenshot:复核动作发生时的页面状态。
|
||||
|
||||
检查器只负责给出候选。复制到生产代码前必须人工确认作用域、唯一性和稳定性。
|
||||
|
||||
## 2. 拾取步骤
|
||||
|
||||
1. 使用真实 `profile_dir` 和有头浏览器打开目标页面。
|
||||
2. 手工进入发生动作的准确页面、标签页和弹窗状态。
|
||||
3. 用 Pick Locator 点选元素,先记录建议 locator。
|
||||
4. 在 DevTools 中确认:
|
||||
- accessible role/name
|
||||
- label、placeholder、可见文本
|
||||
- 稳定 id/name
|
||||
- `data-testid`(仅目标系统已提供或自有系统可维护时)
|
||||
- 所在 iframe、dialog、表格行或卡片
|
||||
5. 先定位业务容器,再在容器内定位动作元素。
|
||||
6. 运行 `count()`;关键动作期望 `1`。
|
||||
7. 验证 `visible`、`enabled`、必要时 `editable/checked`。
|
||||
8. 执行动作并断言 URL、业务容器、成功提示、单号或状态变化。
|
||||
9. 刷新页面、关闭重开浏览器后复测。
|
||||
|
||||
## 3. 定位优先级
|
||||
|
||||
从高到低:
|
||||
|
||||
1. `get_by_role(role, name=...)`
|
||||
2. `get_by_label(...)`
|
||||
3. 自有系统的稳定 `get_by_test_id(...)`
|
||||
4. `get_by_placeholder(...)`、`get_by_alt_text(...)`、`get_by_title(...)`
|
||||
5. 稳定业务文本,优先用于非交互元素或父容器
|
||||
6. 稳定 `id` / `name` / 短 CSS
|
||||
7. 容器 + `filter(has_text=...)` / `has=...`
|
||||
8. 短 XPath
|
||||
9. 已验证的顺序定位
|
||||
|
||||
示例:
|
||||
|
||||
```python
|
||||
dialog = page.get_by_role("dialog", name="订单确认")
|
||||
submit = dialog.get_by_role("button", name="确认提交")
|
||||
if await submit.count() != 1:
|
||||
raise RuntimeError("ERROR:ELEMENT_AMBIGUOUS submit_button")
|
||||
await submit.wait_for(state="visible")
|
||||
await submit.click()
|
||||
await page.get_by_text("提交成功", exact=True).wait_for(state="visible")
|
||||
```
|
||||
|
||||
### 列表与重复按钮
|
||||
|
||||
不要直接选择全页第一个“添加”:
|
||||
|
||||
```python
|
||||
row = page.get_by_role("row").filter(has_text="客户A")
|
||||
button = row.get_by_role("button", name="添加")
|
||||
```
|
||||
|
||||
如果业务键不唯一,先停止并要求补充输入,不要用 `.first` 猜。
|
||||
|
||||
## 4. 特殊上下文
|
||||
|
||||
### iframe
|
||||
|
||||
先确认元素属于哪个 iframe,再使用 `frame_locator`。不得因为主页面找不到就改用坐标。
|
||||
|
||||
### 新标签页和弹窗
|
||||
|
||||
点击前注册 popup/new page 等待;取得新 Page 后再定位。关闭弹窗不等于业务成功。
|
||||
|
||||
### Shadow DOM
|
||||
|
||||
Playwright CSS locator 默认可穿透 open Shadow DOM;XPath 不穿透。closed Shadow DOM 无法正常读取时,评估目标应用是否提供可访问性属性,不能注入破解。
|
||||
|
||||
### 动态列表
|
||||
|
||||
等待列表完成加载后再读取;`locator.all()` 不会等待动态列表稳定。滚动或刷新后重新获取 locator。
|
||||
|
||||
### Canvas
|
||||
|
||||
先确认是否存在可访问性节点或外层业务控件。完全无 DOM 时才进入局部图像方案,并记录分辨率、缩放和识别区域。
|
||||
|
||||
## 5. 验证标准
|
||||
|
||||
- locator 在真实目标页面匹配数量符合元素清单。
|
||||
- 不因语言、窗口宽度、列表顺序或一次刷新就指向不同业务对象。
|
||||
- 动作前检查可见、可用;动作后等待真实业务状态。
|
||||
- 登录态至少结合两个证据:例如稳定用户菜单 + 登录按钮不存在,不能只看单个哈希 class。
|
||||
- 真实和仿真站明确区分:仿真可以使用 `fill()` 加速测试;真实生产输入遵守项目拟人节奏。
|
||||
|
||||
## 6. 禁止模式
|
||||
|
||||
- 用全页所有 `input` 的尺寸和位置猜搜索框。
|
||||
- 用 `.first` 绕过严格模式。
|
||||
- 从 DevTools 复制深层 `nth-child` 或绝对 XPath。
|
||||
- 只等 URL 含 `search` 就判定结果成功。
|
||||
- 页面结构变化后动态生成 selector 并继续提交。
|
||||
- 为方便测试给第三方网站注入 testid。
|
||||
|
||||
## 7. example 路由
|
||||
|
||||
- 真实网站、登录/HITL/滚动:[`../examples/real_browser_rpa/README.md`](../examples/real_browser_rpa/README.md)
|
||||
- 自有 sandbox、表单和 adapter:[`../examples/simulator_browser_rpa/README.md`](../examples/simulator_browser_rpa/README.md)
|
||||
|
||||
复制 example 前先完成目标站元素清单;example 中的任何 URL、selector 和业务字段都不得跨平台照抄。
|
||||
171
development/RPA_DESKTOP.md
Normal file
171
development/RPA_DESKTOP.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# Windows 桌面 RPA 元素拾取与定位标准
|
||||
|
||||
> 先读 [`RPA_ELEMENT_LOCATING.md`](RPA_ELEMENT_LOCATING.md)。Windows 桌面首选 **UI Automation(UIA)**。当前已验证成功案例 `add-friend-wecom` 使用 Python `uiautomation`;`pywinauto(backend="uia")` 是兼容方案。标准约束的是 UIA 方法,不强制混用两个库。
|
||||
|
||||
## 目录
|
||||
|
||||
1. 已验证基线
|
||||
2. 拾取工具和步骤
|
||||
3. 定位优先级
|
||||
4. 窗口与控件规则
|
||||
5. 输入、点击与等待
|
||||
6. UIA 不可用时的降级
|
||||
7. 诊断和留证
|
||||
8. example 路由
|
||||
|
||||
## 1. 已验证基线
|
||||
|
||||
真实成功来源:`D:\OpenClaw\client-marketing\add-friend-wecom`。
|
||||
|
||||
该案例验证了:
|
||||
|
||||
- 通过进程/窗口类名/标题找到企业微信顶层窗口。
|
||||
- 从 HWND 获取 UIA 根控件。
|
||||
- 在父窗口和弹窗范围内按 UIA 属性定位。
|
||||
- 真实激活窗口、点击、逐字输入和轮询状态。
|
||||
- 处理独立弹窗与嵌套弹窗。
|
||||
- 提供 `probe-ui` 诊断和 `dry-run / assisted / auto` 三档。
|
||||
|
||||
example 只提炼通用模式;企业微信名称、窗口类、控件名、列表顺序和业务结果不得复制到其他软件。
|
||||
|
||||
## 2. 拾取工具和步骤
|
||||
|
||||
可使用影刀元素拾取器、Microsoft Inspect/Accessibility Insights、FlaUInspect 或库自带树打印能力。
|
||||
|
||||
操作步骤:
|
||||
|
||||
1. 启动目标软件,登录并手工进入准确页面。
|
||||
2. 记录目标进程名、PID、主窗口标题和 Window Class。
|
||||
3. 用检查器点选目标元素,记录:
|
||||
- AutomationId
|
||||
- Name
|
||||
- ControlType
|
||||
- ClassName
|
||||
- FrameworkId
|
||||
- IsEnabled / IsOffscreen
|
||||
- BoundingRectangle
|
||||
- 父容器链
|
||||
4. 保存截图和原始 UIA 树/拾取器导出文件。
|
||||
5. 从长路径中找出稳定窗口、业务父容器和叶子属性。
|
||||
6. 删除仅反映当前布局的深层 index;确需 index 时写明业务依据。
|
||||
7. 写入 element map。
|
||||
8. 运行 `probe-ui`,验证关键窗口和元素。
|
||||
9. 关闭重开软件、切换一次页面后再次验证。
|
||||
|
||||
### 从拾取结果提炼定位器
|
||||
|
||||
影刀等工具可能导出很长的 UIA 路径。不要整条照抄:
|
||||
|
||||
```text
|
||||
原始:主窗口 → 多层布局 → 列表 → item[2] → status_layout
|
||||
提炼:进程/窗口 → 通讯录列表父容器 → 业务文案或稳定叶子属性
|
||||
```
|
||||
|
||||
长路径用于排障和理解结构;生产定位器使用最短、唯一、可解释的稳定组合。
|
||||
|
||||
## 3. 定位优先级
|
||||
|
||||
从高到低:
|
||||
|
||||
1. `Process/PID + WindowClass + WindowTitle`
|
||||
2. 目标窗口内 `AutomationId + ControlType`
|
||||
3. 稳定 `Name + ControlType`
|
||||
4. 业务父容器 + 稳定子元素属性
|
||||
5. 可见业务文案 + 相对关系
|
||||
6. `ClassName/FrameworkId` 辅助组合
|
||||
7. 有业务依据的 index
|
||||
8. UIA 元素实时 BoundingRectangle 派生坐标
|
||||
9. 局部截图模板/OCR
|
||||
10. 写死绝对坐标:禁止作为生产方案
|
||||
|
||||
顶层窗口或关键按钮出现多个候选时,返回 `ERROR:ELEMENT_AMBIGUOUS`,不得默认使用第一个。
|
||||
|
||||
## 4. 窗口与控件规则
|
||||
|
||||
### 窗口
|
||||
|
||||
- 校验窗口属于预期进程和当前用户会话。
|
||||
- 主窗口、搜索窗、确认窗分别建立作用域。
|
||||
- 应用可能重建窗口;页面切换后重新取 HWND 和 UIA root。
|
||||
- 自动化进程与目标进程权限级别必须兼容;不要为了绕过权限注入目标进程。
|
||||
- 多开场景必须由账号、PID或明确窗口信息选择,歧义时停止。
|
||||
|
||||
### 控件
|
||||
|
||||
- 在最小业务父容器中搜索。
|
||||
- 默认要求唯一匹配。
|
||||
- 检查可见、启用、未离屏和边界矩形有效。
|
||||
- 虚拟列表只包含当前实例化元素;滚动后重新枚举。
|
||||
- 不长期保存 Control 对象。
|
||||
|
||||
## 5. 输入、点击与等待
|
||||
|
||||
### 点击
|
||||
|
||||
1. 激活目标窗口。
|
||||
2. 重新定位元素。
|
||||
3. 检查边界矩形。
|
||||
4. 使用 UIA Click/Invoke 或正常鼠标点击。
|
||||
5. 等待后置窗口/控件/业务状态。
|
||||
|
||||
UIA 点击失败时可以用该元素刚读取的 BoundingRectangle 中心点降级,但必须记录降级、确认窗口前台并验证后置状态;不得静默改点写死坐标。
|
||||
|
||||
### 输入
|
||||
|
||||
- 先点击聚焦。
|
||||
- 清空并逐字输入,敏感值不写日志。
|
||||
- 输入后读取 Value/Text 或通过后续业务状态验证。
|
||||
- 剪贴板可能泄露敏感信息,非必要不使用。
|
||||
|
||||
### 等待
|
||||
|
||||
优先等待:
|
||||
|
||||
- 目标窗口出现/消失。
|
||||
- 控件出现、启用或状态变化。
|
||||
- 列表产生业务结果。
|
||||
- 成功/失败提示出现。
|
||||
|
||||
固定 sleep 只作短缓冲,并使用可中断等待;不能作为成功条件。
|
||||
|
||||
## 6. UIA 不可用时的降级
|
||||
|
||||
先检查:
|
||||
|
||||
1. 权限级别和 Windows 会话是否一致。
|
||||
2. 是否选错进程或子窗口。
|
||||
3. 控件是否离屏或尚未实例化。
|
||||
4. 应用是否为 Electron(可评估 Playwright)。
|
||||
5. 是否为自绘、游戏引擎、远程桌面或安全控件。
|
||||
|
||||
只有控件树确认不可用时才使用图像:
|
||||
|
||||
- 固定或记录 DPI、缩放、分辨率和主题。
|
||||
- 限定目标窗口和搜索区域。
|
||||
- 记录模板来源、置信度阈值和多匹配处理。
|
||||
- 匹配不唯一时停止。
|
||||
- 点击后仍验证业务状态。
|
||||
|
||||
U盾、人脸、动态口令和安全键盘交给人工,不用 OCR、Hook 或坐标绕过。
|
||||
|
||||
## 7. 诊断和留证
|
||||
|
||||
桌面 `health` / `probe-ui` 至少输出:
|
||||
|
||||
- UIA 依赖是否可用。
|
||||
- 目标进程和主窗口是否存在。
|
||||
- 关键窗口/控件是否各唯一匹配。
|
||||
- 当前窗口标题、Class、PID。
|
||||
- DPI/缩放诊断。
|
||||
|
||||
定位失败保存:
|
||||
|
||||
- 目标窗口和全屏截图。
|
||||
- UIA 树。
|
||||
- 顶层窗口枚举。
|
||||
- element map 逻辑名、候选数量和属性。
|
||||
- 应用版本、Windows版本、DPI和会话类型。
|
||||
|
||||
## 8. example 路由
|
||||
|
||||
真实成功参考见 [`../examples/real_desktop_rpa/README.md`](../examples/real_desktop_rpa/README.md)。该 example 默认只演示 `probe`、`dry-run` 和 `assisted`;不可逆动作必须由业务技能显式实现确认策略。
|
||||
176
development/RPA_ELEMENT_LOCATING.md
Normal file
176
development/RPA_ELEMENT_LOCATING.md
Normal file
@@ -0,0 +1,176 @@
|
||||
# RPA 元素定位与拾取总标准
|
||||
|
||||
> 本文定义浏览器、Windows 桌面和手机三端共用的“元素工程”标准。开始写任何界面操作代码前必须先完成元素拾取、元素清单和定位验证。分端实操分别见 [`RPA_BROWSER.md`](RPA_BROWSER.md)、[`RPA_DESKTOP.md`](RPA_DESKTOP.md)、[`RPA_MOBILE.md`](RPA_MOBILE.md)。
|
||||
|
||||
## 目录
|
||||
|
||||
1. 核心术语
|
||||
2. 强制工作流
|
||||
3. 元素清单
|
||||
4. 定位器质量规则
|
||||
5. 动作闭环
|
||||
6. 降级决策树
|
||||
7. 失败留证
|
||||
8. AI 编程约束
|
||||
9. 交付门禁
|
||||
|
||||
## 1. 核心术语
|
||||
|
||||
| 术语 | 定义 |
|
||||
|---|---|
|
||||
| **拾取** | 使用检查器在真实界面上点选元素,读取 DOM、UIA 或移动控件树属性 |
|
||||
| **定位器** | 根据稳定属性在运行时重新找到元素的规则 |
|
||||
| **作用域** | 页面、iframe、窗口、弹窗、父容器、列表行或 WebView 等查找边界 |
|
||||
| **锚点** | 与目标元素存在稳定业务关系的父级、标题、标签或同一行元素 |
|
||||
| **后置状态** | 动作完成后必须出现或消失的真实业务状态 |
|
||||
| **降级** | 主定位器不可用时,改用已验证的备用定位器或图像方案 |
|
||||
| **界面快照** | 失败现场的截图和 DOM/UIA/XML 控件树 |
|
||||
|
||||
## 2. 强制工作流
|
||||
|
||||
按以下顺序开发,不得跳过拾取直接猜 selector:
|
||||
|
||||
1. 在真实目标版本上手工走通业务流程,记录每一步的前置状态和成功表现。
|
||||
2. 使用对应端检查器拾取元素,保存截图和原始 DOM/UIA/XML。
|
||||
3. 识别正确作用域:页面/iframe、进程/窗口、Native/WebView、弹窗或列表行。
|
||||
4. 为每个关键元素填写元素清单。
|
||||
5. 按分端优先级生成候选定位器。
|
||||
6. 验证候选定位器在目标作用域中匹配数量符合预期,关键动作默认必须恰好为 `1`。
|
||||
7. 验证元素可见、启用、未离屏且边界矩形有效。
|
||||
8. 执行动作,并等待真实后置状态。
|
||||
9. 重启目标程序或会话后再次验证;动态列表还要更换数据验证。
|
||||
10. 记录已验证的软件版本、页面状态、系统缩放/设备信息和日期。
|
||||
|
||||
未完成第 2~6 步时,代码只能保留明确的 `TODO: NEED_REAL_UI_INSPECTION`,不得写“看起来可能正确”的 selector。
|
||||
|
||||
## 3. 元素清单
|
||||
|
||||
每个关键输入、按钮、列表、弹窗、成功状态和错误状态都必须登记。推荐 YAML:
|
||||
|
||||
```yaml
|
||||
logical_name: submit_button
|
||||
purpose: 提交当前业务单据
|
||||
surface: browser # browser | windows | android | ios
|
||||
screen: 订单确认
|
||||
scope:
|
||||
kind: dialog
|
||||
locator: role=dialog,name=订单确认
|
||||
primary_locator:
|
||||
strategy: role
|
||||
value: button,name=确认提交
|
||||
fallback_locators: []
|
||||
expected_count: 1
|
||||
ready_condition:
|
||||
visible: true
|
||||
enabled: true
|
||||
postcondition:
|
||||
description: 出现订单号且状态为已提交
|
||||
failure_evidence:
|
||||
- screenshot
|
||||
- ui_tree
|
||||
verified_target_version: "待填写"
|
||||
verified_at: "YYYY-MM-DD"
|
||||
```
|
||||
|
||||
要求:
|
||||
|
||||
- `logical_name` 使用业务语义,不使用 `button1`、`element2`。
|
||||
- `scope` 不得省略;全页面、全桌面或全设备盲搜不作为默认生产方案。
|
||||
- `fallback_locators` 只能填写人工验证过的定位器。
|
||||
- 不可逆动作必须同时登记确认前状态和提交后状态。
|
||||
- selector 变化时更新 `verified_target_version` 和验证日期。
|
||||
|
||||
## 4. 定位器质量规则
|
||||
|
||||
### 4.1 必须满足
|
||||
|
||||
- 关键动作定位器在目标作用域中唯一。
|
||||
- 定位器依赖业务语义、可访问性属性或稳定接口标识。
|
||||
- 每次页面/窗口/列表刷新后重新定位,不长期缓存元素句柄。
|
||||
- 同一元素的主定位器和 fallback 使用不同的稳定证据,不是同一脆弱路径的轻微改写。
|
||||
- 多结果是合法业务场景时,先定位业务容器/行,再在容器内定位按钮。
|
||||
|
||||
### 4.2 默认禁止
|
||||
|
||||
- 用 `.first`、`.nth(0)`、`hwnds[0]`、`instance=0` 消除本应解决的歧义。
|
||||
- 长绝对 XPath、深层 DOM/UIA 路径和大量 index。
|
||||
- 哈希 class、随机生成 id、易变化坐标作为唯一依据。
|
||||
- 扫描所有输入框后按宽度、位置“猜”目标。
|
||||
- 捕获所有异常后继续尝试随机 selector 或随机坐标。
|
||||
- UI结构变化后自动生成新 selector 并继续执行不可逆动作。
|
||||
|
||||
确有稳定业务顺序时可以使用 index,但必须在元素清单中写明锚点、顺序依据、匹配总数和版本验证结果。
|
||||
|
||||
## 5. 动作闭环
|
||||
|
||||
每一个动作实现为一个小状态机:
|
||||
|
||||
```text
|
||||
确认前置状态
|
||||
→ 获取正确作用域
|
||||
→ 重新定位元素
|
||||
→ 校验唯一/可见/可用
|
||||
→ 激活并执行真实输入
|
||||
→ 等待后置状态
|
||||
→ 记录成功证据
|
||||
```
|
||||
|
||||
- 缓冲等待只用于改善稳定性,不能替代后置状态。
|
||||
- 点击后状态不明确时先查询业务状态,禁止再次点击。
|
||||
- 付款、发送、删除、审批、发布等动作如果无法确认结果,返回 `ERROR:RESULT_UNCONFIRMED`。
|
||||
- 列表滚动、页面切换、窗口重建和移动端 Activity 切换后重新获取元素。
|
||||
|
||||
## 6. 降级决策树
|
||||
|
||||
找不到元素时依次检查:
|
||||
|
||||
1. 目标应用、页面或设备是否正确启动并处于预期账号。
|
||||
2. 是否选错页面、iframe、进程、窗口、弹窗、Native/WebView 上下文。
|
||||
3. 元素是否尚未加载、被遮挡、禁用、离屏或在虚拟列表未实例化区域。
|
||||
4. 是否因管理员权限、远程会话、设备授权或驱动版本无法读取控件树。
|
||||
5. 主定位器属性是否变化;对比保存的界面快照。
|
||||
6. 是否可以用已验证的父容器、业务文案或相对锚点定位。
|
||||
7. 控件是否为 Canvas、自绘、远程桌面或未暴露可访问性树。
|
||||
8. 最后才评估局部图像/OCR/坐标方案。
|
||||
|
||||
任何一级确认“界面结构变化”时,截图、导出控件树并停止;不得在生产任务中让 AI 临时猜新定位器。
|
||||
|
||||
## 7. 失败留证
|
||||
|
||||
定位失败至少保存:
|
||||
|
||||
- 当前屏幕/窗口截图。
|
||||
- 当前 DOM、UIA 或移动端 XML。
|
||||
- 页面 URL、进程/PID/窗口标题或设备 serial、包名、Activity、context。
|
||||
- 逻辑元素名、作用域、主定位器和已尝试 fallback。
|
||||
- 每个候选匹配数量、可见/启用/离屏状态和边界矩形。
|
||||
- 目标应用版本、浏览器版本或设备系统版本。
|
||||
- 失败阶段、错误码、时间和 batch_id。
|
||||
|
||||
敏感输入、Cookie、token、手机号和客户数据必须脱敏。
|
||||
|
||||
## 8. AI 编程约束
|
||||
|
||||
向 AI 编程工具提供本模板时,必须要求它:
|
||||
|
||||
1. 先阅读本总标准和对应分端手册。
|
||||
2. 先输出业务步骤表和元素清单,再生成代码。
|
||||
3. 只使用真实拾取得到的属性;缺少属性时明确停止并索要界面快照。
|
||||
4. 把目标系统 selector 集中到 element map/selector catalog,不散落在业务流程中。
|
||||
5. 为每个动作实现前置状态、定位、动作和后置状态。
|
||||
6. 把诊断探针、界面树导出和失败截图作为实现的一部分。
|
||||
7. 不把 mock 通过、命令未报错或弹窗关闭当成真实业务成功。
|
||||
8. 不擅自切换到 Hook、注入、私有协议、验证码破解或其他禁止路线。
|
||||
|
||||
## 9. 交付门禁
|
||||
|
||||
- [ ] 关键元素均有元素清单和真实拾取证据
|
||||
- [ ] 每个不可逆动作默认唯一定位
|
||||
- [ ] 重启会话后定位仍然有效
|
||||
- [ ] 动态数据或列表顺序变化后不会误点
|
||||
- [ ] 每个动作有真实后置状态
|
||||
- [ ] UI结构变化会停止并留证
|
||||
- [ ] fallback 均已人工验证
|
||||
- [ ] 截图和控件树不泄露敏感数据
|
||||
- [ ] 真实主路径至少完整跑通一次
|
||||
- [ ] example 状态明确区分“已验证成功”和“规范骨架”
|
||||
141
development/RPA_MOBILE.md
Normal file
141
development/RPA_MOBILE.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# 手机 RPA 元素拾取与定位标准
|
||||
|
||||
> 先读 [`RPA_ELEMENT_LOCATING.md`](RPA_ELEMENT_LOCATING.md)。Android 首选 uiautomator2 或 Appium UiAutomator2;iOS 使用 Appium + XCUITest/WebDriverAgent。当前模板尚无团队真实成功案例,因此本文是强制设计标准,example 仅为规格骨架,不得宣称已生产验证。
|
||||
|
||||
## 目录
|
||||
|
||||
1. 状态与边界
|
||||
2. 设备准备
|
||||
3. 拾取工具和步骤
|
||||
4. Android 定位优先级
|
||||
5. iOS 定位优先级
|
||||
6. Native/WebView 与动态界面
|
||||
7. 手势、输入和等待
|
||||
8. 降级与留证
|
||||
9. example 路由
|
||||
|
||||
## 1. 状态与边界
|
||||
|
||||
- Android 标准可用于设计和实现,但交付前必须在授权真机和官方 App 上验证。
|
||||
- iOS 必须有 Mac、签名可用的 WebDriverAgent 和授权设备。
|
||||
- 不 Root、不越狱、不 Hook、不修改 APK/IPA、不伪造设备身份。
|
||||
- 短信、人脸、指纹、系统安全确认交给人工。
|
||||
|
||||
## 2. 设备准备
|
||||
|
||||
每次运行前检查:
|
||||
|
||||
- 唯一设备 serial/UDID;多个设备时不得默认取第一个。
|
||||
- USB/网络连接和调试授权。
|
||||
- Android 包名/Activity,或 iOS bundle id。
|
||||
- 屏幕是否解锁、方向、分辨率和系统弹窗状态。
|
||||
- 自动化 driver/service 是否可用。
|
||||
- 设备租约/互斥;同一设备默认串行。
|
||||
|
||||
`doctor` 应输出上述信息,但不得输出设备隐私数据。
|
||||
|
||||
## 3. 拾取工具和步骤
|
||||
|
||||
Android 可使用 Appium Inspector、uiautomator2 的 UIAutoDev/层级导出;iOS 使用 Appium Inspector。
|
||||
|
||||
1. 在真机上手工进入准确页面。
|
||||
2. 刷新 Inspector screenshot 和 page source。
|
||||
3. 在截图或 XML 树中点选元素。
|
||||
4. 记录:
|
||||
- Android:resource-id、content-desc、text、class、package、clickable、enabled、bounds
|
||||
- iOS:name/accessibility id、label、value、type、enabled、visible
|
||||
5. 确认当前 context 是 Native 还是正确 WebView。
|
||||
6. 记录稳定父容器、列表行和业务锚点。
|
||||
7. 在 Inspector 搜索定位器,确认匹配数量。
|
||||
8. 写入 element map,保存 screenshot + XML。
|
||||
9. 重启 App、重新进入页面并复测。
|
||||
|
||||
截图上看到的元素不一定存在于控件树;必须以 page source/可访问性树验证,不能直接凭坐标写代码。
|
||||
|
||||
## 4. Android 定位优先级
|
||||
|
||||
从高到低:
|
||||
|
||||
1. 稳定且唯一的 `resourceId`
|
||||
2. `description/content-desc`(accessibility)
|
||||
3. 稳定 `text + className`
|
||||
4. 业务父容器内的子元素组合
|
||||
5. UiSelector / 相对关系
|
||||
6. 短 XPath
|
||||
7. 已验证的 instance/index
|
||||
8. 图像/坐标
|
||||
|
||||
示例:
|
||||
|
||||
```python
|
||||
search = d(resourceId="com.example:id/search_input")
|
||||
if search.count != 1:
|
||||
raise RuntimeError("ERROR:ELEMENT_AMBIGUOUS search_input")
|
||||
search.click()
|
||||
search.set_text(keyword)
|
||||
if not d(resourceId="com.example:id/result_list").wait(timeout=10):
|
||||
raise RuntimeError("ERROR:RESULT_TIMEOUT")
|
||||
```
|
||||
|
||||
第三方 App 没有 resource-id 时,使用可访问性描述、稳定文案和父容器组合;不得修改第三方 APK 添加 id。
|
||||
|
||||
## 5. iOS 定位优先级
|
||||
|
||||
从高到低:
|
||||
|
||||
1. accessibility id / name
|
||||
2. `type + name/label/value`
|
||||
3. iOS predicate string
|
||||
4. iOS class chain
|
||||
5. XPath
|
||||
6. 图像/坐标
|
||||
|
||||
XPath 依赖完整 XML 且通常较慢,只作后备。多个同名按钮时先定位 navigation bar、cell、alert 或其他业务容器。
|
||||
|
||||
## 6. Native/WebView 与动态界面
|
||||
|
||||
### Native/WebView
|
||||
|
||||
- 先枚举 contexts。
|
||||
- Native 元素使用移动端控件树。
|
||||
- WebView 元素切换到正确 web context 后使用 Web 定位器。
|
||||
- context 数量或名称变化时按包名、URL或页面特征验证,不默认取第一个 WebView。
|
||||
|
||||
### 动态列表
|
||||
|
||||
- 虚拟列表只暴露屏幕附近元素。
|
||||
- 每次滚动后重新定位。
|
||||
- 用业务键/行文案选择条目,不依赖当前 instance。
|
||||
- 到达末尾必须有明确状态或滚动前后内容对比。
|
||||
|
||||
### 系统弹窗
|
||||
|
||||
权限、通知、文件选择等可能属于系统包或 SpringBoard;为其建立独立作用域和显式处理策略,不把它们当业务 App 元素。
|
||||
|
||||
## 7. 手势、输入和等待
|
||||
|
||||
- 点击前确认元素可见、启用且 bounds 有效。
|
||||
- 输入后读取文本或等待结果状态。
|
||||
- 滑动以元素/屏幕相对区域描述,不跨设备写死绝对像素。
|
||||
- 页面切换等待 Activity/view/context 或业务元素变化。
|
||||
- 后台切前台、横竖屏变化后重新获取元素。
|
||||
- 人工验证期间可用 scrcpy/Appium Inspector 辅助观察,程序轮询真实状态并有超时。
|
||||
|
||||
## 8. 降级与留证
|
||||
|
||||
找不到元素先检查 context、Activity、系统弹窗、滚动位置和页面 source。确认控件树未暴露后才评估局部图像/OCR。
|
||||
|
||||
失败保存:
|
||||
|
||||
- 设备 screenshot。
|
||||
- page source XML。
|
||||
- serial 脱敏值、包名、Activity、context 列表。
|
||||
- 屏幕分辨率、方向和系统版本。
|
||||
- element map 逻辑名、定位器和匹配数量。
|
||||
- App 版本、driver 版本和失败阶段。
|
||||
|
||||
图像方案必须限定设备矩阵和搜索区域;匹配不唯一时停止。
|
||||
|
||||
## 9. example 路由
|
||||
|
||||
手机 example 规格见 [`../examples/mobile_rpa/README.md`](../examples/mobile_rpa/README.md)。在团队提供真实成功案例前,该目录只能作为实施清单和验收契约,不能作为“已跑通代码”宣传或直接复制。
|
||||
@@ -8,14 +8,16 @@
|
||||
|
||||
根 `README.md`、`SKILL.md`、`references/`、`development/` 仍按模板主目录规则编写,**不要**从 `examples/` 复制。
|
||||
|
||||
## 四类外部交互方式
|
||||
## 外部交互示例矩阵
|
||||
|
||||
一级目录固定为四种模式(不可删除、不可移动):
|
||||
adapter 仍是 `真实/仿真 × API/RPA` 四档;RPA 再按浏览器、Windows 桌面和手机三种界面拆分。原四类目录不可删除、不可移动,桌面和手机目录用于补足界面维度。
|
||||
|
||||
| 目录 | 类型 | 状态 | 适用场景 |
|
||||
|---|---|---|---|
|
||||
| `real_browser_rpa` | 真实浏览器 RPA | **已有案例** | 真实第三方网站、登录态、人工验证、滚动采集 |
|
||||
| `simulator_browser_rpa` | 仿真浏览器 RPA | **已有案例** | 自有 sandbox、可控页面、表单 RPA、adapter 分层 |
|
||||
| `real_desktop_rpa` | 真实 Windows 桌面 RPA | **已有成功来源并已提炼** | UIA 拾取、窗口/控件、probe、dry-run/assisted |
|
||||
| `mobile_rpa` | Android/iOS RPA | **仅规格,待成功案例** | 设备、控件树、Native/WebView、example 完成契约 |
|
||||
| `real_api` | 真实 API | **占位** | 真实系统接口、token、权限、限流(尚未沉淀可复制实现) |
|
||||
| `simulator_api` | 仿真 API | **占位** | mock/sandbox API、无需浏览器(尚未沉淀可复制实现) |
|
||||
|
||||
@@ -44,6 +46,8 @@
|
||||
|---|---|
|
||||
| 真实网站 + 浏览器 + 登录/验证码/滚动 | **real_browser_rpa** |
|
||||
| 仿真页面 + 浏览器 + 表单/批量提交 | **simulator_browser_rpa** |
|
||||
| Windows 原生软件 + UIA | **real_desktop_rpa** |
|
||||
| Android/iOS App | **mobile_rpa**(当前仅规格,不可作为成功实现参考) |
|
||||
| 真实系统 API | **real_api**(当前占位,不可作为实现参考) |
|
||||
| mock / sandbox API(无浏览器) | **simulator_api**(当前占位,不可作为实现参考) |
|
||||
|
||||
@@ -60,7 +64,8 @@ examples/<mode>/
|
||||
tests/
|
||||
```
|
||||
|
||||
- 仅 `simulator_browser_rpa` 可额外包含 `sandbox/`
|
||||
- `simulator_browser_rpa` 可额外包含 `sandbox/`
|
||||
- `real_desktop_rpa` 可额外包含脱敏的 `assets/element-map` 与 selector capture
|
||||
- 仅未来确实需要样例数据时可加 `fixtures/`
|
||||
- **不要**在 `examples/<mode>/` 下要求或添加:`SKILL.md`、根市场 `README.md`、`references/`、`development/`、`assets/`、`.github/`、release workflow 等完整 skill 仓库层文件
|
||||
|
||||
@@ -76,6 +81,8 @@ examples/<mode>/
|
||||
|
||||
- 真实浏览器 RPA → 参考 `examples/real_browser_rpa/`
|
||||
- 仿真浏览器 RPA → 参考 `examples/simulator_browser_rpa/`(adapter 分层权威参考:`scripts/service/adapter/`)
|
||||
- Windows 桌面 RPA → 参考 `examples/real_desktop_rpa/`(真实成功来源:`add-friend-wecom`)
|
||||
- 手机 RPA → 先按 `examples/mobile_rpa/` 定义完成标准;真实成功案例补齐前不得照骨架宣称跑通
|
||||
- `real_api/`、`simulator_api/` 当前为规划占位,**不得**作为实现参考
|
||||
|
||||
> 示例提供的是**参考架构与边界**,不是业务代码原样复制。复制前先读对应 README 的 copy map 与「禁止照抄」章节。
|
||||
|
||||
94
examples/mobile_rpa/README.md
Normal file
94
examples/mobile_rpa/README.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Mobile RPA Example Specification
|
||||
|
||||
本目录定义 Android/iOS example 必须具备的结构和验收契约。**当前没有团队真实成功案例,因此本目录不是可复制实现,也不得宣称已跑通。**
|
||||
|
||||
先读:
|
||||
|
||||
1. [`../../development/RPA_ELEMENT_LOCATING.md`](../../development/RPA_ELEMENT_LOCATING.md)
|
||||
2. [`../../development/RPA_MOBILE.md`](../../development/RPA_MOBILE.md)
|
||||
|
||||
## 为什么现在不放伪代码实现
|
||||
|
||||
移动端 selector、driver、Activity/context、权限弹窗和设备兼容性必须在真实授权设备上验证。没有成功案例时放一套看似完整的代码,会诱导开发者和 AI 猜 `resource-id`、默认取第一个设备或用坐标硬点,违背模板目标。
|
||||
|
||||
## 未来成功 example 的固定结构
|
||||
|
||||
```text
|
||||
mobile_rpa/
|
||||
README.md
|
||||
android/
|
||||
README.md
|
||||
assets/element-map.yaml
|
||||
scripts/service/device_session.py
|
||||
scripts/service/mobile_locator.py
|
||||
scripts/service/example_flow.py
|
||||
tests/test_locator_contract.py
|
||||
tests/integration/test_real_device.py.sample
|
||||
ios/
|
||||
README.md
|
||||
assets/element-map.yaml
|
||||
scripts/service/device_session.py
|
||||
scripts/service/mobile_locator.py
|
||||
scripts/service/example_flow.py
|
||||
tests/test_locator_contract.py
|
||||
tests/integration/test_real_device.py.sample
|
||||
```
|
||||
|
||||
只有对应端出现真实成功案例后才创建代码文件。
|
||||
|
||||
## Android example 完成标准
|
||||
|
||||
- 唯一设备 serial 和设备互斥
|
||||
- doctor:ADB授权、包名、Activity、屏幕状态、driver
|
||||
- Appium Inspector/UIAutoDev 拾取步骤和原始 XML
|
||||
- element map:resource-id/content-desc/text/class/父容器
|
||||
- Native/WebView context 选择
|
||||
- 点击、输入、滑动和真实后置状态
|
||||
- 权限弹窗、系统弹窗和人工验证
|
||||
- screenshot + page source 失败留证
|
||||
- 虚拟列表滚动后重新定位
|
||||
- 至少一台授权真机完整跑通
|
||||
|
||||
## iOS example 完成标准
|
||||
|
||||
- Mac、WebDriverAgent签名和设备 UDID 准备
|
||||
- doctor:WDA、bundle id、系统版本、屏幕状态
|
||||
- Appium Inspector 拾取和原始 page source
|
||||
- accessibility id/name、predicate/class chain 定位
|
||||
- Native/WebView context 选择
|
||||
- 系统 alert 和人工验证
|
||||
- screenshot + page source 失败留证
|
||||
- 至少一台授权真机完整跑通
|
||||
|
||||
## element map 最小字段
|
||||
|
||||
```yaml
|
||||
logical_name: login_button
|
||||
surface: android
|
||||
screen: 登录页
|
||||
scope:
|
||||
package: com.example.app
|
||||
activity: .LoginActivity
|
||||
primary_locator:
|
||||
strategy: resource_id
|
||||
value: com.example.app:id/login
|
||||
expected_count: 1
|
||||
ready_condition:
|
||||
visible: true
|
||||
enabled: true
|
||||
postcondition:
|
||||
description: 首页业务标记出现
|
||||
verified_device: 待填写
|
||||
verified_app_version: 待填写
|
||||
```
|
||||
|
||||
## 状态升级规则
|
||||
|
||||
满足以下全部条件后,才可把本目录状态改为“已有成功案例”:
|
||||
|
||||
1. 提供用户确认的成功项目来源。
|
||||
2. 在真实授权设备上跑通完整业务闭环。
|
||||
3. 保存脱敏 element map 和失败证据样例。
|
||||
4. 有默认离线单测和显式真实集成测试。
|
||||
5. 经过设备重连、App重启和页面变化验证。
|
||||
6. 用户人工验收通过。
|
||||
@@ -33,6 +33,8 @@ real_browser_rpa/
|
||||
│ └── util/
|
||||
│ ├── constants.py # 平台常量、selector 占位
|
||||
│ └── logging.py # 结构化日志与脱敏
|
||||
├── assets/
|
||||
│ └── element-map.sample.yaml # 元素清单模板;真实拾取后填写
|
||||
└── tests/
|
||||
├── test_human_verification.py # 验证码误判防护
|
||||
└── test_stop_reason.py # 停止原因文案
|
||||
@@ -55,12 +57,13 @@ real_browser_rpa/
|
||||
## 开发新 skill 时如何复制
|
||||
|
||||
1. **先阅读**本 README 及下方「Copy map」「复制边界」章节
|
||||
2. 按 copy map **选择性复制**到新技能 `scripts/`(**不要**整包照搬 `examples/real_browser_rpa/`)
|
||||
3. 修改 `util/constants.py` 中的 `TARGET_PLATFORM`、`DEFAULT_START_URL`、selector 常量
|
||||
4. 替换 `task_rpa.py` 中的登录态 marker、搜索框/结果解析逻辑(**必须 F12 实测**)
|
||||
5. 替换 `human_verification.py` 中的验证码 selector(若目标站点 DOM 不同)
|
||||
6. 在 `task_service.py` 中接入真实 DB、任务日志、RpaVideoSession
|
||||
7. 在新技能根目录补齐 `scripts/main.py` CLI;`SKILL.md` 与根 `README.md` 按模板主目录规则编写,**不要**复制本 README
|
||||
2. **先完成元素工程**:阅读 `development/RPA_ELEMENT_LOCATING.md` 与 `development/RPA_BROWSER.md`,在真实站用 Pick Locator/F12 拾取并填写 element map
|
||||
3. 按 copy map **选择性复制**到新技能 `scripts/`(**不要**整包照搬 `examples/real_browser_rpa/`)
|
||||
4. 修改 `util/constants.py` 中的 `TARGET_PLATFORM`、`DEFAULT_START_URL`、selector 常量
|
||||
5. 替换 `task_rpa.py` 中的登录态 marker、搜索框/结果解析逻辑(**必须 F12 实测**)
|
||||
6. 替换 `human_verification.py` 中的验证码 selector(若目标站点 DOM 不同)
|
||||
7. 在 `task_service.py` 中接入真实 DB、任务日志、RpaVideoSession
|
||||
8. 在新技能根目录补齐 `scripts/main.py` CLI;`SKILL.md` 与根 `README.md` 按模板主目录规则编写,**不要**复制本 README
|
||||
|
||||
## Copy map(复制到新技能 `scripts/`)
|
||||
|
||||
@@ -73,6 +76,7 @@ real_browser_rpa/
|
||||
| `examples/real_browser_rpa/scripts/service/task_service.py` | `scripts/service/task_service.py` | 参考并合并编排层 |
|
||||
| `examples/real_browser_rpa/scripts/util/constants.py` | `scripts/util/constants.py` | 平台常量与 selector(须替换) |
|
||||
| `examples/real_browser_rpa/scripts/util/logging.py` | `scripts/util/logging.py` | 可选,按需合并 |
|
||||
| `examples/real_browser_rpa/assets/element-map.sample.yaml` | `assets/<platform>-element-map.yaml` | 真实 Pick Locator/F12 拾取后填写 |
|
||||
| `examples/real_browser_rpa/tests/` | `tests/` | 可按需复制测试思路 |
|
||||
| `examples/real_browser_rpa/README.md` | — | **只读参考**,不复制为新技能根 README |
|
||||
|
||||
@@ -121,6 +125,8 @@ real_browser_rpa/
|
||||
- 不要照抄平台名、slug、logger 名
|
||||
- 不要依赖 `D:\OpenClaw\client-commons\account-manager\scripts\main.py` 等开发机绝对路径作为生产依赖
|
||||
- 不要把示例里的 result selector 当成真实站点 selector
|
||||
- 不要复制“扫描所有 input 再按宽度/位置打分”的诊断式猜测作为生产主定位器
|
||||
- 不要用 `.first` / `.nth` 绕过多匹配;关键动作必须先解决唯一性
|
||||
- 不要把示例代码原样发布为业务 skill
|
||||
- 不要把真实账号、手机号、cookie、token 写入代码或 README
|
||||
|
||||
|
||||
45
examples/real_browser_rpa/assets/element-map.sample.yaml
Normal file
45
examples/real_browser_rpa/assets/element-map.sample.yaml
Normal file
@@ -0,0 +1,45 @@
|
||||
surface: browser
|
||||
status: placeholder_requires_real_site_inspection
|
||||
elements:
|
||||
- logical_name: search_input
|
||||
purpose: 输入搜索关键词
|
||||
scope:
|
||||
kind: main
|
||||
business_container: 待真实拾取
|
||||
primary_locator:
|
||||
strategy: role_or_label
|
||||
value: 待真实拾取
|
||||
fallback_locators: []
|
||||
expected_count: 1
|
||||
ready_condition:
|
||||
visible: true
|
||||
enabled: true
|
||||
postcondition:
|
||||
description: 搜索结果容器出现
|
||||
verified_target_version: 待填写
|
||||
|
||||
- logical_name: search_button
|
||||
purpose: 提交搜索
|
||||
scope:
|
||||
logical_parent: search_form
|
||||
primary_locator:
|
||||
strategy: role
|
||||
value: button,name=搜索
|
||||
fallback_locators: []
|
||||
expected_count: 1
|
||||
postcondition:
|
||||
description: 结果页或业务错误状态出现
|
||||
verified_target_version: 待填写
|
||||
|
||||
- logical_name: result_container
|
||||
purpose: 承载搜索结果
|
||||
scope:
|
||||
kind: main
|
||||
primary_locator:
|
||||
strategy: stable_business_container
|
||||
value: 待真实拾取
|
||||
fallback_locators: []
|
||||
expected_count: 1
|
||||
postcondition:
|
||||
description: 列表项、空结果或结束标记可区分
|
||||
verified_target_version: 待填写
|
||||
@@ -188,42 +188,22 @@ async def _ensure_logged_in(page, *, wait_sec: int) -> None:
|
||||
async def _find_search_input(page) -> Tuple[Optional[Any], str]:
|
||||
for sel in SEARCH_INPUT_SELECTORS:
|
||||
try:
|
||||
loc = page.locator(sel).first
|
||||
if await _visible(loc):
|
||||
loc = page.locator(sel)
|
||||
count = await loc.count()
|
||||
if count == 1 and await _visible(loc):
|
||||
return loc, sel
|
||||
if count > 1:
|
||||
logger.warning("search_input_ambiguous selector=%s count=%s", sel, count)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
candidates: List[Tuple[int, Any, str]] = []
|
||||
try:
|
||||
inputs = await page.locator("input").all()
|
||||
except Exception:
|
||||
inputs = []
|
||||
for inp in inputs:
|
||||
try:
|
||||
if not await inp.is_visible():
|
||||
continue
|
||||
box = await inp.bounding_box()
|
||||
if not box or box.get("width", 0) < 120:
|
||||
continue
|
||||
placeholder = await inp.get_attribute("placeholder") or ""
|
||||
score = box.get("width", 0)
|
||||
if "搜索" in placeholder:
|
||||
score += 500
|
||||
if box.get("y", 9999) < 200:
|
||||
score += 200
|
||||
candidates.append((score, inp, f"input placeholder={placeholder!r}"))
|
||||
except Exception:
|
||||
continue
|
||||
if candidates:
|
||||
candidates.sort(key=lambda x: x[0], reverse=True)
|
||||
_, inp, sel = candidates[0]
|
||||
return inp, sel
|
||||
|
||||
try:
|
||||
ph = page.get_by_placeholder(re.compile("搜索"))
|
||||
if await ph.count() > 0 and await ph.first.is_visible():
|
||||
return ph.first, "placeholder=搜索"
|
||||
count = await ph.count()
|
||||
if count == 1 and await ph.is_visible():
|
||||
return ph, "placeholder=搜索"
|
||||
if count > 1:
|
||||
logger.warning("search_input_ambiguous selector=placeholder_search count=%s", count)
|
||||
except Exception:
|
||||
pass
|
||||
return None, ""
|
||||
@@ -232,14 +212,24 @@ async def _find_search_input(page) -> Tuple[Optional[Any], str]:
|
||||
async def _find_search_button(page):
|
||||
try:
|
||||
btn = page.get_by_role("button", name="搜索")
|
||||
if await btn.count() > 0 and await btn.first.is_visible():
|
||||
return btn.first
|
||||
count = await btn.count()
|
||||
if count == 1 and await btn.is_visible():
|
||||
return btn
|
||||
if count > 1:
|
||||
logger.warning("search_button_ambiguous selector=role_button_search count=%s", count)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
btn = page.locator(SEARCH_BUTTON_SELECTOR).first
|
||||
if await btn.count() > 0 and await btn.is_visible():
|
||||
btn = page.locator(SEARCH_BUTTON_SELECTOR)
|
||||
count = await btn.count()
|
||||
if count == 1 and await btn.is_visible():
|
||||
return btn
|
||||
if count > 1:
|
||||
logger.warning(
|
||||
"search_button_ambiguous selector=%s count=%s",
|
||||
SEARCH_BUTTON_SELECTOR,
|
||||
count,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
135
examples/real_desktop_rpa/README.md
Normal file
135
examples/real_desktop_rpa/README.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# Real Windows Desktop RPA Example
|
||||
|
||||
这是从已跑通业务技能 `D:\OpenClaw\client-marketing\add-friend-wecom` 提炼的 Windows UIA 参考案例。它用于教技术人员和 AI 编程工具如何从真实拾取结果得到稳定定位器,并完成窗口、控件、动作、等待和诊断闭环。
|
||||
|
||||
## 状态
|
||||
|
||||
- **技术路线已真实验证**:Windows UIA + Python `uiautomation`
|
||||
- **来源业务已真实跑通**:企业微信添加客户
|
||||
- **本目录默认安全模式**:只提供 probe、dry-run、assisted 范式,不默认发送真实邀请
|
||||
- **不是完整 skill 仓库**:按 Copy map 选择性复制
|
||||
|
||||
## 先读
|
||||
|
||||
1. [`../../development/RPA_ELEMENT_LOCATING.md`](../../development/RPA_ELEMENT_LOCATING.md)
|
||||
2. [`../../development/RPA_DESKTOP.md`](../../development/RPA_DESKTOP.md)
|
||||
3. 本 README 的“原始拾取到生产定位器”
|
||||
|
||||
## 成功案例验证了什么
|
||||
|
||||
```text
|
||||
枚举目标顶层窗口
|
||||
→ 按进程/窗口类/标题确认作用域
|
||||
→ 从 HWND 获取 UIA root
|
||||
→ 在父容器中定位元素
|
||||
→ 激活窗口并真实点击/输入
|
||||
→ 等待子窗口或业务结果
|
||||
→ 处理独立/嵌套弹窗
|
||||
→ probe-ui 诊断
|
||||
```
|
||||
|
||||
来源案例还提供 `dry-run / assisted / auto` 三档、频率控制和业务记忆。本 example 只提炼 UIA 元素工程;账号、风控和数据库仍由具体业务技能实现。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
real_desktop_rpa/
|
||||
README.md
|
||||
assets/
|
||||
element-map.sample.yaml
|
||||
selector-capture.sample.xml
|
||||
scripts/
|
||||
service/
|
||||
desktop_session.py
|
||||
uia_locator.py
|
||||
wecom_reference_flow.py
|
||||
util/
|
||||
selectors.py
|
||||
tests/
|
||||
test_selector_contract.py
|
||||
```
|
||||
|
||||
## 原始拾取到生产定位器
|
||||
|
||||
来源案例的影刀 `selectorsV2` 导出通常包含:
|
||||
|
||||
```text
|
||||
Window(class=SearchExternalsWnd,title=添加客户)
|
||||
→ 多层 Layout/index
|
||||
→ search_layout
|
||||
→ searchedit
|
||||
```
|
||||
|
||||
正确提炼方式:
|
||||
|
||||
1. 保留稳定顶层窗口:进程、Window Class、标题。
|
||||
2. 保留最小业务父容器:如 `search_layout`。
|
||||
3. 使用叶子元素稳定属性:如 `Name=searchedit`。
|
||||
4. 删除只反映当前布局的深层 `index`。
|
||||
5. 验证父容器内候选数量为 `1`。
|
||||
6. 重启应用后再次验证。
|
||||
|
||||
不要把完整 XML 路径或 `item[0]` 原样变成生产定位器。
|
||||
|
||||
## Element map
|
||||
|
||||
本目录的 [`assets/element-map.sample.yaml`](assets/element-map.sample.yaml) 只展示来源案例的元素登记方法。复制后必须重新拾取目标软件并全部替换。
|
||||
|
||||
关键规则:
|
||||
|
||||
- 窗口不唯一时失败,不取 `hwnds[0]`。
|
||||
- 控件不唯一时失败,不取第一个匹配。
|
||||
- `item`、`status_layout`、`wbtn_container` 等通用 Name 必须放进稳定父容器。
|
||||
- 行顺序不是身份;用业务文案或稳定属性选择结果行。
|
||||
- 只有主定位成功、窗口前台且矩形有效时,才允许从 UIA BoundingRectangle 降级坐标点击。
|
||||
|
||||
## 三种实施模式
|
||||
|
||||
| 模式 | 行为 | 用途 |
|
||||
|---|---|---|
|
||||
| `probe` | 只检查依赖、窗口和关键控件 | 安装、自检、版本变更排障 |
|
||||
| `dry-run` | 导航并填写,提交前停止 | 开发和验收默认 |
|
||||
| `assisted` | 填写后等待用户人工确认 | 不可逆动作或现场联调 |
|
||||
|
||||
业务技能若增加 `auto`,必须实现明确确认策略、频率限制、业务后置状态和 `RESULT_UNCONFIRMED`。
|
||||
|
||||
## Copy map
|
||||
|
||||
| 来源 | 目标 | 说明 |
|
||||
|---|---|---|
|
||||
| `scripts/service/desktop_session.py` | `scripts/service/desktop_session.py` | 顶层窗口枚举、唯一性、激活 |
|
||||
| `scripts/service/uia_locator.py` | `scripts/service/uia_locator.py` | 作用域内唯一定位、点击、输入 |
|
||||
| `scripts/util/selectors.py` | `scripts/util/<platform>_selectors.py` | element spec;必须全部替换 |
|
||||
| `scripts/service/wecom_reference_flow.py` | `scripts/service/<business>_flow.py` | 只复制流程分层,不复制企微 selector |
|
||||
| `assets/element-map.sample.yaml` | `assets/<platform>-element-map.yaml` | 真实拾取后填写 |
|
||||
| `tests/test_selector_contract.py` | `tests/test_selector_contract.py` | 定位器契约测试思路 |
|
||||
|
||||
## 必须补齐的业务能力
|
||||
|
||||
- 目标应用安装/版本 doctor
|
||||
- 真实业务元素清单
|
||||
- 失败截图、UIA树和顶层窗口快照
|
||||
- Activity/RpaVideoSession 步骤
|
||||
- 不可逆动作确认
|
||||
- 业务成功/失败/结果不明确状态
|
||||
- 频率限制、幂等和断点续跑
|
||||
- 真实集成测试(默认不进 CI)
|
||||
|
||||
## 禁止照抄
|
||||
|
||||
- `WXWork`、`WeWorkWindow`、`SearchExternalsWnd`
|
||||
- `org_op_new`、`searchedit`、`wbtn_container`
|
||||
- 企业微信列表第 0/1 行含义
|
||||
- 来源案例的本机路径、账号和业务数据
|
||||
- 任何未经目标软件真实拾取验证的 fallback
|
||||
|
||||
## 验收
|
||||
|
||||
- `probe` 能明确报告依赖、窗口和关键元素
|
||||
- 每个关键元素在目标作用域内唯一
|
||||
- 关闭重开软件后仍能定位
|
||||
- 多窗口时不会误选
|
||||
- UI变化时停止并保存截图/UIA树
|
||||
- dry-run 不执行不可逆提交
|
||||
- assisted 清楚提示用户确认
|
||||
- 提交后仅在业务后置状态成立时报告成功
|
||||
64
examples/real_desktop_rpa/assets/element-map.sample.yaml
Normal file
64
examples/real_desktop_rpa/assets/element-map.sample.yaml
Normal file
@@ -0,0 +1,64 @@
|
||||
source_case: add-friend-wecom
|
||||
status: verified_source_redacted_example
|
||||
surface: windows
|
||||
driver: uiautomation
|
||||
elements:
|
||||
- logical_name: main_window
|
||||
purpose: 企业微信主窗口
|
||||
scope:
|
||||
process_name: WXWork
|
||||
primary_locator:
|
||||
window_class: WeWorkWindow
|
||||
title: 企业微信
|
||||
expected_count: 1
|
||||
postcondition: UIA root 可读取
|
||||
replace_in_new_skill: true
|
||||
|
||||
- logical_name: contacts_entry
|
||||
purpose: 打开通讯录
|
||||
scope:
|
||||
logical_parent: main_window
|
||||
primary_locator:
|
||||
name: org_op_new
|
||||
control_type: TreeItem
|
||||
expected_count: 1
|
||||
postcondition: 通讯录业务面板出现
|
||||
replace_in_new_skill: true
|
||||
|
||||
- logical_name: add_customer_window
|
||||
purpose: 添加客户窗口
|
||||
scope:
|
||||
process_name: WXWork
|
||||
primary_locator:
|
||||
window_class: SearchExternalsWnd
|
||||
title: 添加客户
|
||||
expected_count: 1
|
||||
postcondition: search_input 可定位
|
||||
replace_in_new_skill: true
|
||||
|
||||
- logical_name: search_input
|
||||
purpose: 输入待搜索账号
|
||||
scope:
|
||||
logical_parent: add_customer_window
|
||||
ancestor_name: search_layout
|
||||
primary_locator:
|
||||
name: searchedit
|
||||
expected_count: 1
|
||||
ready_condition:
|
||||
visible: true
|
||||
enabled: true
|
||||
postcondition: 结果列表或用户不存在弹窗出现
|
||||
replace_in_new_skill: true
|
||||
|
||||
- logical_name: verify_message_input
|
||||
purpose: 填写添加验证语
|
||||
scope:
|
||||
window_class: InputReasonWnd
|
||||
title_any:
|
||||
- 输入认证信息
|
||||
- 发送添加邀请
|
||||
primary_locator:
|
||||
name: name_edit
|
||||
expected_count: 1
|
||||
postcondition: 输入值可读取或发送按钮可用
|
||||
replace_in_new_skill: true
|
||||
12
examples/real_desktop_rpa/assets/selector-capture.sample.xml
Normal file
12
examples/real_desktop_rpa/assets/selector-capture.sample.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
脱敏教学摘录:展示“拾取器长路径”而非可直接复制的生产 selector。
|
||||
生产代码应保留稳定窗口、业务父容器和叶子属性,删除无业务含义的深层 index。
|
||||
-->
|
||||
<selector name="search_input" source="inspector-export">
|
||||
<window process="WXWork" class="SearchExternalsWnd" title="添加客户" />
|
||||
<uia name="content_container" index="1" />
|
||||
<uia name="search_layout" index="0" />
|
||||
<uia name="inner_search_layout" index="1" />
|
||||
<uia name="searchedit" index="1" />
|
||||
</selector>
|
||||
111
examples/real_desktop_rpa/scripts/service/desktop_session.py
Normal file
111
examples/real_desktop_rpa/scripts/service/desktop_session.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""Windows 顶层窗口作用域:枚举、唯一性校验和激活。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import ntpath
|
||||
from ctypes import wintypes
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable
|
||||
|
||||
from util.selectors import WindowSpec
|
||||
|
||||
|
||||
class DesktopRpaError(RuntimeError):
|
||||
def __init__(self, code: str, message: str) -> None:
|
||||
super().__init__(f"ERROR:{code} {message}")
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WindowCandidate:
|
||||
hwnd: int
|
||||
pid: int
|
||||
process_name: str
|
||||
class_name: str
|
||||
title: str
|
||||
visible: bool
|
||||
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
kernel32 = ctypes.windll.kernel32
|
||||
SW_RESTORE = 9
|
||||
PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
|
||||
|
||||
|
||||
def _process_name(pid: int) -> str:
|
||||
handle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, False, pid)
|
||||
if not handle:
|
||||
return ""
|
||||
try:
|
||||
size = wintypes.DWORD(32768)
|
||||
buffer = ctypes.create_unicode_buffer(size.value)
|
||||
if not kernel32.QueryFullProcessImageNameW(
|
||||
handle,
|
||||
0,
|
||||
buffer,
|
||||
ctypes.byref(size),
|
||||
):
|
||||
return ""
|
||||
return ntpath.splitext(ntpath.basename(buffer.value))[0]
|
||||
finally:
|
||||
kernel32.CloseHandle(handle)
|
||||
|
||||
|
||||
def enumerate_top_windows() -> list[WindowCandidate]:
|
||||
found: list[WindowCandidate] = []
|
||||
|
||||
@ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HWND, wintypes.LPARAM)
|
||||
def callback(hwnd, _lparam):
|
||||
pid = wintypes.DWORD()
|
||||
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
||||
cls = ctypes.create_unicode_buffer(256)
|
||||
user32.GetClassNameW(hwnd, cls, 256)
|
||||
length = user32.GetWindowTextLengthW(hwnd)
|
||||
title = ctypes.create_unicode_buffer(length + 1)
|
||||
user32.GetWindowTextW(hwnd, title, length + 1)
|
||||
found.append(
|
||||
WindowCandidate(
|
||||
hwnd=int(hwnd),
|
||||
pid=int(pid.value),
|
||||
process_name=_process_name(int(pid.value)),
|
||||
class_name=cls.value,
|
||||
title=title.value,
|
||||
visible=bool(user32.IsWindowVisible(hwnd)),
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
user32.EnumWindows(callback, 0)
|
||||
return found
|
||||
|
||||
|
||||
def resolve_unique_window(
|
||||
spec: WindowSpec,
|
||||
*,
|
||||
candidates: Iterable[WindowCandidate] | None = None,
|
||||
) -> WindowCandidate:
|
||||
pool = list(candidates) if candidates is not None else enumerate_top_windows()
|
||||
matched = [
|
||||
item
|
||||
for item in pool
|
||||
if item.visible
|
||||
and item.process_name.casefold() == spec.process_name.casefold()
|
||||
and item.class_name == spec.class_name
|
||||
and (spec.title is None or item.title == spec.title)
|
||||
]
|
||||
if not matched:
|
||||
raise DesktopRpaError("WINDOW_NOT_FOUND", f"{spec.logical_name} 未找到")
|
||||
if len(matched) != 1:
|
||||
raise DesktopRpaError(
|
||||
"ELEMENT_AMBIGUOUS",
|
||||
f"{spec.logical_name} 匹配 {len(matched)} 个窗口,请按 PID/账号补充作用域",
|
||||
)
|
||||
return matched[0]
|
||||
|
||||
|
||||
def activate_window(window: WindowCandidate) -> None:
|
||||
user32.ShowWindow(window.hwnd, SW_RESTORE)
|
||||
if not user32.SetForegroundWindow(window.hwnd):
|
||||
raise DesktopRpaError("WINDOW_ACTIVATE_FAILED", f"无法激活 hwnd={window.hwnd}")
|
||||
96
examples/real_desktop_rpa/scripts/service/uia_locator.py
Normal file
96
examples/real_desktop_rpa/scripts/service/uia_locator.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""作用域内 UIA 唯一定位、真实点击和逐字输入。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
from typing import Any, Iterator
|
||||
|
||||
from service.desktop_session import DesktopRpaError, WindowCandidate, activate_window
|
||||
from util.selectors import ElementSpec
|
||||
|
||||
|
||||
def require_uiautomation():
|
||||
try:
|
||||
import uiautomation as auto
|
||||
except ImportError as exc:
|
||||
raise DesktopRpaError("UIA_UNAVAILABLE", "缺少 uiautomation 依赖") from exc
|
||||
return auto
|
||||
|
||||
|
||||
def root_from_window(window: WindowCandidate):
|
||||
auto = require_uiautomation()
|
||||
return auto.ControlFromHandle(window.hwnd)
|
||||
|
||||
|
||||
def _walk(root: Any, *, depth: int = 0, max_depth: int = 25) -> Iterator[Any]:
|
||||
if depth > max_depth:
|
||||
return
|
||||
yield root
|
||||
if depth == max_depth:
|
||||
return
|
||||
try:
|
||||
children = root.GetChildren()
|
||||
except Exception:
|
||||
children = []
|
||||
for child in children:
|
||||
yield from _walk(child, depth=depth + 1, max_depth=max_depth)
|
||||
|
||||
|
||||
def _matches(control: Any, spec: ElementSpec) -> bool:
|
||||
if spec.name is not None and (getattr(control, "Name", None) or "") != spec.name:
|
||||
return False
|
||||
if spec.automation_id is not None and (
|
||||
getattr(control, "AutomationId", None) or ""
|
||||
) != spec.automation_id:
|
||||
return False
|
||||
if spec.control_type is not None and (
|
||||
(getattr(control, "ControlTypeName", None) or "").removesuffix("Control")
|
||||
!= spec.control_type.removesuffix("Control")
|
||||
):
|
||||
return False
|
||||
if spec.class_name is not None and (
|
||||
getattr(control, "ClassName", None) or ""
|
||||
) != spec.class_name:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def locate_unique(root: Any, spec: ElementSpec, *, max_depth: int = 25):
|
||||
search_root = root
|
||||
if spec.ancestor_name:
|
||||
ancestor_spec = ElementSpec(
|
||||
logical_name=f"{spec.logical_name}.ancestor",
|
||||
name=spec.ancestor_name,
|
||||
)
|
||||
search_root = locate_unique(root, ancestor_spec, max_depth=max_depth)
|
||||
|
||||
matched = [control for control in _walk(search_root, max_depth=max_depth) if _matches(control, spec)]
|
||||
if not matched:
|
||||
raise DesktopRpaError("ELEMENT_NOT_FOUND", f"{spec.logical_name} 未找到")
|
||||
if len(matched) != spec.expected_count:
|
||||
raise DesktopRpaError(
|
||||
"ELEMENT_AMBIGUOUS",
|
||||
f"{spec.logical_name} 期望 {spec.expected_count} 个,实际 {len(matched)} 个",
|
||||
)
|
||||
return matched[0]
|
||||
|
||||
|
||||
def click_control(control: Any, *, window: WindowCandidate) -> None:
|
||||
activate_window(window)
|
||||
rect = control.BoundingRectangle
|
||||
if rect.width() <= 0 or rect.height() <= 0 or rect.left < -10000:
|
||||
raise DesktopRpaError("ELEMENT_NOT_READY", "目标元素不可见或边界矩形无效")
|
||||
control.Click(
|
||||
x=random.randint(-2, 2),
|
||||
y=random.randint(-2, 2),
|
||||
simulateMove=True,
|
||||
)
|
||||
|
||||
|
||||
def type_text(control: Any, text: str, *, window: WindowCandidate) -> None:
|
||||
auto = require_uiautomation()
|
||||
click_control(control, window=window)
|
||||
auto.SendKeys("{Ctrl}a", waitTime=0.05)
|
||||
auto.SendKeys("{Delete}", waitTime=0.05)
|
||||
for char in text:
|
||||
auto.SendKeys(char, waitTime=random.uniform(0.04, 0.12))
|
||||
@@ -0,0 +1,37 @@
|
||||
"""企业微信成功案例的安全提炼:probe 和 dry-run,不默认发送邀请。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
from service.desktop_session import resolve_unique_window
|
||||
from service.uia_locator import locate_unique, root_from_window, type_text
|
||||
from util.selectors import ADD_CUSTOMER_WINDOW, CONTACTS_ENTRY, MAIN_WINDOW, SEARCH_INPUT
|
||||
|
||||
|
||||
def probe() -> dict[str, Any]:
|
||||
main = resolve_unique_window(MAIN_WINDOW)
|
||||
root = root_from_window(main)
|
||||
contacts = locate_unique(root, CONTACTS_ENTRY)
|
||||
return {
|
||||
"success": True,
|
||||
"main_window": asdict(main),
|
||||
"contacts_entry": {
|
||||
"name": getattr(contacts, "Name", ""),
|
||||
"control_type": getattr(contacts, "ControlTypeName", ""),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def fill_search_dry_run(target: str) -> dict[str, Any]:
|
||||
"""要求操作者已打开“添加客户”窗口;只填写搜索框,不提交。"""
|
||||
search_window = resolve_unique_window(ADD_CUSTOMER_WINDOW)
|
||||
root = root_from_window(search_window)
|
||||
search_input = locate_unique(root, SEARCH_INPUT)
|
||||
type_text(search_input, target, window=search_window)
|
||||
return {
|
||||
"success": True,
|
||||
"mode": "dry-run",
|
||||
"message": "已填写搜索条件,未发送邀请",
|
||||
}
|
||||
44
examples/real_desktop_rpa/scripts/util/selectors.py
Normal file
44
examples/real_desktop_rpa/scripts/util/selectors.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""脱敏 UIA element specs;复制到新技能后必须用真实拾取结果替换。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WindowSpec:
|
||||
logical_name: str
|
||||
process_name: str
|
||||
class_name: str
|
||||
title: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ElementSpec:
|
||||
logical_name: str
|
||||
name: Optional[str] = None
|
||||
automation_id: Optional[str] = None
|
||||
control_type: Optional[str] = None
|
||||
class_name: Optional[str] = None
|
||||
ancestor_name: Optional[str] = None
|
||||
expected_count: int = 1
|
||||
|
||||
|
||||
MAIN_WINDOW = WindowSpec("main_window", "WXWork", "WeWorkWindow", "企业微信")
|
||||
ADD_CUSTOMER_WINDOW = WindowSpec(
|
||||
"add_customer_window",
|
||||
"WXWork",
|
||||
"SearchExternalsWnd",
|
||||
"添加客户",
|
||||
)
|
||||
CONTACTS_ENTRY = ElementSpec(
|
||||
"contacts_entry",
|
||||
name="org_op_new",
|
||||
control_type="TreeItem",
|
||||
)
|
||||
SEARCH_INPUT = ElementSpec(
|
||||
"search_input",
|
||||
name="searchedit",
|
||||
ancestor_name="search_layout",
|
||||
)
|
||||
44
examples/real_desktop_rpa/tests/test_selector_contract.py
Normal file
44
examples/real_desktop_rpa/tests/test_selector_contract.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
|
||||
from service.desktop_session import ( # noqa: E402
|
||||
DesktopRpaError,
|
||||
WindowCandidate,
|
||||
resolve_unique_window,
|
||||
)
|
||||
from util.selectors import MAIN_WINDOW # noqa: E402
|
||||
|
||||
|
||||
class SelectorContractTest(unittest.TestCase):
|
||||
def test_unique_window_is_required(self) -> None:
|
||||
candidate = WindowCandidate(1, 10, "WXWork", "WeWorkWindow", "企业微信", True)
|
||||
self.assertEqual(
|
||||
resolve_unique_window(MAIN_WINDOW, candidates=[candidate]),
|
||||
candidate,
|
||||
)
|
||||
|
||||
def test_duplicate_windows_fail_instead_of_taking_first(self) -> None:
|
||||
candidates = [
|
||||
WindowCandidate(1, 10, "WXWork", "WeWorkWindow", "企业微信", True),
|
||||
WindowCandidate(2, 11, "WXWork", "WeWorkWindow", "企业微信", True),
|
||||
]
|
||||
with self.assertRaises(DesktopRpaError) as ctx:
|
||||
resolve_unique_window(MAIN_WINDOW, candidates=candidates)
|
||||
self.assertEqual(ctx.exception.code, "ELEMENT_AMBIGUOUS")
|
||||
|
||||
def test_hidden_window_is_not_a_candidate(self) -> None:
|
||||
hidden = WindowCandidate(1, 10, "WXWork", "WeWorkWindow", "企业微信", False)
|
||||
with self.assertRaises(DesktopRpaError) as ctx:
|
||||
resolve_unique_window(MAIN_WINDOW, candidates=[hidden])
|
||||
self.assertEqual(ctx.exception.code, "WINDOW_NOT_FOUND")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -18,6 +18,7 @@
|
||||
- **真实第三方网站 + 高风控 + 人工验证码** → 优先看 [`../real_browser_rpa/README.md`](../real_browser_rpa/README.md)
|
||||
- 目标系统有稳定 **官方 API** → 不要硬写浏览器 RPA
|
||||
- **不要把本示例的业务字段、selector、sandbox URL 原样复制**到你的 skill
|
||||
- 复制前先读 `development/RPA_ELEMENT_LOCATING.md` 与 `development/RPA_BROWSER.md`;即使 DOM 可控也要定义作用域、唯一性和后置状态
|
||||
|
||||
## 目录结构
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。"""
|
||||
|
||||
SKILL_SLUG = "your-skill-slug"
|
||||
SKILL_VERSION = "1.0.58"
|
||||
SKILL_VERSION = "1.0.59"
|
||||
LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug"
|
||||
PLATFORM_KIT_MIN_VERSION = "1.2.2"
|
||||
|
||||
@@ -498,12 +498,60 @@ class TestDocsStandards(unittest.TestCase):
|
||||
|
||||
def test_examples_top_level_modes_present(self) -> None:
|
||||
examples_dir = os.path.join(get_skill_root(), "examples")
|
||||
for mode in ("real_" + "api", "real_browser_rpa", "simulator_api", "simulator_browser_rpa"):
|
||||
for mode in (
|
||||
"real_" + "api",
|
||||
"real_browser_rpa",
|
||||
"simulator_api",
|
||||
"simulator_browser_rpa",
|
||||
"real_desktop_rpa",
|
||||
"mobile_rpa",
|
||||
):
|
||||
self.assertTrue(
|
||||
os.path.isdir(os.path.join(examples_dir, mode)),
|
||||
msg=f"examples/{mode}/ must exist",
|
||||
)
|
||||
|
||||
def test_three_surface_locator_standards_are_present(self) -> None:
|
||||
required = {
|
||||
"development/RPA_ELEMENT_LOCATING.md": (
|
||||
"元素清单",
|
||||
"expected_count",
|
||||
"ERROR:RESULT_UNCONFIRMED",
|
||||
"AI 编程约束",
|
||||
),
|
||||
"development/RPA_BROWSER.md": (
|
||||
"Pick Locator",
|
||||
"iframe",
|
||||
"Shadow DOM",
|
||||
"禁止模式",
|
||||
),
|
||||
"development/RPA_DESKTOP.md": (
|
||||
"uiautomation",
|
||||
"add-friend-wecom",
|
||||
"AutomationId",
|
||||
"probe-ui",
|
||||
),
|
||||
"development/RPA_MOBILE.md": (
|
||||
"Appium Inspector",
|
||||
"resourceId",
|
||||
"Native/WebView",
|
||||
"尚无团队真实成功案例",
|
||||
),
|
||||
}
|
||||
for rel, markers in required.items():
|
||||
text = self._read(rel)
|
||||
for marker in markers:
|
||||
self.assertIn(marker, text, msg=f"{rel} missing {marker!r}")
|
||||
|
||||
def test_desktop_and_mobile_examples_state_validation_level(self) -> None:
|
||||
desktop = self._read("examples/real_desktop_rpa/README.md")
|
||||
self.assertIn("技术路线已真实验证", desktop)
|
||||
self.assertIn("add-friend-wecom", desktop)
|
||||
self.assertIn("dry-run", desktop)
|
||||
mobile = self._read("examples/mobile_rpa/README.md")
|
||||
self.assertIn("不是可复制实现", mobile)
|
||||
self.assertIn("不得宣称已跑通", mobile)
|
||||
|
||||
def test_examples_are_not_full_skill_repos(self) -> None:
|
||||
examples_dir = os.path.join(get_skill_root(), "examples")
|
||||
for mode in os.listdir(examples_dir):
|
||||
|
||||
Reference in New Issue
Block a user