Compare commits

..

6 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
144455fbe0 test(config): add POLICY-CONFIG-004 hard gate for user-facing .env.example comments (v1.0.48)
All checks were successful
技能自动化发布 / release (push) Successful in 8s
2026-07-17 19:06:21 +08:00
5ec9648148 docs(config): user-facing .env.example comments for config UI (v1.0.47)
All checks were successful
技能自动化发布 / release (push) Successful in 12s
2026-07-17 18:25:37 +08:00
ec049fdc2a feat(docs): add market four-tab materials (tutorial/demo/changelog) for v1.0.46
All checks were successful
技能自动化发布 / release (push) Successful in 13s
2026-07-17 11:55:29 +08:00
31 changed files with 960 additions and 213 deletions

View File

@@ -1,30 +1,53 @@
# ── 运行模式 / adapter 档位(见 development/ADAPTER.md── # 运行模式:决定本技能用模拟数据还是真实浏览器/接口去执行
OPENCLAW_TEST_TARGET=mock # mock | simulator_rpa | real_api | real_rpa OPENCLAW_TEST_TARGET=mock # 可选:mock=本地模拟,simulator_rpa=仿真站点,real_api=真实接口real_rpa=真实浏览器;模板默认 mock日常正式使用请改为 real_rpa
# ── 目标系统 ── # 目标网站地址:技能要访问的网站或服务地址
TARGET_BASE_URL=https://sandbox.jc2009.com # 仿真/生产地址,可被进程环境变量覆盖 TARGET_BASE_URL=https://sandbox.jc2009.com # 填写完整网址(含 https://);换环境时再改,一般保持默认即可
# ── 默认账号(仅非敏感标识;密码走 account-manager── # 默认登录账号:仅填写非敏感的账号标识(如工号/登录名/手机号)
DEFAULT_LOGIN_ID=04110001 DEFAULT_LOGIN_ID= # 留空=按平台自动选用或登记;有值则按该标识筛选;不要填密码
# ── 浏览器 / RPA ── # 默认账号编号:仅填账号管理里的纯数字主键(一般留空)
OPENCLAW_BROWSER_HEADLESS=0 # 0=有头(默认) 1=无头(CI) DEFAULT_ACCOUNT_ID= # 留空=自动;只填纯数字;手机号请填上方登录账号
OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启反检测指纹(默认)
# ── 存证 ── # 是否显示浏览器窗口:网页自动化时是否弹出可见浏览器
OPENCLAW_RECORD_VIDEO=0 # 0=默认不录屏1=启用录屏+字幕+旁白+背景音+MP4 OPENCLAW_BROWSER_HEADLESS=0 # 0=显示窗口必须便于介入与排查1=后台静默(勿作日常用法)
OPENCLAW_ARTIFACTS_ON_FAILURE=1 # 1=失败截图(默认)
# ── 节流 / 超时 ── # 浏览器防检测:降低被网站识别为自动化工具的概率
STEP_DELAY_MIN=1.0 # 步骤间随机等待下限(秒) OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启推荐0=关闭;一般保持默认
STEP_DELAY_MAX=5.0 # 步骤间随机等待上限(秒)
HUMAN_WAIT_TIMEOUT=180 # 滑块/验证码/2FA 等人工超时(秒)
# ── 数据目录子路径(见 development/DATA_PATHS.md留空则用 skill 数据目录下默认子文件夹)── # 是否录制操作过程:用于留证、复现问题或向他人演示
# 相对路径相对于 {JIANGCHANG_DATA_ROOT}/{USER_ID}/{slug}/,不是 workspace / CWD OPENCLAW_RECORD_VIDEO=0 # 0=不录屏默认1=录屏并生成带说明的视频文件;需要留证时再改为 1
# SKILL_DOWNLOAD_DIR=downloads
# SKILL_IMPORT_DIR=imports # 失败时是否截图:任务出错时自动保存现场截图,便于排查
# SKILL_EXPORT_DIR=exports OPENCLAW_ARTIFACTS_ON_FAILURE=1 # 1=失败时截图推荐0=不截图
# SKILL_UPLOAD_DIR=uploads
# SKILL_CACHE_DIR=cache # 步骤最短等待:两步操作之间随机停顿的下限,模拟真人节奏
# SKILL_TEMP_DIR=temp STEP_DELAY_MIN=1.0 # 单位:秒;一般保持默认,页面不稳定时可略调大
# 步骤最长等待:两步操作之间随机停顿的上限
STEP_DELAY_MAX=5.0 # 单位:秒;须大于等于最短等待;一般保持默认
# 人工处理超时:遇到滑块、验证码、二次验证时,等待人工处理的最长时间
HUMAN_WAIT_TIMEOUT=180 # 单位:秒;来不及处理时可调大,如 300
# 以下为可选目录配置。一般不用改;取消注释并填写后,才覆盖默认子文件夹。
# 填写相对路径时,相对于本技能的数据目录(不是当前工作目录)。
# 下载目录:技能保存下载文件的位置
# SKILL_DOWNLOAD_DIR=downloads # 相对本技能数据目录;留空或保持注释则用默认 downloads
# 导入目录:待导入文件放置的位置
# SKILL_IMPORT_DIR=imports # 相对本技能数据目录;留空或保持注释则用默认 imports
# 导出目录:技能导出文件的保存位置
# SKILL_EXPORT_DIR=exports # 相对本技能数据目录;留空或保持注释则用默认 exports
# 上传目录:待上传文件放置的位置
# SKILL_UPLOAD_DIR=uploads # 相对本技能数据目录;留空或保持注释则用默认 uploads
# 缓存目录:临时缓存文件的位置
# SKILL_CACHE_DIR=cache # 相对本技能数据目录;留空或保持注释则用默认 cache
# 临时目录:运行中临时文件的位置
# SKILL_TEMP_DIR=temp # 相对本技能数据目录;留空或保持注释则用默认 temp

43
CHANGELOG.md Normal file
View File

@@ -0,0 +1,43 @@
# Changelog
本文件对应技能市场 **「更新日志」** Tab。
发布 CI 会按当前 tag 版本(如 `v1.0.46``1.0.46`)提取匹配的 `##` 小节,写入该版本的 `changelog`
约定:
- 每个正式版本一节,标题形如 `## 1.0.46``## [1.0.46] - YYYY-MM-DD`
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
- 打 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
- 新增配置注释硬门禁 `POLICY-CONFIG-004``.env.example` 每个配置项必须有中文行头与行尾说明,禁止开发文档指向,测试不通过则无法发布
## 1.0.47
- 规范 `.env.example` 注释:面向「数据管理 → 配置管理」用户,行头说明「是什么」、行尾说明「怎么填」
- 去掉注释中的开发文档指向与黑话;`CONFIG.md` / 发布检查清单同步约束,避免各技能注释写法不一致
## 1.0.46
- 模板默认增加市场材料:`TUTORIAL.md`(使用教程)、`DEMO.md`(演示)、`CHANGELOG.md`(更新日志),与根 `README.md`(技能说明)组成四 Tab 内容源
- 发布 CI 同步 `tutorial_md` / `demo_md` / `demo_video_url` / 版本 `changelog`
## 1.0.45
- 沉淀通用 Agent 路由与 how-to 文档标准(见仓库提交说明)

24
DEMO.md Normal file
View File

@@ -0,0 +1,24 @@
---
demo_video_url: ""
---
> **模板说明(复制后删除本段)**
> 本文对应技能市场 **「演示」** Tab。
> - frontmatter 的 `demo_video_url`:演示视频地址(发布时同步为 `demo_video_url`);没有视频时保持空字符串 `""`。
> - 正文(发布时同步为 `demo_md`):可选,写视频看点、时间轴或无视频时的图文演示说明。
> **不要**把演示链接写进 `SKILL.md`(市场材料 ≠ Agent 契约)。
> 复制为业务技能后:填入真实可访问的视频 URL并删掉本提示段。
# 演示说明
用一两句话说明这段演示帮助用户看懂什么(例如:从发起任务到看到结果的完整过程)。
## 建议观看顺序
1. 如何发起一次典型任务
2. 执行过程中会看到什么提示 / 进度
3. 成功后的结果长什么样
## 没有视频时
可在此用简短图文描述关键界面与结果样例;有视频后把 `demo_video_url` 填上即可,正文可保留为补充说明。

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.45 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 必须能分开触发;禁止默认绑成不可拆的一步。一键无准备态的技能可跳过本条。
@@ -38,7 +38,7 @@ allowed-tools:
| 短查询 / 只读 / 秒级(`health` / `version` / `stats` / `list` / `get` / `logs` 等) | OpenClaw bash/exec + **共享 Python** 跑 CLI见下方「共享 Python」 | 否CLI 当场结束) | | 短查询 / 只读 / 秒级(`health` / `version` / `stats` / `list` / `get` / `logs` 等) | OpenClaw bash/exec + **共享 Python** 跑 CLI见下方「共享 Python」 | 否CLI 当场结束) |
| 已在 `actions.json` 声明且 `executionProfile: "sync"` 的能力 | 可用 `run_skill_action`,或同等效果的短 CLI同一内核 | 否 | | 已在 `actions.json` 声明且 `executionProfile: "sync"` 的能力 | 可用 `run_skill_action`,或同等效果的短 CLI同一内核 | 否 |
| 长耗时 / RPA / 浏览器 / 需要进度或取消;或 `executionProfile: "async"` | **必须** `run_skill_action`(先 `list_skill_actions` / `get_skill_action` | **是**(仅因为该 Action 是 async | | 长耗时 / RPA / 浏览器 / 需要进度或取消;或 `executionProfile: "async"` | **必须** `run_skill_action`(先 `list_skill_actions` / `get_skill_action` | **是**(仅因为该 Action 是 async |
| 用户只问用法 / 能做什么 | 读根目录 `README.md`**不要**为答用法去列目录或默认打开 `references/` | — | | 用户只问用法 / 能做什么 | 读根目录 `README.md`逐步操作再读 `TUTORIAL.md`**不要**为答用法去列目录或默认打开 `references/` | — |
### 用户意图路由表(业务技能复制后必改) ### 用户意图路由表(业务技能复制后必改)
@@ -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` 做只读诊断。
@@ -84,9 +84,9 @@ python {baseDir}/scripts/main.py init-db
## 面向用户问答LLM 规则) ## 面向用户问答LLM 规则)
- 本文(`SKILL.md`)是 LLM / OpenClaw 平台读取的技能入口,**不是**用户市场说明。 - 本文(`SKILL.md`)是 LLM / OpenClaw 平台读取的技能入口,**不是**用户市场说明。
- 根目录 `README.md` 是面向普通用户的说明 - 根目录市场材料面向普通用户(见下方「文档分工」),**不是** Agent 调用契约
当用户询问以下问题时,**必须优先读取**根目录 `README.md`,并用用户能理解的业务语言回答: 当用户询问以下问题时,**必须优先读取**根目录 `README.md`(能力说明)与 `TUTORIAL.md`(逐步用法),并用用户能理解的业务语言回答:
- 这个技能是做什么的 - 这个技能是做什么的
- 这个技能怎么用 - 这个技能怎么用
@@ -97,7 +97,7 @@ python {baseDir}/scripts/main.py init-db
**回答「怎么用 / 能做什么」时的要求:** **回答「怎么用 / 能做什么」时的要求:**
1. `README.md` 的「能帮你做什么」「你可以这样告诉 Agent」「常见问题」等章节为主组织回答 1. 「能做什么 / 场景 / FAQ」以 `README.md` 为主;「第一次怎么用完」以 `TUTORIAL.md` 为主
2. **禁止**在回答中粘贴 `python …/scripts/main.py …` 或其它命令行(除非用户明确索要命令,或自称技术人员/开发者)。 2. **禁止**在回答中粘贴 `python …/scripts/main.py …` 或其它命令行(除非用户明确索要命令,或自称技术人员/开发者)。
3. **禁止**为答用法而去 `dir` / `ls` / 列技能目录,或默认打开 `references/``development/`;仅当用户明确问命令、参数、集成或排错时再读 `references/` 3. **禁止**为答用法而去 `dir` / `ls` / 列技能目录,或默认打开 `references/``development/`;仅当用户明确问命令、参数、集成或排错时再读 `references/`
4. **不要**向普通用户暴露 Playwright、DOM、Python、RPA、库表名、`error_code` 等实现细节,除非用户明确询问技术实现。 4. **不要**向普通用户暴露 Playwright、DOM、Python、RPA、库表名、`error_code` 等实现细节,除非用户明确询问技术实现。
@@ -106,14 +106,17 @@ python {baseDir}/scripts/main.py init-db
- **不要**把 `SKILL.md``references/``development/` 中的技术细节直接作为用户回答。 - **不要**把 `SKILL.md``references/``development/` 中的技术细节直接作为用户回答。
- **只有当**用户明确询问命令、参数、输出结构、开发、调试、集成或排错时,才读取 `references/``development/` 主要给开发者 / 编码助手,对话 Agent **不必**默认打开。 - **只有当**用户明确询问命令、参数、输出结构、开发、调试、集成或排错时,才读取 `references/``development/` 主要给开发者 / 编码助手,对话 Agent **不必**默认打开。
- `README.md` `SKILL.md` / `references` 表述冲突:对用户展示与市场说明`README.md` 为准;对执行契约以 `SKILL.md` / `references` 为准。 -市场材料`SKILL.md` / `references` 表述冲突:对用户展示以 `README.md` / `TUTORIAL.md` 为准;对执行契约以 `SKILL.md` / `references` 为准。
- 用户未问技术实现时,**不要**讨论 slug 命名规则;开发者问命名或 slug 时,读取 `development/NAMING.md` - 用户未问技术实现时,**不要**讨论 slug 命名规则;开发者问命名或 slug 时,读取 `development/NAMING.md`
## 文档分工 ## 文档分工
| 文档 | 读者 | 用途 | | 文档 | 读者 | 用途 |
|------|------|------| |------|------|------|
| 根目录 `README.md` | 普通用户 | 技能市场详情页说明(`metadata.readme_md` 主来源 | | 根目录 `README.md` | 普通用户 | 市场 **说明** Tab`readme_md` |
| 根目录 `TUTORIAL.md` | 普通用户 | 市场 **教程** Tab`tutorial_md` |
| 根目录 `DEMO.md` | 普通用户 | 市场 **演示** Tab`demo_video_url` + `demo_md` |
| 根目录 `CHANGELOG.md` | 普通用户 | 市场 **更新日志** Tab按版本小节 → `changelog` |
| `SKILL.md`(本文) | LLM / OpenClaw 平台 | 技能入口、触发与运行契约摘要 | | `SKILL.md`(本文) | LLM / OpenClaw 平台 | 技能入口、触发与运行契约摘要 |
| `references/` | Agent 编排 / 调用 | CLI、Action、schema 契约Agent 主读) | | `references/` | Agent 编排 / 调用 | CLI、Action、schema 契约Agent 主读) |
| `development/` | 开发者 / AI 编程代理 | 需求、开发教程、测试、深度技术规范 | | `development/` | 开发者 / AI 编程代理 | 需求、开发教程、测试、深度技术规范 |
@@ -132,7 +135,7 @@ python {baseDir}/scripts/main.py init-db
2. 业务技能仓库中**不得**存在 `.openclaw-skill-template`(仅 template 源仓库保留)。 2. 业务技能仓库中**不得**存在 `.openclaw-skill-template`(仅 template 源仓库保留)。
3. 复制目录为你的新 skill 仓库,全局替换 `your-skill-slug` 等占位词。 3. 复制目录为你的新 skill 仓库,全局替换 `your-skill-slug` 等占位词。
4.`development/DEVELOPMENT.md` 完成开发与文档定制。 4.`development/DEVELOPMENT.md` 完成开发与文档定制。
5. 用户市场说明写入根 `README.md`Agent 调用契约见 `references/CLI.md``references/SCHEMA.md``references/ACTIONS.md` 5. 用户市场四 Tab 材料写入根 `README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md`Agent 调用契约见 `references/CLI.md``references/SCHEMA.md``references/ACTIONS.md`
6. 同步修改 `scripts/util/constants.py` 中的 `SKILL_SLUG` / `SKILL_VERSION` 6. 同步修改 `scripts/util/constants.py` 中的 `SKILL_SLUG` / `SKILL_VERSION`
7. 本地 SQLite 的中文表名/字段名、标准时间字段与字段顺序规范见 `references/SCHEMA.md` 7. 本地 SQLite 的中文表名/字段名、标准时间字段与字段顺序规范见 `references/SCHEMA.md`

65
TUTORIAL.md Normal file
View File

@@ -0,0 +1,65 @@
> **模板说明(复制后删除本段)**
> 本文对应技能市场 **「使用教程」** Tab发布时同步为 `tutorial_md`)。
> 面向普通用户写**可跟着做的步骤**,不要写 CLI、目录结构、release、开发规范。
> 与根 `README.md`技能说明分工README 讲「是什么 / 能做什么」;本文讲「第一次怎么用完」。
> **复制后验收章节(标题可微调):**「开始前」「第一步…」「第二步…」「常见卡住怎么办」。
# 【技能名称】使用教程
用最短路径带用户完成一次成功使用。步骤要具体,可用「打开匠厂 → …」这类操作语言。
---
## 开始前
确认:
1. 已安装并登录匠厂客户端
2. 已在技能市场安装本技能(或更新到最新版)
3. 【网页自动化类技能】首次运行一般会自动登记本平台账号;也可在「账号管理」中指定登录标识。浏览器会话由系统隔离管理——**不要**用本机日常浏览器用户目录去跑真实站
4. 【按平台】若本技能说明「需要先登录目标站」,请在弹出的有头浏览器中按提示登录或扫码;「登记账号」不等于「已登录目标站」。若说明「可不登录也能用」,仍须有可用的账号会话目录
5. 【如需】需要的文件 / 时间范围 / 单号等已准备好
---
## 第一步:打开技能并说清目标
在匠厂里新建任务或打开对话,用自然语言说明要做什么,例如:
> “帮我【完成某件业务事】,范围是【时间 / 对象 / 条件】。”
建议一次只交代一件事;范围说清楚,减少来回确认。
---
## 第二步:按提示补充信息
如果 Agent 问你缺什么(账号、文件、筛选条件),按提示补充即可。
- 有文件时:按界面提示选择或拖入
- 有账号时:确认已在对应平台登录,且具备操作权限
---
## 第三步:等待结果并核对
任务结束后,请核对:
- 是否成功完成
- 处理了多少条 / 哪些失败
- 生成的文件、表格或记录是否在预期位置
若失败,先看失败原因说明,再按下一节排查。
---
## 常见卡住怎么办
| 现象 | 你可以先试 |
|------|------------|
| 提示未登录 / 无权限 | 确认匠厂已登录;业务账号是否在线、权限是否足够 |
| 提示缺少必要信息 | 把时间范围、单号、文件等说完整后再试一次 |
| 一直没有结果 | 到任务中心查看进度或失败原因;必要时重新发起 |
| 结果不对 | 用更明确的条件再说一次,或缩小范围重试 |
仍无法解决时,把「你说了什么 + 界面提示原文」发给支持人员,便于排查。

View File

@@ -6,6 +6,6 @@
`skill-actions.schema.json` 使用 `additionalProperties: false`。正式支持 `bind.tables`toolbar Action 必填);`row`/`batch` placements 与 `concurrency`/`locks` 仍为预留。`placements``executionProfile` 正交——进任务中心只看 `async`,与入口无关。表级 Action 与 Agent 契约见 [`references/ACTIONS.md`](../references/ACTIONS.md);开发深度规范见 [`development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md)。 `skill-actions.schema.json` 使用 `additionalProperties: false`。正式支持 `bind.tables`toolbar Action 必填);`row`/`batch` placements 与 `concurrency`/`locks` 仍为预留。`placements``executionProfile` 正交——进任务中心只看 `async`,与入口无关。表级 Action 与 Agent 契约见 [`references/ACTIONS.md`](../references/ACTIONS.md);开发深度规范见 [`development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md)。
- 用户市场说明见根目录 [`README.md`](../README.md) - 用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.md)
- Agent 调用/编排参考见 [`references/`](../references/)(含 [`ACTIONS.md`](../references/ACTIONS.md) - Agent 调用/编排参考见 [`references/`](../references/)(含 [`ACTIONS.md`](../references/ACTIONS.md)
- 开发规范见 [`development/`](../development/) - 开发规范见 [`development/`](../development/)

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

@@ -11,44 +11,112 @@
4. **技能升级合并缺失项**`merge_missing_env_keys` 会把 `.env.example` 中新增、但用户 `.env` 尚未包含的 key **追加**进去(带注释块)。 4. **技能升级合并缺失项**`merge_missing_env_keys` 会把 `.env.example` 中新增、但用户 `.env` 尚未包含的 key **追加**进去(带注释块)。
5. **敏感信息绝不进 `.env`**:密码/密钥/口令走 account-manager 的 `secret-ref`(见下"红线")。 5. **敏感信息绝不进 `.env`**:密码/密钥/口令走 account-manager 的 `secret-ref`(见下"红线")。
## 标准配置项(`.env` 分组 ## `.env.example` 注释 = 配置管理用户文案(硬规则
每个 skill 的 `.env.example` 至少包含这些通用项(按需增减): 匠厂宿主「数据管理 → 配置管理」会把 `.env` / `.env.example` 的注释展示给**最终用户**(英文 key 可保留作标题)。因此注释必须按**产品文案**来写,不能按开发备忘录来写。
### 行头与行尾分工
| 位置 | 职责 | 要求 |
|------|------|------|
| **行头**`KEY=` 上一行的 `# …` | **是什么**:中文说明该配置项干什么 | **每个活跃 key 必有**一行完整中文行头 |
| **行尾**`KEY=值` 同一行 ` #` 之后) | **怎么填**:可选值、单位、推荐默认、何时要改 | 有枚举、开关、单位时**必有**;纯自由文本也应尽量写清填法 |
宿主当前会把「行头 + 行尾」拼成 key 下方的多行说明。按上表书写后,用户看到的自然是「先懂是什么,再知道怎么填」。
**不要**依赖 `# ── 分组 ──` 装饰线给整组做唯一说明:宿主只会把行头挂到**紧挨着的下一个活跃 key**,后面的 key 不会自动继承。分组可用空行区分;**逐 key 自带行头(+ 行尾)才是硬要求**。
### 必须做到
1. 每个会进入配置管理的活跃 `KEY=` 都有完整中文**行头**。
2. 枚举 / `0|1` 开关 / 带单位的数值:必须有中文**行尾**,解释每个可选值或单位,并给出推荐。
3. 业务专属 key 同样适用本规则,不得只留英文 key、不写中文说明。
4. 开发向细节(四档 adapter 实现、录屏管线、路径解析 API写在 `development/ADAPTER.md``RPA.md``DATA_PATHS.md` 等文档;**禁止**写进 `.env.example` 注释。
**机器门禁**`POLICY-CONFIG-004``tests/test_development_policy_guard.py`)会在默认测试 / `release.ps1` 中校验上述「行头+行尾中文、禁止开发文档指向」;不通过不得发布。
### 禁止出现在注释中的内容
- `见 development/...``见 ADAPTER.md``见 RPA.md``见 DATA_PATHS.md` 等仓库文档指向
- 默认假设读者是开发者的黑话:如单独甩 `adapter 档位``CI``account-manager``CWD``workspace` 而不解释
- 只写裸枚举(如 `mock | real_rpa`)却不说明每个值对用户意味着什么
- 多个 key 共用一个行头,后面的 key 没有任何自己的说明
若必须提到账号体系:写成用户能懂的「请在账号管理中登记密码 / 平台标识须一致」,不要只写内部模块名。
### 正例
```ini ```ini
# ── 运行模式 / adapter 档位(见 ADAPTER.md── # 运行模式:决定本技能用模拟数据还是真实浏览器/接口去执行
OPENCLAW_TEST_TARGET=mock # mock | simulator_rpa | real_api | real_rpa OPENCLAW_TEST_TARGET=mock # 可选mock=本地模拟,simulator_rpa=仿真站点,real_api=真实接口real_rpa=真实浏览器;模板默认 mock日常正式使用请改为 real_rpa
# ── 目标系统(示例占位,复制后改为业务地址)── # 是否显示浏览器窗口:网页自动化时是否弹出可见浏览器
TARGET_BASE_URL=https://sandbox.jc2009.com OPENCLAW_BROWSER_HEADLESS=0 # 0=显示窗口必须便于介入与排查1=后台静默(勿作日常用法)
# ── 默认账号(仅非敏感标识;密码走 account-manager──
# 浏览器 RPA simulator_rpa 联调账号来自 account-managerplatform + profile_dir不是此处的 DEFAULT_LOGIN_ID
DEFAULT_LOGIN_ID=04110001
# ── 浏览器 / RPA ──
OPENCLAW_BROWSER_HEADLESS=0 # 0=有头(默认) 1=无头(CI)
OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启反检测指纹(默认)
# ── 录屏与失败存证 ──
OPENCLAW_RECORD_VIDEO=0 # 0=默认不录屏1=启用录屏+字幕+旁白+背景音+MP4见 RPA.md
OPENCLAW_ARTIFACTS_ON_FAILURE=1 # 1=失败截图(默认)
# ── 节流 / 超时 ──
STEP_DELAY_MIN=1.0
STEP_DELAY_MAX=5.0
HUMAN_WAIT_TIMEOUT=180 # 滑块/验证码/2FA 等人工超时(秒)
``` ```
### 反例(禁止)
```ini
# ── 运行模式 / adapter 档位(见 development/ADAPTER.md──
OPENCLAW_TEST_TARGET=real_rpa # 生产默认真实 RPA单测/CI 可改为 mock
# ── 好看视频 / 百度账号(须与 account-manager 平台 key 一致)──
TARGET_PLATFORM=baidu
HAOKAN_VIDEO_SELECTOR=video.art-video
```
问题:文档路径与开发黑话;行尾半给用户半给 CI后续 key 无行头/行尾,用户只看到英文名。
## 标准配置项(`.env` 分组)
每个 skill 的 `.env.example` 至少包含这些通用项(按需增减)。**注释写法须与仓库根目录 `.env.example` 保持同级清晰度**(行头 = 是什么,行尾 = 怎么填):
```ini
# 运行模式:决定本技能用模拟数据还是真实浏览器/接口去执行
OPENCLAW_TEST_TARGET=mock # 可选mock=本地模拟simulator_rpa=仿真站点real_api=真实接口real_rpa=真实浏览器;模板默认 mock日常正式使用请改为 real_rpa
# 目标网站地址:技能要访问的网站或服务地址
TARGET_BASE_URL=https://sandbox.jc2009.com # 填写完整网址(含 https://);换环境时再改,一般保持默认即可
# 默认登录账号:仅填写非敏感的账号标识(如工号/登录名)
DEFAULT_LOGIN_ID= # 留空=按平台自动选用或登记;有值则按该标识筛选;不要填密码
# 默认账号编号:仅填账号管理里的纯数字主键(一般留空)
DEFAULT_ACCOUNT_ID= # 留空=自动;只填纯数字;手机号/用户名请填上方登录账号
# 是否显示浏览器窗口:网页自动化时是否弹出可见浏览器
OPENCLAW_BROWSER_HEADLESS=0 # 0=显示窗口必须便于介入与排查1=后台静默(勿作日常用法)
# 浏览器防检测:降低被网站识别为自动化工具的概率
OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启推荐0=关闭;一般保持默认
# 是否录制操作过程:用于留证、复现问题或向他人演示
OPENCLAW_RECORD_VIDEO=0 # 0=不录屏默认1=录屏并生成带说明的视频文件;需要留证时再改为 1
# 失败时是否截图:任务出错时自动保存现场截图,便于排查
OPENCLAW_ARTIFACTS_ON_FAILURE=1 # 1=失败时截图推荐0=不截图
# 步骤最短等待:两步操作之间随机停顿的下限,模拟真人节奏
STEP_DELAY_MIN=1.0 # 单位:秒;一般保持默认,页面不稳定时可略调大
# 步骤最长等待:两步操作之间随机停顿的上限
STEP_DELAY_MAX=5.0 # 单位:秒;须大于等于最短等待;一般保持默认
# 人工处理超时:遇到滑块、验证码、二次验证时,等待人工处理的最长时间
HUMAN_WAIT_TIMEOUT=180 # 单位:秒;来不及处理时可调大,如 300
```
四档运行模式的**实现与测试约定**见 [`ADAPTER.md`](ADAPTER.md);录屏管线见 [`RPA.md`](RPA.md)。这些内容**不要**写回 `.env.example` 注释。
**业务专属配置必须带技能前缀**(如 `DEMO_XXX``MY_SKILL_XXX``SKILL_DOWNLOAD_DIR`**不要污染全局命名空间**(禁止 `SCRAPE_1688_*``RECEIVE_ORDER_*` 等跨技能前缀写入模板)。 **业务专属配置必须带技能前缀**(如 `DEMO_XXX``MY_SKILL_XXX``SKILL_DOWNLOAD_DIR`**不要污染全局命名空间**(禁止 `SCRAPE_1688_*``RECEIVE_ORDER_*` 等跨技能前缀写入模板)。
### 数据目录子路径(下载 / 导入 / 导出) ### 数据目录子路径(下载 / 导入 / 导出)
技能**自己写入**的文件默认在 `{JIANGCHANG_DATA_ROOT}/{USER_ID}/{slug}/` 下标准子目录(`downloads/``imports/``exports/` 等)。须通过 `util.runtime_paths.resolve_data_path()``get_*_dir()` 解析,**禁止** `os.path.abspath(config.get(...))` 相对 CWD。 技能**自己写入**的文件默认在 `{JIANGCHANG_DATA_ROOT}/{USER_ID}/{slug}/` 下标准子目录(`downloads/``imports/``exports/` 等)。须通过 `util.runtime_paths.resolve_data_path()``get_*_dir()` 解析,**禁止** `os.path.abspath(config.get(...))` 相对 CWD。
- 权威说明:[`DATA_PATHS.md`](DATA_PATHS.md) - 权威说明(开发)[`DATA_PATHS.md`](DATA_PATHS.md)
- `.env.example` 中路径类配置**留空即用默认**;若写相对路径,必须相对 **skill 数据目录****禁止** `./outputs/...` 这类相对 workspace 的写法 - `.env.example` 中路径类配置**留空或保持注释即用默认**;若写相对路径,必须相对 **skill 数据目录****禁止** `./outputs/...` 这类相对 workspace 的写法
- 可选 env`SKILL_DOWNLOAD_DIR``SKILL_IMPORT_DIR``SKILL_EXPORT_DIR`(见 DATA_PATHS.md - 可选 env`SKILL_DOWNLOAD_DIR``SKILL_IMPORT_DIR``SKILL_EXPORT_DIR`;若作为活跃 key 出现在配置管理中,同样须有用户向行头/行尾(见模板根 `.env.example`
### 录屏开关(`OPENCLAW_RECORD_VIDEO` ### 录屏开关(`OPENCLAW_RECORD_VIDEO`
@@ -96,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`
@@ -110,7 +188,7 @@ config.get_float("STEP_DELAY_MIN", 1.0)
## 相关文档 ## 相关文档
- `RPA.md` — 三端 RPA 标准与各开关含义 - `RPA.md` — 三端 RPA 标准与各开关含义(开发向;勿写入 `.env` 注释)
- `ADAPTER.md``OPENCLAW_TEST_TARGET` 四档模式 - `ADAPTER.md``OPENCLAW_TEST_TARGET` 四档模式(开发向;勿写入 `.env` 注释)
- `RUNTIME.md``JIANGCHANG_DATA_ROOT` 与数据根约定 - `RUNTIME.md``JIANGCHANG_DATA_ROOT` 与数据根约定
- `DATA_PATHS.md` — 下载/导入/导出等子目录与 `resolve_data_path` 规范 - `DATA_PATHS.md` — 下载/导入/导出等子目录与 `resolve_data_path` 规范

View File

@@ -106,6 +106,9 @@ your-skill/
├─ tests/ ├─ tests/
├─ .gitignore ├─ .gitignore
├─ README.md ├─ README.md
├─ TUTORIAL.md
├─ DEMO.md
├─ CHANGELOG.md
├─ requirements.txt ├─ requirements.txt
├─ release.ps1 ├─ release.ps1
└─ SKILL.md └─ SKILL.md
@@ -114,7 +117,10 @@ your-skill/
各目录职责如下: 各目录职责如下:
- `assets/`放示例输出、schema、静态说明资源 - `assets/`放示例输出、schema、静态说明资源
- `README.md`面向用户的市场详情页说明 - `README.md`市场 **说明** Tab`readme_md`
- `TUTORIAL.md`:市场 **教程** Tab`tutorial_md`
- `DEMO.md`:市场 **演示** Tab`demo_video_url` + `demo_md`
- `CHANGELOG.md`:市场 **更新日志** Tab按版本小节同步 `changelog`
- `SKILL.md`:面向 LLM / 平台的技能入口 - `SKILL.md`:面向 LLM / 平台的技能入口
- `references/`Agent 运行/编排/调用时渐进加载的补充上下文(如 CLI、SCHEMA - `references/`Agent 运行/编排/调用时渐进加载的补充上下文(如 CLI、SCHEMA
- `development/`:面向开发者与 AI 编程代理的开发资料、需求、测试与技术规范 - `development/`:面向开发者与 AI 编程代理的开发资料、需求、测试与技术规范
@@ -158,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`
@@ -179,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
@@ -195,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 时的冲突风险。推荐范围写法:
@@ -305,7 +312,7 @@ git remote -v
复制后优先改下面这些地方: 复制后优先改下面这些地方:
1. `SKILL.md` 1. `SKILL.md`
2. 根目录 `README.md`(用户市场说明) 2. 根目录市场四 Tab`README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md`
3. `scripts/util/constants.py` 3. `scripts/util/constants.py`
4. `references/` 与 `development/` 下的文案 4. `references/` 与 `development/` 下的文案
5. `scripts/service/` 下的业务占位实现(优先改 `task_service.py` 5. `scripts/service/` 下的业务占位实现(优先改 `task_service.py`
@@ -339,7 +346,7 @@ git remote -v
如需浏览器自动化,不要在模板里保留空的 `platform_playwright.py`;请按业务新建 `xxx_playwright.py`(命名自定),并在 `task_service.py` 中按需引用。 如需浏览器自动化,不要在模板里保留空的 `platform_playwright.py`;请按业务新建 `xxx_playwright.py`(命名自定),并在 `task_service.py` 中按需引用。
## 6. `SKILL.md` 与根 `README.md` 应该怎么写 ## 6. `SKILL.md` 与市场四 Tab 材料应该怎么写
### `SKILL.md`LLM / 平台入口) ### `SKILL.md`LLM / 平台入口)
@@ -351,19 +358,26 @@ git remote -v
- 何时触发本技能、CLI 入口摘要、运行契约要点 - 何时触发本技能、CLI 入口摘要、运行契约要点
- 指向 `references/`Agent 调用)与 `development/`(开发规范)的分工说明 - 指向 `references/`Agent 调用)与 `development/`(开发规范)的分工说明
不要在 `SKILL.md` 里写大量用户市场文案或实现细节。 不要在 `SKILL.md` 里写大量用户市场文案或实现细节**不要**把演示视频 URL 写进 `SKILL.md`(放 `DEMO.md` frontmatter
### 根 `README.md`(用户市场说明 ### 市场四 Tab根目录面向普通用户
根 `README.md` 是技能市场详情页主来源(`metadata.readme_md`),面向普通用户。 发布 CI 会把下列文件同步到技能市场详情 Tabs有文件才更新对应字段
应包含 YAML frontmatter 的 `description`(市场列表简介),正文用用户能理解的语言说明: | 文件 | 市场 Tab | 同步字段 |
|------|----------|----------|
| `README.md` | 说明 | `readme_md`frontmatter `description` 作列表简介) |
| `TUTORIAL.md` | 教程 | `tutorial_md` |
| `DEMO.md` | 演示 | `demo_md` + frontmatter `demo_video_url` |
| `CHANGELOG.md` | 更新日志 | 匹配当前 tag 版本的 `##` 小节 → `changelog` |
- 能做什么、适合什么场景、使用前准备什么 **`README.md`**:能力与场景说明。含 frontmatter `description`;写能做什么、场景、准备、如何告诉 Agent、结果、注意事项、FAQ。不要写开发教程、目录规范、release、requirements。
- 如何自然语言告诉 Agent
- 执行结果、注意事项、常见问题
不要在根 `README.md` 里写开发教程、目录规范、release、requirements 等技术细节 **`TUTORIAL.md`**:第一次怎么用完的逐步操作。写「开始前 → 步骤 → 卡住怎么办」;不要写 CLI / 目录 / 开发规范
**`DEMO.md`**演示视频与补充说明。frontmatter 必须含 `demo_video_url`(无视频时用 `""`);正文可选。
**`CHANGELOG.md`**:每个正式版本一节(如 `## 1.0.46`)。打 tag 前为即将发布的版本新增小节;用户可读的变化,少写内部实现。
实现细节放在: 实现细节放在:
@@ -402,9 +416,12 @@ metadata:
当前规范 skill 的文档按读者拆分: 当前规范 skill 的文档按读者拆分:
**根 `README.md`** **根市场材料(四 Tab**
- 用户市场详情页说明(含 frontmatter `description` - `README.md` — 说明(含 frontmatter `description`
- `TUTORIAL.md` — 使用教程
- `DEMO.md` — 演示(`demo_video_url` + 正文)
- `CHANGELOG.md` — 按版本更新日志
**`SKILL.md`** **`SKILL.md`**
@@ -442,7 +459,7 @@ metadata:
比如 `skill-actions.schema.json`、日志记录、机读 JSON 的 schema 比如 `skill-actions.schema.json`、日志记录、机读 JSON 的 schema
不要把正式研发文档放到 `assets/`。 不要把正式研发文档放到 `assets/`。
用户说明进根 `README.md`Agent 编排资料进 `references/`;开发规范进 `development/`。 用户市场四 Tab 进根 `README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md`Agent 编排资料进 `references/`;开发规范进 `development/`。
## 9. `cli` 层怎么写 ## 9. `cli` 层怎么写
@@ -768,7 +785,7 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
如果工作流失败,不要继续做平台验证,而应该先回到代码仓库排查,例如: 如果工作流失败,不要继续做平台验证,而应该先回到代码仓库排查,例如:
- 发布脚本是否正常推送 - 发布脚本是否正常推送
- `SKILL.md`、根 `README.md`、`scripts/`、`references/`、`development/` 是否齐全 - `SKILL.md`、根市场四 Tab`README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md`、`scripts/`、`references/`、`development/` 是否齐全
- 工作流文件是否存在 - 工作流文件是否存在
- 发布包结构是否符合模板规范 - 发布包结构是否符合模板规范
@@ -837,6 +854,7 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
- [ ] `service` 下的核心业务文件(如 `task_service.py`)已按领域改名并实现 - [ ] `service` 下的核心业务文件(如 `task_service.py`)已按领域改名并实现
- [ ] 没有残留旧平台名 - [ ] 没有残留旧平台名
- [ ] `health` / `version` 可运行 - [ ] `health` / `version` 可运行
- [ ] `.env.example` 注释已按 [`CONFIG.md`](CONFIG.md)「行头 = 是什么、行尾 = 怎么填」写全(面向配置管理用户;无 `见 development/...` 等开发文档指向)
- [ ] `.gitignore` 生效,没有把 `__pycache__` 提交进去 - [ ] `.gitignore` 生效,没有把 `__pycache__` 提交进去
- [ ] `release.ps1` 存在(发布前会自动跑 `python tests/run_tests.py -v`;失败不得打 tag - [ ] `release.ps1` 存在(发布前会自动跑 `python tests/run_tests.py -v`;失败不得打 tag
- [ ] `.github/workflows/release_skill.yaml` 存在 - [ ] `.github/workflows/release_skill.yaml` 存在
@@ -851,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. 常见错误
@@ -903,7 +922,7 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
现在的新模板原则是: 现在的新模板原则是:
- 不做旧结构兼容 - 不做旧结构兼容
- 用户说明在根 `README.md`Agent 资料在 `references/`,开发规范在 `development/` - 用户市场四 Tab 在根 `README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md`Agent 资料在 `references/`,开发规范在 `development/`
- 统一走 `scripts/main.py` 作为 CLI 入口 - 统一走 `scripts/main.py` 作为 CLI 入口
### 错误 5平台放 slug 最前 ### 错误 5平台放 slug 最前
@@ -965,7 +984,7 @@ git remote add origin <新技能仓库 URL>
如果让一个新人照着做,我建议他按这个顺序: 如果让一个新人照着做,我建议他按这个顺序:
1. 按 [`NAMING.md`](NAMING.md) 确定 slug复制模板并改目录名 1. 按 [`NAMING.md`](NAMING.md) 确定 slug复制模板并改目录名
2. 改 `SKILL.md` 与根 `README.md` 2. 改 `SKILL.md` 与根市场四 Tab`README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md`
3. 改 `scripts/util/constants.py` 3. 改 `scripts/util/constants.py`
4. 改 `references/` 与 `development/` 4. 改 `references/` 与 `development/`
5. 改 `scripts/cli/app.py` 5. 改 `scripts/cli/app.py`
@@ -981,7 +1000,7 @@ git remote add origin <新技能仓库 URL>
- 目录结构统一 - 目录结构统一
- 入口统一为 `scripts/main.py` - 入口统一为 `scripts/main.py`
- 用户说明在根 `README.md`Agent 资料在 `references/`,开发规范在 `development/` - 用户市场四 Tab 在根 `README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md`Agent 资料在 `references/`,开发规范在 `development/`
- 业务核心逻辑统一放 `scripts/service/` - 业务核心逻辑统一放 `scripts/service/`
- 不再使用旧模板历史结构 - 不再使用旧模板历史结构

View File

@@ -13,11 +13,13 @@
| POLICY-CONFIG-001 | 仓库根目录必须存在 `.env.example` | development/CONFIG.md §核心原则、§标准配置项 | hard | 文件存在性 | `tests/test_development_policy_guard.py::TestPolicyConfig001` | | POLICY-CONFIG-001 | 仓库根目录必须存在 `.env.example` | development/CONFIG.md §核心原则、§标准配置项 | hard | 文件存在性 | `tests/test_development_policy_guard.py::TestPolicyConfig001` |
| POLICY-CONFIG-002 | `.gitignore` 必须忽略 `.env``*.env.local` | development/CONFIG.md §红线:敏感信息不进 .env | hard | 解析 `.gitignore` 行 | `tests/test_development_policy_guard.py::TestPolicyConfig002` | | POLICY-CONFIG-002 | `.gitignore` 必须忽略 `.env``*.env.local` | development/CONFIG.md §红线:敏感信息不进 .env | hard | 解析 `.gitignore` 行 | `tests/test_development_policy_guard.py::TestPolicyConfig002` |
| POLICY-CONFIG-003 | 业务代码不得直接 `os.environ` / `os.getenv` / `os.putenv` / `environ.get`;须经 `jiangchang_skill_core.config.get*`(允许 `main.py`、测试、config_bootstrap/runtime 边界文件) | development/CONFIG.md §配置 bootstrapdevelopment/ADAPTER.md §档位 dispatch | hard | 扫描 `scripts/**/*.py` 并排除边界文件 | `tests/test_development_policy_guard.py::TestPolicyConfig003` | | POLICY-CONFIG-003 | 业务代码不得直接 `os.environ` / `os.getenv` / `os.putenv` / `environ.get`;须经 `jiangchang_skill_core.config.get*`(允许 `main.py`、测试、config_bootstrap/runtime 边界文件) | development/CONFIG.md §配置 bootstrapdevelopment/ADAPTER.md §档位 dispatch | hard | 扫描 `scripts/**/*.py` 并排除边界文件 | `tests/test_development_policy_guard.py::TestPolicyConfig003` |
| POLICY-CONFIG-004 | `.env.example` 每个活跃 `KEY=` 必须有中文**行头**(是什么)与中文**行尾**(怎么填);注释禁止 `development/``*.md` 文档指向、`account-manager``adapter 档位` 等开发黑话 | development/CONFIG.md §.env.example 注释 = 配置管理用户文案 | hard | 解析 `.env.example` 活跃行与注释 | `tests/test_development_policy_guard.py::TestPolicyConfig004` |
| POLICY-TESTING-001 | 默认根目录 `tests/test_*.py` 不得硬编码 `real_api` / `real_rpa` / `OPENCLAW_TEST_TARGET`(白名单仅 `test_adapter_profile_policy.py`,用于档位 helper 自测) | development/TESTING.md §默认必跑测试要做什么、§4 测试目标档位 | hard | 扫描 `tests/test_*.py` 非注释行;白名单外禁止上述文本 | `tests/test_development_policy_guard.py::TestPolicyTesting001` | | POLICY-TESTING-001 | 默认根目录 `tests/test_*.py` 不得硬编码 `real_api` / `real_rpa` / `OPENCLAW_TEST_TARGET`(白名单仅 `test_adapter_profile_policy.py`,用于档位 helper 自测) | development/TESTING.md §默认必跑测试要做什么、§4 测试目标档位 | hard | 扫描 `tests/test_*.py` 非注释行;白名单外禁止上述文本 | `tests/test_development_policy_guard.py::TestPolicyTesting001` |
| POLICY-TESTING-002 | `tests/integration/` 下 Python 测试文件若存在,默认应为 `.sample`,不得进入默认套件 | development/TESTING.md §7 真实联调测试的安全约束、§8 新 skill 的最小测试清单 | hard | 检查 `tests/integration/*.py``.sample` 后缀 | `tests/test_development_policy_guard.py::TestPolicyTesting002` | | POLICY-TESTING-002 | `tests/integration/` 下 Python 测试文件若存在,默认应为 `.sample`,不得进入默认套件 | development/TESTING.md §7 真实联调测试的安全约束、§8 新 skill 的最小测试清单 | hard | 检查 `tests/integration/*.py``.sample` 后缀 | `tests/test_development_policy_guard.py::TestPolicyTesting002` |
| 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` |
@@ -57,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

@@ -17,4 +17,4 @@
脚手架与 Git 防串库:[`../tools/README.md`](../tools/README.md)`scaffold_skill.ps1`)。 脚手架与 Git 防串库:[`../tools/README.md`](../tools/README.md)`scaffold_skill.ps1`)。
Agent 调用契约见 [`../references/CLI.md`](../references/CLI.md)、[`../references/SCHEMA.md`](../references/SCHEMA.md)、[`../references/ACTIONS.md`](../references/ACTIONS.md)。 Agent 调用契约见 [`../references/CLI.md`](../references/CLI.md)、[`../references/SCHEMA.md`](../references/SCHEMA.md)、[`../references/ACTIONS.md`](../references/ACTIONS.md)。
用户市场说明见根目录 [`README.md`](../README.md),不要写进本目录。 用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.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. 文档目标
@@ -98,11 +105,12 @@
- 目录结构必须符合当前 `skill-template` 规范 - 目录结构必须符合当前 `skill-template` 规范
- 入口必须统一为 `scripts/main.py` - 入口必须统一为 `scripts/main.py`
- 用户说明写在根 `README.md`LLM/运行契约写在 `SKILL.md`;开发规范在 `development/` - 用户市场四 Tab 写在根 `README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md`LLM/运行契约写在 `SKILL.md`;开发规范在 `development/`
- 输出格式应尽量机读友好;错误前缀统一为 `ERROR:` - 输出格式应尽量机读友好;错误前缀统一为 `ERROR:`
- 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,
[ *,
login_id: Optional[str] = None,
) -> Dict[str, Any]:
start_url = (config.get("TARGET_BASE_URL") or DEFAULT_START_URL).strip() or DEFAULT_START_URL
lid = (login_id or "").strip() or None
label = f"{platform} {lid}" if lid else DEFAULT_ACCOUNT_LABEL
argv = [
"account", "account",
"pick-web", "ensure-web",
"--platform", "--platform",
platform, platform,
"--url",
start_url,
"--auth-strategy",
DEFAULT_AUTH_STRATEGY,
"--label",
label,
"--lease", "--lease",
"--holder", "--holder",
LEASE_HOLDER, LEASE_HOLDER,
"--ttl-sec", "--ttl-sec",
LEASE_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,
[ *,
login_id: Optional[str] = None,
) -> Dict[str, Any]:
start_url = (config.get("TARGET_BASE_URL") or DEFAULT_START_URL).strip() or DEFAULT_START_URL
lid = (login_id or "").strip() or None
label = f"{platform} {lid}" if lid else DEFAULT_ACCOUNT_LABEL
argv = [
"account", "account",
"pick-web", "ensure-web",
"--platform", "--platform",
platform, platform,
"--url",
start_url,
"--auth-strategy",
DEFAULT_AUTH_STRATEGY,
"--label",
label,
"--lease", "--lease",
"--holder", "--holder",
LEASE_HOLDER, LEASE_HOLDER,
"--ttl-sec", "--ttl-sec",
LEASE_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

@@ -2,7 +2,7 @@
本文件说明 `assets/actions.json``scripts/main.py` CLI 的对应关系,供 **Agent 编排**与宿主 **Skill Action Runtime** 使用。这是 Agent 侧关于 Action / 任务中心的**主契约**;实现细节与开发规范见 [`../development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md)。 本文件说明 `assets/actions.json``scripts/main.py` CLI 的对应关系,供 **Agent 编排**与宿主 **Skill Action Runtime** 使用。这是 Agent 侧关于 Action / 任务中心的**主契约**;实现细节与开发规范见 [`../development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md)。
**边界:** 用户市场说明见根目录 [`README.md`](../README.md)CLI 参数见 [`CLI.md`](CLI.md)。 **边界:** 用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.md)CLI 参数见 [`CLI.md`](CLI.md)。
## Agent 何时用 Action与 CLI 的分工) ## Agent 何时用 Action与 CLI 的分工)
@@ -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

@@ -5,13 +5,16 @@
**边界规则:** **边界规则:**
- 本文**不得**包含 YAML frontmatter**不得**包含 `description` 字段。 - 本文**不得**包含 YAML frontmatter**不得**包含 `description` 字段。
- 技能市场详情与普通用户说明**必须**放在根目录 [`README.md`](../README.md)。 - 技能市场四 Tab 材料**必须**放在根目录[`README.md`](../README.md)、[`TUTORIAL.md`](../TUTORIAL.md)、[`DEMO.md`](../DEMO.md)、[`CHANGELOG.md`](../CHANGELOG.md)
- **Agent 主读本目录**`CLI.md` / `ACTIONS.md` / `SCHEMA.md`)与根 [`SKILL.md`](../SKILL.md)。 - **Agent 主读本目录**`CLI.md` / `ACTIONS.md` / `SCHEMA.md`)与根 [`SKILL.md`](../SKILL.md)。
- [`../development/`](../development/) 是开发者 / 编码助手深度规范,**不是**对话 Agent 的默认读盘路径。 - [`../development/`](../development/) 是开发者 / 编码助手深度规范,**不是**对话 Agent 的默认读盘路径。
| 文档 | 用途 | | 文档 | 用途 |
|------|------| |------|------|
| 根目录 [`README.md`](../README.md) | 用户市场详情页说明 | | 根目录 [`README.md`](../README.md) | 市场说明 Tab`readme_md` |
| 根目录 [`TUTORIAL.md`](../TUTORIAL.md) | 市场教程 Tab`tutorial_md` |
| 根目录 [`DEMO.md`](../DEMO.md) | 市场演示 Tab`demo_video_url` + `demo_md` |
| 根目录 [`CHANGELOG.md`](../CHANGELOG.md) | 市场更新日志 Tab版本小节 → `changelog` |
| [`SKILL.md`](../SKILL.md) | LLM / OpenClaw 平台技能入口与触发摘要 | | [`SKILL.md`](../SKILL.md) | LLM / OpenClaw 平台技能入口与触发摘要 |
| [`CLI.md`](CLI.md) | 共享 Python、命令行入口、参数与短操作契约 | | [`CLI.md`](CLI.md) | 共享 Python、命令行入口、参数与短操作契约 |
| [`ACTIONS.md`](ACTIONS.md) | Skill Action、sync/async、任务中心、多入口可选能力 | | [`ACTIONS.md`](ACTIONS.md) | Skill Action、sync/async、任务中心、多入口可选能力 |

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.45" 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

@@ -29,11 +29,13 @@ POLICY_IDS = (
"POLICY-CONFIG-001", "POLICY-CONFIG-001",
"POLICY-CONFIG-002", "POLICY-CONFIG-002",
"POLICY-CONFIG-003", "POLICY-CONFIG-003",
"POLICY-CONFIG-004",
"POLICY-TESTING-001", "POLICY-TESTING-001",
"POLICY-TESTING-002", "POLICY-TESTING-002",
"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",
@@ -86,6 +88,22 @@ CONFIG_ENV_FORBIDDEN_PATTERNS = (
re.compile(r"\benviron\.get\b"), re.compile(r"\benviron\.get\b"),
) )
CONFIG_004_SOURCE = "development/CONFIG.md §.env.example 注释 = 配置管理用户文案"
CONFIG_004_ENV_KEY_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
CONFIG_004_CJK_RE = re.compile(r"[\u4e00-\u9fff]")
# 注释中禁止出现的开发向指向 / 黑话(大小写不敏感子串)
CONFIG_004_FORBIDDEN_COMMENT_SUBSTRINGS = (
"development/",
"adapter.md",
"rpa.md",
"data_paths.md",
"config.md",
"account-manager",
"见 development",
"adapter 档位",
"adapter档位",
)
TESTING_REAL_TARGET_ALLOWED_ROOT_TESTS = frozenset({"test_adapter_profile_policy.py"}) TESTING_REAL_TARGET_ALLOWED_ROOT_TESTS = frozenset({"test_adapter_profile_policy.py"})
TESTING_001_FORBIDDEN_SUBSTRINGS = ( TESTING_001_FORBIDDEN_SUBSTRINGS = (
@@ -125,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"
@@ -149,6 +179,77 @@ def _policy_msg(policy_id: str, source: str, detail: str) -> str:
return f"{policy_id} [{source}]: {detail}" return f"{policy_id} [{source}]: {detail}"
def _strip_env_quotes(value: str) -> str:
trimmed = value.strip()
if (
(trimmed.startswith('"') and trimmed.endswith('"'))
or (trimmed.startswith("'") and trimmed.endswith("'"))
):
return trimmed[1:-1]
return trimmed
def _iter_env_example_active_entries(path: str):
"""Yield (lineno, key, value, header_comments, inline_comment) for active KEY= lines."""
pending: list[str] = []
with open(path, encoding="utf-8") as handle:
for lineno, line in enumerate(handle, 1):
stripped = line.strip()
if not stripped:
pending = []
continue
if stripped.startswith("#"):
pending.append(stripped.lstrip("#").strip())
continue
eq = stripped.find("=")
if eq <= 0:
pending = []
continue
key = stripped[:eq].strip()
if not CONFIG_004_ENV_KEY_RE.match(key):
pending = []
continue
rest = stripped[eq + 1 :]
inline: str | None = None
if not rest.startswith('"') and not rest.startswith("'"):
hash_at = rest.find(" #")
if hash_at >= 0:
inline = rest[hash_at + 2 :].strip()
rest = rest[:hash_at]
value = _strip_env_quotes(rest)
yield lineno, key, value, list(pending), inline
pending = []
def _config_004_comment_offenders(path: str) -> list[str]:
offenders: list[str] = []
for lineno, key, _value, headers, inline in _iter_env_example_active_entries(path):
if not headers:
offenders.append(
f".env.example:{lineno}: {key}= 缺少行头注释(须用中文说明「是什么」)"
)
elif not any(CONFIG_004_CJK_RE.search(part) for part in headers):
offenders.append(
f".env.example:{lineno}: {key}= 行头注释须含中文(说明「是什么」)"
)
if not inline:
offenders.append(
f".env.example:{lineno}: {key}= 缺少行尾注释(须用中文说明「怎么填」)"
)
elif not CONFIG_004_CJK_RE.search(inline):
offenders.append(
f".env.example:{lineno}: {key}= 行尾注释须含中文(说明「怎么填」)"
)
combined = "\n".join([*headers, inline or ""]).lower()
for token in CONFIG_004_FORBIDDEN_COMMENT_SUBSTRINGS:
if token.lower() in combined:
offenders.append(
f".env.example:{lineno}: {key}= 注释禁止含 {token!r}(面向用户,勿写开发文档指向/黑话)"
)
break
return offenders
def _rel(skill_root: str, path: str) -> str: def _rel(skill_root: str, path: str) -> str:
return os.path.relpath(path, skill_root).replace("\\", "/") return os.path.relpath(path, skill_root).replace("\\", "/")
@@ -385,6 +486,23 @@ class TestPolicyConfig003(unittest.TestCase):
) )
class TestPolicyConfig004(unittest.TestCase):
def test_env_example_comments_are_user_facing(self) -> None:
path = os.path.join(get_skill_root(), ".env.example")
self.assertTrue(os.path.isfile(path), msg=".env.example missing")
offenders = _config_004_comment_offenders(path)
self.assertEqual(
offenders,
[],
msg=_policy_msg(
"POLICY-CONFIG-004",
CONFIG_004_SOURCE,
"\n".join(offenders)
or "every active KEY in .env.example needs Chinese header+inline comments",
),
)
class TestPolicyTesting001(unittest.TestCase): class TestPolicyTesting001(unittest.TestCase):
def test_root_tests_do_not_hardcode_real_targets(self) -> None: def test_root_tests_do_not_hardcode_real_targets(self) -> None:
skill_root = get_skill_root() skill_root = get_skill_root()
@@ -510,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

@@ -141,6 +141,47 @@ class TestDocsStandards(unittest.TestCase):
msg="SKILL.md must state README.md is user-facing documentation", msg="SKILL.md must state README.md is user-facing documentation",
) )
def test_market_tab_docs_exist(self) -> None:
root = get_skill_root()
for name in ("README.md", "TUTORIAL.md", "DEMO.md", "CHANGELOG.md"):
path = os.path.join(root, name)
self.assertTrue(os.path.isfile(path), msg=f"{name} must exist at skill root")
def test_demo_md_frontmatter_has_demo_video_url(self) -> None:
text = self._read("DEMO.md")
self.assertTrue(text.startswith("---"), "DEMO.md must start with YAML frontmatter")
frontmatter = self._parse_frontmatter(text)
self.assertIn(
"demo_video_url:",
frontmatter,
"DEMO.md frontmatter must include demo_video_url",
)
def test_changelog_has_section_for_skill_version(self) -> None:
skill_fm = self._parse_frontmatter(self._read("SKILL.md"))
match = re.search(r"(?m)^version:\s*[\"']?([0-9]+\.[0-9]+\.[0-9]+)", skill_fm)
self.assertIsNotNone(match, "SKILL.md must declare semver version")
version = match.group(1)
changelog = self._read("CHANGELOG.md")
self.assertRegex(
changelog,
rf"(?m)^##\s*\[?v?{re.escape(version)}\]?",
msg=f"CHANGELOG.md must include a ## section for version {version}",
)
def test_skill_md_documents_market_four_tabs(self) -> None:
text = self._read("SKILL.md")
for name in ("TUTORIAL.md", "DEMO.md", "CHANGELOG.md", "readme_md", "tutorial_md"):
self.assertIn(name, text, msg=f"SKILL.md must document market material {name}")
def test_development_md_documents_market_four_tabs(self) -> None:
text = self._read("development/DEVELOPMENT.md")
self.assertIn("TUTORIAL.md", text)
self.assertIn("DEMO.md", text)
self.assertIn("CHANGELOG.md", text)
self.assertIn("demo_video_url", text)
self.assertIn("tutorial_md", text)
def test_naming_md_exists_and_covers_standards(self) -> None: def test_naming_md_exists_and_covers_standards(self) -> None:
text = self._read("development/NAMING.md") text = self._read("development/NAMING.md")
self.assertIn("verb-noun-platform", text) self.assertIn("verb-noun-platform", text)
@@ -197,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",
) )