Compare commits

..

3 Commits

Author SHA1 Message Date
1ff70e26a5 Release v1.0.51: precipitate ensure-web, config empty-value, and login-required standards
All checks were successful
技能自动化发布 / release (push) Successful in 7s
2026-07-18 16:30:06 +08:00
0b9a8b2107 Release v1.0.50: web RPA profile/headed hard rules (POLICY-RPA-004)
All checks were successful
技能自动化发布 / release (push) Successful in 17s
2026-07-18 09:31:40 +08:00
287816aa2a docs(actions): align executionProfile mental model with host default sync (v1.0.49)
All checks were successful
技能自动化发布 / release (push) Successful in 8s
2026-07-18 08:44:16 +08:00
27 changed files with 517 additions and 140 deletions

View File

@@ -4,11 +4,14 @@ OPENCLAW_TEST_TARGET=mock # 可选mock=本地模拟simulator_
# 目标网站地址:技能要访问的网站或服务地址 # 目标网站地址:技能要访问的网站或服务地址
TARGET_BASE_URL=https://sandbox.jc2009.com # 填写完整网址(含 https://);换环境时再改,一般保持默认即可 TARGET_BASE_URL=https://sandbox.jc2009.com # 填写完整网址(含 https://);换环境时再改,一般保持默认即可
# 默认登录账号:仅填写非敏感的账号标识(如工号/登录名) # 默认登录账号:仅填写非敏感的账号标识(如工号/登录名/手机号
DEFAULT_LOGIN_ID=04110001 # 只填账号标识,不要填密码;密码请在「账号管理」中登记 DEFAULT_LOGIN_ID= # 留空=按平台自动选用或登记;有值则按该标识筛选;不要填密码
# 是否显示浏览器窗口:打开后能否看见自动化操作过程 # 默认账号编号:仅填账号管理里的纯数字主键(一般留空)
OPENCLAW_BROWSER_HEADLESS=0 # 0=显示窗口推荐便于排查1=后台静默运行 DEFAULT_ACCOUNT_ID= # 留空=自动;只填纯数字;手机号请填上方登录账号
# 是否显示浏览器窗口:网页自动化时是否弹出可见浏览器
OPENCLAW_BROWSER_HEADLESS=0 # 0=显示窗口必须便于介入与排查1=后台静默(勿作日常用法)
# 浏览器防检测:降低被网站识别为自动化工具的概率 # 浏览器防检测:降低被网站识别为自动化工具的概率
OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启推荐0=关闭;一般保持默认 OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启推荐0=关闭;一般保持默认

View File

@@ -9,6 +9,21 @@
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节 - 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段 - 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
## 1.0.51
- 沉淀网页 RPA 首启标准:业务技能默认 `ensure-web`(有则用、无则登记);登记账号 ≠ 已登录目标站;租约全忙返回占用错误且不重复建号
- 明确会话依赖型采集须声明登录 `required` 并做登录门禁;配置留空 + 行尾说明不得当成假默认值;账号可用登录标识选择,展示优先登录名
- 最低共享库版本声明上调至 1.2.2(含配置空值解析修复);说明宿主按需升级、不因源上有更新自动追新
## 1.0.50
- 沉淀网页 RPA 硬规则:必须先经账号管理取得 `profile_dir` 再开浏览器;生产有头;站点登录按平台约定(不全局强制);新增 `POLICY-RPA-004` 自动检测
- 用户教程 / 技能说明占位强调「先登记账号」需求模板增加登录策略三选一required / optional / not_needed
## 1.0.49
- 明确 Skill Action 心智:默认按 sync仅长任务 / RPA 显式 async 进任务中心;匠厂宿主省略 `executionProfile` 时按 sync新技能仍须显式声明
## 1.0.48 ## 1.0.48
- 新增配置注释硬门禁 `POLICY-CONFIG-004``.env.example` 每个配置项必须有中文行头与行尾说明,禁止开发文档指向,测试不通过则无法发布 - 新增配置注释硬门禁 `POLICY-CONFIG-004``.env.example` 每个配置项必须有中文行头与行尾说明,禁止开发文档指向,测试不通过则无法发布

View File

@@ -40,9 +40,11 @@ description: "用用户能理解的方式说明这个技能能做什么、适合
说明用户在使用前要准备的东西,例如: 说明用户在使用前要准备的东西,例如:
- 相关账号已登录,且具备查询、导出或提交权限 - **网页自动化类:** 首次运行可由技能自动登记平台账号(浏览器会话由系统隔离管理)。也可在「账号管理」里指定登录标识。登记账号 ≠ 已登录目标站
- **是否要登录目标站:** 按本技能说明(有的平台必须登录,有的公开页可不登录);与「是否有账号记录」不是一回事
- 相关业务权限(查询、导出、提交等)已具备
- 需要处理的文件、时间范围、订单号、客户名称、项目名称等信息 - 需要处理的文件、时间范围、订单号、客户名称、项目名称等信息
- 如果涉及外部系统,请确认网络、账号、验证码或审批流程可正常使用 - 如果涉及验证码或人工确认,请保证能及时在弹出的浏览器窗口中操作
--- ---

View File

@@ -1,12 +1,12 @@
--- ---
name: 技能开发模板(通用业务版) name: 技能开发模板(通用业务版)
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。" description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
version: 1.0.48 version: 1.0.51
author: 深圳匠厂科技有限公司 author: 深圳匠厂科技有限公司
metadata: metadata:
openclaw: openclaw:
slug: your-skill-slug slug: your-skill-slug
platform_kit_min_version: "1.2.0" platform_kit_min_version: "1.2.2"
emoji: "📦" emoji: "📦"
category: "通用" category: "通用"
developer_ids: developer_ids:
@@ -27,7 +27,7 @@ allowed-tools:
- **单业务内核、多入口**:数据管理 / 定时任务 / Agent / 技能详情 / 直接 CLI 必须复用同一 domain service禁止按入口复制业务逻辑禁止按 `source` 分叉业务行为。 - **单业务内核、多入口**:数据管理 / 定时任务 / Agent / 技能详情 / 直接 CLI 必须复用同一 domain service禁止按入口复制业务逻辑禁止按 `source` 分叉业务行为。
- **`placements`**:只决定 Action **出现在哪里**toolbar / cron / agent / skill-detail - **`placements`**:只决定 Action **出现在哪里**toolbar / cron / agent / skill-detail
- **`bind.tables`**:只决定数据管理出现在哪些表;含 `toolbar` 时**必须**显式非空绑定。 - **`bind.tables`**:只决定数据管理出现在哪些表;含 `toolbar` 时**必须**显式非空绑定。
- **`executionProfile`**:只决定执行方式——`sync` 当场返回结果,`async` 进**任务中心**。与 placements / 入口**无关**每个 Action **必须**显式写 `sync``async` - **`executionProfile`**:只决定执行方式——`sync` 当场返回结果,`async` 进**任务中心**。与 placements / 入口**无关**。心智上多数为 sync仅长任务 / RPA 标 async宿主省略该字段时按 sync。每个 Action **必须**显式写 `sync``async`
- **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;特殊业务报告才另做 `export-*` - **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;特殊业务报告才另做 `export-*`
- **准备 vs 执行(适用则拆)**:若技能存在「先写入/准备本地数据」与「再对外产生副作用」两段Agent 必须能分开触发;禁止默认绑成不可拆的一步。一键无准备态的技能可跳过本条。 - **准备 vs 执行(适用则拆)**:若技能存在「先写入/准备本地数据」与「再对外产生副作用」两段Agent 必须能分开触发;禁止默认绑成不可拆的一步。一键无准备态的技能可跳过本条。
@@ -74,7 +74,7 @@ python {baseDir}/scripts/main.py config-path
python {baseDir}/scripts/main.py init-db python {baseDir}/scripts/main.py init-db
``` ```
- 公共库来自共享 venv 的 `jiangchang-platform-kit>=1.2.0`(包名路径 `jiangchang_skill_core`**不要 vendor** `scripts/jiangchang_skill_core/` - 公共库来自共享 venv 的 `jiangchang-platform-kit>=1.2.2`(包名路径 `jiangchang_skill_core`**不要 vendor** `scripts/jiangchang_skill_core/`
- 根目录 `requirements.txt` **只声明技能特有**依赖;不要写 `jiangchang-platform-kit` / `playwright` - 根目录 `requirements.txt` **只声明技能特有**依赖;不要写 `jiangchang-platform-kit` / `playwright`
- `metadata.openclaw.platform_kit_min_version` 是宿主兼容声明,**不是** pip 依赖。 - `metadata.openclaw.platform_kit_min_version` 是宿主兼容声明,**不是** pip 依赖。
- `health``collect_runtime_diagnostics` 做只读诊断。 - `health``collect_runtime_diagnostics` 做只读诊断。

View File

@@ -16,7 +16,9 @@
1. 已安装并登录匠厂客户端 1. 已安装并登录匠厂客户端
2. 已在技能市场安装本技能(或更新到最新版) 2. 已在技能市场安装本技能(或更新到最新版)
3.如需】相关账号已登录、权限可用;需要的文件 / 时间范围 / 单号等已准备好 3.网页自动化类技能】首次运行一般会自动登记本平台账号;也可在「账号管理」中指定登录标识。浏览器会话由系统隔离管理——**不要**用本机日常浏览器用户目录去跑真实站
4. 【按平台】若本技能说明「需要先登录目标站」,请在弹出的有头浏览器中按提示登录或扫码;「登记账号」不等于「已登录目标站」。若说明「可不登录也能用」,仍须有可用的账号会话目录
5. 【如需】需要的文件 / 时间范围 / 单号等已准备好
--- ---

View File

@@ -100,7 +100,9 @@ def get_adapter():
2. **普通兄弟技能调用**,优先走统一 **`service.sibling_bridge`**`call_sibling_json`**不要**在 `task_service.py``task_rpa.py` 等业务流程文件中到处散落 `subprocess.run` 2. **普通兄弟技能调用**,优先走统一 **`service.sibling_bridge`**`call_sibling_json`**不要**在 `task_service.py``task_rpa.py` 等业务流程文件中到处散落 `subprocess.run`
3. **account-manager 账号/租约能力**是例外:可参考 `examples/real_browser_rpa/scripts/service/account_client.py` 与 `examples/simulator_browser_rpa/scripts/service/account_client.py`,封装为**单一** `account_client.py`;允许在该文件内部集中通过 subprocess 调 account-manager CLI。**禁止** import `rpa_helpers` 等 account-manager 内部模块;`simulator_rpa``real_rpa` 均走同一 `pick_web_account` / `release_lease` 模式。 3. **account-manager 账号/租约能力**是例外:可参考 `examples/real_browser_rpa/scripts/service/account_client.py` 与 `examples/simulator_browser_rpa/scripts/service/account_client.py`,封装为**单一** `account_client.py`;允许在该文件内部集中通过 subprocess 调 account-manager CLI。**禁止** import `rpa_helpers` 等 account-manager 内部模块;`simulator_rpa``real_rpa` 均走同一 `pick_web_account` / `release_lease` 模式。
4. **不允许**直接 `import account-manager` 的内部 Python 模块(如 `service/``util/``db/`)。 4. **不允许**直接 `import account-manager` 的内部 Python 模块(如 `service/``util/``db/`)。
5. **pick lease 后必须 `finally release lease`**;进程被 kill 后可能残留 lease需在运维文档说明排查方式查 account-manager lease 列表 / 手动释放)。 5. **pick/ensure lease 后必须 `finally release lease`**;进程被 kill 后可能残留 lease需在运维文档说明排查方式查 account-manager lease 列表 / 手动释放)。
6. **网页 RPA 硬规则**(详见 [`RPA.md`](RPA.md) §0.2):必须先经 `pick_web_account`(底层 **`ensure-web` / `pick-web --ensure`**)拿到 `profile_dir` 再开浏览器;拿不到则失败退出;生产有头;**是否站点登录**按平台约定,但**不等于**可以跳过 account-manager。登记账号 ≠ 站点已登录。
7. **账号选择优先级**:纯数字 `DEFAULT_ACCOUNT_ID``get <id>``DEFAULT_LOGIN_ID` / 非数字标识 → `ensure-web --login-id`;皆空 → 按平台 `ensure-web`。人读展示优先 `login_id` / `account_label`,不要只用数字 id。多条匹配 → `ERROR:AMBIGUOUS_ACCOUNT`
```python ```python
# 普通兄弟技能 — 走 sibling_bridge # 普通兄弟技能 — 走 sibling_bridge
@@ -110,10 +112,10 @@ result = call_sibling_json("account-manager", ["list", "--limit", "10"])
``` ```
```python ```python
# account-manager 账号/租约 — 集中在 account_client.py # account-manager 账号/租约 — 集中在 account_client.pyensure-web
from service.account_client import pick_web_account, release_lease from service.account_client import pick_web_account, release_lease
account = pick_web_account(platform="target_platform") account = pick_web_account(platform="target_platform", login_id=None)
try: try:
... ...
finally: finally:

View File

@@ -50,8 +50,8 @@
# 运行模式:决定本技能用模拟数据还是真实浏览器/接口去执行 # 运行模式:决定本技能用模拟数据还是真实浏览器/接口去执行
OPENCLAW_TEST_TARGET=mock # 可选mock=本地模拟simulator_rpa=仿真站点real_api=真实接口real_rpa=真实浏览器;模板默认 mock日常正式使用请改为 real_rpa OPENCLAW_TEST_TARGET=mock # 可选mock=本地模拟simulator_rpa=仿真站点real_api=真实接口real_rpa=真实浏览器;模板默认 mock日常正式使用请改为 real_rpa
# 是否显示浏览器窗口:打开后能否看见自动化操作过程 # 是否显示浏览器窗口:网页自动化时是否弹出可见浏览器
OPENCLAW_BROWSER_HEADLESS=0 # 0=显示窗口(推荐,便于排查)1=后台静默运行 OPENCLAW_BROWSER_HEADLESS=0 # 0=显示窗口(必须,便于介入与排查)1=后台静默(勿作日常用法)
``` ```
### 反例(禁止) ### 反例(禁止)
@@ -79,10 +79,13 @@ OPENCLAW_TEST_TARGET=mock # 可选mock=本地模拟simulator_
TARGET_BASE_URL=https://sandbox.jc2009.com # 填写完整网址(含 https://);换环境时再改,一般保持默认即可 TARGET_BASE_URL=https://sandbox.jc2009.com # 填写完整网址(含 https://);换环境时再改,一般保持默认即可
# 默认登录账号:仅填写非敏感的账号标识(如工号/登录名) # 默认登录账号:仅填写非敏感的账号标识(如工号/登录名)
DEFAULT_LOGIN_ID=04110001 # 只填账号标识,不要填密码;密码请在「账号管理」中登记 DEFAULT_LOGIN_ID= # 留空=按平台自动选用或登记;有值则按该标识筛选;不要填密码
# 是否显示浏览器窗口:打开后能否看见自动化操作过程 # 默认账号编号:仅填账号管理里的纯数字主键(一般留空)
OPENCLAW_BROWSER_HEADLESS=0 # 0=显示窗口推荐便于排查1=后台静默运行 DEFAULT_ACCOUNT_ID= # 留空=自动;只填纯数字;手机号/用户名请填上方登录账号
# 是否显示浏览器窗口:网页自动化时是否弹出可见浏览器
OPENCLAW_BROWSER_HEADLESS=0 # 0=显示窗口必须便于介入与排查1=后台静默(勿作日常用法)
# 浏览器防检测:降低被网站识别为自动化工具的概率 # 浏览器防检测:降低被网站识别为自动化工具的概率
OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启推荐0=关闭;一般保持默认 OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启推荐0=关闭;一般保持默认
@@ -161,6 +164,16 @@ config.get_bool("OPENCLAW_BROWSER_HEADLESS")
config.get_float("STEP_DELAY_MIN", 1.0) config.get_float("STEP_DELAY_MIN", 1.0)
``` ```
### 空值、行尾注释与假默认(硬规则)
- **留空即未配置**`KEY=``KEY= # 说明` 必须读成空字符串,再走技能自己的「自动 / ensure」逻辑**禁止**把示例说明、`# 可填…` 当成真实配置值。
- **需要 platform-kit ≥ 1.2.2**:旧版解析曾把「空值 + 行尾 `#`」误当成 `# 可填…` 这种假值。技能若依赖正确空值语义,须在 `SKILL.md` 声明 `platform_kit_min_version: "1.2.2"`(或更高)。
- **网页账号配置语义**(有则用):
- `DEFAULT_ACCOUNT_ID`:仅**纯数字**视为账号主键;非数字非空可当 login_id不推荐优先用 `DEFAULT_LOGIN_ID`
- `DEFAULT_LOGIN_ID`:手机号 / 用户名等;留空则按平台 `ensure-web`
- 占位串(`#…``none``n/a``default-account`)一律当未配置
- **不要**在 `.env.example` 里塞「看起来像真值」的演示账号(如假手机号),以免首次落盘后被当成用户配置。
## health / config-path ## health / config-path
- **`health`**:输出 `collect_runtime_diagnostics` 字段(`platform_kit_version_ok``ffmpeg_path` 等),**不打印敏感值**;补充 `env_path` / `env_exists` / `example_path` - **`health`**:输出 `collect_runtime_diagnostics` 字段(`platform_kit_version_ok``ffmpeg_path` 等),**不打印敏感值**;补充 `env_path` / `env_exists` / `example_path`

View File

@@ -164,13 +164,13 @@ scripts/
作用:常量、日志、路径、时间工具、通用帮助函数 作用:常量、日志、路径、时间工具、通用帮助函数
`util/logging_config.py``jiangchang_skill_core.unified_logging` 的**薄封装**,业务代码应通过它获取 logger `util/logging_config.py``jiangchang_skill_core.unified_logging` 的**薄封装**,业务代码应通过它获取 logger
公共能力config、logging、runtime_env、rpa、media_assets、video_session、runtime_diagnostics、activity/SRCP从共享 runtime 的 `jiangchang-platform-kit>=1.2.0` import**不得**在 `scripts/` 下保留 `jiangchang_skill_core/` 副本。 公共能力config、logging、runtime_env、rpa、media_assets、video_session、runtime_diagnostics、activity/SRCP从共享 runtime 的 `jiangchang-platform-kit>=1.2.2` import**不得**在 `scripts/` 下保留 `jiangchang_skill_core/` 副本。
## 3.2 开发 RPA 类 skill ## 3.2 开发 RPA 类 skill
若 skill 需要浏览器/桌面/手机自动化,按以下顺序落地: 若 skill 需要浏览器/桌面/手机自动化,按以下顺序落地:
1. **先读三份标准**`RPA.md`三端范式与反反爬)、`CONFIG.md``.env` 落盘与读取)、`ADAPTER.md`(四档 adapter 1. **先读三份标准**`RPA.md`含 §0.2Profile 强制 / 有头 / 登录分平台)、`CONFIG.md``.env` 落盘与读取)、`ADAPTER.md`(四档 adapter
2. **从 examples 选择性复制**(不要整包照搬 `examples/<mode>/`,见各 example README 的 copy map 2. **从 examples 选择性复制**(不要整包照搬 `examples/<mode>/`,见各 example README 的 copy map
- 若是 **真实浏览器 RPA**(真实网站、登录态、验证码、滚动采集),**必须先读** `examples/real_browser_rpa/README.md`,再按需复制到 `scripts/service/` - 若是 **真实浏览器 RPA**(真实网站、登录态、验证码、滚动采集),**必须先读** `examples/real_browser_rpa/README.md`,再按需复制到 `scripts/service/`
- `examples/real_browser_rpa/scripts/service/browser_session.py` - `examples/real_browser_rpa/scripts/service/browser_session.py`
@@ -185,6 +185,7 @@ scripts/
- `examples/simulator_browser_rpa/scripts/service/task_service.py`async 编排) - `examples/simulator_browser_rpa/scripts/service/task_service.py`async 编排)
- `sandbox/demo_app.html` 仅留在 examples不进入生产 skill - `sandbox/demo_app.html` 仅留在 examples不进入生产 skill
- **禁止**`rpa_helpers`、sync Playwright 用于完整技能 RPA 主路径、`task_service` 散落 account-manager subprocess - **禁止**`rpa_helpers`、sync Playwright 用于完整技能 RPA 主路径、`task_service` 散落 account-manager subprocess
- **网页硬规则**:未拿到 `profile_dir` 禁止开浏览器;生产 `OPENCLAW_BROWSER_HEADLESS=0`;站点是否登录写进 REQUIREMENTS / 用户教程,**不要**写成「可不接 account-manager」
3. **只用共享库,不在 skill 里重写反反爬** 3. **只用共享库,不在 skill 里重写反反爬**
```python ```python
from jiangchang_skill_core import config from jiangchang_skill_core import config
@@ -201,7 +202,7 @@ scripts/
技能根目录的 `requirements.txt` 是**标准文件**,用于声明本技能**特有** Python 三方依赖。 技能根目录的 `requirements.txt` 是**标准文件**,用于声明本技能**特有** Python 三方依赖。
- **公共依赖**`jiangchang-platform-kit`、`playwright`、config、runtime diagnostics、RPA 公共能力等)由**宿主共享 runtime** 提供,**不要**写入技能 `requirements.txt`。 - **公共依赖**`jiangchang-platform-kit`、`playwright`、config、runtime diagnostics、RPA 公共能力等)由**宿主共享 runtime** 提供,**不要**写入技能 `requirements.txt`。
- `SKILL.md` 的 `metadata.openclaw.platform_kit_min_version`(当前 `1.2.0`)是运行契约/兼容性声明,供宿主安装与启用时校验,**不是** pip 依赖声明。 - `SKILL.md` 的 `metadata.openclaw.platform_kit_min_version`(当前 `1.2.2`)是运行契约/兼容性声明,供宿主安装与启用时校验,**不是** pip 依赖声明。
- 匠厂宿主安装/更新技能后,会将技能 `requirements.txt` 安装到共享 venv`{JIANGCHANG_DATA_ROOT}/python-runtime/.venv`。 - 匠厂宿主安装/更新技能后,会将技能 `requirements.txt` 安装到共享 venv`{JIANGCHANG_DATA_ROOT}/python-runtime/.venv`。
- **不要**在业务代码中 `subprocess` / `pip install`;缺依赖由 `health` 报错,由宿主负责安装。 - **不要**在业务代码中 `subprocess` / `pip install`;缺依赖由 `health` 报错,由宿主负责安装。
- **版本约束尽量收窄**,降低多技能共享 venv 时的冲突风险。推荐范围写法: - **版本约束尽量收窄**,降低多技能共享 venv 时的冲突风险。推荐范围写法:
@@ -868,6 +869,7 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
- [ ] 本仓库**不是** skill-template 的误复制(根目录**无** `.openclaw-skill-template` - [ ] 本仓库**不是** skill-template 的误复制(根目录**无** `.openclaw-skill-template`
- [ ] `git remote -v` 指向**本技能**远端URL 不含 skill-template 仓库名 - [ ] `git remote -v` 指向**本技能**远端URL 不含 skill-template 仓库名
- [ ] `git log` 首条提交属于本技能(非模板历史) - [ ] `git log` 首条提交属于本技能(非模板历史)
- [ ] 网页 RPA`ensure-web` 取得 `profile_dir` 再开浏览器;`.env` 默认有头REQUIREMENTS 写明登录策略required/optional/not_needed会话依赖型勿抄 optional用户 README/TUTORIAL 区分「登记账号」与「站点登录」(`RPA.md` §0.2 / `POLICY-RPA-004`);依赖 kit 空值解析等修复时上调 `platform_kit_min_version`
## 17. 常见错误 ## 17. 常见错误

View File

@@ -19,6 +19,7 @@
| POLICY-RPA-001 | 不得 import/use account-manager 内部 `rpa_helpers` / `inject_account_manager_scripts_path` / `get_account_credential` | development/ADAPTER.md §兄弟依赖development/RPA.md §1.7 | hard | 扫描 `scripts/**/*.py` 禁止模式 | `tests/test_development_policy_guard.py::TestPolicyRpa001`(另见 `tests/test_no_rpa_helpers_import.py` | | POLICY-RPA-001 | 不得 import/use account-manager 内部 `rpa_helpers` / `inject_account_manager_scripts_path` / `get_account_credential` | development/ADAPTER.md §兄弟依赖development/RPA.md §1.7 | hard | 扫描 `scripts/**/*.py` 禁止模式 | `tests/test_development_policy_guard.py::TestPolicyRpa001`(另见 `tests/test_no_rpa_helpers_import.py` |
| POLICY-RPA-002 | RPA 交付代码不得直接执行 ffmpeg 拼 MP4应使用 `RpaVideoSession`(允许 `ffmpeg_path` 等诊断字段) | development/RPA.md §5.3 录屏成片标准 | hard | 扫描 `scripts/**/*.py` 禁止 `subprocess` / `os.system` / `os.popen` 直接调用 ffmpeg`task_run_support.py` 保留豁免) | `tests/test_development_policy_guard.py::TestPolicyRpa002` | | POLICY-RPA-002 | RPA 交付代码不得直接执行 ffmpeg 拼 MP4应使用 `RpaVideoSession`(允许 `ffmpeg_path` 等诊断字段) | development/RPA.md §5.3 录屏成片标准 | hard | 扫描 `scripts/**/*.py` 禁止 `subprocess` / `os.system` / `os.popen` 直接调用 ffmpeg`task_run_support.py` 保留豁免) | `tests/test_development_policy_guard.py::TestPolicyRpa002` |
| POLICY-RPA-003 | 即使默认 `OPENCLAW_RECORD_VIDEO=0`RPA / 长任务模板也必须保留录屏能力:`RpaVideoSession``video.add_step`、video artifact merge 到 `result_summary` | development/RPA.md §5.3development/LOGGING.mddevelopment/CONFIG.md | hard | 扫描 `scripts/service/*.py`service 层整体须含上述接入) | `tests/test_development_policy_guard.py::TestPolicyRpa003` | | POLICY-RPA-003 | 即使默认 `OPENCLAW_RECORD_VIDEO=0`RPA / 长任务模板也必须保留录屏能力:`RpaVideoSession``video.add_step`、video artifact merge 到 `result_summary` | development/RPA.md §5.3development/LOGGING.mddevelopment/CONFIG.md | hard | 扫描 `scripts/service/*.py`service 层整体须含上述接入) | `tests/test_development_policy_guard.py::TestPolicyRpa003` |
| POLICY-RPA-004 | 网页 RPA`.env.example` 若声明 `OPENCLAW_BROWSER_HEADLESS` 则默认必须为 `0``scripts/` 若使用 `launch_persistent_*` 则必须同时存在 `pick_web_account`(先 Profile 再开浏览器;实现上应走 ensure-web登录不全局强制但不得跳过 account-manager | development/RPA.md §0.2development/ADAPTER.md §兄弟依赖 | hard | 解析 `.env.example` + 扫描 `scripts/**/*.py` | `tests/test_development_policy_guard.py::TestPolicyRpa004` |
| POLICY-PACKAGING-001 | `scripts/**/*.py` 单文件 < 1000 行 | development/DEVELOPMENT.md §3.4 发布打包约束development/RUNTIME.md §发布打包约束 | hard | 行数统计 | **已有测试覆盖**`tests/test_release_packaging_constraints.py::test_scripts_py_files_under_pyarmor_line_limit` | | POLICY-PACKAGING-001 | `scripts/**/*.py` 单文件 < 1000 行 | development/DEVELOPMENT.md §3.4 发布打包约束development/RUNTIME.md §发布打包约束 | hard | 行数统计 | **已有测试覆盖**`tests/test_release_packaging_constraints.py::test_scripts_py_files_under_pyarmor_line_limit` |
| POLICY-PACKAGING-002 | 文本文件 UTF-8 without BOM`U+FEFF` | development/DEVELOPMENT.md §3.4development/RUNTIME.md §编码与输出 | hard | BOM / 解码检查 | **已有测试覆盖**`tests/test_release_packaging_constraints.py::test_text_files_are_utf8_without_bom` | | POLICY-PACKAGING-002 | 文本文件 UTF-8 without BOM`U+FEFF` | development/DEVELOPMENT.md §3.4development/RUNTIME.md §编码与输出 | hard | BOM / 解码检查 | **已有测试覆盖**`tests/test_release_packaging_constraints.py::test_text_files_are_utf8_without_bom` |
| POLICY-DOCS-001 | 本矩阵存在且包含上述全部 `policy_id` | 本次规范化约定 | hard | 解析 `development/POLICY_MATRIX.md` | `tests/test_development_policy_guard.py::TestPolicyDocs001` | | POLICY-DOCS-001 | 本矩阵存在且包含上述全部 `policy_id` | 本次规范化约定 | hard | 解析 `development/POLICY_MATRIX.md` | `tests/test_development_policy_guard.py::TestPolicyDocs001` |
@@ -58,5 +59,5 @@
| RPA 拟人操作、选择器纪律、HITL 超时 | development/RPA.md §0§1 | 行为与 DOM 质量,无法静态扫描 | | RPA 拟人操作、选择器纪律、HITL 超时 | development/RPA.md §0§1 | 行为与 DOM 质量,无法静态扫描 |
| adapter 四档契约测试覆盖 timeout/unauthorized 等 | development/ADAPTER.md §contract tests | 需业务实现后人工补测 | | adapter 四档契约测试覆盖 timeout/unauthorized 等 | development/ADAPTER.md §contract tests | 需业务实现后人工补测 |
| `SKILL.md` / `constants.SKILL_SLUG` 一致性 | development/DEVELOPMENT.md §16 | 已有 `tests/test_skill_metadata.py` | | `SKILL.md` / `constants.SKILL_SLUG` 一致性 | development/DEVELOPMENT.md §16 | 已有 `tests/test_skill_metadata.py` |
| platform_kit_min_version >= 1.2.0 | development/RUNTIME.md | 已有 `tests/test_platform_import.py` | | platform_kit_min_version >= 1.2.2 | development/RUNTIME.md | 已有 `tests/test_platform_import.py` |
| 宿主按 `bind.tables` 过滤 toolbar 按钮 | references/ACTIONS.md | 待宿主升级后完整生效;模板侧已强制显式绑定 | | 宿主按 `bind.tables` 过滤 toolbar 按钮 | references/ACTIONS.md | 待宿主升级后完整生效;模板侧已强制显式绑定 |

View File

@@ -22,6 +22,13 @@
- 中文 description一句话 - 中文 description一句话
- account-manager platform_key如有 - account-manager platform_key如有
- 命名形态:标准型 / scope 型 - 命名形态:标准型 / scope 型
- **网页 RPA如有— 登录策略(三选一,须写明):**
- `required`:目标站必须登录才能闭环(用户须完成/保持站点登录;技能每轮主路径须有登录门禁 + 验证码/人工等待)
- `optional`:登录可提升能力,但不登录也能完成主路径(**仅当**公开页确可闭环时选用;会话依赖型采集/评论/私信**禁止**默认抄成 optional
- `not_needed`:公开页即可,不要求站点登录
- 无论上列哪一种:**都必须**经 account-manager **`ensure-web`**(或等价)取得 `profile_dir`(见 `development/RPA.md` §0.2);不得使用系统默认浏览器用户目录
- **登记 ≠ 登录**ensure/add-web 只建账号与 Profile站点登录由本技能在有头浏览器中完成
- **反模式**:把「需要登录态才能采到数据」的需求写成 `optional`,再省略 `ensure_logged_in`
## 1. 文档目标 ## 1. 文档目标
@@ -103,6 +110,7 @@
- Windows 环境下需保证 UTF-8 输出兼容 - Windows 环境下需保证 UTF-8 输出兼容
- 必须具备基本日志能力;敏感字段脱敏 - 必须具备基本日志能力;敏感字段脱敏
- RPA 类 skillPlaywright 由宿主共享 runtime 提供;技能侧不要 `playwright install`URL 不要放进 `launch_persistent_context``args` - RPA 类 skillPlaywright 由宿主共享 runtime 提供;技能侧不要 `playwright install`URL 不要放进 `launch_persistent_context``args`
- 网页 RPA启动前必须经 ensure-web 拿到 `profile_dir`;生产有头;站点登录策略见 §0required / optional / not_needed`required` 时每轮须登录门禁
## 6. 输入输出要求 ## 6. 输入输出要求

View File

@@ -2,7 +2,7 @@
> 本文是团队 RPA 开发的**统一标准**。任何需要"自动操作软件界面"的 skill都应先读这份文档按这里的选型和范式落地不要每个项目重新踩坑。 > 本文是团队 RPA 开发的**统一标准**。任何需要"自动操作软件界面"的 skill都应先读这份文档按这里的选型和范式落地不要每个项目重新踩坑。
我们开发的各类 skill本质上都是在替人操作三类界面**浏览器、桌面软件、手机软件**。三类的底层技术不同,但**工程范式相同**保持登录态、有头运行、拟人操作、失败存证、人工兜底。 我们开发的各类 skill本质上都是在替人操作三类界面**浏览器、桌面软件、手机软件**。三类的底层技术不同,但**工程范式相同**可控会话Profile、有头运行、拟人操作、失败存证、人工兜底。
--- ---
@@ -12,8 +12,8 @@
| 约定 | 说明 | | 约定 | 说明 |
|------|------| |------|------|
| **保持登录态** | 复用持久化 Profile / session避免每次重新登录触发风控账号由 account-manager 下发,不硬编码 | | **可控会话 / Profile** | 网页版见下方 **§0.2**(必须走 account-manager 的 `profile_dir`);桌面/手机用对应持久会话,不硬编码密码 |
| **有头运行** | 默认有头headless 易被识别 / 难人工介入);`OPENCLAW_BROWSER_HEADLESS=1` 仅给 CI | | **有头运行** | 生产 RPA **必须有头**(见 §0.2);禁止把无头当交付/联调常态 |
| **拟人操作** | 真实事件isTrusted=true逐字输入、随机延迟、贝塞尔鼠标轨迹严禁 JS 直接设值/JS 点击/JS 跳转 | | **拟人操作** | 真实事件isTrusted=true逐字输入、随机延迟、贝塞尔鼠标轨迹严禁 JS 直接设值/JS 点击/JS 跳转 |
| **步骤间随机等待** | 每两步操作之间 `random_delay(min,max)`,区间由 `.env` 配置(默认 1~5s | | **步骤间随机等待** | 每两步操作之间 `random_delay(min,max)`,区间由 `.env` 配置(默认 1~5s |
| **人工兜底HITL** | 滑块 / 短信验证码 / 人脸 / U盾 / 动态口令 → **停下来轮询等人工**,超时报 `ERROR:XXX_NEED_HUMAN`,绝不自动硬闯 | | **人工兜底HITL** | 滑块 / 短信验证码 / 人脸 / U盾 / 动态口令 → **停下来轮询等人工**,超时报 `ERROR:XXX_NEED_HUMAN`,绝不自动硬闯 |
@@ -44,6 +44,32 @@ async def open_login(page):
规范权威定义:[`LOGGING.md`](LOGGING.md) §2.5。金样代码:[`scripts/service/task_service.py`](../scripts/service/task_service.py)。 规范权威定义:[`LOGGING.md`](LOGGING.md) §2.5。金样代码:[`scripts/service/task_service.py`](../scripts/service/task_service.py)。
### 0.2 网页版 RPA 硬规则Profile / 有头 / 登录分平台)
凡**打开网页、用浏览器自动化**的技能(真实站或仿真站),必须遵守(`POLICY-RPA-004`
| 规则 | 要求 |
|------|------|
| **Profile 强制** | 启动浏览器前必须已从 **account-manager**(经本技能 `account_client`)拿到可用的 **`profile_dir`(及租约约定)**。只用该目录做 `launch_persistent_context`。 |
| **首启用 ensure** | 业务技能默认走 **`account ensure-web`**(或 `pick-web --ensure`),经 `account_client.pick_web_account` 封装。无匹配账号时自动 `add-web` 再返回;**有账号但均被租约占用 → `ERROR:LEASE_CONFLICT`,禁止再新建重复账号**。 |
| **拿不到就停** | 未拿到 `profile_dir`、ensure 失败、租约失败 → **立即失败退出,禁止开浏览器**。不得静默落到系统默认 Chrome/Edge 用户目录。 |
| **有头强制** | 生产与用户侧联调:**`OPENCLAW_BROWSER_HEADLESS=0`(有头)**。禁止把无头(`=1`)当作交付或真实站联调常态(无头易风控、难人工介入,且易污染身份)。 |
| **登录不全局强制** | **是否要在目标站完成登录**由平台决定(如部分公开页可不登录也能闭环)。由做该技能的同事在 `REQUIREMENTS.md` / 用户教程中约定;**模板不强制「所有网页 RPA 必须先登录」**。 |
| **登记 ≠ 登录** | `ensure-web` / `add-web` 只保证有账号记录与 Profile 目录;**不等于**目标站已登录。站点登录仍由本技能 `ensure_logged_in`(或等价)在有头浏览器中完成。 |
| **登录 ≠ Profile** | 「可不登录」**绝不等于**「可以不接 account-manager」。无论目标站要不要登录网页操作都依赖账号管理下发的 **Profile**。 |
| **登录策略 = required 时** | 每轮业务主路径在 `goto` 后必须过登录门禁 + 验证码/人工等待(`HUMAN_WAIT_TIMEOUT`**禁止**把会话依赖型采集默认抄成 `optional`。 |
**反模式(禁止):**
- 未拿到 `profile_dir``launch` / 仍打开页面
- 用系统默认用户数据目录或随意空目录反复跑真实站 → **污染本机 Profile、加重风控**
- 把「本站可不登录」写成「可以不走 account-manager」
- 真实站联调默认无头狂跑
- 仅用哈希 class / 不稳定 CSS 作为**唯一**登录态判定(须结合稳定文案、`get_by_role`、URL/业务 DOMF12 实页确认)
- 把「业务空结果」(如无评论)与 `LEASE_CONFLICT` / `REQUIRE_LOGIN` 混成同一错误码
**参考实现:** `examples/real_browser_rpa/``examples/simulator_browser_rpa/``pick_web_account` → ensure-web → RPA → `finally release_lease`)。
--- ---
## 1. 浏览器(标准已成熟) ## 1. 浏览器(标准已成熟)
@@ -55,21 +81,22 @@ async def open_login(page):
| 项 | 标准 | | 项 | 标准 |
|----|------| |----|------|
| 浏览器 | **优先系统 Chrome/Edge** + `launch_persistent_context``channel="chrome"` 或 Edge不用内置 Chromium**技能内不要 `playwright install`** | | 浏览器 | **优先系统 Chrome/Edge** + `launch_persistent_context``channel="chrome"` 或 Edge不用内置 Chromium**技能内不要 `playwright install`** |
| 登录态 | 持久 Profile 目录账号、profile、lease **统一走 account-manager**(或对应兄弟技能),不硬编码密码 | | Profile / 账号 | **必须**经 account-manager **`ensure-web`**(或 `pick-web --ensure`)取得 `profile_dir`(见 §0.2);密码不进 `.env`**是否站点登录**按平台在需求/教程约定;登记 ≠ 已登录 |
| CDP | **仅作诊断 / 桌面宿主类场景****不要**作为强风控站点的默认生产路径 | | CDP | **仅作诊断 / 桌面宿主类场景****不要**作为强风控站点的默认生产路径 |
| 行为 | 模拟真实用户:真实点击、键盘、鼠标、地址栏输入;**不要**拼接搜索结果 URL、DOM 注入、`el.value=`、JS 跳转 | | 行为 | 模拟真实用户:真实点击、键盘、鼠标、地址栏输入;**不要**拼接搜索结果 URL、DOM 注入、`el.value=`、JS 跳转 |
| 模式 | 默认有头 `OPENCLAW_BROWSER_HEADLESS=0`;无头仅 CI | | 模式 | **有头** `OPENCLAW_BROWSER_HEADLESS=0`(生产强制,见 §0.2 |
| 反检测 | stealth 默认开 `OPENCLAW_PLAYWRIGHT_STEALTH=1`(见 1.1 | | 反检测 | stealth 默认开 `OPENCLAW_PLAYWRIGHT_STEALTH=1`(见 1.1 |
### 1.1 Playwright 启动标准 ### 1.1 Playwright 启动标准
1. **默认有头**`OPENCLAW_BROWSER_HEADLESS=0``.env.example` 默认)。 1. **有头**`OPENCLAW_BROWSER_HEADLESS=0``.env.example` 默认;网页 RPA 生产路径禁止改成无头常态,见 §0.2)。
2. **stealth 默认开**`OPENCLAW_PLAYWRIGHT_STEALTH=1`;通过 `add_init_script` 注入指纹淡化脚本。 2. **stealth 默认开**`OPENCLAW_PLAYWRIGHT_STEALTH=1`;通过 `add_init_script` 注入指纹淡化脚本。
3. **不要在技能里自行安装 playwright**;由宿主共享 runtime 提供。 3. **不要在技能里自行安装 playwright**;由宿主共享 runtime 提供。
4. **不要默认传 `--no-sandbox`**(除非特定容器环境且已评估风险)。 4. **不要默认传 `--no-sandbox`**(除非特定容器环境且已评估风险)。
5. **不要默认传 `--disable-blink-features=AutomationControlled`**platform-kit stealth 已覆盖,额外 flag 可能适得其反。 5. **不要默认传 `--disable-blink-features=AutomationControlled`**platform-kit stealth 已覆盖,额外 flag 可能适得其反。
6. **可以** `ignore_default_args=["--enable-automation"]`platform-kit `launch_persistent_browser` 已处理)。 6. **可以** `ignore_default_args=["--enable-automation"]`platform-kit `launch_persistent_browser` 已处理)。
7. **强风控平台**:优先真实点击、键盘、鼠标、地址栏、持久 profile**不要**直接拼接搜索结果 URL 或 DOM 注入。 7. **强风控平台**:优先真实点击、键盘、鼠标、地址栏、account-manager 持久 profile**不要**直接拼接搜索结果 URL 或 DOM 注入。
8. **先 Profile 再开浏览器**`user_data_dir` 必须来自 ensure/pick 到的 `profile_dir`;未拿到则不得进入本节后续步骤。
指纹淡化stealth典型项`navigator.webdriver=undefined``chrome.runtime``permissions.query``plugins``languages` 等。共享实现见 `jiangchang_skill_core.rpa`platform-kit **>= 1.2.0**)。 指纹淡化stealth典型项`navigator.webdriver=undefined``chrome.runtime``permissions.query``plugins``languages` 等。共享实现见 `jiangchang_skill_core.rpa`platform-kit **>= 1.2.0**)。
@@ -186,7 +213,7 @@ from jiangchang_skill_core.rpa.stealth import stealth_enabled, STEALTH_INIT_SCRI
| Playwright | **async** 贯穿;禁止 sync Playwright 用于完整 RPA 主路径 | | Playwright | **async** 贯穿;禁止 sync Playwright 用于完整 RPA 主路径 |
| 分层 | **薄 adapter** + `{domain}_playwright.py`(示例:`simulator_playwright.py`+ `account_client.py` subprocess | | 分层 | **薄 adapter** + `{domain}_playwright.py`(示例:`simulator_playwright.py`+ `account_client.py` subprocess |
| 登录 | **双层**:门户 HITL`#portal-user` / `#portal-pass` 类泛化 DOM+ 业务系统登录(技能内自写) | | 登录 | **双层**:门户 HITL`#portal-user` / `#portal-pass` 类泛化 DOM+ 业务系统登录(技能内自写) |
| 账号 | `url` 用行业根,不用 `/login``auth_strategy=per_session_manual``pick_web_account` + `release_lease` | | 账号 | `url` 用行业根,不用 `/login``auth_strategy=per_session_manual``pick_web_account`(底层 ensure-web+ `release_lease` |
| 禁止 | **不要** `import account-manager``rpa_helpers` 等内部模块 | | 禁止 | **不要** `import account-manager``rpa_helpers` 等内部模块 |
| Selector | 用户可见文案 / `get_by_role` / `name` 优先;`data-testid` 有则用、无则 fallback共享 sandbox **不要求**为技能加 testid | | Selector | 用户可见文案 / `get_by_role` / `name` 优先;`data-testid` 有则用、无则 fallback共享 sandbox **不要求**为技能加 testid |
| Profile | Chrome persistent profile 可能缓存旧 SPA → 联调排障:手工 `--user-data-dir` 清站点数据 | | Profile | Chrome persistent profile 可能缓存旧 SPA → 联调排障:手工 `--user-data-dir` 清站点数据 |
@@ -260,14 +287,18 @@ skill 退出/抛错统一用 `ERROR:` 前缀 + 稳定码,方便宿主与上层
| 错误码 | 含义 | 上层处理建议 | | 错误码 | 含义 | 上层处理建议 |
|--------|------|------| |--------|------|------|
| `ERROR:REQUIRE_LOGIN` | 未登录 / 登录态失效 | 触发登录流程 | | `ERROR:REQUIRE_LOGIN` | 未登录 / 登录态失效 | 触发登录流程(有头等待人工) |
| `ERROR:LOGIN_TIMEOUT` | 等待人工登录超时 | 提示用户重跑并及时操作 | | `ERROR:LOGIN_TIMEOUT` | 等待人工登录超时 | 提示用户重跑并及时操作 |
| `ERROR:CAPTCHA_NEED_HUMAN` | 命中滑块/验证码拦截 | 暂停等人工,或转人工队列 | | `ERROR:CAPTCHA_NEED_HUMAN` | 命中滑块/验证码拦截 | 暂停等人工,或转人工队列 |
| `ERROR:LEASE_CONFLICT` | 已有账号但均被租约占用ensure 不新建) | 稍后重试或释放租约;**不是**「无账号」 |
| `ERROR:AMBIGUOUS_ACCOUNT` | `--login-id` / `--label` 匹配到多条 | 改用数字 id 或更精确标识 |
| `ERROR:RATE_LIMITED` | 触发频控 | 退避后重试 | | `ERROR:RATE_LIMITED` | 触发频控 | 退避后重试 |
| `ERROR:MISSING_BROWSER` | 未检测到 Chrome/Edge | 提示安装 | | `ERROR:MISSING_BROWSER` | 未检测到 Chrome/Edge | 提示安装 |
| `ERROR:DEVICE_NOT_READY` | 手机未连接/未授权 | 检查 USB/ADB | | `ERROR:DEVICE_NOT_READY` | 手机未连接/未授权 | 检查 USB/ADB |
| `ERROR:WINDOW_NOT_FOUND` | 桌面目标窗口未找到 | 检查程序是否启动 | | `ERROR:WINDOW_NOT_FOUND` | 桌面目标窗口未找到 | 检查程序是否启动 |
业务空结果(如「本页无评论」)用业务码或成功空列表表达,**不要**复用上表账号/登录错误码。
--- ---
## 5. 存证与录屏规范 ## 5. 存证与录屏规范

View File

@@ -2,10 +2,20 @@
## 共享 Python Runtime ## 共享 Python Runtime
**skill-template** 及复制出的新技能,公共能力均来自宿主匠厂安装的共享 Python Runtime`jiangchang-platform-kit>=1.2.0` 及其传递依赖,含 `playwright`)。`jiangchang_skill_core` **不得**在技能仓库内 vendored应由共享 venv 的 site-packages 提供。 **skill-template** 及复制出的新技能,公共能力均来自宿主匠厂安装的共享 Python Runtime`jiangchang-platform-kit>=1.2.2` 及其传递依赖,含 `playwright`)。`jiangchang_skill_core` **不得**在技能仓库内 vendored应由共享 venv 的 site-packages 提供。
技能根目录 `requirements.txt` **只声明技能特有依赖****不要**重复声明 `jiangchang-platform-kit``playwright``SKILL.md``platform_kit_min_version` 是运行契约,**不是** pip 依赖声明。 技能根目录 `requirements.txt` **只声明技能特有依赖****不要**重复声明 `jiangchang-platform-kit``playwright``SKILL.md``platform_kit_min_version` 是运行契约,**不是** pip 依赖声明。
### 何时上调 `platform_kit_min_version`
| 情况 | 做法 |
|------|------|
| 技能依赖 kit 某次修复/新 API`.env` 空值+行尾 `#` 解析、新 RPA helper | 把 `metadata.openclaw.platform_kit_min_version` 调到**需要的最低版本**(例如 `"1.2.2"` |
| PyPI/私有源刚发了更新,但本技能行为未依赖 | **不必**仅为「追新」抬版本 |
| 宿主升级时机 | 宿主在技能/宿主声明的最低版本**未满足**时按需升级共享 venv**不会**仅因源上有更新就后台拉 latest |
模板默认声明须与当前文档所依赖的最低 kit 能力对齐(见根 `SKILL.md`)。
| 场景 | 推荐做法 | | 场景 | 推荐做法 |
|------|----------| |------|----------|
| 日常运行(宿主 / Agent | 使用 PATH 上的共享 venv `python``{JIANGCHANG_DATA_ROOT}/python-runtime/.venv` | | 日常运行(宿主 / Agent | 使用 PATH 上的共享 venv `python``{JIANGCHANG_DATA_ROOT}/python-runtime/.venv` |
@@ -27,7 +37,7 @@ Unix未注入时:
共享解释器通常位于 `{JIANGCHANG_DATA_ROOT}/python-runtime/.venv`。数据根由宿主注入(见 `jiangchang_skill_core.runtime_env`)。 共享解释器通常位于 `{JIANGCHANG_DATA_ROOT}/python-runtime/.venv`。数据根由宿主注入(见 `jiangchang_skill_core.runtime_env`)。
## Runtime 诊断platform-kit 1.2.0+ ## Runtime 诊断platform-kit 1.2.2+
`health` 命令通过 **`jiangchang_skill_core.collect_runtime_diagnostics`** 输出共享 runtime 诊断,**不在技能内重复实现**。典型字段: `health` 命令通过 **`jiangchang_skill_core.collect_runtime_diagnostics`** 输出共享 runtime 诊断,**不在技能内重复实现**。典型字段:
@@ -45,7 +55,7 @@ Unix未注入时:
- 用户实际 `.env``{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/{skill_slug}/.env` - 用户实际 `.env``{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/{skill_slug}/.env`
- `scripts/main.py``cli.app.main()` 启动时调用 `util.config_bootstrap.bootstrap_skill_config()` - `scripts/main.py``cli.app.main()` 启动时调用 `util.config_bootstrap.bootstrap_skill_config()`
- 配置优先级:**进程环境变量** > **用户 `.env`** > **`.env.example` 默认值**。 - 配置优先级:**进程环境变量** > **用户 `.env`** > **`.env.example` 默认值**。
- 公共 `config` / `merge_missing_env_keys` 来自共享 runtime 的 `jiangchang-platform-kit>=1.2.0`**不得** vendored `scripts/jiangchang_skill_core/` - 公共 `config` / `merge_missing_env_keys` 来自共享 runtime 的 `jiangchang-platform-kit>=1.2.2`**不得** vendored `scripts/jiangchang_skill_core/`
## media-assets / ffmpeg / 背景音乐 ## media-assets / ffmpeg / 背景音乐

View File

@@ -23,16 +23,23 @@
4. **长耗时建议 async** — 开浏览器、RPA、HTTP 大文件下载等建议 `executionProfile: "async"`,进**任务中心**;这是对**方法**的选择,**不**限制可以出现在哪些 placement / 入口。 4. **长耗时建议 async** — 开浏览器、RPA、HTTP 大文件下载等建议 `executionProfile: "async"`,进**任务中心**;这是对**方法**的选择,**不**限制可以出现在哪些 placement / 入口。
5. **Agent短 CLI / 长 Action** — 短查询可用共享 Python bash CLI长任务 / RPA **禁止** `exec` + `process poll`,必须 `run_skill_action` 5. **Agent短 CLI / 长 Action** — 短查询可用共享 Python bash CLI长任务 / RPA **禁止** `exec` + `process poll`,必须 `run_skill_action`
6. **进度走 Journal**`emit` / `video.add_step` → Run JournalUI / Skill Run Card / 任务中心展示;**不靠 stdout 流**。 6. **进度走 Journal**`emit` / `video.add_step` → Run JournalUI / Skill Run Card / 任务中心展示;**不靠 stdout 流**。
7. **显式声明 `executionProfile`** — 每个 action **必须**写明 `sync``async`禁止依赖宿主缺省;禁止第三种模糊模式(`auto` / `background` / `deferred` 等)。 7. **显式声明 `executionProfile`**心智默认 sync、例外才 async每个 action **必须**写明 `sync``async`;宿主省略时按 sync新技能仍禁止赌缺省;禁止第三种模糊模式(`auto` / `background` / `deferred` 等)。
--- ---
## 2. `executionProfile`sync vs async ## 2. `executionProfile`sync vs async
### 心智(先记住)
1. **默认按 sync 想**:多数 Action诊断、统计、入队、秒级查询是短调用当场返回**不进**任务中心。
2. **只有特殊情况标 async**:开浏览器 / RPA / 长下载 / 需要进度·暂停·停止时,才显式 `"executionProfile": "async"`,进入**任务中心**。
3. **匠厂宿主**`actions.json` 若**省略** `executionProfile`,按 **`sync`** 处理(不再把漏写当成进任务中心)。
4. **新技能仍须显式写出** `sync``async`(模板 Schema / `POLICY-SKILL-ACTION-002` 硬门禁),禁止赌宿主缺省;显式写出也便于人和 AI 阅读。
| 值 | 宿主行为 | 超时 | 任务中心 | | 值 | 宿主行为 | 超时 | 任务中心 |
|----|----------|------|----------| |----|----------|------|----------|
| **`sync`** | 调用方等待 CLI 结束,直接返回结构化结果或结构化错误;**不**创建后台 Job | 默认约 **30s**`SKILL_ACTION_SYNC_TIMEOUT_MS` | ❌ 不进 | | **`sync`**(或省略时的宿主缺省) | 调用方等待 CLI 结束,直接返回结构化结果或结构化错误;**不**创建后台 Job | 默认约 **30s**`SKILL_ACTION_SYNC_TIMEOUT_MS` | ❌ 不进 |
| **`async`** | 后台 spawn Job立即返回 `jobId``emit` / `checkpoint` / `finish`;长任务应支持暂停/停止/取消检查 | Job 本身无默认墙钟上限 | ✅ 进入 | | **`async`**(须显式声明) | 后台 spawn Job立即返回 `jobId``emit` / `checkpoint` / `finish`;长任务应支持暂停/停止/取消检查 | Job 本身无默认墙钟上限 | ✅ 进入 |
直接执行 CLI终端等待进程结束。宿主经 Action 调用同一 CLI再按 `executionProfile` 分流。技能不得自建任务中心。 直接执行 CLI终端等待进程结束。宿主经 Action 调用同一 CLI再按 `executionProfile` 分流。技能不得自建任务中心。
@@ -47,7 +54,7 @@
合法:`sync|async` × `toolbar|cron|agent|skill-detail` 合法:`sync|async` × `toolbar|cron|agent|skill-detail`
历史说明:旧文档曾错误地把数据管理按钮入口与异步执行方式绑死。该约束**已废止**;模板测试与 Schema **不得**再建立「入口位置决定 sync/async」一类硬耦合。宿主省略 `executionProfile` 时的兼容行为以宿主实现为准,**新技能不得依赖** 历史说明:旧文档曾错误地把数据管理按钮入口与异步执行方式绑死。该约束**已废止**;模板测试与 Schema **不得**再建立「入口位置决定 sync/async」一类硬耦合。旧版宿主曾「省略 → async」**当前宿主为「省略 → sync」**。新技能仍须显式声明,不得依赖省略行为
--- ---

View File

@@ -137,7 +137,8 @@ Golden fixture 流程同理([`tests/samples/test_golden_cases.py.sample`](../t
浏览器 RPA 走 `simulator_rpa` 档位联调前,逐项确认: 浏览器 RPA 走 `simulator_rpa` 档位联调前,逐项确认:
- [ ] 数据目录 `.env``OPENCLAW_TEST_TARGET=simulator_rpa`(非 `mock` / `unit` - [ ] 数据目录 `.env``OPENCLAW_TEST_TARGET=simulator_rpa`(非 `mock` / `unit`
- [ ] account-manager`platform ensure` + 账号 `active` + 有 `profile_dir` - [ ] account-manager`ensure-web`(或 `pick-web --ensure`+ 账号`profile_dir`(无 profile 禁止开浏览器,见 `RPA.md` §0.2
- [ ] `OPENCLAW_BROWSER_HEADLESS=0`(联调有头;勿默认无头)
- [ ] 目标 sandbox UI 已部署(跨团队;本地可用 `sandbox/demo_app.html` - [ ] 目标 sandbox UI 已部署(跨团队;本地可用 `sandbox/demo_app.html`
- [ ] 失败先查 `rpa-artifacts/` 截图,再查 Chrome profile 缓存(`--user-data-dir` 手工打开清站点数据) - [ ] 失败先查 `rpa-artifacts/` 截图,再查 Chrome profile 缓存(`--user-data-dir` 手工打开清站点数据)
- [ ] 默认 `python tests/run_tests.py -v` 仍全部通过mock 离线example 内 `pytest` 不启真实浏览器 - [ ] 默认 `python tests/run_tests.py -v` 仍全部通过mock 离线example 内 `pytest` 不启真实浏览器
@@ -261,7 +262,7 @@ Golden fixture 流程同理([`tests/samples/test_golden_cases.py.sample`](../t
- [ ] `requirements.txt` **不含** `jiangchang-platform-kit` / `playwright` - [ ] `requirements.txt` **不含** `jiangchang-platform-kit` / `playwright`
- [ ]`scripts/jiangchang_skill_core/` vendored 副本 - [ ]`scripts/jiangchang_skill_core/` vendored 副本
- [ ] `platform_kit_min_version` **>= 1.2.0**`SKILL.md` + `constants.py` - [ ] `platform_kit_min_version` **>= 1.2.2**`SKILL.md` + `constants.py`
- [ ] `health` 能输出 `platform_kit_version_ok`(或等价诊断行) - [ ] `health` 能输出 `platform_kit_version_ok`(或等价诊断行)
- [ ] `config-path` 可输出用户 `.env` 路径 JSON - [ ] `config-path` 可输出用户 `.env` 路径 JSON
- [ ] `pytest.ini` 存在且 `python_files` 只收集 `test_*.py` / `*_test.py` - [ ] `pytest.ini` 存在且 `python_files` 只收集 `test_*.py` / `*_test.py`

View File

@@ -39,7 +39,7 @@ real_browser_rpa/
## 核心流程 ## 核心流程
1. **pick account** — 通过 `account_client.pick_web_account()` 获取 `profile_dir` 与租约 1. **ensure account** — 通过 `account_client.pick_web_account()`(底层 `ensure-web`获取 `profile_dir` 与租约;无账号则自动登记,全忙则 `LEASE_CONFLICT` 不重复建号。登记 ≠ 站点已登录。
2. **启动浏览器**`launch_persistent_context` + `new_page()` + `page.goto(start_url)` 2. **启动浏览器**`launch_persistent_context` + `new_page()` + `page.goto(start_url)`
3. **人工验证(启动后)** — 检测滑块/短信,等待用户完成 3. **人工验证(启动后)** — 检测滑块/短信,等待用户完成
4. **等待登录** — 检测登录按钮消失 / 登录态 marker 出现 4. **等待登录** — 检测登录按钮消失 / 登录态 marker 出现
@@ -79,6 +79,10 @@ real_browser_rpa/
**浏览器与启动约束:** **浏览器与启动约束:**
- **先 Profile 再开浏览器**:必须 `pick_web_account`(底层 ensure-web拿到 `profile_dir`;拿不到则失败退出,禁止落到系统默认用户目录(见模板 `development/RPA.md` §0.2
- **RPA 等待**:主路径用 `interruptible_sleep`,禁止裸 `asyncio.sleep``POLICY-CONTROL-003`
- **有头**:生产 / 联调 `OPENCLAW_BROWSER_HEADLESS=0`;勿把无头当交付常态
- **登录按平台**:是否要求站点登录由本技能约定;「可不登录」≠「可不接 account-manager」
- **不要**在技能内安装 Playwright浏览器与 Python 包由宿主/runtime 提供 - **不要**在技能内安装 Playwright浏览器与 Python 包由宿主/runtime 提供
- `launch_persistent_context``args` **只放 Chrome 参数****不要把 URL 放进 `args`** - `launch_persistent_context``args` **只放 Chrome 参数****不要把 URL 放进 `args`**
- 页面必须通过 `new_page()` + `goto()`,或通过真实地址栏/点击进入 - 页面必须通过 `new_page()` + `goto()`,或通过真实地址栏/点击进入
@@ -139,7 +143,7 @@ real_browser_rpa/
- **不 import account-manager 内部模块** — 只通过 CLI/subprocess 调用 - **不 import account-manager 内部模块** — 只通过 CLI/subprocess 调用
- **不自动破解验证码** — 滑块/短信只检测 + 等待人工完成 - **不自动破解验证码** — 滑块/短信只检测 + 等待人工完成
- **日志脱敏** — 不输出完整手机号/账号 - **日志脱敏** — 不输出完整手机号/账号
- **租约释放** — `pick-web --lease` 后必须在 `finally` 释放 - **租约释放** — `ensure-web` / `pick-web --lease` 后必须在 `finally` 释放
- **失败留痕** — 真实 skill 应在关键失败点截图(示例中已留注释位) - **失败留痕** — 真实 skill 应在关键失败点截图(示例中已留注释位)
## 常见坑 ## 常见坑

View File

@@ -1,4 +1,7 @@
"""account-manager CLI 集成(仅 subprocess不 import 兄弟 skill 内部模块)。""" """account-manager CLI 集成(仅 subprocess不 import 兄弟 skill 内部模块)。
业务首启默认 ``ensure-web``(有则用、无则 add-web登记 ≠ 站点已登录。
"""
from __future__ import annotations from __future__ import annotations
@@ -7,8 +10,9 @@ import logging
import os import os
import subprocess import subprocess
import sys import sys
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional, Tuple
from jiangchang_skill_core import config
from jiangchang_skill_core.runtime_env import get_sibling_skills_root from jiangchang_skill_core.runtime_env import get_sibling_skills_root
from util.constants import LEASE_HOLDER, LEASE_TTL_SEC, TARGET_PLATFORM from util.constants import LEASE_HOLDER, LEASE_TTL_SEC, TARGET_PLATFORM
@@ -19,12 +23,17 @@ logger = logging.getLogger(__name__)
PLACEHOLDER_PLATFORM = TARGET_PLATFORM PLACEHOLDER_PLATFORM = TARGET_PLATFORM
ACCOUNT_SETUP_MESSAGE = ( ACCOUNT_SETUP_MESSAGE = (
f"找到可用的 {PLACEHOLDER_PLATFORM} 账号,所以还没有打开浏览器。" f"能取得可用的 {PLACEHOLDER_PLATFORM} 账号ensure-web 失败),所以还没有打开浏览器。"
f"先在 account-manager 中添加 platform={PLACEHOLDER_PLATFORM}" f"确认已安装 account-manager,并检查 platform={PLACEHOLDER_PLATFORM} 账号状态后重试。"
"status=active、带 profile_dir 的账号后重新运行。"
) )
LEASE_BUSY_MESSAGE = "目标平台账号当前被其他任务占用,请等待释放租约后重试。" LEASE_BUSY_MESSAGE = (
"该平台下已有账号,但均被其他任务占用。请稍后重试或释放租约;不会自动新建账号。"
)
DEFAULT_AUTH_STRATEGY = "qr_code_manual"
DEFAULT_ACCOUNT_LABEL = f"{PLACEHOLDER_PLATFORM} 默认"
DEFAULT_START_URL = "https://www.example.com/"
class AccountManagerError(Exception): class AccountManagerError(Exception):
@@ -38,13 +47,65 @@ def mask_login_id(login_id: str) -> str:
return mask_text(login_id) return mask_text(login_id)
def format_account_label(account: dict) -> str:
"""人读账号标识:优先 login_id / 备注,最后才用数字 id。"""
for key in ("login_id", "account_label", "label"):
v = account.get(key)
if v is not None and str(v).strip():
return str(v).strip()
for key in ("id", "account_id"):
v = account.get(key)
if v is not None and str(v).strip():
return str(v).strip()
return ""
def _is_placeholder_config(raw: str) -> bool:
s = (raw or "").strip()
if not s:
return True
if s.startswith("#"):
return True
low = s.lower()
if low in ("default-account", "none", "null", "n/a", "-"):
return True
return False
def resolve_account_selectors_from_config() -> Tuple[Optional[str], Optional[str]]:
"""从配置解析 (account_id, login_id)。
- ``DEFAULT_ACCOUNT_ID``:仅纯数字视为账号主键;其它非空非占位当作 login_id
- ``DEFAULT_LOGIN_ID``:手机号/用户名(可覆盖上一项推导的 login_id
- 两者皆空:返回 (None, None) → 调用方走按平台 ensure-web
"""
account_id: Optional[str] = None
login_id: Optional[str] = None
aid_raw = (config.get("DEFAULT_ACCOUNT_ID") or "").strip()
if not _is_placeholder_config(aid_raw):
if aid_raw.isdigit():
account_id = aid_raw
else:
login_id = aid_raw
logger.info("default_account_id_treated_as_login_id value=%s", aid_raw)
lid_raw = (config.get("DEFAULT_LOGIN_ID") or "").strip()
if not _is_placeholder_config(lid_raw):
login_id = lid_raw
if account_id:
return account_id, None
return None, login_id
def _resolve_account_manager_main() -> str: def _resolve_account_manager_main() -> str:
"""解析 account-manager CLI 入口路径。 """解析 account-manager CLI 入口路径。
优先级ACCOUNT_MANAGER_ROOT → get_sibling_skills_root(路径推断 / JIANGCHANG_SKILLS_ROOT→ 开发机兜底。 优先级ACCOUNT_MANAGER_ROOT → get_sibling_skills_root → 开发机兜底。
末尾 dev 路径仅用于模板/本机调试,复制到真实 skill 后不要依赖个人机器绝对路径。 末尾 dev 路径仅用于模板/本机调试,复制到真实 skill 后不要依赖个人机器绝对路径。
""" """
env_root = (os.getenv("ACCOUNT_MANAGER_ROOT") or "").strip() env_root = (config.get("ACCOUNT_MANAGER_ROOT") or os.getenv("ACCOUNT_MANAGER_ROOT") or "").strip()
if env_root and os.path.isfile(os.path.join(env_root, "scripts", "main.py")): if env_root and os.path.isfile(os.path.join(env_root, "scripts", "main.py")):
return os.path.join(os.path.abspath(env_root), "scripts", "main.py") return os.path.join(os.path.abspath(env_root), "scripts", "main.py")
@@ -103,44 +164,66 @@ def _validate_pick_payload(payload: dict[str, Any]) -> dict[str, Any]:
code = _normalize_error_code(str(err.get("code") or "")) code = _normalize_error_code(str(err.get("code") or ""))
message = str(err.get("message") or "") message = str(err.get("message") or "")
if code == "LEASE_CONFLICT" or "LEASE_CONFLICT" in code: if code == "LEASE_CONFLICT" or "LEASE_CONFLICT" in code:
raise AccountManagerError("LEASE_CONFLICT", LEASE_BUSY_MESSAGE) raise AccountManagerError("LEASE_CONFLICT", message or LEASE_BUSY_MESSAGE)
if code == "AMBIGUOUS_ACCOUNT" or "AMBIGUOUS_ACCOUNT" in code:
raise AccountManagerError(
"AMBIGUOUS_ACCOUNT",
message or "匹配到多条账号,请改用数字 id 或更精确的登录标识。",
)
if code in ("NO_ACCOUNT", "ACCOUNT_NOT_FOUND") or "NO_ACCOUNT" in code: if code in ("NO_ACCOUNT", "ACCOUNT_NOT_FOUND") or "NO_ACCOUNT" in code:
raise AccountManagerError("NO_ACCOUNT", message or "没有可用账号。") raise AccountManagerError("NO_ACCOUNT", message or "没有可用账号。")
raise AccountManagerError(code or "PICK_WEB_FAILED", message or "pick-web 失败。") raise AccountManagerError(code or "ENSURE_WEB_FAILED", message or "ensure-web 失败。")
if not isinstance(payload, dict): if not isinstance(payload, dict):
raise AccountManagerError("ACCOUNT_NOT_FOUND", "pick-web 返回格式异常。") raise AccountManagerError("ACCOUNT_NOT_FOUND", "ensure-web 返回格式异常。")
if not payload.get("profile_dir"): if not payload.get("profile_dir"):
raise AccountManagerError("ACCOUNT_NOT_FOUND", "pick-web 返回的账号缺少 profile_dir。") raise AccountManagerError("ACCOUNT_NOT_FOUND", "ensure-web 返回的账号缺少 profile_dir。")
return payload return payload
def _pick_web_with_lease(platform: str) -> Dict[str, Any]: def _ensure_web_with_lease(
proc = _run_argv( platform: str,
[ *,
"account", login_id: Optional[str] = None,
"pick-web", ) -> Dict[str, Any]:
"--platform", start_url = (config.get("TARGET_BASE_URL") or DEFAULT_START_URL).strip() or DEFAULT_START_URL
platform, lid = (login_id or "").strip() or None
"--lease", label = f"{platform} {lid}" if lid else DEFAULT_ACCOUNT_LABEL
"--holder", argv = [
LEASE_HOLDER, "account",
"--ttl-sec", "ensure-web",
LEASE_TTL_SEC, "--platform",
] platform,
) "--url",
start_url,
"--auth-strategy",
DEFAULT_AUTH_STRATEGY,
"--label",
label,
"--lease",
"--holder",
LEASE_HOLDER,
"--ttl-sec",
LEASE_TTL_SEC,
"--purpose",
"rpa",
]
if lid:
argv.extend(["--login-id", lid])
proc = _run_argv(argv)
out = proc.stdout or "" out = proc.stdout or ""
if proc.returncode != 0 and not out.strip(): if proc.returncode != 0 and not out.strip():
raise AccountManagerError( raise AccountManagerError(
"PICK_WEB_FAILED", "ENSURE_WEB_FAILED",
(proc.stderr or "").strip() or "pick-web 子进程失败", (proc.stderr or "").strip() or "ensure-web 子进程失败",
) )
payload = _parse_last_json(out) payload = _parse_last_json(out)
if isinstance(payload, dict) and payload.get("success") is False: if isinstance(payload, dict) and payload.get("success") is False:
return _validate_pick_payload(payload) return _validate_pick_payload(payload)
if not isinstance(payload, dict): if not isinstance(payload, dict):
raise AccountManagerError("ACCOUNT_NOT_FOUND", "pick-web 返回格式异常。") raise AccountManagerError("ACCOUNT_NOT_FOUND", "ensure-web 返回格式异常。")
return payload return _validate_pick_payload(payload)
def _pick_by_id(platform: str, account_id: int) -> Dict[str, Any]: def _pick_by_id(platform: str, account_id: int) -> Dict[str, Any]:
@@ -170,19 +253,32 @@ def _pick_by_id(platform: str, account_id: int) -> Dict[str, Any]:
return data return data
def pick_web_account(platform: str, account_id: Optional[str] = None) -> dict: def pick_web_account(
"""获取网页账号profile_dir + lease_token""" platform: str,
account_id: Optional[str] = None,
login_id: Optional[str] = None,
) -> dict:
"""获取网页账号profile_dir + lease_token
优先级:数字 account_id → login_idensure-web --login-id→ 按平台 ensure-web。
"""
platform_key = (platform or PLACEHOLDER_PLATFORM).strip() or PLACEHOLDER_PLATFORM platform_key = (platform or PLACEHOLDER_PLATFORM).strip() or PLACEHOLDER_PLATFORM
if account_id: aid_raw = (account_id or "").strip()
try: if aid_raw and not _is_placeholder_config(aid_raw):
aid = int(account_id) if not aid_raw.isdigit():
except (TypeError, ValueError): if not (login_id or "").strip():
raise AccountManagerError("ACCOUNT_NOT_FOUND", f"账号 ID 无效:{account_id}") login_id = aid_raw
return _pick_by_id(platform_key, aid) aid_raw = ""
else:
return _pick_by_id(platform_key, int(aid_raw))
lid = (login_id or "").strip() or None
if lid and _is_placeholder_config(lid):
lid = None
try: try:
return _pick_web_with_lease(platform_key) return _ensure_web_with_lease(platform_key, login_id=lid)
except AccountManagerError as exc: except AccountManagerError as exc:
if exc.code in ("NO_ACCOUNT", "ACCOUNT_NOT_FOUND") or "NO_ACCOUNT" in exc.code: if exc.code in ("NO_ACCOUNT", "ACCOUNT_NOT_FOUND") or "NO_ACCOUNT" in exc.code:
raise AccountManagerError("ACCOUNT_SETUP_REQUIRED", ACCOUNT_SETUP_MESSAGE) from exc raise AccountManagerError("ACCOUNT_SETUP_REQUIRED", ACCOUNT_SETUP_MESSAGE) from exc

View File

@@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import logging import logging
import os import os
import random import random
@@ -11,6 +10,8 @@ import time
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, Tuple from typing import Any, Callable, Dict, List, Optional, Tuple
from jiangchang_skill_core.activity import interruptible_sleep
from service.browser_session import close_browser_context, get_start_url, start_browser_session from service.browser_session import close_browser_context, get_start_url, start_browser_session
from service.human_verification import ( from service.human_verification import (
HumanVerificationWaitResult, HumanVerificationWaitResult,
@@ -102,11 +103,11 @@ def _step_cb(cb: Optional[StepCallback], text: str) -> None:
async def _random_delay() -> None: async def _random_delay() -> None:
lo = int(os.getenv("RPA_STEP_DELAY_MIN_MS") or "900") lo = int(os.getenv("RPA_STEP_DELAY_MIN_MS") or "900")
hi = int(os.getenv("RPA_STEP_DELAY_MAX_MS") or "2600") hi = int(os.getenv("RPA_STEP_DELAY_MAX_MS") or "2600")
await asyncio.sleep(random.uniform(lo / 1000.0, hi / 1000.0)) await interruptible_sleep(random.uniform(lo / 1000.0, hi / 1000.0))
async def _scroll_wait() -> None: async def _scroll_wait() -> None:
await asyncio.sleep(random.uniform(1.2, 3.5)) await interruptible_sleep(random.uniform(1.2, 3.5))
def _headless() -> bool: def _headless() -> bool:
@@ -156,7 +157,7 @@ async def _open_login_panel_if_needed(page) -> None:
btn = page.locator(LOGGED_OUT_SELECTOR).first btn = page.locator(LOGGED_OUT_SELECTOR).first
if await _visible(btn): if await _visible(btn):
await btn.click() await btn.click()
await asyncio.sleep(random.uniform(1.0, 3.0)) await interruptible_sleep(random.uniform(1.0, 3.0))
except Exception: except Exception:
pass pass
@@ -175,7 +176,7 @@ async def _ensure_logged_in(page, *, wait_sec: int) -> None:
if await _is_logged_in(page): if await _is_logged_in(page):
print("[登录] 检测到登录成功") print("[登录] 检测到登录成功")
return return
await asyncio.sleep(2.0) await interruptible_sleep(2.0)
raise RuntimeError( raise RuntimeError(
f"ERROR:LOGIN_TIMEOUT 浏览器已打开,但未完成登录。" f"ERROR:LOGIN_TIMEOUT 浏览器已打开,但未完成登录。"
@@ -253,7 +254,7 @@ async def _wait_search_results(page, timeout_sec: float = 45.0) -> bool:
return True return True
except Exception: except Exception:
pass pass
await asyncio.sleep(0.8) await interruptible_sleep(0.8)
return False return False

View File

@@ -6,7 +6,12 @@ import asyncio
import uuid import uuid
from typing import Any, Optional from typing import Any, Optional
from service.account_client import AccountManagerError, pick_web_account, release_lease from service.account_client import (
AccountManagerError,
pick_web_account,
release_lease,
resolve_account_selectors_from_config,
)
from service.task_rpa import ( from service.task_rpa import (
ScrapeRunResult, ScrapeRunResult,
format_stop_reason_for_user, format_stop_reason_for_user,
@@ -79,7 +84,12 @@ async def run_keyword_search_task(
account: Optional[dict[str, Any]] = None account: Optional[dict[str, Any]] = None
try: try:
account = pick_web_account(platform, account_id) cfg_account_id, cfg_login_id = resolve_account_selectors_from_config()
account = pick_web_account(
platform,
account_id=account_id or cfg_account_id,
login_id=cfg_login_id,
)
lease_token = account.get("lease_token") lease_token = account.get("lease_token")
scrape_result: ScrapeRunResult = await run_keyword_search_async( scrape_result: ScrapeRunResult = await run_keyword_search_async(

View File

@@ -50,13 +50,19 @@ simulator_browser_rpa/
| 文件 | 职责 | | 文件 | 职责 |
|---|---| |---|---|
| `browser_session.py` | async 系统 Chrome/Edge + launch args**不在此 goto** | | `browser_session.py` | async 系统 Chrome/Edge + launch args**不在此 goto** |
| `account_client.py` | **唯一** account-manager subprocess 封装(`pick_web_account` / `release_lease` | | `account_client.py` | **唯一** account-manager subprocess 封装(`pick_web_account`→ensure-web / `release_lease` |
| `simulator_playwright.py` | 门户轮询、登录、批量表单、PIN、解析 `batch_id`、截图 | | `simulator_playwright.py` | 门户轮询、登录、批量表单、PIN、解析 `batch_id`、截图 |
| `adapter/simulator_rpa.py` | **薄** adapterpick 账号、lease、委托 RPA、`finally release_lease` | | `adapter/simulator_rpa.py` | **薄** adapterpick 账号、lease、委托 RPA、`finally release_lease` |
| `adapter/mock.py` | 不启浏览器,供 unit/mock/CI | | `adapter/mock.py` | 不启浏览器,供 unit/mock/CI |
| `task_service.py` | async 校验输入 → 选 adapter → `await submit_batch` | | `task_service.py` | async 校验输入 → 选 adapter → `await submit_batch` |
| `sandbox/demo_app.html` | 可控 DOM含可选门户门闩 + 原批量提交流程 | | `sandbox/demo_app.html` | 可控 DOM含可选门户门闩 + 原批量提交流程 |
## 网页 RPA 硬规则(与模板 `development/RPA.md` §0.2 一致)
- **先 Profile 再开浏览器**`simulator_rpa` 档必须 `pick_web_account`ensure-web拿到 `profile_dir`;拿不到则失败,禁止系统默认用户目录
- **有头**:联调 / 交付默认 `OPENCLAW_BROWSER_HEADLESS=0`
- **登录按平台约定**:本示例需要仿真登录;其他技能可为 optional / not_needed但**仍须**走 account-manager
## 核心流程 ## 核心流程
1. `await run_batch_submit(target, items)` 校验参数 1. `await run_batch_submit(target, items)` 校验参数

View File

@@ -1,4 +1,7 @@
"""account-manager CLI 集成(仅 subprocess不 import 兄弟 skill 内部模块)。""" """account-manager CLI 集成(仅 subprocess不 import 兄弟 skill 内部模块)。
业务首启默认 ``ensure-web``(有则用、无则 add-web登记 ≠ 站点已登录。
"""
from __future__ import annotations from __future__ import annotations
@@ -7,8 +10,9 @@ import logging
import os import os
import subprocess import subprocess
import sys import sys
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional, Tuple
from jiangchang_skill_core import config
from jiangchang_skill_core.runtime_env import get_sibling_skills_root from jiangchang_skill_core.runtime_env import get_sibling_skills_root
from util.constants import LEASE_HOLDER, LEASE_TTL_SEC, TARGET_PLATFORM from util.constants import LEASE_HOLDER, LEASE_TTL_SEC, TARGET_PLATFORM
@@ -19,12 +23,17 @@ logger = logging.getLogger(__name__)
PLACEHOLDER_PLATFORM = TARGET_PLATFORM PLACEHOLDER_PLATFORM = TARGET_PLATFORM
ACCOUNT_SETUP_MESSAGE = ( ACCOUNT_SETUP_MESSAGE = (
f"找到可用的 {PLACEHOLDER_PLATFORM} 账号,所以还没有打开浏览器。" f"能取得可用的 {PLACEHOLDER_PLATFORM} 账号ensure-web 失败),所以还没有打开浏览器。"
f"先在 account-manager 中添加 platform={PLACEHOLDER_PLATFORM}" f"确认已安装 account-manager,并检查 platform={PLACEHOLDER_PLATFORM} 账号状态后重试。"
"status=active、带 profile_dir 的账号后重新运行。"
) )
LEASE_BUSY_MESSAGE = "目标平台账号当前被其他任务占用,请等待释放租约后重试。" LEASE_BUSY_MESSAGE = (
"该平台下已有账号,但均被其他任务占用。请稍后重试或释放租约;不会自动新建账号。"
)
DEFAULT_AUTH_STRATEGY = "qr_code_manual"
DEFAULT_ACCOUNT_LABEL = f"{PLACEHOLDER_PLATFORM} 默认"
DEFAULT_START_URL = "https://www.example.com/"
class AccountManagerError(Exception): class AccountManagerError(Exception):
@@ -38,12 +47,65 @@ def mask_login_id(login_id: str) -> str:
return mask_text(login_id) return mask_text(login_id)
def format_account_label(account: dict) -> str:
"""人读账号标识:优先 login_id / 备注,最后才用数字 id。"""
for key in ("login_id", "account_label", "label"):
v = account.get(key)
if v is not None and str(v).strip():
return str(v).strip()
for key in ("id", "account_id"):
v = account.get(key)
if v is not None and str(v).strip():
return str(v).strip()
return ""
def _is_placeholder_config(raw: str) -> bool:
s = (raw or "").strip()
if not s:
return True
if s.startswith("#"):
return True
low = s.lower()
if low in ("default-account", "none", "null", "n/a", "-"):
return True
return False
def resolve_account_selectors_from_config() -> Tuple[Optional[str], Optional[str]]:
"""从配置解析 (account_id, login_id)。
- ``DEFAULT_ACCOUNT_ID``:仅纯数字视为账号主键;其它非空非占位当作 login_id
- ``DEFAULT_LOGIN_ID``:手机号/用户名(可覆盖上一项推导的 login_id
- 两者皆空:返回 (None, None) → 调用方走按平台 ensure-web
"""
account_id: Optional[str] = None
login_id: Optional[str] = None
aid_raw = (config.get("DEFAULT_ACCOUNT_ID") or "").strip()
if not _is_placeholder_config(aid_raw):
if aid_raw.isdigit():
account_id = aid_raw
else:
login_id = aid_raw
logger.info("default_account_id_treated_as_login_id value=%s", aid_raw)
lid_raw = (config.get("DEFAULT_LOGIN_ID") or "").strip()
if not _is_placeholder_config(lid_raw):
login_id = lid_raw
if account_id:
return account_id, None
return None, login_id
def _resolve_account_manager_main() -> str: def _resolve_account_manager_main() -> str:
"""解析 account-manager CLI 入口路径。 """解析 account-manager CLI 入口路径。
优先级ACCOUNT_MANAGER_ROOT → get_sibling_skills_root → 开发机兜底。 优先级ACCOUNT_MANAGER_ROOT → get_sibling_skills_root → 开发机兜底。
末尾 dev 路径仅用于模板/本机调试,复制到真实 skill 后不要依赖个人机器绝对路径。
""" """
env_root = (os.getenv("ACCOUNT_MANAGER_ROOT") or "").strip() env_root = (config.get("ACCOUNT_MANAGER_ROOT") or os.getenv("ACCOUNT_MANAGER_ROOT") or "").strip()
if env_root and os.path.isfile(os.path.join(env_root, "scripts", "main.py")): if env_root and os.path.isfile(os.path.join(env_root, "scripts", "main.py")):
return os.path.join(os.path.abspath(env_root), "scripts", "main.py") return os.path.join(os.path.abspath(env_root), "scripts", "main.py")
@@ -53,6 +115,7 @@ def _resolve_account_manager_main() -> str:
if os.path.isfile(candidate): if os.path.isfile(candidate):
return candidate return candidate
# 开发环境兜底:仅模板/本机调试;生产依赖宿主注入的路径变量,勿硬编码个人目录
dev = r"D:\OpenClaw\client-commons\account-manager\scripts\main.py" dev = r"D:\OpenClaw\client-commons\account-manager\scripts\main.py"
if os.path.isfile(dev): if os.path.isfile(dev):
return dev return dev
@@ -101,44 +164,66 @@ def _validate_pick_payload(payload: dict[str, Any]) -> dict[str, Any]:
code = _normalize_error_code(str(err.get("code") or "")) code = _normalize_error_code(str(err.get("code") or ""))
message = str(err.get("message") or "") message = str(err.get("message") or "")
if code == "LEASE_CONFLICT" or "LEASE_CONFLICT" in code: if code == "LEASE_CONFLICT" or "LEASE_CONFLICT" in code:
raise AccountManagerError("LEASE_CONFLICT", LEASE_BUSY_MESSAGE) raise AccountManagerError("LEASE_CONFLICT", message or LEASE_BUSY_MESSAGE)
if code == "AMBIGUOUS_ACCOUNT" or "AMBIGUOUS_ACCOUNT" in code:
raise AccountManagerError(
"AMBIGUOUS_ACCOUNT",
message or "匹配到多条账号,请改用数字 id 或更精确的登录标识。",
)
if code in ("NO_ACCOUNT", "ACCOUNT_NOT_FOUND") or "NO_ACCOUNT" in code: if code in ("NO_ACCOUNT", "ACCOUNT_NOT_FOUND") or "NO_ACCOUNT" in code:
raise AccountManagerError("NO_ACCOUNT", message or "没有可用账号。") raise AccountManagerError("NO_ACCOUNT", message or "没有可用账号。")
raise AccountManagerError(code or "PICK_WEB_FAILED", message or "pick-web 失败。") raise AccountManagerError(code or "ENSURE_WEB_FAILED", message or "ensure-web 失败。")
if not isinstance(payload, dict): if not isinstance(payload, dict):
raise AccountManagerError("ACCOUNT_NOT_FOUND", "pick-web 返回格式异常。") raise AccountManagerError("ACCOUNT_NOT_FOUND", "ensure-web 返回格式异常。")
if not payload.get("profile_dir"): if not payload.get("profile_dir"):
raise AccountManagerError("ACCOUNT_NOT_FOUND", "pick-web 返回的账号缺少 profile_dir。") raise AccountManagerError("ACCOUNT_NOT_FOUND", "ensure-web 返回的账号缺少 profile_dir。")
return payload return payload
def _pick_web_with_lease(platform: str) -> Dict[str, Any]: def _ensure_web_with_lease(
proc = _run_argv( platform: str,
[ *,
"account", login_id: Optional[str] = None,
"pick-web", ) -> Dict[str, Any]:
"--platform", start_url = (config.get("TARGET_BASE_URL") or DEFAULT_START_URL).strip() or DEFAULT_START_URL
platform, lid = (login_id or "").strip() or None
"--lease", label = f"{platform} {lid}" if lid else DEFAULT_ACCOUNT_LABEL
"--holder", argv = [
LEASE_HOLDER, "account",
"--ttl-sec", "ensure-web",
LEASE_TTL_SEC, "--platform",
] platform,
) "--url",
start_url,
"--auth-strategy",
DEFAULT_AUTH_STRATEGY,
"--label",
label,
"--lease",
"--holder",
LEASE_HOLDER,
"--ttl-sec",
LEASE_TTL_SEC,
"--purpose",
"rpa",
]
if lid:
argv.extend(["--login-id", lid])
proc = _run_argv(argv)
out = proc.stdout or "" out = proc.stdout or ""
if proc.returncode != 0 and not out.strip(): if proc.returncode != 0 and not out.strip():
raise AccountManagerError( raise AccountManagerError(
"PICK_WEB_FAILED", "ENSURE_WEB_FAILED",
(proc.stderr or "").strip() or "pick-web 子进程失败", (proc.stderr or "").strip() or "ensure-web 子进程失败",
) )
payload = _parse_last_json(out) payload = _parse_last_json(out)
if isinstance(payload, dict) and payload.get("success") is False: if isinstance(payload, dict) and payload.get("success") is False:
return _validate_pick_payload(payload) return _validate_pick_payload(payload)
if not isinstance(payload, dict): if not isinstance(payload, dict):
raise AccountManagerError("ACCOUNT_NOT_FOUND", "pick-web 返回格式异常。") raise AccountManagerError("ACCOUNT_NOT_FOUND", "ensure-web 返回格式异常。")
return payload return _validate_pick_payload(payload)
def _pick_by_id(platform: str, account_id: int) -> Dict[str, Any]: def _pick_by_id(platform: str, account_id: int) -> Dict[str, Any]:
@@ -168,19 +253,32 @@ def _pick_by_id(platform: str, account_id: int) -> Dict[str, Any]:
return data return data
def pick_web_account(platform: str, account_id: Optional[str] = None) -> dict: def pick_web_account(
"""获取网页账号profile_dir + lease_token""" platform: str,
account_id: Optional[str] = None,
login_id: Optional[str] = None,
) -> dict:
"""获取网页账号profile_dir + lease_token
优先级:数字 account_id → login_idensure-web --login-id→ 按平台 ensure-web。
"""
platform_key = (platform or PLACEHOLDER_PLATFORM).strip() or PLACEHOLDER_PLATFORM platform_key = (platform or PLACEHOLDER_PLATFORM).strip() or PLACEHOLDER_PLATFORM
if account_id: aid_raw = (account_id or "").strip()
try: if aid_raw and not _is_placeholder_config(aid_raw):
aid = int(account_id) if not aid_raw.isdigit():
except (TypeError, ValueError): if not (login_id or "").strip():
raise AccountManagerError("ACCOUNT_NOT_FOUND", f"账号 ID 无效:{account_id}") login_id = aid_raw
return _pick_by_id(platform_key, aid) aid_raw = ""
else:
return _pick_by_id(platform_key, int(aid_raw))
lid = (login_id or "").strip() or None
if lid and _is_placeholder_config(lid):
lid = None
try: try:
return _pick_web_with_lease(platform_key) return _ensure_web_with_lease(platform_key, login_id=lid)
except AccountManagerError as exc: except AccountManagerError as exc:
if exc.code in ("NO_ACCOUNT", "ACCOUNT_NOT_FOUND") or "NO_ACCOUNT" in exc.code: if exc.code in ("NO_ACCOUNT", "ACCOUNT_NOT_FOUND") or "NO_ACCOUNT" in exc.code:
raise AccountManagerError("ACCOUNT_SETUP_REQUIRED", ACCOUNT_SETUP_MESSAGE) from exc raise AccountManagerError("ACCOUNT_SETUP_REQUIRED", ACCOUNT_SETUP_MESSAGE) from exc

View File

@@ -161,16 +161,16 @@ python {skill_root}/scripts/main.py <command> <args...>
### `executionProfile`(仅 sync / async ### `executionProfile`(仅 sync / async
模板与新技能**只允许**这两种值;每个 Action **必须**显式写出;禁止发明 `auto` / `background` / `deferred`**禁止**依赖宿主「省略字段时的兼容默认」 **心智**:多数 Action 按 **sync**(短调用、不进任务中心);只有长耗时 / RPA / 需进度控制时才标 **async**。匠厂宿主若**省略**该字段,按 **sync** 处理;模板与新技能仍要求**每个 Action 显式写出** `sync``async`禁止发明 `auto` / `background` / `deferred`,也禁止赌省略行为
| 值 | 宿主行为 | 任务中心 | | 值 | 宿主行为 | 任务中心 |
|----|----------|----------| |----|----------|----------|
| `sync` | 调用方等待结束,直接返回结构化结果或结构化错误;**不**创建后台 Job | 不进 | | `sync`(或省略时的宿主缺省) | 调用方等待结束,直接返回结构化结果或结构化错误;**不**创建后台 Job | 不进 |
| `async` | 宿主创建后台 Job立即返回 `jobId`;用 `emit` / `checkpoint` / `finish` 报告进度与终态 | **进入** | | `async`(须显式声明) | 宿主创建后台 Job立即返回 `jobId`;用 `emit` / `checkpoint` / `finish` 报告进度与终态 | **进入** |
合法组合(全部允许):`sync|async` × `toolbar|cron|agent|skill-detail` 合法组合(全部允许):`sync|async` × `toolbar|cron|agent|skill-detail`
经验建议(**不是** placement 硬限制):耗时不可预测、需要暂停/停止/进度展示的任务,用 `async` 经验建议(**不是** placement 硬限制):耗时不可预测、需要暂停/停止/进度展示的任务,用 `async`;其余默认写 `sync`
直接执行短 CLI 时,终端自然等待进程结束。宿主通过 Action 调用同一 CLI 时,再按 `executionProfile` 决定同步等待还是建 Job。技能**不得**自行实现第二套任务中心或后台队列。 直接执行短 CLI 时,终端自然等待进程结束。宿主通过 Action 调用同一 CLI 时,再按 `executionProfile` 决定同步等待还是建 Job。技能**不得**自行实现第二套任务中心或后台队列。

View File

@@ -1,6 +1,6 @@
"""技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger""" """技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger"""
SKILL_SLUG = "your-skill-slug" SKILL_SLUG = "your-skill-slug"
SKILL_VERSION = "1.0.48" SKILL_VERSION = "1.0.51"
LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug" LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug"
PLATFORM_KIT_MIN_VERSION = "1.2.0" PLATFORM_KIT_MIN_VERSION = "1.2.2"

View File

@@ -35,7 +35,7 @@ def get_skill_root() -> str:
return _SKILL_ROOT return _SKILL_ROOT
def platform_kit_version_patch(version: str = "1.2.0"): def platform_kit_version_patch(version: str = "1.2.2"):
"""Mock installed jiangchang-platform-kit version for health/diagnostics tests.""" """Mock installed jiangchang-platform-kit version for health/diagnostics tests."""
from unittest.mock import patch from unittest.mock import patch

View File

@@ -35,6 +35,7 @@ POLICY_IDS = (
"POLICY-RPA-001", "POLICY-RPA-001",
"POLICY-RPA-002", "POLICY-RPA-002",
"POLICY-RPA-003", "POLICY-RPA-003",
"POLICY-RPA-004",
"POLICY-PACKAGING-001", "POLICY-PACKAGING-001",
"POLICY-PACKAGING-002", "POLICY-PACKAGING-002",
"POLICY-DOCS-001", "POLICY-DOCS-001",
@@ -142,6 +143,18 @@ RPA_003_SOURCE = (
"development/RPA.md §5.3; development/TESTING.md §12; development/POLICY_MATRIX.md" "development/RPA.md §5.3; development/TESTING.md §12; development/POLICY_MATRIX.md"
) )
RPA_004_SOURCE = (
"development/RPA.md §0.2; development/ADAPTER.md §兄弟依赖; development/POLICY_MATRIX.md"
)
RPA_004_HEADLESS_KEY_RE = re.compile(
r"(?m)^\s*OPENCLAW_BROWSER_HEADLESS\s*=\s*([^\s#]+)"
)
RPA_004_LAUNCH_MARKERS = (
"launch_persistent_context",
"launch_persistent_browser",
)
RPA_004_PICK_MARKER = "pick_web_account"
LOGGING_001_SOURCE = "development/LOGGING.md; development/RUNTIME.md" LOGGING_001_SOURCE = "development/LOGGING.md; development/RUNTIME.md"
LOGGING_002_SOURCE = "development/LOGGING.md; development/DEVELOPMENT.md" LOGGING_002_SOURCE = "development/LOGGING.md; development/DEVELOPMENT.md"
LOGGING_003_SOURCE = "development/LOGGING.md" LOGGING_003_SOURCE = "development/LOGGING.md"
@@ -615,6 +628,47 @@ class TestPolicyRpa003(unittest.TestCase):
) )
class TestPolicyRpa004(unittest.TestCase):
def test_env_example_browser_headless_defaults_to_headed(self) -> None:
skill_root = get_skill_root()
env_path = os.path.join(skill_root, ".env.example")
self.assertTrue(os.path.isfile(env_path), msg=".env.example missing")
text = _read_text(env_path)
match = RPA_004_HEADLESS_KEY_RE.search(text)
if match is None:
return
value = match.group(1).strip().strip("\"'")
self.assertEqual(
value,
"0",
msg=_policy_msg(
"POLICY-RPA-004",
RPA_004_SOURCE,
f".env.example OPENCLAW_BROWSER_HEADLESS must default to 0 (headed), got {value!r}",
),
)
def test_launch_persistent_requires_pick_web_account(self) -> None:
skill_root = get_skill_root()
script_texts = [
_read_text(path) for path in _walk_files(skill_root, "scripts", suffix=".py")
]
combined = "\n".join(script_texts)
uses_launch = any(marker in combined for marker in RPA_004_LAUNCH_MARKERS)
if not uses_launch:
return
self.assertIn(
RPA_004_PICK_MARKER,
combined,
msg=_policy_msg(
"POLICY-RPA-004",
RPA_004_SOURCE,
"scripts/ uses launch_persistent_* but missing pick_web_account "
"(must obtain profile_dir before opening browser)",
),
)
def _scan_sensitive_logging_assignments(skill_root: str) -> list[str]: def _scan_sensitive_logging_assignments(skill_root: str) -> list[str]:
offenders: list[str] = [] offenders: list[str] = []
for path in _walk_files(skill_root, "scripts", suffix=".py"): for path in _walk_files(skill_root, "scripts", suffix=".py"):

View File

@@ -238,6 +238,17 @@ class TestDocsStandards(unittest.TestCase):
msg="RPA.md must mention jc2009 / industry simulator / portal gate", msg="RPA.md must mention jc2009 / industry simulator / portal gate",
) )
def test_rpa_md_covers_web_profile_headed_and_login_split(self) -> None:
text = self._read("development/RPA.md")
self.assertIn("0.2", text)
self.assertIn("profile_dir", text)
self.assertIn("POLICY-RPA-004", text)
self.assertIn("有头", text)
self.assertTrue(
"登录不全局强制" in text or "登录 ≠ Profile" in text or "登录≠Profile" in text,
msg="RPA.md §0.2 must separate site-login from Profile requirement",
)
def test_simulator_example_readme_covers_async_and_account_manager(self) -> None: def test_simulator_example_readme_covers_async_and_account_manager(self) -> None:
text = self._read("examples/simulator_browser_rpa/README.md") text = self._read("examples/simulator_browser_rpa/README.md")
self.assertIn("async", text.lower()) self.assertIn("async", text.lower())

View File

@@ -83,16 +83,16 @@ class TestPlatformImportSource(unittest.TestCase):
) )
) )
def test_platform_kit_min_version_is_1_2_0(self) -> None: def test_platform_kit_min_version_is_at_least_declared(self) -> None:
from jiangchang_skill_core import version_ge from jiangchang_skill_core import version_ge
from util.constants import PLATFORM_KIT_MIN_VERSION from util.constants import PLATFORM_KIT_MIN_VERSION
self.assertEqual(PLATFORM_KIT_MIN_VERSION, "1.2.0") self.assertEqual(PLATFORM_KIT_MIN_VERSION, "1.2.2")
md_path = os.path.join(get_skill_root(), "SKILL.md") md_path = os.path.join(get_skill_root(), "SKILL.md")
with open(md_path, encoding="utf-8") as f: with open(md_path, encoding="utf-8") as f:
md = f.read() md = f.read()
self.assertEqual(_parse_platform_kit_min_version(md), "1.2.0") self.assertEqual(_parse_platform_kit_min_version(md), "1.2.2")
req_path = os.path.join(get_skill_root(), "requirements.txt") req_path = os.path.join(get_skill_root(), "requirements.txt")
with open(req_path, encoding="utf-8") as f: with open(req_path, encoding="utf-8") as f:

View File

@@ -34,7 +34,7 @@ FORBIDDEN_PHRASES = (
POSITIVE_MARKERS = ( POSITIVE_MARKERS = (
"jiangchang-platform-kit", "jiangchang-platform-kit",
"1.2.0", "1.2.2",
"共享 runtime", "共享 runtime",
) )