Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1ff70e26a5 | |||
| 0b9a8b2107 | |||
| 287816aa2a | |||
| 144455fbe0 | |||
| 5ec9648148 | |||
| ec049fdc2a | |||
| 917df04712 | |||
| b016b3b411 | |||
| 296798a0f3 |
71
.env.example
71
.env.example
@@ -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)
|
||||
OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启反检测指纹(默认)
|
||||
# 默认账号编号:仅填账号管理里的纯数字主键(一般留空)
|
||||
DEFAULT_ACCOUNT_ID= # 留空=自动;只填纯数字;手机号请填上方登录账号
|
||||
|
||||
# ── 存证 ──
|
||||
OPENCLAW_RECORD_VIDEO=0 # 0=默认不录屏;1=启用录屏+字幕+旁白+背景音+MP4
|
||||
OPENCLAW_ARTIFACTS_ON_FAILURE=1 # 1=失败截图(默认)
|
||||
# 是否显示浏览器窗口:网页自动化时是否弹出可见浏览器
|
||||
OPENCLAW_BROWSER_HEADLESS=0 # 0=显示窗口(必须,便于介入与排查);1=后台静默(勿作日常用法)
|
||||
|
||||
# ── 节流 / 超时 ──
|
||||
STEP_DELAY_MIN=1.0 # 步骤间随机等待下限(秒)
|
||||
STEP_DELAY_MAX=5.0 # 步骤间随机等待上限(秒)
|
||||
HUMAN_WAIT_TIMEOUT=180 # 滑块/验证码/2FA 等人工超时(秒)
|
||||
# 浏览器防检测:降低被网站识别为自动化工具的概率
|
||||
OPENCLAW_PLAYWRIGHT_STEALTH=1 # 1=开启(推荐),0=关闭;一般保持默认
|
||||
|
||||
# ── 数据目录子路径(见 development/DATA_PATHS.md;留空则用 skill 数据目录下默认子文件夹)──
|
||||
# 相对路径相对于 {JIANGCHANG_DATA_ROOT}/{USER_ID}/{slug}/,不是 workspace / CWD
|
||||
# SKILL_DOWNLOAD_DIR=downloads
|
||||
# SKILL_IMPORT_DIR=imports
|
||||
# SKILL_EXPORT_DIR=exports
|
||||
# SKILL_UPLOAD_DIR=uploads
|
||||
# SKILL_CACHE_DIR=cache
|
||||
# SKILL_TEMP_DIR=temp
|
||||
# 是否录制操作过程:用于留证、复现问题或向他人演示
|
||||
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
|
||||
|
||||
# 以下为可选目录配置。一般不用改;取消注释并填写后,才覆盖默认子文件夹。
|
||||
# 填写相对路径时,相对于本技能的数据目录(不是当前工作目录)。
|
||||
|
||||
# 下载目录:技能保存下载文件的位置
|
||||
# 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
43
CHANGELOG.md
Normal 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
24
DEMO.md
Normal 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` 填上即可,正文可保留为补充说明。
|
||||
@@ -6,6 +6,7 @@ description: "用用户能理解的方式说明这个技能能做什么、适合
|
||||
> 本文是技能市场详情与普通用户说明,**不是** Agent 参考索引、CLI 契约或开发文档。
|
||||
> frontmatter 的 `description` 必须是用户能看懂的一句话能力描述;**不得**写成 CLI、schema、references、development、契约、数据结构、开发规范或 Agent 参考索引。
|
||||
> 正文只回答用户关心的问题,**不要**把 Playwright、DOM、Python、RPA、references、development、schema 等作为面向用户的主说明内容;若业务上必须出现技术词,须用用户能理解的方式解释,且不能喧宾夺主。
|
||||
> **复制后验收章节(标题可微调,建议保留):**「能帮你做什么」「适合什么场景」「使用前需要准备什么」「你可以这样告诉 Agent」「执行后你会得到什么」「注意事项」「常见问题」。
|
||||
|
||||
# 【技能名称】
|
||||
|
||||
@@ -39,9 +40,11 @@ description: "用用户能理解的方式说明这个技能能做什么、适合
|
||||
|
||||
说明用户在使用前要准备的东西,例如:
|
||||
|
||||
- 相关账号已登录,且具备查询、导出或提交权限
|
||||
- **网页自动化类:** 首次运行可由技能自动登记平台账号(浏览器会话由系统隔离管理)。也可在「账号管理」里指定登录标识。登记账号 ≠ 已登录目标站
|
||||
- **是否要登录目标站:** 按本技能说明(有的平台必须登录,有的公开页可不登录);与「是否有账号记录」不是一回事
|
||||
- 相关业务权限(查询、导出、提交等)已具备
|
||||
- 需要处理的文件、时间范围、订单号、客户名称、项目名称等信息
|
||||
- 如果涉及外部系统,请确认网络、账号、验证码或审批流程可正常使用
|
||||
- 如果涉及验证码或人工确认,请保证能及时在弹出的浏览器窗口中操作
|
||||
|
||||
---
|
||||
|
||||
|
||||
117
SKILL.md
117
SKILL.md
@@ -1,12 +1,12 @@
|
||||
---
|
||||
name: 技能开发模板(通用业务版)
|
||||
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
|
||||
version: 1.0.42
|
||||
version: 1.0.51
|
||||
author: 深圳匠厂科技有限公司
|
||||
metadata:
|
||||
openclaw:
|
||||
slug: your-skill-slug
|
||||
platform_kit_min_version: "1.2.0"
|
||||
platform_kit_min_version: "1.2.2"
|
||||
emoji: "📦"
|
||||
category: "通用"
|
||||
developer_ids:
|
||||
@@ -25,27 +25,68 @@ allowed-tools:
|
||||
### 架构铁律(先读)
|
||||
|
||||
- **单业务内核、多入口**:数据管理 / 定时任务 / Agent / 技能详情 / 直接 CLI 必须复用同一 domain service;禁止按入口复制业务逻辑,禁止按 `source` 分叉业务行为。
|
||||
- **`placements`**:只决定 Action 出现在哪里。
|
||||
- **`placements`**:只决定 Action **出现在哪里**(toolbar / cron / agent / skill-detail)。
|
||||
- **`bind.tables`**:只决定数据管理出现在哪些表;含 `toolbar` 时**必须**显式非空绑定。
|
||||
- **`executionProfile`**:只决定 sync 等待还是 async Job;**必须**显式声明 `sync` 或 `async`;与 placements **正交**(任意合法组合均可)。
|
||||
- **`async`**:宿主建 Job,立即返回 `jobId`,进度见**任务中心** / Skill Run Card。
|
||||
- **`executionProfile`**:只决定执行方式——`sync` 当场返回结果,`async` 进**任务中心**。与 placements / 入口**无关**。心智上多数为 sync,仅长任务 / RPA 标 async;宿主省略该字段时按 sync。每个 Action **仍必须**显式写 `sync` 或 `async`。
|
||||
- **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;特殊业务报告才另做 `export-*`。
|
||||
- **准备 vs 执行(适用则拆)**:若技能存在「先写入/准备本地数据」与「再对外产生副作用」两段,Agent 必须能分开触发;禁止默认绑成不可拆的一步。一键无准备态的技能可跳过本条。
|
||||
|
||||
### 编排要点
|
||||
### 如何触发(能力类型)
|
||||
|
||||
- **长耗时 / RPA / 浏览器任务**:必须通过宿主 **Skill Action**(`run_skill_action`)调用 `assets/actions.json` 中的 action;建议 `executionProfile: "async"`。
|
||||
- **禁止**对长任务使用 `exec python …/scripts/main.py run` 或 `process poll` 干等 CLI 返回。
|
||||
- 只读查询(`health` / `stats` 等)可用 `"sync"`,立即返回结果。
|
||||
- 批量处理用参数化 `pickCount` / `--pick N` 顺序消费队列,**不要并发**同一 Profile。
|
||||
- 详细契约:`development/SKILL_ACTION_RUNTIME.md`、`references/ACTIONS.md`、`references/SCHEMA.md`。
|
||||
- 模板源仓库本身仅示范 `health` / `version` / `config-path` / `init-db`(运维/诊断 CLI);定制后按业务增加 Action,**一个能力一个 Action + 多 placements**。
|
||||
| 你要做什么 | Agent 怎么做 | 进任务中心? |
|
||||
|------------|--------------|--------------|
|
||||
| 短查询 / 只读 / 秒级(`health` / `version` / `stats` / `list` / `get` / `logs` 等) | OpenClaw bash/exec + **共享 Python** 跑 CLI(见下方「共享 Python」) | 否(CLI 当场结束) |
|
||||
| 已在 `actions.json` 声明且 `executionProfile: "sync"` 的能力 | 可用 `run_skill_action`,或同等效果的短 CLI(同一内核) | 否 |
|
||||
| 长耗时 / RPA / 浏览器 / 需要进度或取消;或 `executionProfile: "async"` | **必须** `run_skill_action`(先 `list_skill_actions` / `get_skill_action`) | **是**(仅因为该 Action 是 async) |
|
||||
| 用户只问用法 / 能做什么 | 读根目录 `README.md`;逐步操作再读 `TUTORIAL.md`;**不要**为答用法去列目录或默认打开 `references/` | — |
|
||||
|
||||
### 用户意图路由表(业务技能复制后必改)
|
||||
|
||||
复制为业务技能后,**必须**用本技能真实意图改写下表(保留三列结构)。格子写清:短 CLI 子命令,或 `run_skill_action` → **具体 action id**(及主要 `input`)。勿留模板占位语上线。
|
||||
|
||||
| 用户意图(本技能自填) | Agent 怎么做 | 进任务中心? |
|
||||
|------------------------|--------------|--------------|
|
||||
| (例)查询 / 统计 / 列表 | 短 CLI:`<your-read-commands>` | 否 |
|
||||
| (例)写入 / 导入 / 改本地状态 | 短 CLI:`<your-prep-commands>` | 否 |
|
||||
| (例)长副作用 / RPA / 需进度 | `run_skill_action` → `<your-async-action-id>`,`input.…` | 是(因该 Action 为 async) |
|
||||
|
||||
有长任务 / RPA 时:上表至少一行指向真实的 async action id,且该 id 须出现在 `assets/actions.json`;细节分流见 `references/CLI.md`。
|
||||
|
||||
**硬规则:**
|
||||
|
||||
1. **禁止**对长任务使用 `exec`/`bash` 跑 `…/scripts/main.py run`(或等价业务命令)再 `process poll` 干等。
|
||||
2. **禁止**用 `uv run python …` 跑技能脚本(会建临时环境,缺 `jiangchang_skill_core`)。
|
||||
3. **禁止** Agent 自行 `pip install`。
|
||||
4. Action **可选**:无数据管理 / 定时 / Agent 结构化调用需求时可不提供 `actions.json`;有长任务 / RPA 时**必须**提供至少一条 `executionProfile: "async"` 且 `placements` 含 `agent` 的 Action。
|
||||
5. **一个业务能力一个 Action**,用多个 `placements` 挂入口;底层仍是同一 `scripts/main.py` CLI。
|
||||
6. 是否进任务中心**只**看该 Action 的 `executionProfile`,与数据管理 / 定时 / Agent 入口无关。
|
||||
|
||||
Agent 执行契约细节:`references/CLI.md`、`references/ACTIONS.md`、`references/SCHEMA.md`(不要把 `development/` 当 Agent 主读路径)。
|
||||
|
||||
## 共享 Python(必读)
|
||||
|
||||
宿主共享 runtime 在 `{JIANGCHANG_DATA_ROOT}/python-runtime/.venv`,并已前置到 PATH(同时设 `VIRTUAL_ENV`)。短 CLI 一条命令直接跑:
|
||||
|
||||
```bash
|
||||
python {baseDir}/scripts/main.py health
|
||||
python {baseDir}/scripts/main.py version
|
||||
python {baseDir}/scripts/main.py config-path
|
||||
python {baseDir}/scripts/main.py init-db
|
||||
```
|
||||
|
||||
- 公共库来自共享 venv 的 `jiangchang-platform-kit>=1.2.2`(包名路径 `jiangchang_skill_core`);**不要 vendor** `scripts/jiangchang_skill_core/`。
|
||||
- 根目录 `requirements.txt` **只声明技能特有**依赖;不要写 `jiangchang-platform-kit` / `playwright`。
|
||||
- `metadata.openclaw.platform_kit_min_version` 是宿主兼容声明,**不是** pip 依赖。
|
||||
- `health` 用 `collect_runtime_diagnostics` 做只读诊断。
|
||||
|
||||
配置:仓库 `.env.example` 为模板;用户 `.env` 在 `{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/{slug}/.env`。优先级:进程环境变量 > 用户 `.env` > `.env.example`。
|
||||
|
||||
## 面向用户问答(LLM 规则)
|
||||
|
||||
- 本文(`SKILL.md`)是 LLM / OpenClaw 平台读取的技能入口,**不是**用户市场说明。
|
||||
- 根目录 `README.md` 是面向普通用户的说明。
|
||||
- 根目录市场材料面向普通用户(见下方「文档分工」),**不是** Agent 调用契约。
|
||||
|
||||
当用户询问以下问题时,**必须优先读取**根目录 `README.md`,并用用户能理解的业务语言回答:
|
||||
当用户询问以下问题时,**必须优先读取**根目录 `README.md`(能力说明)与 `TUTORIAL.md`(逐步用法),并用用户能理解的业务语言回答:
|
||||
|
||||
- 这个技能是做什么的
|
||||
- 这个技能怎么用
|
||||
@@ -54,20 +95,31 @@ allowed-tools:
|
||||
- 执行后会得到什么
|
||||
- 能不能帮我完成某个业务任务
|
||||
|
||||
**回答「怎么用 / 能做什么」时的要求:**
|
||||
|
||||
1. 「能做什么 / 场景 / FAQ」以 `README.md` 为主;「第一次怎么用完」以 `TUTORIAL.md` 为主。
|
||||
2. **禁止**在回答中粘贴 `python …/scripts/main.py …` 或其它命令行(除非用户明确索要命令,或自称技术人员/开发者)。
|
||||
3. **禁止**为答用法而去 `dir` / `ls` / 列技能目录,或默认打开 `references/`、`development/`;仅当用户明确问命令、参数、集成或排错时再读 `references/`。
|
||||
4. **不要**向普通用户暴露 Playwright、DOM、Python、RPA、库表名、`error_code` 等实现细节,除非用户明确询问技术实现。
|
||||
|
||||
**文档读取边界:**
|
||||
|
||||
- **不要**把 `SKILL.md`、`references/`、`development/` 中的技术细节直接作为用户回答。
|
||||
- **只有当**用户明确询问命令、参数、输出结构、开发、调试、集成或排错时,才读取 `references/` 或 `development/`。
|
||||
- 若 `README.md` 与 `SKILL.md` / `references` 表述冲突:对用户展示与市场说明以 `README.md` 为准;对执行契约、CLI、schema、运行约束以 `SKILL.md` / `references` 为准。
|
||||
- 回答用户时**不要**暴露 Playwright、DOM、Python、RPA 等实现细节,除非用户明确询问技术实现。
|
||||
- **只有当**用户明确询问命令、参数、输出结构、开发、调试、集成或排错时,才读取 `references/`;`development/` 主要给开发者 / 编码助手,对话 Agent **不必**默认打开。
|
||||
- 若市场材料与 `SKILL.md` / `references` 表述冲突:对用户展示以 `README.md` / `TUTORIAL.md` 为准;对执行契约以 `SKILL.md` / `references` 为准。
|
||||
- 用户未问技术实现时,**不要**讨论 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 平台 | 技能入口、触发与运行契约摘要 |
|
||||
| `references/` | Agent 编排/调用 | 渐进式加载:CLI 契约、字段 schema 等 |
|
||||
| `development/` | 开发者 / AI 编程代理 | 需求、开发教程、测试、技术规范 |
|
||||
| `references/` | Agent 编排 / 调用 | CLI、Action、schema 契约(Agent 主读) |
|
||||
| `development/` | 开发者 / AI 编程代理 | 需求、开发教程、测试、深度技术规范 |
|
||||
|
||||
## 目录约定
|
||||
|
||||
@@ -77,36 +129,15 @@ allowed-tools:
|
||||
- CLI 入口固定为 `scripts/main.py`
|
||||
- 业务逻辑按 `cli / db / service / util` 分层
|
||||
|
||||
## 最小命令
|
||||
|
||||
```bash
|
||||
python {baseDir}/scripts/main.py health
|
||||
python {baseDir}/scripts/main.py config-path
|
||||
python {baseDir}/scripts/main.py version
|
||||
python {baseDir}/scripts/main.py init-db
|
||||
```
|
||||
|
||||
配置:仓库 `.env.example` 为模板;用户 `.env` 在 `{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/{slug}/.env`,启动时自动 bootstrap。优先级:进程环境变量 > 用户 `.env` > `.env.example`。
|
||||
|
||||
## 运行依赖
|
||||
|
||||
- Python 运行环境由匠厂宿主注入**共享 runtime**:`{JIANGCHANG_DATA_ROOT}/python-runtime/.venv`。
|
||||
- 公共能力来自共享 runtime 安装的 `jiangchang-platform-kit>=1.2.0`(`jiangchang_skill_core` 包);**不要 vendor** `scripts/jiangchang_skill_core/`,新技能不得在仓库内保留该目录副本。
|
||||
- config、logging、runtime_env、rpa、media-assets、video_session、runtime_diagnostics 等均从共享 venv 的 `jiangchang_skill_core` import,而非技能目录副本。
|
||||
- 根目录 `requirements.txt` **只声明技能特有** Python 三方依赖;`jiangchang-platform-kit`、`playwright` 等公共能力由宿主共享 runtime 提供,**不要**写入技能 requirements。
|
||||
- `metadata.openclaw.platform_kit_min_version`(当前 `1.2.0`)是运行契约/兼容性声明,供宿主安装与启用时校验,**不是**技能 pip 依赖声明。
|
||||
- Skill 代码**不要**自行 `pip install`;系统级依赖(如 VC++ Runtime)仅在 `health` / preflight 中提示用户安装。
|
||||
- `health` 使用 `collect_runtime_diagnostics` 输出统一 runtime 诊断(只读,不下载/修复 media-assets)。
|
||||
|
||||
## 定制入口
|
||||
|
||||
1. 从本模板创建新业务技能时,**不得保留模板 `.git`**;优先使用 [`tools/scaffold_skill.ps1`](tools/scaffold_skill.ps1)(见 [`tools/README.md`](tools/README.md))。
|
||||
2. 业务技能仓库中**不得**存在 `.openclaw-skill-template`(仅 template 源仓库保留)。
|
||||
3. 复制目录为你的新 skill 仓库,全局替换 `your-skill-slug` 等占位词。
|
||||
4. 按 `development/DEVELOPMENT.md` 完成开发与文档定制。
|
||||
5. 用户市场说明写入根 `README.md`;Agent 调用契约见 `references/CLI.md`、`references/SCHEMA.md`、`references/ACTIONS.md`;长任务 / 任务中心见 `development/SKILL_ACTION_RUNTIME.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`。
|
||||
7. 本地 SQLite 的中文表名/字段名、标准时间字段与字段顺序规范见 `references/SCHEMA.md`(元数据表 `_jiangchang_*` + `CREATE TABLE` 物理顺序)。
|
||||
7. 本地 SQLite 的中文表名/字段名、标准时间字段与字段顺序规范见 `references/SCHEMA.md`。
|
||||
|
||||
## 技能命名与 slug
|
||||
|
||||
|
||||
65
TUTORIAL.md
Normal file
65
TUTORIAL.md
Normal file
@@ -0,0 +1,65 @@
|
||||
> **模板说明(复制后删除本段)**
|
||||
> 本文对应技能市场 **「使用教程」** Tab(发布时同步为 `tutorial_md`)。
|
||||
> 面向普通用户写**可跟着做的步骤**,不要写 CLI、目录结构、release、开发规范。
|
||||
> 与根 `README.md`(技能说明)分工:README 讲「是什么 / 能做什么」;本文讲「第一次怎么用完」。
|
||||
> **复制后验收章节(标题可微调):**「开始前」「第一步…」「第二步…」「常见卡住怎么办」。
|
||||
|
||||
# 【技能名称】使用教程
|
||||
|
||||
用最短路径带用户完成一次成功使用。步骤要具体,可用「打开匠厂 → …」这类操作语言。
|
||||
|
||||
---
|
||||
|
||||
## 开始前
|
||||
|
||||
确认:
|
||||
|
||||
1. 已安装并登录匠厂客户端
|
||||
2. 已在技能市场安装本技能(或更新到最新版)
|
||||
3. 【网页自动化类技能】首次运行一般会自动登记本平台账号;也可在「账号管理」中指定登录标识。浏览器会话由系统隔离管理——**不要**用本机日常浏览器用户目录去跑真实站
|
||||
4. 【按平台】若本技能说明「需要先登录目标站」,请在弹出的有头浏览器中按提示登录或扫码;「登记账号」不等于「已登录目标站」。若说明「可不登录也能用」,仍须有可用的账号会话目录
|
||||
5. 【如需】需要的文件 / 时间范围 / 单号等已准备好
|
||||
|
||||
---
|
||||
|
||||
## 第一步:打开技能并说清目标
|
||||
|
||||
在匠厂里新建任务或打开对话,用自然语言说明要做什么,例如:
|
||||
|
||||
> “帮我【完成某件业务事】,范围是【时间 / 对象 / 条件】。”
|
||||
|
||||
建议一次只交代一件事;范围说清楚,减少来回确认。
|
||||
|
||||
---
|
||||
|
||||
## 第二步:按提示补充信息
|
||||
|
||||
如果 Agent 问你缺什么(账号、文件、筛选条件),按提示补充即可。
|
||||
|
||||
- 有文件时:按界面提示选择或拖入
|
||||
- 有账号时:确认已在对应平台登录,且具备操作权限
|
||||
|
||||
---
|
||||
|
||||
## 第三步:等待结果并核对
|
||||
|
||||
任务结束后,请核对:
|
||||
|
||||
- 是否成功完成
|
||||
- 处理了多少条 / 哪些失败
|
||||
- 生成的文件、表格或记录是否在预期位置
|
||||
|
||||
若失败,先看失败原因说明,再按下一节排查。
|
||||
|
||||
---
|
||||
|
||||
## 常见卡住怎么办
|
||||
|
||||
| 现象 | 你可以先试 |
|
||||
|------|------------|
|
||||
| 提示未登录 / 无权限 | 确认匠厂已登录;业务账号是否在线、权限是否足够 |
|
||||
| 提示缺少必要信息 | 把时间范围、单号、文件等说完整后再试一次 |
|
||||
| 一直没有结果 | 到任务中心查看进度或失败原因;必要时重新发起 |
|
||||
| 结果不对 | 用更明确的条件再说一次,或缩小范围重试 |
|
||||
|
||||
仍无法解决时,把「你说了什么 + 界面提示原文」发给支持人员,便于排查。
|
||||
@@ -4,8 +4,8 @@
|
||||
- `examples/`:CLI 成功输出形状示例(虚构路径与数据)。
|
||||
- `schemas/`:轻量 JSON Schema(`skill-actions.schema.json` 为**新技能严格规范**;`task-log-record.schema.json` 等)。
|
||||
|
||||
`skill-actions.schema.json` 使用 `additionalProperties: false`。正式支持 `bind.tables`(toolbar Action 必填);`row`/`batch` placements 与 `concurrency`/`locks` 仍为预留。`placements` 与 `executionProfile` 正交。表级 Action 示例见 [`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))
|
||||
- 开发规范见 [`development/`](../development/)
|
||||
|
||||
@@ -100,7 +100,9 @@ def get_adapter():
|
||||
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` 模式。
|
||||
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
|
||||
# 普通兄弟技能 — 走 sibling_bridge
|
||||
@@ -110,10 +112,10 @@ result = call_sibling_json("account-manager", ["list", "--limit", "10"])
|
||||
```
|
||||
|
||||
```python
|
||||
# account-manager 账号/租约 — 集中在 account_client.py
|
||||
# account-manager 账号/租约 — 集中在 account_client.py(ensure-web)
|
||||
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:
|
||||
...
|
||||
finally:
|
||||
|
||||
@@ -11,44 +11,112 @@
|
||||
4. **技能升级合并缺失项**:`merge_missing_env_keys` 会把 `.env.example` 中新增、但用户 `.env` 尚未包含的 key **追加**进去(带注释块)。
|
||||
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
|
||||
# ── 运行模式 / 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
|
||||
|
||||
# ── 默认账号(仅非敏感标识;密码走 account-manager)──
|
||||
# 浏览器 RPA simulator_rpa 联调账号来自 account-manager(platform + 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 等人工超时(秒)
|
||||
# 是否显示浏览器窗口:网页自动化时是否弹出可见浏览器
|
||||
OPENCLAW_BROWSER_HEADLESS=0 # 0=显示窗口(必须,便于介入与排查);1=后台静默(勿作日常用法)
|
||||
```
|
||||
|
||||
### 反例(禁止)
|
||||
|
||||
```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_*` 等跨技能前缀写入模板)。
|
||||
|
||||
### 数据目录子路径(下载 / 导入 / 导出)
|
||||
|
||||
技能**自己写入**的文件默认在 `{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)
|
||||
- `.env.example` 中路径类配置**留空即用默认**;若写相对路径,必须相对 **skill 数据目录**,**禁止** `./outputs/...` 这类相对 workspace 的写法
|
||||
- 可选 env:`SKILL_DOWNLOAD_DIR`、`SKILL_IMPORT_DIR`、`SKILL_EXPORT_DIR` 等(见 DATA_PATHS.md)
|
||||
- 权威说明(开发):[`DATA_PATHS.md`](DATA_PATHS.md)
|
||||
- `.env.example` 中路径类配置**留空或保持注释即用默认**;若写相对路径,必须相对 **skill 数据目录**,**禁止** `./outputs/...` 这类相对 workspace 的写法
|
||||
- 可选 env:`SKILL_DOWNLOAD_DIR`、`SKILL_IMPORT_DIR`、`SKILL_EXPORT_DIR` 等;若作为活跃 key 出现在配置管理中,同样须有用户向行头/行尾(见模板根 `.env.example`)
|
||||
|
||||
### 录屏开关(`OPENCLAW_RECORD_VIDEO`)
|
||||
|
||||
@@ -96,6 +164,16 @@ config.get_bool("OPENCLAW_BROWSER_HEADLESS")
|
||||
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`**:输出 `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 标准与各开关含义
|
||||
- `ADAPTER.md` — `OPENCLAW_TEST_TARGET` 四档模式
|
||||
- `RPA.md` — 三端 RPA 标准与各开关含义(开发向;勿写入 `.env` 注释)
|
||||
- `ADAPTER.md` — `OPENCLAW_TEST_TARGET` 四档模式(开发向;勿写入 `.env` 注释)
|
||||
- `RUNTIME.md` — `JIANGCHANG_DATA_ROOT` 与数据根约定
|
||||
- `DATA_PATHS.md` — 下载/导入/导出等子目录与 `resolve_data_path` 规范
|
||||
|
||||
@@ -106,6 +106,9 @@ your-skill/
|
||||
├─ tests/
|
||||
├─ .gitignore
|
||||
├─ README.md
|
||||
├─ TUTORIAL.md
|
||||
├─ DEMO.md
|
||||
├─ CHANGELOG.md
|
||||
├─ requirements.txt
|
||||
├─ release.ps1
|
||||
└─ SKILL.md
|
||||
@@ -114,7 +117,10 @@ your-skill/
|
||||
各目录职责如下:
|
||||
|
||||
- `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 / 平台的技能入口
|
||||
- `references/`:Agent 运行/编排/调用时渐进加载的补充上下文(如 CLI、SCHEMA)
|
||||
- `development/`:面向开发者与 AI 编程代理的开发资料、需求、测试与技术规范
|
||||
@@ -158,13 +164,13 @@ scripts/
|
||||
作用:常量、日志、路径、时间工具、通用帮助函数
|
||||
`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
|
||||
|
||||
若 skill 需要浏览器/桌面/手机自动化,按以下顺序落地:
|
||||
|
||||
1. **先读三份标准**:`RPA.md`(三端范式与反反爬)、`CONFIG.md`(`.env` 落盘与读取)、`ADAPTER.md`(四档 adapter)。
|
||||
1. **先读三份标准**:`RPA.md`(含 §0.2:Profile 强制 / 有头 / 登录分平台)、`CONFIG.md`(`.env` 落盘与读取)、`ADAPTER.md`(四档 adapter)。
|
||||
2. **从 examples 选择性复制**(不要整包照搬 `examples/<mode>/`,见各 example README 的 copy map):
|
||||
- 若是 **真实浏览器 RPA**(真实网站、登录态、验证码、滚动采集),**必须先读** `examples/real_browser_rpa/README.md`,再按需复制到 `scripts/service/`:
|
||||
- `examples/real_browser_rpa/scripts/service/browser_session.py`
|
||||
@@ -179,6 +185,7 @@ scripts/
|
||||
- `examples/simulator_browser_rpa/scripts/service/task_service.py`(async 编排)
|
||||
- `sandbox/demo_app.html` 仅留在 examples,不进入生产 skill
|
||||
- **禁止**:`rpa_helpers`、sync Playwright 用于完整技能 RPA 主路径、`task_service` 散落 account-manager subprocess
|
||||
- **网页硬规则**:未拿到 `profile_dir` 禁止开浏览器;生产 `OPENCLAW_BROWSER_HEADLESS=0`;站点是否登录写进 REQUIREMENTS / 用户教程,**不要**写成「可不接 account-manager」
|
||||
3. **只用共享库,不在 skill 里重写反反爬**:
|
||||
```python
|
||||
from jiangchang_skill_core import config
|
||||
@@ -195,7 +202,7 @@ scripts/
|
||||
技能根目录的 `requirements.txt` 是**标准文件**,用于声明本技能**特有** Python 三方依赖。
|
||||
|
||||
- **公共依赖**(`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`。
|
||||
- **不要**在业务代码中 `subprocess` / `pip install`;缺依赖由 `health` 报错,由宿主负责安装。
|
||||
- **版本约束尽量收窄**,降低多技能共享 venv 时的冲突风险。推荐范围写法:
|
||||
@@ -305,7 +312,7 @@ git remote -v
|
||||
复制后优先改下面这些地方:
|
||||
|
||||
1. `SKILL.md`
|
||||
2. 根目录 `README.md`(用户市场说明)
|
||||
2. 根目录市场四 Tab:`README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md`
|
||||
3. `scripts/util/constants.py`
|
||||
4. `references/` 与 `development/` 下的文案
|
||||
5. `scripts/service/` 下的业务占位实现(优先改 `task_service.py`)
|
||||
@@ -339,7 +346,7 @@ git remote -v
|
||||
|
||||
如需浏览器自动化,不要在模板里保留空的 `platform_playwright.py`;请按业务新建 `xxx_playwright.py`(命名自定),并在 `task_service.py` 中按需引用。
|
||||
|
||||
## 6. `SKILL.md` 与根 `README.md` 应该怎么写
|
||||
## 6. `SKILL.md` 与市场四 Tab 材料应该怎么写
|
||||
|
||||
### `SKILL.md`(LLM / 平台入口)
|
||||
|
||||
@@ -351,19 +358,26 @@ git remote -v
|
||||
- 何时触发本技能、CLI 入口摘要、运行契约要点
|
||||
- 指向 `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` |
|
||||
|
||||
- 能做什么、适合什么场景、使用前准备什么
|
||||
- 如何自然语言告诉 Agent
|
||||
- 执行结果、注意事项、常见问题
|
||||
**`README.md`**:能力与场景说明。含 frontmatter `description`;写能做什么、场景、准备、如何告诉 Agent、结果、注意事项、FAQ。不要写开发教程、目录规范、release、requirements。
|
||||
|
||||
不要在根 `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 的文档按读者拆分:
|
||||
|
||||
**根 `README.md`**
|
||||
**根市场材料(四 Tab)**
|
||||
|
||||
- 用户市场详情页说明(含 frontmatter `description`)
|
||||
- `README.md` — 说明(含 frontmatter `description`)
|
||||
- `TUTORIAL.md` — 使用教程
|
||||
- `DEMO.md` — 演示(`demo_video_url` + 正文)
|
||||
- `CHANGELOG.md` — 按版本更新日志
|
||||
|
||||
**`SKILL.md`**
|
||||
|
||||
@@ -442,7 +459,7 @@ metadata:
|
||||
比如 `skill-actions.schema.json`、日志记录、机读 JSON 的 schema
|
||||
|
||||
不要把正式研发文档放到 `assets/`。
|
||||
用户说明进根 `README.md`;Agent 编排资料进 `references/`;开发规范进 `development/`。
|
||||
用户市场四 Tab 进根 `README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md`;Agent 编排资料进 `references/`;开发规范进 `development/`。
|
||||
|
||||
## 9. `cli` 层怎么写
|
||||
|
||||
@@ -628,11 +645,20 @@ cmd_stats() # 只读 sync:JSON,无 job_context
|
||||
|
||||
### 1. 验证入口
|
||||
|
||||
本地验证请优先使用宿主共享 Python(与 Agent CLI 相同契约),例如:
|
||||
|
||||
```bash
|
||||
python scripts/main.py health
|
||||
python scripts/main.py version
|
||||
```
|
||||
|
||||
**禁止**对技能脚本使用 `uv run python`。
|
||||
|
||||
```bash
|
||||
python scripts/main.py -h
|
||||
python scripts/main.py <your-command> -h
|
||||
```
|
||||
|
||||
### 2. 验证 CLI 路由
|
||||
|
||||
```bash
|
||||
@@ -702,6 +728,8 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
|
||||
|
||||
在执行 `release.ps1` 之前,必须先在本地确认 `python tests/run_tests.py -v` 通过。测试不通过不得发起正式发布。
|
||||
|
||||
**发布前必须**将 `SKILL.md` 的 `version:` 与 `scripts/util/constants.py` 的 `SKILL_VERSION` 调整为将打出的版本(例如下一 patch),再执行 `release.ps1`。`release.ps1` **不会**自动改这两个字段;漏改会造成 git tag 与技能版本号不一致。
|
||||
|
||||
`release.ps1` 会在 **auto commit 之前**、推送 tag 之前,对**当前工作区**自动执行默认测试(`python tests/run_tests.py -v`);失败则中止 release,不会留下失败状态的自动提交。仅紧急排障可使用 `-SkipTests` 跳过(会打印黄色警告)。`-DryRun` 预览发布时**仍会实际运行**默认测试,便于在不打 tag 的情况下验证门禁。
|
||||
|
||||
新增或调整开发规范时,须同步 [`development/POLICY_MATRIX.md`](POLICY_MATRIX.md) 与 [`tests/test_development_policy_guard.py`](../tests/test_development_policy_guard.py)(或注明由既有测试覆盖)。
|
||||
@@ -757,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/` 是否齐全
|
||||
- 工作流文件是否存在
|
||||
- 发布包结构是否符合模板规范
|
||||
|
||||
@@ -826,6 +854,7 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
|
||||
- [ ] `service` 下的核心业务文件(如 `task_service.py`)已按领域改名并实现
|
||||
- [ ] 没有残留旧平台名
|
||||
- [ ] `health` / `version` 可运行
|
||||
- [ ] `.env.example` 注释已按 [`CONFIG.md`](CONFIG.md)「行头 = 是什么、行尾 = 怎么填」写全(面向配置管理用户;无 `见 development/...` 等开发文档指向)
|
||||
- [ ] `.gitignore` 生效,没有把 `__pycache__` 提交进去
|
||||
- [ ] `release.ps1` 存在(发布前会自动跑 `python tests/run_tests.py -v`;失败不得打 tag)
|
||||
- [ ] `.github/workflows/release_skill.yaml` 存在
|
||||
@@ -840,6 +869,7 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
|
||||
- [ ] 本仓库**不是** skill-template 的误复制(根目录**无** `.openclaw-skill-template`)
|
||||
- [ ] `git remote -v` 指向**本技能**远端,URL 不含 skill-template 仓库名
|
||||
- [ ] `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. 常见错误
|
||||
|
||||
@@ -892,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 入口
|
||||
|
||||
### 错误 5:平台放 slug 最前
|
||||
@@ -954,7 +984,7 @@ git remote add origin <新技能仓库 URL>
|
||||
如果让一个新人照着做,我建议他按这个顺序:
|
||||
|
||||
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`
|
||||
4. 改 `references/` 与 `development/`
|
||||
5. 改 `scripts/cli/app.py`
|
||||
@@ -970,7 +1000,7 @@ git remote add origin <新技能仓库 URL>
|
||||
|
||||
- 目录结构统一
|
||||
- 入口统一为 `scripts/main.py`
|
||||
- 用户说明在根 `README.md`,Agent 资料在 `references/`,开发规范在 `development/`
|
||||
- 用户市场四 Tab 在根 `README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md`,Agent 资料在 `references/`,开发规范在 `development/`
|
||||
- 业务核心逻辑统一放 `scripts/service/`
|
||||
- 不再使用旧模板历史结构
|
||||
|
||||
|
||||
@@ -7,15 +7,19 @@
|
||||
| POLICY-STRUCTURE-001 | 标准 `scripts/` 分层:`main.py`、`cli/`、`service/`、`db/`、`util/` 必须存在 | development/DEVELOPMENT.md §2 新 skill 的标准目录结构;development/RUNTIME.md §目录结构 | hard | 目录存在性检查 | `tests/test_development_policy_guard.py::TestPolicyStructure001` |
|
||||
| POLICY-RUNTIME-001 | 不得 vendored `scripts/jiangchang_skill_core/` | development/RUNTIME.md §共享 Python Runtime、§明确禁止 | hard | 目录不存在检查 | `tests/test_development_policy_guard.py::TestPolicyRuntime001`(另见 `tests/test_platform_import.py`) |
|
||||
| POLICY-RUNTIME-002 | `requirements.txt` 非注释依赖行不得声明 `jiangchang-platform-kit` / `playwright` | development/RUNTIME.md §共享 Python Runtime;development/DEVELOPMENT.md §3.1 requirements.txt 依赖规范 | hard | 解析依赖行 | `tests/test_development_policy_guard.py::TestPolicyRuntime002`(另见 `tests/test_platform_import.py`) |
|
||||
| POLICY-RUNTIME-003 | Agent/CLI 文档须要求共享 Python(python-runtime / PATH 上的 `python` + `{baseDir}`);须明确**禁止**对技能脚本使用 `uv run python` | development/RUNTIME.md;references/CLI.md;SKILL.md | hard | 文档标记扫描 | `tests/test_docs_standards.py::test_cli_md_requires_shared_python_not_uv_run`;`tests/test_docs_standards.py::test_skill_md_requires_shared_python_not_uv_run` |
|
||||
| POLICY-AGENT-001 | `SKILL.md` 须含「用户意图路由表」结构;`CLI.md` 须含短 CLI 白名单 / Action 强制黑名单表;用法回答须禁止无故粘贴 CLI、禁止为答用法列目录挖 references | SKILL.md;references/CLI.md | hard | 文档标记扫描 | `tests/test_docs_standards.py::test_skill_md_has_intent_routing_table`;`tests/test_docs_standards.py::test_cli_md_has_agent_routing_tables`;`tests/test_docs_standards.py::test_skill_md_how_to_forbids_cli_paste` |
|
||||
| POLICY-INSTALL-001 | 交付执行文件不得含 `pip install` / `python -m pip install` / `uv pip install` / `playwright install` | development/RUNTIME.md §共享 Python Runtime;development/RPA.md §1.4 Playwright 与安装边界;development/DEVELOPMENT.md §3.1 | hard | 扫描 `scripts/**/*.py`、`*.ps1`、`*.sh`、`requirements.txt`、`.github/workflows/*`(不扫 `development/*.md`) | `tests/test_development_policy_guard.py::TestPolicyInstall001` |
|
||||
| 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-003 | 业务代码不得直接 `os.environ` / `os.getenv` / `os.putenv` / `environ.get`;须经 `jiangchang_skill_core.config.get*`(允许 `main.py`、测试、config_bootstrap/runtime 边界文件) | development/CONFIG.md §配置 bootstrap;development/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-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-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.3;development/LOGGING.md;development/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.2;development/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-002 | 文本文件 UTF-8 without BOM,无 `U+FEFF` | development/DEVELOPMENT.md §3.4;development/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` |
|
||||
@@ -55,5 +59,5 @@
|
||||
| RPA 拟人操作、选择器纪律、HITL 超时 | development/RPA.md §0–§1 | 行为与 DOM 质量,无法静态扫描 |
|
||||
| adapter 四档契约测试覆盖 timeout/unauthorized 等 | development/ADAPTER.md §contract tests | 需业务实现后人工补测 |
|
||||
| `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 | 待宿主升级后完整生效;模板侧已强制显式绑定 |
|
||||
|
||||
@@ -17,4 +17,4 @@
|
||||
脚手架与 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)。
|
||||
用户市场说明见根目录 [`README.md`](../README.md),不要写进本目录。
|
||||
用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.md),不要写进本目录。
|
||||
|
||||
@@ -22,6 +22,13 @@
|
||||
- 中文 description(一句话):
|
||||
- account-manager platform_key(如有):
|
||||
- 命名形态:标准型 / 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. 文档目标
|
||||
|
||||
@@ -98,11 +105,12 @@
|
||||
|
||||
- 目录结构必须符合当前 `skill-template` 规范
|
||||
- 入口必须统一为 `scripts/main.py`
|
||||
- 用户说明写在根 `README.md`;LLM/运行契约写在 `SKILL.md`;开发规范在 `development/`
|
||||
- 用户市场四 Tab 写在根 `README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md`;LLM/运行契约写在 `SKILL.md`;开发规范在 `development/`
|
||||
- 输出格式应尽量机读友好;错误前缀统一为 `ERROR:`
|
||||
- Windows 环境下需保证 UTF-8 输出兼容
|
||||
- 必须具备基本日志能力;敏感字段脱敏
|
||||
- RPA 类 skill:Playwright 由宿主共享 runtime 提供;技能侧不要 `playwright install`;URL 不要放进 `launch_persistent_context` 的 `args`
|
||||
- 网页 RPA:启动前必须经 ensure-web 拿到 `profile_dir`;生产有头;站点登录策略见 §0(required / optional / not_needed);`required` 时每轮须登录门禁
|
||||
|
||||
## 6. 输入输出要求
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> 本文是团队 RPA 开发的**统一标准**。任何需要"自动操作软件界面"的 skill,都应先读这份文档,按这里的选型和范式落地,不要每个项目重新踩坑。
|
||||
|
||||
我们开发的各类 skill,本质上都是在替人操作三类界面:**浏览器、桌面软件、手机软件**。三类的底层技术不同,但**工程范式相同**:保持登录态、有头运行、拟人操作、失败存证、人工兜底。
|
||||
我们开发的各类 skill,本质上都是在替人操作三类界面:**浏览器、桌面软件、手机软件**。三类的底层技术不同,但**工程范式相同**:可控会话(Profile)、有头运行、拟人操作、失败存证、人工兜底。
|
||||
|
||||
---
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
| 约定 | 说明 |
|
||||
|------|------|
|
||||
| **保持登录态** | 复用持久化 Profile / session,避免每次重新登录触发风控;账号由 account-manager 下发,不硬编码 |
|
||||
| **有头运行** | 默认有头(headless 易被识别 / 难人工介入);`OPENCLAW_BROWSER_HEADLESS=1` 仅给 CI |
|
||||
| **可控会话 / Profile** | 网页版见下方 **§0.2**(必须走 account-manager 的 `profile_dir`);桌面/手机用对应持久会话,不硬编码密码 |
|
||||
| **有头运行** | 生产 RPA **必须有头**(见 §0.2);禁止把无头当交付/联调常态 |
|
||||
| **拟人操作** | 真实事件(isTrusted=true),逐字输入、随机延迟、贝塞尔鼠标轨迹;严禁 JS 直接设值/JS 点击/JS 跳转 |
|
||||
| **步骤间随机等待** | 每两步操作之间 `random_delay(min,max)`,区间由 `.env` 配置(默认 1~5s) |
|
||||
| **人工兜底(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)。
|
||||
|
||||
### 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/业务 DOM;F12 实页确认)
|
||||
- 把「业务空结果」(如无评论)与 `LEASE_CONFLICT` / `REQUIRE_LOGIN` 混成同一错误码
|
||||
|
||||
**参考实现:** `examples/real_browser_rpa/`、`examples/simulator_browser_rpa/`(`pick_web_account` → ensure-web → RPA → `finally release_lease`)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 浏览器(标准已成熟)
|
||||
@@ -55,21 +81,22 @@ async def open_login(page):
|
||||
| 项 | 标准 |
|
||||
|----|------|
|
||||
| 浏览器 | **优先系统 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 | **仅作诊断 / 桌面宿主类场景**;**不要**作为强风控站点的默认生产路径 |
|
||||
| 行为 | 模拟真实用户:真实点击、键盘、鼠标、地址栏输入;**不要**拼接搜索结果 URL、DOM 注入、`el.value=`、JS 跳转 |
|
||||
| 模式 | 默认有头 `OPENCLAW_BROWSER_HEADLESS=0`;无头仅 CI |
|
||||
| 模式 | **有头** `OPENCLAW_BROWSER_HEADLESS=0`(生产强制,见 §0.2) |
|
||||
| 反检测 | stealth 默认开 `OPENCLAW_PLAYWRIGHT_STEALTH=1`(见 1.1) |
|
||||
|
||||
### 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` 注入指纹淡化脚本。
|
||||
3. **不要在技能里自行安装 playwright**;由宿主共享 runtime 提供。
|
||||
4. **不要默认传 `--no-sandbox`**(除非特定容器环境且已评估风险)。
|
||||
5. **不要默认传 `--disable-blink-features=AutomationControlled`**;platform-kit stealth 已覆盖,额外 flag 可能适得其反。
|
||||
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**)。
|
||||
|
||||
@@ -186,7 +213,7 @@ from jiangchang_skill_core.rpa.stealth import stealth_enabled, STEALTH_INIT_SCRI
|
||||
| Playwright | **async** 贯穿;禁止 sync Playwright 用于完整 RPA 主路径 |
|
||||
| 分层 | **薄 adapter** + `{domain}_playwright.py`(示例:`simulator_playwright.py`)+ `account_client.py` subprocess |
|
||||
| 登录 | **双层**:门户 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` 等内部模块 |
|
||||
| Selector | 用户可见文案 / `get_by_role` / `name` 优先;`data-testid` 有则用、无则 fallback;共享 sandbox **不要求**为技能加 testid |
|
||||
| Profile | Chrome persistent profile 可能缓存旧 SPA → 联调排障:手工 `--user-data-dir` 清站点数据 |
|
||||
@@ -260,14 +287,18 @@ skill 退出/抛错统一用 `ERROR:` 前缀 + 稳定码,方便宿主与上层
|
||||
|
||||
| 错误码 | 含义 | 上层处理建议 |
|
||||
|--------|------|------|
|
||||
| `ERROR:REQUIRE_LOGIN` | 未登录 / 登录态失效 | 触发登录流程 |
|
||||
| `ERROR:REQUIRE_LOGIN` | 未登录 / 登录态失效 | 触发登录流程(有头等待人工) |
|
||||
| `ERROR:LOGIN_TIMEOUT` | 等待人工登录超时 | 提示用户重跑并及时操作 |
|
||||
| `ERROR:CAPTCHA_NEED_HUMAN` | 命中滑块/验证码拦截 | 暂停等人工,或转人工队列 |
|
||||
| `ERROR:LEASE_CONFLICT` | 已有账号但均被租约占用(ensure 不新建) | 稍后重试或释放租约;**不是**「无账号」 |
|
||||
| `ERROR:AMBIGUOUS_ACCOUNT` | `--login-id` / `--label` 匹配到多条 | 改用数字 id 或更精确标识 |
|
||||
| `ERROR:RATE_LIMITED` | 触发频控 | 退避后重试 |
|
||||
| `ERROR:MISSING_BROWSER` | 未检测到 Chrome/Edge | 提示安装 |
|
||||
| `ERROR:DEVICE_NOT_READY` | 手机未连接/未授权 | 检查 USB/ADB |
|
||||
| `ERROR:WINDOW_NOT_FOUND` | 桌面目标窗口未找到 | 检查程序是否启动 |
|
||||
|
||||
业务空结果(如「本页无评论」)用业务码或成功空列表表达,**不要**复用上表账号/登录错误码。
|
||||
|
||||
---
|
||||
|
||||
## 5. 存证与录屏规范
|
||||
|
||||
@@ -2,29 +2,42 @@
|
||||
|
||||
## 共享 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 依赖声明。
|
||||
|
||||
### 何时上调 `platform_kit_min_version`
|
||||
|
||||
| 情况 | 做法 |
|
||||
|------|------|
|
||||
| 技能依赖 kit 某次修复/新 API(如 `.env` 空值+行尾 `#` 解析、新 RPA helper) | 把 `metadata.openclaw.platform_kit_min_version` 调到**需要的最低版本**(例如 `"1.2.2"`) |
|
||||
| PyPI/私有源刚发了更新,但本技能行为未依赖 | **不必**仅为「追新」抬版本 |
|
||||
| 宿主升级时机 | 宿主在技能/宿主声明的最低版本**未满足**时按需升级共享 venv;**不会**仅因源上有更新就后台拉 latest |
|
||||
|
||||
模板默认声明须与当前文档所依赖的最低 kit 能力对齐(见根 `SKILL.md`)。
|
||||
|
||||
| 场景 | 推荐做法 |
|
||||
|------|----------|
|
||||
| 日常运行 | 由宿主匠厂触发技能 |
|
||||
| 手工排查 / 测试机 | 使用共享 runtime 的 `python.exe` 执行 `scripts/main.py` |
|
||||
| **不推荐** | 在技能目录内 `uv run python …` — 可能创建临时 venv,加载不到宿主共享 runtime |
|
||||
| 日常运行(宿主 / Agent) | 使用 PATH 上的共享 venv `python`(`{JIANGCHANG_DATA_ROOT}/python-runtime/.venv`) |
|
||||
| 手工排查 / 测试机(环境未注入时) | `{JIANGCHANG_DATA_ROOT}/python-runtime/.venv/.../python` 执行 `scripts/main.py` |
|
||||
| **禁止** | 在技能目录内 `uv run python …` — 会创建临时 venv,加载不到宿主共享 runtime |
|
||||
|
||||
占位命令(路径因环境而异,勿写死本机目录):
|
||||
|
||||
```text
|
||||
Windows:
|
||||
推荐(已注入环境时):
|
||||
python {baseDir}/scripts/main.py health
|
||||
|
||||
Windows(未注入时):
|
||||
{JIANGCHANG_DATA_ROOT}\python-runtime\.venv\Scripts\python.exe {baseDir}\scripts\main.py health
|
||||
|
||||
通用:
|
||||
<shared-python> {baseDir}/scripts/main.py health
|
||||
Unix(未注入时):
|
||||
{JIANGCHANG_DATA_ROOT}/python-runtime/.venv/bin/python3 {baseDir}/scripts/main.py health
|
||||
```
|
||||
|
||||
`<shared-python>` 通常位于 `{JIANGCHANG_DATA_ROOT}/python-runtime/.venv`。数据根由宿主注入;开发模式下也可能通过 `JIANGCHANG_DATA_ROOT` 解析(见 `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 诊断,**不在技能内重复实现**。典型字段:
|
||||
|
||||
@@ -42,7 +55,7 @@ Windows:
|
||||
- 用户实际 `.env`:`{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/{skill_slug}/.env`。
|
||||
- `scripts/main.py` 与 `cli.app.main()` 启动时调用 `util.config_bootstrap.bootstrap_skill_config()`。
|
||||
- 配置优先级:**进程环境变量** > **用户 `.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 / 背景音乐
|
||||
|
||||
|
||||
@@ -20,19 +20,26 @@
|
||||
1. **宿主管调度,技能管业务** — 不自行实现任务中心 / Job 轮询 / 后台守护进程。
|
||||
2. **单业务内核、多入口** — 数据管理、定时任务、Agent、技能详情与直接 CLI 复用同一 domain service;禁止按入口复制业务逻辑。
|
||||
3. **五职责正交** — `placements` / `bind.tables` / `executionProfile` / `entrypoint` / domain service 各管一事(见 ACTIONS.md)。
|
||||
4. **长耗时建议 async** — 开浏览器、RPA、HTTP 大文件下载等建议 `executionProfile: "async"`,进**任务中心**;这是选择建议,**不**限制可以出现在哪些 placement。
|
||||
5. **Agent 禁止 `exec` 直跑长任务 CLI** — 必须 `run_skill_action`;禁止 `exec python main.py run` + `process poll` 干等。
|
||||
4. **长耗时建议 async** — 开浏览器、RPA、HTTP 大文件下载等建议 `executionProfile: "async"`,进**任务中心**;这是对**方法**的选择,**不**限制可以出现在哪些 placement / 入口。
|
||||
5. **Agent:短 CLI / 长 Action** — 短查询可用共享 Python bash CLI;长任务 / RPA **禁止** `exec` + `process poll`,必须 `run_skill_action`。
|
||||
6. **进度走 Journal** — `emit` / `video.add_step` → Run Journal;UI / 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
|
||||
|
||||
### 心智(先记住)
|
||||
|
||||
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`) | ❌ 不进 |
|
||||
| **`async`** | 后台 spawn Job,立即返回 `jobId`;`emit` / `checkpoint` / `finish`;长任务应支持暂停/停止/取消检查 | Job 本身无默认墙钟上限 | ✅ 进入 |
|
||||
| **`sync`**(或省略时的宿主缺省) | 调用方等待 CLI 结束,直接返回结构化结果或结构化错误;**不**创建后台 Job | 默认约 **30s**(`SKILL_ACTION_SYNC_TIMEOUT_MS`) | ❌ 不进 |
|
||||
| **`async`**(须显式声明) | 后台 spawn Job,立即返回 `jobId`;`emit` / `checkpoint` / `finish`;长任务应支持暂停/停止/取消检查 | Job 本身无默认墙钟上限 | ✅ 进入 |
|
||||
|
||||
直接执行 CLI:终端等待进程结束。宿主经 Action 调用同一 CLI:再按 `executionProfile` 分流。技能不得自建任务中心。
|
||||
|
||||
@@ -47,7 +54,7 @@
|
||||
|
||||
合法:`sync|async` × `toolbar|cron|agent|skill-detail`。
|
||||
|
||||
历史说明:旧文档曾错误地把数据管理按钮入口与异步执行方式绑死。该约束**已废止**;模板测试与 Schema **不得**再建立「入口位置决定 sync/async」一类硬耦合。宿主省略 `executionProfile` 时的兼容行为以宿主实现为准,**新技能不得依赖**。
|
||||
历史说明:旧文档曾错误地把数据管理按钮入口与异步执行方式绑死。该约束**已废止**;模板测试与 Schema **不得**再建立「入口位置决定 sync/async」一类硬耦合。旧版宿主曾「省略 → async」;**当前宿主为「省略 → sync」**。新技能仍须显式声明,不得依赖省略行为。
|
||||
|
||||
---
|
||||
|
||||
@@ -160,15 +167,15 @@ cmd_stats() # 只读 sync:JSON stdout,无 job_context
|
||||
|
||||
## 8. Agent / SKILL.md 契约(复制后必改)
|
||||
|
||||
业务技能 `SKILL.md` 应明确:
|
||||
业务技能 `SKILL.md` / `references/` 应明确(Agent 主读,勿只写在本文):
|
||||
|
||||
1. 副作用任务必须通过宿主 **Skill Action**(`run_skill_action`)调用。
|
||||
2. **禁止**对长任务使用 `exec` + `process poll`。
|
||||
1. 短查询:共享 Python(PATH 上的 `python`)跑 CLI;**禁止** `uv run`。
|
||||
2. 长任务 / RPA / async Action:必须通过宿主 **Skill Action**(`run_skill_action`);**禁止** `exec` + `process poll`。
|
||||
3. 单条 vs 批量 action id 与参数(如 `url` / `pickCount`)。
|
||||
4. 长任务进度看**任务中心** / Skill Run Card,不等 CLI stdout。
|
||||
5. 多入口复用同一业务内核;异步 Action 进入任务中心。
|
||||
4. `executionProfile: async` → 进度看**任务中心** / Skill Run Card;`sync` → 当场结果。与入口无关。
|
||||
5. 多入口复用同一业务内核。
|
||||
|
||||
模板源仓库的 `SKILL.md` 含占位段落;scaffold 后按业务改写。
|
||||
模板源仓库的 `SKILL.md` / `references/ACTIONS.md` 含占位段落;scaffold 后按业务改写。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -137,7 +137,8 @@ Golden fixture 流程同理([`tests/samples/test_golden_cases.py.sample`](../t
|
||||
浏览器 RPA 走 `simulator_rpa` 档位联调前,逐项确认:
|
||||
|
||||
- [ ] 数据目录 `.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`)
|
||||
- [ ] 失败先查 `rpa-artifacts/` 截图,再查 Chrome profile 缓存(`--user-data-dir` 手工打开清站点数据)
|
||||
- [ ] 默认 `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`
|
||||
- [ ] 无 `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`(或等价诊断行)
|
||||
- [ ] `config-path` 可输出用户 `.env` 路径 JSON
|
||||
- [ ] `pytest.ini` 存在且 `python_files` 只收集 `test_*.py` / `*_test.py`
|
||||
|
||||
@@ -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)`
|
||||
3. **人工验证(启动后)** — 检测滑块/短信,等待用户完成
|
||||
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 提供
|
||||
- `launch_persistent_context` 的 `args` **只放 Chrome 参数**;**不要把 URL 放进 `args`**
|
||||
- 页面必须通过 `new_page()` + `goto()`,或通过真实地址栏/点击进入
|
||||
@@ -139,7 +143,7 @@ real_browser_rpa/
|
||||
- **不 import account-manager 内部模块** — 只通过 CLI/subprocess 调用
|
||||
- **不自动破解验证码** — 滑块/短信只检测 + 等待人工完成
|
||||
- **日志脱敏** — 不输出完整手机号/账号
|
||||
- **租约释放** — `pick-web --lease` 后必须在 `finally` 释放
|
||||
- **租约释放** — `ensure-web` / `pick-web --lease` 后必须在 `finally` 释放
|
||||
- **失败留痕** — 真实 skill 应在关键失败点截图(示例中已留注释位)
|
||||
|
||||
## 常见坑
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""account-manager CLI 集成(仅 subprocess,不 import 兄弟 skill 内部模块)。"""
|
||||
"""account-manager CLI 集成(仅 subprocess,不 import 兄弟 skill 内部模块)。
|
||||
|
||||
业务首启默认 ``ensure-web``(有则用、无则 add-web);登记 ≠ 站点已登录。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -7,8 +10,9 @@ import logging
|
||||
import os
|
||||
import subprocess
|
||||
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 util.constants import LEASE_HOLDER, LEASE_TTL_SEC, TARGET_PLATFORM
|
||||
@@ -19,12 +23,17 @@ logger = logging.getLogger(__name__)
|
||||
PLACEHOLDER_PLATFORM = TARGET_PLATFORM
|
||||
|
||||
ACCOUNT_SETUP_MESSAGE = (
|
||||
f"未找到可用的 {PLACEHOLDER_PLATFORM} 账号,所以还没有打开浏览器。"
|
||||
f"请先在 account-manager 中添加 platform={PLACEHOLDER_PLATFORM}、"
|
||||
"status=active、带 profile_dir 的账号后重新运行。"
|
||||
f"未能取得可用的 {PLACEHOLDER_PLATFORM} 账号(ensure-web 失败),所以还没有打开浏览器。"
|
||||
f"请确认已安装 account-manager,并检查 platform={PLACEHOLDER_PLATFORM} 账号状态后重试。"
|
||||
)
|
||||
|
||||
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):
|
||||
@@ -38,13 +47,65 @@ def mask_login_id(login_id: str) -> str:
|
||||
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:
|
||||
"""解析 account-manager CLI 入口路径。
|
||||
|
||||
优先级:ACCOUNT_MANAGER_ROOT → get_sibling_skills_root(路径推断 / JIANGCHANG_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")):
|
||||
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 ""))
|
||||
message = str(err.get("message") or "")
|
||||
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:
|
||||
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):
|
||||
raise AccountManagerError("ACCOUNT_NOT_FOUND", "pick-web 返回格式异常。")
|
||||
raise AccountManagerError("ACCOUNT_NOT_FOUND", "ensure-web 返回格式异常。")
|
||||
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
|
||||
|
||||
|
||||
def _pick_web_with_lease(platform: str) -> Dict[str, Any]:
|
||||
proc = _run_argv(
|
||||
[
|
||||
"account",
|
||||
"pick-web",
|
||||
"--platform",
|
||||
platform,
|
||||
"--lease",
|
||||
"--holder",
|
||||
LEASE_HOLDER,
|
||||
"--ttl-sec",
|
||||
LEASE_TTL_SEC,
|
||||
]
|
||||
)
|
||||
def _ensure_web_with_lease(
|
||||
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",
|
||||
"ensure-web",
|
||||
"--platform",
|
||||
platform,
|
||||
"--url",
|
||||
start_url,
|
||||
"--auth-strategy",
|
||||
DEFAULT_AUTH_STRATEGY,
|
||||
"--label",
|
||||
label,
|
||||
"--lease",
|
||||
"--holder",
|
||||
LEASE_HOLDER,
|
||||
"--ttl-sec",
|
||||
LEASE_TTL_SEC,
|
||||
"--purpose",
|
||||
"rpa",
|
||||
]
|
||||
if lid:
|
||||
argv.extend(["--login-id", lid])
|
||||
|
||||
proc = _run_argv(argv)
|
||||
out = proc.stdout or ""
|
||||
if proc.returncode != 0 and not out.strip():
|
||||
raise AccountManagerError(
|
||||
"PICK_WEB_FAILED",
|
||||
(proc.stderr or "").strip() or "pick-web 子进程失败",
|
||||
"ENSURE_WEB_FAILED",
|
||||
(proc.stderr or "").strip() or "ensure-web 子进程失败",
|
||||
)
|
||||
payload = _parse_last_json(out)
|
||||
if isinstance(payload, dict) and payload.get("success") is False:
|
||||
return _validate_pick_payload(payload)
|
||||
if not isinstance(payload, dict):
|
||||
raise AccountManagerError("ACCOUNT_NOT_FOUND", "pick-web 返回格式异常。")
|
||||
return payload
|
||||
raise AccountManagerError("ACCOUNT_NOT_FOUND", "ensure-web 返回格式异常。")
|
||||
return _validate_pick_payload(payload)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def pick_web_account(platform: str, account_id: Optional[str] = None) -> dict:
|
||||
"""获取网页账号(profile_dir + lease_token)。"""
|
||||
def pick_web_account(
|
||||
platform: str,
|
||||
account_id: Optional[str] = None,
|
||||
login_id: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""获取网页账号(profile_dir + lease_token)。
|
||||
|
||||
优先级:数字 account_id → login_id(ensure-web --login-id)→ 按平台 ensure-web。
|
||||
"""
|
||||
platform_key = (platform or PLACEHOLDER_PLATFORM).strip() or PLACEHOLDER_PLATFORM
|
||||
|
||||
if account_id:
|
||||
try:
|
||||
aid = int(account_id)
|
||||
except (TypeError, ValueError):
|
||||
raise AccountManagerError("ACCOUNT_NOT_FOUND", f"账号 ID 无效:{account_id}")
|
||||
return _pick_by_id(platform_key, aid)
|
||||
aid_raw = (account_id or "").strip()
|
||||
if aid_raw and not _is_placeholder_config(aid_raw):
|
||||
if not aid_raw.isdigit():
|
||||
if not (login_id or "").strip():
|
||||
login_id = aid_raw
|
||||
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:
|
||||
return _pick_web_with_lease(platform_key)
|
||||
return _ensure_web_with_lease(platform_key, login_id=lid)
|
||||
except AccountManagerError as exc:
|
||||
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
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
@@ -11,6 +10,8 @@ import time
|
||||
from dataclasses import dataclass, field
|
||||
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.human_verification import (
|
||||
HumanVerificationWaitResult,
|
||||
@@ -102,11 +103,11 @@ def _step_cb(cb: Optional[StepCallback], text: str) -> None:
|
||||
async def _random_delay() -> None:
|
||||
lo = int(os.getenv("RPA_STEP_DELAY_MIN_MS") or "900")
|
||||
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:
|
||||
await asyncio.sleep(random.uniform(1.2, 3.5))
|
||||
await interruptible_sleep(random.uniform(1.2, 3.5))
|
||||
|
||||
|
||||
def _headless() -> bool:
|
||||
@@ -156,7 +157,7 @@ async def _open_login_panel_if_needed(page) -> None:
|
||||
btn = page.locator(LOGGED_OUT_SELECTOR).first
|
||||
if await _visible(btn):
|
||||
await btn.click()
|
||||
await asyncio.sleep(random.uniform(1.0, 3.0))
|
||||
await interruptible_sleep(random.uniform(1.0, 3.0))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -175,7 +176,7 @@ async def _ensure_logged_in(page, *, wait_sec: int) -> None:
|
||||
if await _is_logged_in(page):
|
||||
print("[登录] 检测到登录成功")
|
||||
return
|
||||
await asyncio.sleep(2.0)
|
||||
await interruptible_sleep(2.0)
|
||||
|
||||
raise RuntimeError(
|
||||
f"ERROR:LOGIN_TIMEOUT 浏览器已打开,但未完成登录。"
|
||||
@@ -253,7 +254,7 @@ async def _wait_search_results(page, timeout_sec: float = 45.0) -> bool:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(0.8)
|
||||
await interruptible_sleep(0.8)
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,12 @@ import asyncio
|
||||
import uuid
|
||||
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 (
|
||||
ScrapeRunResult,
|
||||
format_stop_reason_for_user,
|
||||
@@ -79,7 +84,12 @@ async def run_keyword_search_task(
|
||||
account: Optional[dict[str, Any]] = None
|
||||
|
||||
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")
|
||||
|
||||
scrape_result: ScrapeRunResult = await run_keyword_search_async(
|
||||
|
||||
@@ -50,13 +50,19 @@ simulator_browser_rpa/
|
||||
| 文件 | 职责 |
|
||||
|---|---|
|
||||
| `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`、截图 |
|
||||
| `adapter/simulator_rpa.py` | **薄** adapter:pick 账号、lease、委托 RPA、`finally release_lease` |
|
||||
| `adapter/mock.py` | 不启浏览器,供 unit/mock/CI |
|
||||
| `task_service.py` | async 校验输入 → 选 adapter → `await submit_batch` |
|
||||
| `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,4 +1,7 @@
|
||||
"""account-manager CLI 集成(仅 subprocess,不 import 兄弟 skill 内部模块)。"""
|
||||
"""account-manager CLI 集成(仅 subprocess,不 import 兄弟 skill 内部模块)。
|
||||
|
||||
业务首启默认 ``ensure-web``(有则用、无则 add-web);登记 ≠ 站点已登录。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -7,8 +10,9 @@ import logging
|
||||
import os
|
||||
import subprocess
|
||||
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 util.constants import LEASE_HOLDER, LEASE_TTL_SEC, TARGET_PLATFORM
|
||||
@@ -19,12 +23,17 @@ logger = logging.getLogger(__name__)
|
||||
PLACEHOLDER_PLATFORM = TARGET_PLATFORM
|
||||
|
||||
ACCOUNT_SETUP_MESSAGE = (
|
||||
f"未找到可用的 {PLACEHOLDER_PLATFORM} 账号,所以还没有打开浏览器。"
|
||||
f"请先在 account-manager 中添加 platform={PLACEHOLDER_PLATFORM}、"
|
||||
"status=active、带 profile_dir 的账号后重新运行。"
|
||||
f"未能取得可用的 {PLACEHOLDER_PLATFORM} 账号(ensure-web 失败),所以还没有打开浏览器。"
|
||||
f"请确认已安装 account-manager,并检查 platform={PLACEHOLDER_PLATFORM} 账号状态后重试。"
|
||||
)
|
||||
|
||||
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):
|
||||
@@ -38,12 +47,65 @@ def mask_login_id(login_id: str) -> str:
|
||||
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:
|
||||
"""解析 account-manager CLI 入口路径。
|
||||
|
||||
优先级: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")):
|
||||
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):
|
||||
return candidate
|
||||
|
||||
# 开发环境兜底:仅模板/本机调试;生产依赖宿主注入的路径变量,勿硬编码个人目录
|
||||
dev = r"D:\OpenClaw\client-commons\account-manager\scripts\main.py"
|
||||
if os.path.isfile(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 ""))
|
||||
message = str(err.get("message") or "")
|
||||
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:
|
||||
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):
|
||||
raise AccountManagerError("ACCOUNT_NOT_FOUND", "pick-web 返回格式异常。")
|
||||
raise AccountManagerError("ACCOUNT_NOT_FOUND", "ensure-web 返回格式异常。")
|
||||
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
|
||||
|
||||
|
||||
def _pick_web_with_lease(platform: str) -> Dict[str, Any]:
|
||||
proc = _run_argv(
|
||||
[
|
||||
"account",
|
||||
"pick-web",
|
||||
"--platform",
|
||||
platform,
|
||||
"--lease",
|
||||
"--holder",
|
||||
LEASE_HOLDER,
|
||||
"--ttl-sec",
|
||||
LEASE_TTL_SEC,
|
||||
]
|
||||
)
|
||||
def _ensure_web_with_lease(
|
||||
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",
|
||||
"ensure-web",
|
||||
"--platform",
|
||||
platform,
|
||||
"--url",
|
||||
start_url,
|
||||
"--auth-strategy",
|
||||
DEFAULT_AUTH_STRATEGY,
|
||||
"--label",
|
||||
label,
|
||||
"--lease",
|
||||
"--holder",
|
||||
LEASE_HOLDER,
|
||||
"--ttl-sec",
|
||||
LEASE_TTL_SEC,
|
||||
"--purpose",
|
||||
"rpa",
|
||||
]
|
||||
if lid:
|
||||
argv.extend(["--login-id", lid])
|
||||
|
||||
proc = _run_argv(argv)
|
||||
out = proc.stdout or ""
|
||||
if proc.returncode != 0 and not out.strip():
|
||||
raise AccountManagerError(
|
||||
"PICK_WEB_FAILED",
|
||||
(proc.stderr or "").strip() or "pick-web 子进程失败",
|
||||
"ENSURE_WEB_FAILED",
|
||||
(proc.stderr or "").strip() or "ensure-web 子进程失败",
|
||||
)
|
||||
payload = _parse_last_json(out)
|
||||
if isinstance(payload, dict) and payload.get("success") is False:
|
||||
return _validate_pick_payload(payload)
|
||||
if not isinstance(payload, dict):
|
||||
raise AccountManagerError("ACCOUNT_NOT_FOUND", "pick-web 返回格式异常。")
|
||||
return payload
|
||||
raise AccountManagerError("ACCOUNT_NOT_FOUND", "ensure-web 返回格式异常。")
|
||||
return _validate_pick_payload(payload)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def pick_web_account(platform: str, account_id: Optional[str] = None) -> dict:
|
||||
"""获取网页账号(profile_dir + lease_token)。"""
|
||||
def pick_web_account(
|
||||
platform: str,
|
||||
account_id: Optional[str] = None,
|
||||
login_id: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""获取网页账号(profile_dir + lease_token)。
|
||||
|
||||
优先级:数字 account_id → login_id(ensure-web --login-id)→ 按平台 ensure-web。
|
||||
"""
|
||||
platform_key = (platform or PLACEHOLDER_PLATFORM).strip() or PLACEHOLDER_PLATFORM
|
||||
|
||||
if account_id:
|
||||
try:
|
||||
aid = int(account_id)
|
||||
except (TypeError, ValueError):
|
||||
raise AccountManagerError("ACCOUNT_NOT_FOUND", f"账号 ID 无效:{account_id}")
|
||||
return _pick_by_id(platform_key, aid)
|
||||
aid_raw = (account_id or "").strip()
|
||||
if aid_raw and not _is_placeholder_config(aid_raw):
|
||||
if not aid_raw.isdigit():
|
||||
if not (login_id or "").strip():
|
||||
login_id = aid_raw
|
||||
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:
|
||||
return _pick_web_with_lease(platform_key)
|
||||
return _ensure_web_with_lease(platform_key, login_id=lid)
|
||||
except AccountManagerError as exc:
|
||||
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
|
||||
|
||||
@@ -1,10 +1,39 @@
|
||||
# Skill Action Manifest(匠厂宿主)
|
||||
|
||||
本文件说明 `assets/actions.json` 与 `scripts/main.py` CLI 的对应关系,供 Agent 编排与宿主 **Skill Action Runtime** 使用。
|
||||
本文件说明 `assets/actions.json` 与 `scripts/main.py` CLI 的对应关系,供 **Agent 编排**与宿主 **Skill Action Runtime** 使用。这是 Agent 侧关于 Action / 任务中心的**主契约**;实现细节与开发规范见 [`../development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md)。
|
||||
|
||||
**权威运行时契约(sync/async、任务中心、单内核编排)见 [`../development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md)。**
|
||||
**边界:** 用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.md);CLI 参数见 [`CLI.md`](CLI.md)。
|
||||
|
||||
**边界:** 用户市场说明见根目录 [`README.md`](../README.md);CLI 参数与错误码细节见 [`CLI.md`](CLI.md)。
|
||||
## Agent 何时用 Action(与 CLI 的分工)
|
||||
|
||||
| 场景 | 做法 |
|
||||
|------|------|
|
||||
| 短查询 / 只读 / 秒级 | bash + 共享 Python CLI(见 [`CLI.md`](CLI.md));**不必**强行走 Action |
|
||||
| 数据管理按钮 / 定时任务 / 技能详情按钮 | **必须**在 `actions.json` 声明对应 Action |
|
||||
| 长耗时 / RPA / 需要进度与取消 | **必须** `run_skill_action`;对应 Action 一般为 `executionProfile: "async"` |
|
||||
| 已声明的 `sync` Action | `run_skill_action` 当场返回结果,**不进**任务中心 |
|
||||
| 已声明的 `async` Action | `run_skill_action` 立即返回 `jobId`,**进入**任务中心 |
|
||||
|
||||
**进不进任务中心只取决于该 Action 的 `executionProfile`,与从数据管理、定时任务还是 Agent 触发无关。**
|
||||
|
||||
禁止对长任务 `exec`/`bash` + `process poll` 干等 CLI。禁止 `uv run python` 跑技能脚本。
|
||||
|
||||
## async 副作用 Action 最小组合(复制验收 checklist)
|
||||
|
||||
对每个「会开浏览器 / 长耗时 / 需进度或取消」的 Action,声明时建议同时满足:
|
||||
|
||||
1. `executionProfile: "async"`(进入任务中心)
|
||||
2. 若要让 Agent 触发:`placements` **含** `agent`
|
||||
3. 有可感知副作用时:提供 `confirmation.message`
|
||||
4. 需要用户/Agent 入参时:简单 `inputSchema` + entrypoint 中的 `{{fieldName}}`
|
||||
5. `SKILL.md`「用户意图路由表」与 `CLI.md`「Action 强制黑名单」**出现同一 action id / 命令**
|
||||
|
||||
默认模板 `assets/actions.json` 仅含诊断类 sync Action,**不要**为通过本 checklist 而塞假业务 Action;业务技能按需添加。完整字段见下文 Phase 1。
|
||||
|
||||
## 可选:域前缀 CLI(嵌套子命令)
|
||||
|
||||
小技能可继续扁平:`entrypoint.command = "run"`。
|
||||
命令变多时,可采用**域前缀**:`command` 为域(如 `<domain>`),业务动词与参数放在 `args` 数组(如 `["<verb>", "--flag", "{{field}}"]`)。与扁平入口等价,须仍指向同一 domain service。
|
||||
|
||||
## 模板严格规范 vs 宿主向后兼容
|
||||
|
||||
@@ -117,7 +146,7 @@ Action 是**可选能力**:
|
||||
|
||||
## 执行模型
|
||||
|
||||
宿主将 action 转为 argv,再执行:
|
||||
宿主将 action 转为 argv,再用**共享 Python**(用户数据目录下 `python-runtime/.venv`)执行:
|
||||
|
||||
```text
|
||||
python {skill_root}/scripts/main.py <command> <args...>
|
||||
@@ -132,18 +161,18 @@ python {skill_root}/scripts/main.py <command> <args...>
|
||||
|
||||
### `executionProfile`(仅 sync / async)
|
||||
|
||||
模板与新技能**只允许**这两种值;禁止发明 `auto` / `background` / `deferred` 等第三种模式。
|
||||
**心智**:多数 Action 按 **sync**(短调用、不进任务中心);只有长耗时 / RPA / 需进度控制时才标 **async**。匠厂宿主若**省略**该字段,按 **sync** 处理;模板与新技能仍要求**每个 Action 显式写出** `sync` 或 `async`,禁止发明 `auto` / `background` / `deferred`,也禁止赌省略行为。
|
||||
|
||||
| 值 | 宿主行为 | 任务中心 |
|
||||
|----|----------|----------|
|
||||
| `sync` | 调用方等待结束,直接返回结构化结果或结构化错误;**不**创建后台 Job | 不进 |
|
||||
| `async` | 宿主创建后台 Job,立即返回 `jobId`;用 `emit` / `checkpoint` / `finish` 报告进度与终态 | **进入** |
|
||||
| `sync`(或省略时的宿主缺省) | 调用方等待结束,直接返回结构化结果或结构化错误;**不**创建后台 Job | 不进 |
|
||||
| `async`(须显式声明) | 宿主创建后台 Job,立即返回 `jobId`;用 `emit` / `checkpoint` / `finish` 报告进度与终态 | **进入** |
|
||||
|
||||
合法组合(全部允许):`sync|async` × `toolbar|cron|agent|skill-detail`。
|
||||
|
||||
经验建议(**不是** placement 硬限制):耗时不可预测、需要暂停/停止/进度展示的任务,建议用 `async`。
|
||||
经验建议(**不是** placement 硬限制):耗时不可预测、需要暂停/停止/进度展示的任务,用 `async`;其余默认写 `sync`。
|
||||
|
||||
直接执行 CLI 时,终端自然等待进程结束;宿主通过 Action 调用同一 CLI 时,再按 `executionProfile` 决定同步等待还是建 Job。技能**不得**自行实现第二套任务中心或后台队列。
|
||||
直接执行短 CLI 时,终端自然等待进程结束。宿主通过 Action 调用同一 CLI 时,再按 `executionProfile` 决定同步等待还是建 Job。技能**不得**自行实现第二套任务中心或后台队列。
|
||||
|
||||
### CLI 输出契约
|
||||
|
||||
|
||||
@@ -1,6 +1,33 @@
|
||||
# 通用业务技能模板 CLI 标准
|
||||
|
||||
将 `{baseDir}` 替换为技能根目录(含 `SKILL.md`、`scripts/` 的目录)。所有命令通过 `python {baseDir}/scripts/main.py` 调用。
|
||||
将 `{baseDir}` 替换为技能根目录(含 `SKILL.md`、`scripts/` 的目录)。
|
||||
|
||||
## 解释器(必读)
|
||||
|
||||
必须使用宿主**共享 Python runtime**(`{JIANGCHANG_DATA_ROOT}/python-runtime/.venv`,已在 PATH / `VIRTUAL_ENV`)。一条命令直接跑,不要先探系统 Python。
|
||||
|
||||
```bash
|
||||
python {baseDir}/scripts/main.py <command> …
|
||||
```
|
||||
|
||||
| 场景 | 写法 |
|
||||
|------|------|
|
||||
| **推荐(Agent)** | `python {baseDir}/scripts/main.py <command> …` |
|
||||
| 手工排查(环境未注入时) | `{JIANGCHANG_DATA_ROOT}/python-runtime/.venv/Scripts/python.exe`(Windows)或 `…/.venv/bin/python3`(Unix) |
|
||||
|
||||
**禁止**对技能脚本使用 `uv run python …`(会创建临时环境,加载不到 `jiangchang-platform-kit` / `jiangchang_skill_core`)。
|
||||
**禁止** Agent 自行 `pip install` / `uv pip install` 往临时环境塞依赖。
|
||||
|
||||
## Agent 分流(业务技能复制后必改)
|
||||
|
||||
用本表列出**本技能**哪些子命令可走短 CLI,哪些必须走 Skill Action。默认:短查询/只读写本地可用短 CLI;长耗时 / RPA / 需进度取消进**黑名单**。有 async/RPA 的技能:**黑名单不得为空**,且黑名单命令须映射到 `actions.json` 中的 async Action。
|
||||
|
||||
| 类型 | 本技能命令(自填) | Agent |
|
||||
|------|--------------------|-------|
|
||||
| **短 CLI 白名单** | (例)`health` / `version` / `logs` / `<your-read-or-prep>` | 可用 bash/exec + 共享 `python` |
|
||||
| **Action 强制黑名单** | (例)`<your-long-or-rpa-command>` | **禁止** bash + `process poll`;必须 `run_skill_action` |
|
||||
|
||||
细节与 Action 清单见 [`ACTIONS.md`](ACTIONS.md)。
|
||||
|
||||
## 最小命令
|
||||
|
||||
@@ -18,13 +45,13 @@ python {baseDir}/scripts/main.py init-db
|
||||
- **`config-path`**:输出 JSON,包含 `skill`、`env_path`、`example_path`。
|
||||
- **`version`**:输出 JSON(`version`、`skill`)。
|
||||
- **`init-db`**:幂等创建/迁移本地 SQLite(含 `_jiangchang_*` 展示元数据)。成功输出一行 JSON(`ok` / `skill` / `db_path`),退出码 `0`。宿主可在技能安装或更新成功后静默调用;不要并入 `health`。
|
||||
- **`run`**:长时间无 stdout **不代表卡死**;RPA / 外部调用期间应通过 Activity 或 Run Journal 看进度。
|
||||
- **`run` / RPA / 长任务**:CLI 仍是同一内核;**Agent 禁止**用 bash 干等长任务结束。须走宿主 Skill Action(`run_skill_action`),且该 Action 一般为 `executionProfile: "async"` → **任务中心**。长时间无 stdout **不代表卡死**;进度看任务中心 / Run Journal。
|
||||
- **排查顺序**(由快到慢):
|
||||
1. `python {baseDir}/scripts/main.py logs`
|
||||
2. `python {baseDir}/scripts/main.py log-get <log_id>`
|
||||
3. 统一日志文件 `{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/logs/jiangchang.log`(含 `trace_id`、`task_failed` 等)
|
||||
4. Run Journal `{JIANGCHANG_DATA_ROOT}/.jiangchang/runs/{job_id}.jsonl`
|
||||
5. RPA artifacts / `video_log` / 失败截图(见 `development/RPA.md`);未开启录屏时可能只有截图或日志
|
||||
5. RPA artifacts / `video_log` / 失败截图(开发规范见 `development/RPA.md`);未开启录屏时可能只有截图或日志
|
||||
- **录屏默认关闭**:模板 `.env.example` 为 `OPENCLAW_RECORD_VIDEO=0`;若未开启录屏,任务**可能不会**打印录屏路径。
|
||||
- 需要录屏时在用户 `.env` 或进程环境变量中设置 **`OPENCLAW_RECORD_VIDEO=1`**;开启后任务完成时 CLI 应打印录屏路径、录屏日志、视频/音频诊断(见 `task_run_support._print_video_summary`),且 `result_summary` 含 video 字段。
|
||||
|
||||
@@ -42,7 +69,7 @@ python {baseDir}/scripts/main.py init-db
|
||||
|
||||
`health` 委托 `jiangchang_skill_core.collect_runtime_diagnostics`,典型字段:
|
||||
|
||||
- `python_executable` — 当前 Python 解释器路径
|
||||
- `python_executable` — 当前 Python 解释器路径(应落在共享 python-runtime)
|
||||
- `platform_kit_version` / `platform_kit_min_version` / `platform_kit_version_ok`
|
||||
- `jiangchang_skill_core_file` — 公共库加载来源(若来自技能目录副本会报 warning)
|
||||
- `media_assets_root`、`ffmpeg_path`、`ffmpeg_available`、`background_music_mp3_count`
|
||||
@@ -59,38 +86,7 @@ python {baseDir}/scripts/main.py logs --task-type demo --status failed
|
||||
python {baseDir}/scripts/main.py log-get <log_id>
|
||||
```
|
||||
|
||||
| 命令 | 用途 |
|
||||
|------|------|
|
||||
| `run` | 执行一次任务 |
|
||||
| `logs` | 列出最近 N 条任务日志 |
|
||||
| `log-get` | 按 log_id 查看单条任务日志(JSON) |
|
||||
|
||||
## 查询/导出类(可选)
|
||||
|
||||
复制后按需增加,例如:`contacts`、`contact-get`、`export` 或业务等价命令。
|
||||
|
||||
## 若你的技能依赖兄弟技能
|
||||
|
||||
使用 `service.sibling_bridge.call_sibling_json(skill_slug, args)`,不要硬编码兄弟技能名到 `sibling_bridge.py` 自身;并在 `SKILL.md` 声明 `dependencies.required`。
|
||||
|
||||
## 手工排查命令(推荐)
|
||||
|
||||
**建议使用宿主共享 python-runtime**,避免技能目录内临时 venv 加载不到公共库:
|
||||
|
||||
```text
|
||||
Windows:
|
||||
{JIANGCHANG_DATA_ROOT}\python-runtime\.venv\Scripts\python.exe {baseDir}\scripts\main.py health
|
||||
|
||||
通用:
|
||||
<shared-python> {baseDir}/scripts/main.py health
|
||||
```
|
||||
|
||||
`<shared-python>` 通常位于 `{JIANGCHANG_DATA_ROOT}/python-runtime/.venv`。
|
||||
|
||||
**不建议**在生产/测试机使用技能目录内 `uv run python`,以免加载不到共享 runtime 中的 `jiangchang-platform-kit` / `playwright`。
|
||||
|
||||
## 模板约定
|
||||
|
||||
- 最小模板至少保留 `health` / `config-path` / `version`
|
||||
- 通用业务技能建议使用 `run` / `logs` / `log-get`
|
||||
- 不要再用旧模板的 `publish` / `--account-id` / `--article-id` 命名
|
||||
| 命令 | 用途 | Agent 注意 |
|
||||
|------|------|------------|
|
||||
| `run` | 执行一次任务(常为长任务 / RPA) | **走 `run_skill_action`**,不要 bash poll |
|
||||
| `logs` / `log-get` | 查任务日志 | 可用短 CLI |
|
||||
|
||||
@@ -5,17 +5,20 @@
|
||||
**边界规则:**
|
||||
|
||||
- 本文**不得**包含 YAML frontmatter,**不得**包含 `description` 字段。
|
||||
- 技能市场详情与普通用户说明**必须**放在根目录 [`README.md`](../README.md)。
|
||||
- 本目录只保留运行时引用资料(如 `CLI.md`、`SCHEMA.md`);开发规范放在 [`../development/`](../development/)。
|
||||
- 技能市场四 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)。
|
||||
- [`../development/`](../development/) 是开发者 / 编码助手深度规范,**不是**对话 Agent 的默认读盘路径。
|
||||
|
||||
| 文档 | 用途 |
|
||||
|------|------|
|
||||
| 根目录 [`README.md`](../README.md) | 用户市场详情页说明 |
|
||||
| [`SKILL.md`](../SKILL.md) | LLM / OpenClaw 平台技能入口 |
|
||||
| [`CLI.md`](CLI.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 平台技能入口与触发摘要 |
|
||||
| [`CLI.md`](CLI.md) | 共享 Python、命令行入口、参数与短操作契约 |
|
||||
| [`ACTIONS.md`](ACTIONS.md) | Skill Action、sync/async、任务中心、多入口(可选能力) |
|
||||
| [`SCHEMA.md`](SCHEMA.md) | 本地 SQLite 与数据管理展示契约 |
|
||||
| [`ACTIONS.md`](ACTIONS.md) | Skill Action manifest(**可选**能力)与宿主入口契约 |
|
||||
| [`../development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md) | sync/async、任务中心、Agent 禁令、编排与队列 pick(长任务必读) |
|
||||
| [`../development/`](../development/) | 开发、测试、技术规范(给开发者 / AI 编程代理) |
|
||||
| [`../development/`](../development/) | 开发、测试、RPA 深度规范(给人 / 编码助手) |
|
||||
|
||||
按需阅读:编排任务时优先 `CLI.md`;解析结构化字段时读 `SCHEMA.md`;配置宿主按钮/定时任务/Agent 入口时读 `ACTIONS.md` + `SKILL_ACTION_RUNTIME.md`;运行时环境细节见 `../development/RUNTIME.md`。
|
||||
按需阅读:短操作优先 `CLI.md`;Action / 任务中心读 `ACTIONS.md`;结构化字段读 `SCHEMA.md`。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。"""
|
||||
|
||||
SKILL_SLUG = "your-skill-slug"
|
||||
SKILL_VERSION = "1.0.42"
|
||||
SKILL_VERSION = "1.0.51"
|
||||
LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug"
|
||||
PLATFORM_KIT_MIN_VERSION = "1.2.0"
|
||||
PLATFORM_KIT_MIN_VERSION = "1.2.2"
|
||||
|
||||
@@ -35,7 +35,7 @@ def get_skill_root() -> str:
|
||||
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."""
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
@@ -24,15 +24,18 @@ POLICY_IDS = (
|
||||
"POLICY-STRUCTURE-001",
|
||||
"POLICY-RUNTIME-001",
|
||||
"POLICY-RUNTIME-002",
|
||||
"POLICY-RUNTIME-003",
|
||||
"POLICY-INSTALL-001",
|
||||
"POLICY-CONFIG-001",
|
||||
"POLICY-CONFIG-002",
|
||||
"POLICY-CONFIG-003",
|
||||
"POLICY-CONFIG-004",
|
||||
"POLICY-TESTING-001",
|
||||
"POLICY-TESTING-002",
|
||||
"POLICY-RPA-001",
|
||||
"POLICY-RPA-002",
|
||||
"POLICY-RPA-003",
|
||||
"POLICY-RPA-004",
|
||||
"POLICY-PACKAGING-001",
|
||||
"POLICY-PACKAGING-002",
|
||||
"POLICY-DOCS-001",
|
||||
@@ -85,6 +88,22 @@ CONFIG_ENV_FORBIDDEN_PATTERNS = (
|
||||
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_001_FORBIDDEN_SUBSTRINGS = (
|
||||
@@ -124,6 +143,18 @@ RPA_003_SOURCE = (
|
||||
"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_002_SOURCE = "development/LOGGING.md; development/DEVELOPMENT.md"
|
||||
LOGGING_003_SOURCE = "development/LOGGING.md"
|
||||
@@ -148,6 +179,77 @@ def _policy_msg(policy_id: str, source: str, detail: str) -> str:
|
||||
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:
|
||||
return os.path.relpath(path, skill_root).replace("\\", "/")
|
||||
|
||||
@@ -384,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):
|
||||
def test_root_tests_do_not_hardcode_real_targets(self) -> None:
|
||||
skill_root = get_skill_root()
|
||||
@@ -509,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]:
|
||||
offenders: list[str] = []
|
||||
for path in _walk_files(skill_root, "scripts", suffix=".py"):
|
||||
|
||||
@@ -141,6 +141,47 @@ class TestDocsStandards(unittest.TestCase):
|
||||
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:
|
||||
text = self._read("development/NAMING.md")
|
||||
self.assertIn("verb-noun-platform", text)
|
||||
@@ -197,6 +238,17 @@ class TestDocsStandards(unittest.TestCase):
|
||||
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:
|
||||
text = self._read("examples/simulator_browser_rpa/README.md")
|
||||
self.assertIn("async", text.lower())
|
||||
@@ -302,10 +354,71 @@ class TestDocsStandards(unittest.TestCase):
|
||||
self.assertNotIn("ALLOW_" + "REAL_" + "API", text)
|
||||
self.assertNotIn("ALLOW_WRITE_ACTIONS", text)
|
||||
|
||||
def test_cli_md_mentions_shared_python_runtime(self) -> None:
|
||||
def test_cli_md_requires_shared_python_not_uv_run(self) -> None:
|
||||
text = self._read("references/CLI.md")
|
||||
self.assertIn("python-runtime", text)
|
||||
self.assertIn("python {baseDir}/scripts/main.py", text)
|
||||
self.assertNotIn("JIANGCHANG_PYTHON_EXE", text)
|
||||
self.assertIn("禁止", text)
|
||||
self.assertIn("uv run python", text)
|
||||
self.assertNotRegex(
|
||||
text,
|
||||
r"(?m)^(?!\*\*禁止\*\*).*\*\*建议\*\*.*uv run python",
|
||||
msg="CLI.md must not recommend uv run for skill scripts",
|
||||
)
|
||||
|
||||
def test_cli_md_has_agent_routing_tables(self) -> None:
|
||||
text = self._read("references/CLI.md")
|
||||
self.assertIn("短 CLI 白名单", text)
|
||||
self.assertIn("Action 强制黑名单", text)
|
||||
self.assertIn("run_skill_action", text)
|
||||
|
||||
def test_skill_md_requires_shared_python_not_uv_run(self) -> None:
|
||||
text = self._read("SKILL.md")
|
||||
self.assertIn("python {baseDir}/scripts/main.py", text)
|
||||
self.assertNotIn("JIANGCHANG_PYTHON_EXE", text)
|
||||
self.assertIn("uv run python", text)
|
||||
self.assertIn("禁止", text)
|
||||
self.assertIn("run_skill_action", text)
|
||||
self.assertIn("executionProfile", text)
|
||||
self.assertTrue(
|
||||
"任务中心" in text and "无关" in text,
|
||||
msg="SKILL.md must state Task Center is independent of entry point",
|
||||
)
|
||||
|
||||
def test_skill_md_has_intent_routing_table(self) -> None:
|
||||
text = self._read("SKILL.md")
|
||||
self.assertIn("用户意图路由表", text)
|
||||
self.assertIn("用户意图", text)
|
||||
self.assertIn("准备 vs 执行", text)
|
||||
|
||||
def test_skill_md_how_to_forbids_cli_paste(self) -> None:
|
||||
text = self._read("SKILL.md")
|
||||
self.assertIn("回答「怎么用", text)
|
||||
self.assertIn("禁止", text)
|
||||
self.assertTrue(
|
||||
"粘贴" in text and "python" in text,
|
||||
msg="SKILL.md must forbid pasting python CLI when answering how-to",
|
||||
)
|
||||
self.assertTrue(
|
||||
"dir" in text or "列" in text,
|
||||
msg="SKILL.md must discourage directory probing for how-to answers",
|
||||
)
|
||||
|
||||
def test_actions_md_has_async_side_effect_checklist(self) -> None:
|
||||
text = self._read("references/ACTIONS.md")
|
||||
self.assertIn("最小组合", text)
|
||||
self.assertIn("confirmation.message", text)
|
||||
self.assertIn("用户意图路由表", text)
|
||||
|
||||
def test_actions_md_states_profile_not_entry_for_task_center(self) -> None:
|
||||
text = self._read("references/ACTIONS.md")
|
||||
self.assertIn("executionProfile", text)
|
||||
self.assertIn("任务中心", text)
|
||||
self.assertTrue(
|
||||
"无关" in text or "orthogonal" in text.lower() or "正交" in text,
|
||||
msg="ACTIONS.md must keep placements orthogonal to Task Center",
|
||||
)
|
||||
|
||||
def test_testing_md_mentions_pytest_txt_collection_guard(self) -> None:
|
||||
text = self._read("development/TESTING.md")
|
||||
|
||||
@@ -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 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")
|
||||
with open(md_path, encoding="utf-8") as f:
|
||||
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")
|
||||
with open(req_path, encoding="utf-8") as f:
|
||||
|
||||
@@ -34,7 +34,7 @@ FORBIDDEN_PHRASES = (
|
||||
|
||||
POSITIVE_MARKERS = (
|
||||
"jiangchang-platform-kit",
|
||||
"1.2.0",
|
||||
"1.2.2",
|
||||
"共享 runtime",
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user