diff --git a/SKILL.md b/SKILL.md index e111e3c..106c1fc 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 技能开发模板(通用业务版) description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。" -version: 1.0.39 +version: 1.0.40 author: 深圳匠厂科技有限公司 metadata: openclaw: @@ -20,6 +20,14 @@ allowed-tools: 这是一个**用于复制的新技能模板**,不是业务 skill 本身。复制后请替换占位内容,实现你的真实业务逻辑。 +## Agent / 宿主编排(复制为业务技能后必改) + +- **长耗时 / RPA / 浏览器任务**:必须通过宿主 **Skill Action**(`run_skill_action`)调用已声明在 `assets/actions.json` 中的 action;`executionProfile` 须为 `"async"`,进度见**任务中心** / Skill Run Card。 +- **禁止**对长任务使用 `exec python …/scripts/main.py run` 或 `process poll` 干等 CLI 返回。 +- 只读查询(`health` / `stats` 等)可用 `executionProfile: "sync"`,立即返回结果。 +- 批量处理用参数化 `pickCount` / `--pick N` 顺序消费队列,**不要并发**同一 Profile;契约见 `development/SKILL_ACTION_RUNTIME.md`、`references/ACTIONS.md`。 +- 模板源仓库本身仅示范 `health` / `version` / `config-path`(sync);定制 RPA 技能后须自行增加 async action。 + ## 面向用户问答(LLM 规则) - 本文(`SKILL.md`)是 LLM / OpenClaw 平台读取的技能入口,**不是**用户市场说明。 @@ -83,7 +91,7 @@ python {baseDir}/scripts/main.py version 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`。 +5. 用户市场说明写入根 `README.md`;Agent 调用契约见 `references/CLI.md`、`references/SCHEMA.md`、`references/ACTIONS.md`;长任务 / 任务中心见 `development/SKILL_ACTION_RUNTIME.md`。 6. 同步修改 `scripts/util/constants.py` 中的 `SKILL_SLUG` / `SKILL_VERSION`。 7. 本地 SQLite 的中文表名/字段名、标准时间字段与字段顺序规范见 `references/SCHEMA.md`(元数据表 `_jiangchang_*` + `CREATE TABLE` 物理顺序)。 diff --git a/assets/README.md b/assets/README.md index 240724a..b48102e 100644 --- a/assets/README.md +++ b/assets/README.md @@ -1,10 +1,10 @@ # assets -- `actions.json`:Skill Action manifest 最小示例(**可选**;不需要宿主入口时可删除)。 +- `actions.json`:Skill Action manifest 最小示例(**可选**;不需要宿主入口时可删除)。模板要求每个 action 显式声明 `executionProfile`(`sync` \| `async`)。 - `examples/`:CLI 成功输出形状示例(虚构路径与数据)。 - `schemas/`:轻量 JSON Schema(`skill-actions.schema.json` 为**新技能严格规范**;`task-log-record.schema.json` 等)。 -`skill-actions.schema.json` 使用 `additionalProperties: false` 表达模板推荐契约;宿主运行时可能更宽松以兼容历史 manifest。`row`/`batch` placements 与 `bind`/`concurrency`/`locks` 为预留能力,当前模板示例不得使用。 +`skill-actions.schema.json` 使用 `additionalProperties: false` 表达模板推荐契约;宿主运行时可能更宽松以兼容历史 manifest。`row`/`batch` placements 与 `bind`/`concurrency`/`locks` 为预留能力,当前模板示例不得使用。RPA/长耗时 action 必须 `executionProfile: "async"`,见 [`development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md)。 - 用户市场说明见根目录 [`README.md`](../README.md) - Agent 调用/编排参考见 [`references/`](../references/)(含 [`ACTIONS.md`](../references/ACTIONS.md)) diff --git a/assets/actions.json b/assets/actions.json index c047984..3ac67db 100644 --- a/assets/actions.json +++ b/assets/actions.json @@ -5,8 +5,9 @@ { "id": "health", "label": "运行检查", - "description": "检查技能运行环境并返回结构化结果。", + "description": "检查技能运行环境并返回结构化结果,不启动浏览器。", "placements": ["skill-detail", "agent"], + "executionProfile": "sync", "entrypoint": { "type": "cli", "command": "health", @@ -18,6 +19,7 @@ "label": "查看版本", "description": "返回技能版本和机器标识。", "placements": ["skill-detail"], + "executionProfile": "sync", "entrypoint": { "type": "cli", "command": "version", @@ -29,6 +31,7 @@ "label": "查看配置位置", "description": "返回技能配置文件位置。", "placements": ["skill-detail"], + "executionProfile": "sync", "entrypoint": { "type": "cli", "command": "config-path", diff --git a/assets/schemas/skill-actions.schema.json b/assets/schemas/skill-actions.schema.json index 38d37a4..43cf793 100644 --- a/assets/schemas/skill-actions.schema.json +++ b/assets/schemas/skill-actions.schema.json @@ -120,7 +120,12 @@ }, "entrypoint": { "$ref": "#/$defs/entrypoint" }, "inputSchema": { "$ref": "#/$defs/inputSchema" }, - "confirmation": { "$ref": "#/$defs/confirmation" } + "confirmation": { "$ref": "#/$defs/confirmation" }, + "executionProfile": { + "type": "string", + "enum": ["sync", "async"], + "description": "sync=宿主内联等待结果(约 30s);async=后台 Job 进任务中心。模板要求显式写出;RPA/长耗时必须 async。" + } }, "description": "模板 Phase 1 不包含 bind/concurrency/locks 等预留能力字段;宿主类型可能定义这些字段,但新技能不得依赖" } diff --git a/development/DEVELOPMENT.md b/development/DEVELOPMENT.md index 0b86dea..04ef044 100644 --- a/development/DEVELOPMENT.md +++ b/development/DEVELOPMENT.md @@ -525,9 +525,11 @@ metadata: 若技能需要出现在宿主**数据管理按钮**、**技能详情**、**定时任务**或 **Agent 直接调用**中,提供 `assets/actions.json`。 -- 契约说明见 [`references/ACTIONS.md`](../references/ACTIONS.md) +- **运行时金标准**见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md)(sync/async、任务中心、Agent 禁令) +- 字段契约见 [`references/ACTIONS.md`](../references/ACTIONS.md) - JSON Schema 见 [`assets/schemas/skill-actions.schema.json`](../assets/schemas/skill-actions.schema.json) -- 模板最小示例仅暴露 `health` / `version` / `config-path` 三个 CLI 子命令 +- 模板最小示例仅暴露 `health` / `version` / `config-path`(均显式 `executionProfile: "sync"`) +- 含浏览器 RPA / 长耗时的业务技能:**必须**增加至少一条 `"executionProfile": "async"` 且 `placements` 含 `agent` 的 action(`POLICY-SKILL-ACTION-001`) - 没有宿主直接操作需求时,**可以删除** `actions.json`;删除后勿保留失效引用 - 复制为新技能后,scaffold 会自动将 `your-skill-slug` 与 `your_skill_slug` 替换为新 slug(后者用于 `LOG_LOGGER_NAME` 等下划线形式);action 业务命令由技能作者自行调整 @@ -539,6 +541,25 @@ python {skillRoot}/scripts/main.py CLI 必须提供稳定退出码、结构化成功输出,以及 `ERROR::` / `HINT:` 错误契约(详见 `references/ACTIONS.md`)。 +### 编排范式:单内核 + 多入口 + +长任务 / RPA 技能推荐拆分(勿把 `job_context` 塞进纯业务内核): + +```text +execute_single_(...) # 鉴权 + 业务 + task_log;无 job_context +cmd_run(...) # 单条:job_context + execute_single + finish +cmd_run_pick(N, ...) # 批量顺序:job_context + for-loop + emit(progress) + finish(partial|success|failed) +cmd_stats() # 只读 sync:JSON,无 job_context +``` + +- 批量数量用 **`pickCount` / `--pick N` 参数**(任意正整数),不写死条数。 +- **顺序执行、不并发**同一浏览器 Profile / account lease。 +- 详情见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) §5–§6。 + +### 队列 + pick(可选) + +有「待处理列表」时:队列表 `pending → processing → success/failed`;数据管理录入或 `import-*` 入库;`run --pick N` 消费;失败用 `reset` 回 `pending`(不自动重试)。Manifest 上配置 `*-run-pick`(async,toolbar/cron/agent)。 + ## 12. 如何接兄弟技能 如果 skill 要依赖兄弟 skill,不要在业务代码里写死绝对路径。 diff --git a/development/LOGGING.md b/development/LOGGING.md index 7f842a7..0cea01d 100644 --- a/development/LOGGING.md +++ b/development/LOGGING.md @@ -96,6 +96,8 @@ OpenClaw 技能常在客户电脑上异步执行,且大量依赖 RPA、浏览 2. 关键步骤 `emit(...)` 或 `@rpa_step` / `video.add_step` 3. 每个出口 `finish(status=..., message=..., skill=SKILL_SLUG, **fields)` — **不要**手写多行 JSON 结果 +**单内核多入口**:`execute_single_*` 不含 `job_context`;仅 `cmd_run` / `cmd_run_pick` 包裹。批量 pick 循环内 `checkpoint()` + `emit(..., progress=...)`;部分失败用 `finish(status="partial")`。完整范式见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md)。 + RPA 等待用 `interruptible_sleep`,**不要**裸 `asyncio.sleep`(否则暂停响应滞后)。 暂停在**步骤边界**生效(当前 `@rpa_step` / `emit` 执行完后、下一步开始前),与影刀「当前指令完成后暂停」一致。 @@ -306,4 +308,5 @@ def cmd_run(target=None, input_id=None): | [`CLI.md`](../references/CLI.md) | `logs` / `log-get` 命令与手工排查入口 | | [`CONFIG.md`](CONFIG.md) | 敏感配置不进日志;`.env` 边界 | | [`DEVELOPMENT.md`](DEVELOPMENT.md) | `service/` 层日志职责与发布前检查清单 | +| [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) | 任务中心 / async Job 与 `job_context` 边界 | | [`POLICY_MATRIX.md`](POLICY_MATRIX.md) | 可自动检测的 logging hard policy 索引 | diff --git a/development/POLICY_MATRIX.md b/development/POLICY_MATRIX.md index a7e56b9..ee20121 100644 --- a/development/POLICY_MATRIX.md +++ b/development/POLICY_MATRIX.md @@ -28,6 +28,7 @@ | POLICY-CONTROL-002 | `task_service.py` 须使用 `job_context(` 与 `finish(` | development/LOGGING.md §2.5、§7 | hard | 扫描 `scripts/service/task_service.py` | `tests/test_development_policy_guard.py::TestPolicyControl002` | | POLICY-CONTROL-003 | `scripts/service/*.py` 不得裸 `asyncio.sleep`(RPA 等待须 `interruptible_sleep`) | development/RPA.md §0.1 | hard | 扫描 service 层 Python | `tests/test_development_policy_guard.py::TestPolicyControl003` | | POLICY-DATA-PATH-001 | 技能拥有路径须经 `resolve_data_path` / `get_*_dir`;`.env.example` 禁止 `./` CWD 相对路径;业务代码禁止 `abspath(config.get(...))` | development/DATA_PATHS.md | hard | 扫描 `.env.example` 与 `scripts/**/*.py` | `tests/test_development_policy_guard.py::TestPolicyDataPath001` | +| POLICY-SKILL-ACTION-001 | 存在 `development/SKILL_ACTION_RUNTIME.md`;schema 含 `executionProfile`;业务技能(非模板占位 slug)若 service 含 `RpaVideoSession` 则 `actions.json` 须有 `executionProfile=async` 且 placements 含 `agent` 的 action | development/SKILL_ACTION_RUNTIME.md;references/ACTIONS.md | hard | 文档/schema/manifest 检查 | `tests/test_development_policy_guard.py::TestPolicySkillAction001` | --- diff --git a/development/README.md b/development/README.md index b33029f..d71f606 100644 --- a/development/README.md +++ b/development/README.md @@ -11,9 +11,10 @@ 7. [`RPA.md`](RPA.md) — 涉及浏览器 / 桌面 / 手机自动化时 8. [`CONFIG.md`](CONFIG.md) — `.env` 规范与 bootstrap 机制 9. [`DATA_PATHS.md`](DATA_PATHS.md) — 下载/导入/导出等本地文件路径标准(涉及文件读写时必读) -10. [`RUNTIME.md`](RUNTIME.md) — 共享 runtime、数据路径、发布打包与编码约定 +10. [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) — Skill Action sync/async、任务中心、Agent 禁令、队列 pick(涉及 RPA / 长任务 / 数据管理按钮 / Cron / Agent 时必读) +11. [`RUNTIME.md`](RUNTIME.md) — 共享 runtime、数据路径、发布打包与编码约定 脚手架与 Git 防串库:[`../tools/README.md`](../tools/README.md)(`scaffold_skill.ps1`)。 -Agent 调用契约见 [`../references/CLI.md`](../references/CLI.md)、[`../references/SCHEMA.md`](../references/SCHEMA.md)。 +Agent 调用契约见 [`../references/CLI.md`](../references/CLI.md)、[`../references/SCHEMA.md`](../references/SCHEMA.md)、[`../references/ACTIONS.md`](../references/ACTIONS.md)。 用户市场说明见根目录 [`README.md`](../README.md),不要写进本目录。 diff --git a/development/SKILL_ACTION_RUNTIME.md b/development/SKILL_ACTION_RUNTIME.md new file mode 100644 index 0000000..db5adab --- /dev/null +++ b/development/SKILL_ACTION_RUNTIME.md @@ -0,0 +1,162 @@ +# Skill Action Runtime(宿主任务中心契约) + +本文是匠厂宿主 **Skill Action / 任务中心** 的技能侧金标准。涉及浏览器 RPA、长耗时任务、数据管理按钮、定时任务或 Agent 直接调用时**必读**。 + +**边界:** + +| 文档 | 职责 | +|------|------| +| 本文 | sync/async、任务中心、Agent 禁令、编排范式、队列 pick | +| [`../references/ACTIONS.md`](../references/ACTIONS.md) | `actions.json` 字段、placements、inputSchema | +| [`LOGGING.md`](LOGGING.md) | `job_context` / `emit` / `finish` / Run Journal | +| [`DATA_PATHS.md`](DATA_PATHS.md) | 下载/导入等落盘路径 | + +宿主实现细节以匠厂 Host API 为准;技能侧只需保证 manifest 与 CLI 契约稳定。 + +--- + +## 1. 核心原则 + +1. **宿主管调度,技能管业务** — 不自行实现任务中心 / Job 轮询 / 后台守护进程。 +2. **长耗时 = async** — 开浏览器、RPA、HTTP 大文件下载等必须 `executionProfile: "async"`,进**任务中心**。 +3. **Agent 禁止 `exec` 直跑长任务 CLI** — 必须 `run_skill_action`;禁止 `exec python main.py run` + `process poll` 干等。 +4. **进度走 Journal** — `emit` / `video.add_step` → Run Journal;UI / Skill Run Card / 任务中心展示;**不靠 stdout 流**。 +5. **显式声明 `executionProfile`** — 宿主省略时默认 `async`;模板要求每个 action **写明** `sync` 或 `async`,避免歧义。 + +--- + +## 2. `executionProfile`:sync vs async + +| 值 | 宿主行为 | 超时 | 任务中心 | +|----|----------|------|----------| +| **`sync`** | 内联等待 CLI 结束,把结果直接返回调用方 | 默认约 **30s**(`SKILL_ACTION_SYNC_TIMEOUT_MS`) | ❌ 不进 | +| **`async`** | 后台 spawn Job,立即返回 `jobId` | Job 本身无默认墙钟上限(可暂停/停止) | ✅ 进入 | +| **省略** | 宿主视为 **async** | 同 async | ✅ | + +### 硬规则 + +| 场景 | 必须 | +|------|------| +| 浏览器 / Playwright / RPA 主路径 | `async` + 建议 `confirmation` | +| 单次可能超过 ~20s 的网络/下载 | `async` | +| `health` / `version` / `config-path` / 纯统计 / 快速导入 | `sync` | +| 数据管理 **toolbar** | **只放 async**(宿主前端对 sync 结果会报错) | + +### Agent 路径启发式(宿主行为,技能作者需知) + +- 显式写了 `executionProfile` → 以显式为准。 +- 未写时:描述含「统计 / 检查 / 不启动浏览器」等只读语义 → Agent 侧可当 **sync**;有 `confirmation` 的副作用 → **async**。 +- **模板要求一律显式写出**,不要依赖启发式。 + +--- + +## 3. 三入口如何感知 sync / async + +三个入口统一走 `POST /api/skill-actions/run`,带 `source.kind`: + +| 入口 | `source.kind` | 推荐 | +|------|---------------|------| +| **Agent** | `agent` | `run_skill_action`;长任务 async,秒回 jobId | +| **数据管理** | `data-management` | toolbar 按钮触发 async action | +| **定时任务** | `cron` | Cron 配置 async action + `pickCount` 等参数 | + +技能 **不要** 在 Python 里为三个入口写三套分支;宿主读 manifest 分流。 + +--- + +## 4. Action 分类与 placements 矩阵 + +| Action 类型 | `executionProfile` | 典型 placements | `confirmation` | +|-------------|--------------------|-----------------|----------------| +| 运行检查 / 版本 / 配置路径 | `sync` | `skill-detail`, `agent`(只读) | 无 | +| 库 / 队列统计 | `sync` | `skill-detail`, `agent` | 无 | +| 数据导入 / 失败重置 | `sync` | `skill-detail`, `agent` | 可选 | +| **单条**副作用 RPA | `async` | `agent`, `skill-detail` | **必须** | +| **批量顺序** RPA(pick) | `async` | **`toolbar`**, `cron`, `agent` | **必须** | + +禁忌: + +- 高副作用 async action 不要默认只挂 `toolbar`(应有 confirmation)。 +- 含 `sensitive` 参数的 action 不要挂 `toolbar` / `cron`(除非有明确安全设计)。 +- 不要为了「按钮多」暴露所有 CLI 子命令。 + +--- + +## 5. 技能内编排范式(单内核 + 多入口) + +推荐结构(对齐 create-account / download-video 金标准): + +```text +execute_single_(...) # 纯业务:鉴权 + 核心逻辑 + task_log;不含 job_context +cmd_run(...) # 单条:job_context + execute_single + finish +cmd_run_pick(N, ...) # 批量:job_context + for-loop execute_single + emit(progress) + finish +cmd_stats() # 只读 sync:JSON stdout,无 job_context +``` + +要点: + +1. **`job_context` 只包编排层**(`cmd_run` / `cmd_run_pick`),不包 `execute_single_*`。 +2. 批量 **顺序、不并发**;数量由参数 `pickCount` / `--pick N` 传入(不写死 100/200)。 +3. pick 循环内:`checkpoint()` + `emit("正在处理 {i}/{total}", progress={...})`。 +4. 结束:全成功 `finish(success)`;部分失败 `finish(partial)`;全失败 `finish(failed)`。 +5. 同一浏览器 Profile / account lease:**禁止并发**;顺序执行 + lease 即可。 + +详见 [`LOGGING.md`](LOGGING.md) §2.5 与 `scripts/service/task_service.py` 示范。 + +--- + +## 6. 队列 + pick(可选,Type B 批量) + +有「待处理列表」的技能(账号开通、视频下载、帖子发布等)建议: + +| 能力 | 约定 | +|------|------| +| 队列表 | `status`: `pending` → `processing` → `success` / `failed` | +| 入库 | 数据管理 CRUD,或 `import-*`(相对 `{skill_data}/imports/`,见 [`DATA_PATHS.md`](DATA_PATHS.md)) | +| 消费 | `run --pick N`,默认仅 `pending` | +| 重试 | `reset` 或数据管理改 `pending`;**failed 不要自动重试** | +| 批次 | 可选 `batch_id`,便于筛选与任务中心对照 | +| Manifest | `*-run-pick`:`async`,`placements` 含 `toolbar` / `cron` / `agent`,`inputSchema.pickCount` | + +批量有两种合理形态(都是**顺序**): + +| 形态 | 任务中心条数 | 适用 | +|------|-------------|------| +| 一次 `run-pick` 处理 N 条 | **1** 条 Job | 同类 RPA、同 Profile、定时消费队列 | +| Agent/UI 触发 N 次单条 `run` | **N** 条 Job | 每条独立追踪 / 独立重试 | + +默认优先 **一次 pick、一条 Job**(与 create-account `--pick`、download `--pick` 一致)。 + +--- + +## 7. Agent / SKILL.md 契约(复制后必改) + +业务技能 `SKILL.md` 应明确: + +1. 副作用任务必须通过宿主 **Skill Action**(`run_skill_action`)调用。 +2. **禁止**对长任务使用 `exec` + `process poll`。 +3. 单条 vs 批量 action id 与参数(如 `url` / `pickCount`)。 +4. 长任务进度看**任务中心** / Skill Run Card,不等 CLI stdout。 + +模板源仓库的 `SKILL.md` 含占位段落;scaffold 后按业务改写。 + +--- + +## 8. Policy + +自动检测索引见 [`POLICY_MATRIX.md`](POLICY_MATRIX.md): + +- **POLICY-SKILL-ACTION-001**:`SKILL_ACTION_RUNTIME.md` 存在;schema 含 `executionProfile`;有 RPA 的业务技能必须声明 async agent action。 + +--- + +## 9. 与宿主 API(参考) + +| API | 用途 | +|-----|------| +| `GET /api/skill-actions/{skillSlug}` | 读 manifest | +| `POST /api/skill-actions/run` | 执行 action(sync inline / async job) | +| `GET /api/skill-actions/jobs` | 任务中心列表 | +| `GET /api/skill-actions/jobs/{jobId}` | 单 Job 详情 / 事件 | + +Agent 工具(插件):`list_skill_actions` → `get_skill_action` → `run_skill_action`;正常对话**不要**轮询 `get_skill_action_job`(交给 UI / 任务中心)。 diff --git a/references/ACTIONS.md b/references/ACTIONS.md index 3ded582..c532f2e 100644 --- a/references/ACTIONS.md +++ b/references/ACTIONS.md @@ -2,6 +2,8 @@ 本文件说明 `assets/actions.json` 与 `scripts/main.py` CLI 的对应关系,供 Agent 编排与宿主 **Skill Action Runtime** 使用。 +**权威运行时契约(sync/async、任务中心、编排范式)见 [`../development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md)。** + **边界:** 用户市场说明见根目录 [`README.md`](../README.md);CLI 参数与错误码细节见 [`CLI.md`](CLI.md)。 ## 模板严格规范 vs 宿主向后兼容 @@ -21,6 +23,7 @@ Action 是**可选能力**: - 没有宿主直接操作需求的技能,**可以不提供** `actions.json`。 - 需要出现在**数据管理按钮**、**技能详情**、**定时任务**或 **Agent 直接调用**中的技能,**必须**提供 `assets/actions.json`。 +- **含浏览器 RPA / 长耗时任务**的技能:**必须**提供至少一条 `executionProfile: "async"` 且 `placements` 含 `agent` 的 action(见 `POLICY-SKILL-ACTION-001`)。 ## 文件位置 @@ -49,11 +52,12 @@ Action 是**可选能力**: | 层级 | 稳定字段 / 能力 | |------|----------------| | 顶层 | `schemaVersion`、`skill`、`actions` | -| action | `id`、`label`、`description`、`placements`、`entrypoint`;可选 `inputSchema`、`confirmation` | +| action | `id`、`label`、`description`、`placements`、`entrypoint`;可选 `inputSchema`、`confirmation`、**`executionProfile`** | | entrypoint | `type = cli`、`command`、`args`(**必填** JSON 数组,项为 `string` / `number` / `boolean`) | | placements | `toolbar`、`skill-detail`、`agent`、`cron` | | inputSchema | 根 `type: object`;properties 使用 `string` / `number` / `boolean`;支持 `enum`、`default`、`required`、`sensitive` | | confirmation | `{ "message": "..." }` | +| **executionProfile** | `"sync"` \| `"async"`(**模板要求显式写出**) | ## 预留能力(当前不可当作已实现能力) @@ -82,6 +86,18 @@ python {skill_root}/scripts/main.py 完整子命令定义见 [`scripts/cli/app.py`](../scripts/cli/app.py)。 +### `executionProfile`(sync / async) + +| 值 | 宿主行为 | 任务中心 | +|----|----------|----------| +| `sync` | 内联等待 CLI(默认约 30s) | 不进 | +| `async` | 后台 Job,立即返回 `jobId` | **进入** | +| 省略 | 宿主默认按 **async** | 进入 | + +- **RPA / 浏览器 / 长耗时** → 必须 `"async"`,并建议加 `confirmation`。 +- **数据管理 toolbar** → 只挂 **async** action。 +- 完整规则见 [`../development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md)。 + ### CLI 输出契约 技能 CLI 必须: @@ -108,9 +124,9 @@ HINT: | 值 | 含义 | |----|------| -| `toolbar` | 数据管理第一排技能按钮 | +| `toolbar` | 数据管理第一排技能按钮(**仅 async**) | | `skill-detail` | 技能详情或技能市场详情页 | -| `agent` | Agent 可直接调用 | +| `agent` | Agent 可直接调用(`run_skill_action`) | | `cron` | 定时任务可直接调用 | **预留能力(当前宿主可能尚未完整支持,模板示例请勿使用):** @@ -120,7 +136,15 @@ HINT: | `row` | 行内操作(未来扩展) | | `batch` | 批量操作(未来扩展) | -可以在 manifest 中作为未来扩展值定义,但不要让技能作者误以为 `row` / `batch` 已经完整可用。 +### Action 类型 × placements 矩阵 + +| Action 类型 | executionProfile | 典型 placements | confirmation | +|-------------|------------------|-----------------|--------------| +| health / version / config-path | sync | skill-detail, agent | 无 | +| 统计 / 只读查询 | sync | skill-detail, agent | 无 | +| 导入 / 重置 | sync | skill-detail, agent | 可选 | +| 单条副作用 RPA | async | agent, skill-detail | **必须** | +| 批量顺序 pick RPA | async | **toolbar**, cron, agent | **必须** | ## inputSchema 与参数表单 @@ -152,6 +176,20 @@ HINT: } ``` +批量 pick 参数约定(有队列表时): + +```json +{ + "pickCount": { + "type": "number", + "title": "处理数量", + "description": "从队列选取 pending 记录条数", + "default": 1, + "minimum": 1 + } +} +``` + 约定: - `boolean` 在宿主中渲染为开关 @@ -166,23 +204,32 @@ HINT: ## 风险与入口配置 - 查询、诊断、版本检查可以放到 `skill-detail` 或 `agent` -- 低风险、常用动作才考虑放 `toolbar` +- 低风险、常用动作才考虑放 `toolbar`(且须为 async) - 会修改本地数据、访问外部系统、执行长时间任务的动作必须谨慎配置 - 有副作用的动作应添加 `confirmation.message` - 高风险动作不应默认出现在 `toolbar` - 含 `sensitive` 参数的 action 不应放入 `toolbar` 或 `cron`,除非有明确安全设计 - 不要为了「按钮多」而暴露所有 CLI 子命令 - 一个 CLI 命令可以拆成多个语义清晰的 action,但每个 action 必须有明确语义 +- **Agent 必须用 `run_skill_action`,禁止 `exec` 直跑长任务 CLI**(见 `SKILL_ACTION_RUNTIME.md`) ## 模板示例 action 一览 -| id | CLI | placements | 说明 | -|----|-----|------------|------| -| `health` | `health` | skill-detail, agent | 运行环境检查 | -| `version` | `version` | skill-detail | 版本 JSON | -| `config-path` | `config-path` | skill-detail | 配置路径 JSON | +| id | CLI | executionProfile | placements | 说明 | +|----|-----|------------------|------------|------| +| `health` | `health` | sync | skill-detail, agent | 运行环境检查 | +| `version` | `version` | sync | skill-detail | 版本 JSON | +| `config-path` | `config-path` | sync | skill-detail | 配置路径 JSON | -复制为新技能后,请按业务需要增删 action;不需要宿主入口时可删除整个 `actions.json`。 +业务技能复制后,按需增加例如: + +| 模式 | id 示例 | executionProfile | placements | +|------|---------|------------------|------------| +| 单条 RPA | `your-run` | async | agent, skill-detail | +| 队列 pick | `your-run-pick` | async | toolbar, cron, agent | +| 统计 | `your-stats` | sync | skill-detail, agent | + +不需要宿主入口时可删除整个 `actions.json`(无 RPA 长任务时)。 ## 与宿主 API(参考) diff --git a/references/README.md b/references/README.md index 3e864f6..6a287b5 100644 --- a/references/README.md +++ b/references/README.md @@ -15,6 +15,7 @@ | [`CLI.md`](CLI.md) | 命令行入口、参数与调用契约 | | [`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 编程代理) | -按需阅读:编排任务时优先 `CLI.md`;解析结构化字段时读 `SCHEMA.md`;配置宿主按钮/定时任务/Agent 入口时读 `ACTIONS.md`;运行时环境细节见 `../development/RUNTIME.md`。 +按需阅读:编排任务时优先 `CLI.md`;解析结构化字段时读 `SCHEMA.md`;配置宿主按钮/定时任务/Agent 入口时读 `ACTIONS.md` + `SKILL_ACTION_RUNTIME.md`;运行时环境细节见 `../development/RUNTIME.md`。 diff --git a/scripts/util/constants.py b/scripts/util/constants.py index 2c23c41..e7853e8 100644 --- a/scripts/util/constants.py +++ b/scripts/util/constants.py @@ -1,6 +1,6 @@ """技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。""" SKILL_SLUG = "your-skill-slug" -SKILL_VERSION = "1.0.39" +SKILL_VERSION = "1.0.40" LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug" PLATFORM_KIT_MIN_VERSION = "1.2.0" diff --git a/tests/test_development_policy_guard.py b/tests/test_development_policy_guard.py index 09cd295..87bf0f1 100644 --- a/tests/test_development_policy_guard.py +++ b/tests/test_development_policy_guard.py @@ -45,6 +45,7 @@ POLICY_IDS = ( "POLICY-CONTROL-002", "POLICY-CONTROL-003", "POLICY-DATA-PATH-001", + "POLICY-SKILL-ACTION-001", ) STRUCTURE_PATHS = ( @@ -734,6 +735,142 @@ class TestPolicyDataPath001(unittest.TestCase): ) +_TEMPLATE_PLACEHOLDER_SLUG = "your-skill-slug" +_RPA_SESSION_TOKENS = ("RpaVideoSession",) + + +def _service_has_rpa_video_session(skill_root: str) -> bool: + service_dir = os.path.join(skill_root, "scripts", "service") + if not os.path.isdir(service_dir): + return False + for rel in _walk_files(skill_root, "scripts/service", suffix=".py"): + text = _read_text(os.path.join(skill_root, rel)) + if any(token in text for token in _RPA_SESSION_TOKENS): + return True + return False + + +def _load_actions_json(skill_root: str) -> dict | None: + path = os.path.join(skill_root, "assets", "actions.json") + if not os.path.isfile(path): + return None + import json + + with open(path, encoding="utf-8") as f: + return json.load(f) + + +class TestPolicySkillAction001(unittest.TestCase): + def test_skill_action_runtime_doc_exists(self) -> None: + path = os.path.join(get_skill_root(), "development", "SKILL_ACTION_RUNTIME.md") + self.assertTrue( + os.path.isfile(path), + msg=_policy_msg( + "POLICY-SKILL-ACTION-001", + "development/SKILL_ACTION_RUNTIME.md", + "file missing", + ), + ) + text = _read_text(path) + for marker in ("executionProfile", "async", "任务中心", "run_skill_action"): + self.assertIn( + marker, + text, + msg=_policy_msg( + "POLICY-SKILL-ACTION-001", + "development/SKILL_ACTION_RUNTIME.md", + f"missing marker {marker!r}", + ), + ) + + def test_schema_defines_execution_profile(self) -> None: + import json + + path = os.path.join(get_skill_root(), "assets", "schemas", "skill-actions.schema.json") + self.assertTrue(os.path.isfile(path)) + with open(path, encoding="utf-8") as f: + schema = json.load(f) + action_props = schema.get("$defs", {}).get("action", {}).get("properties", {}) + profile = action_props.get("executionProfile") + self.assertIsInstance( + profile, + dict, + msg=_policy_msg( + "POLICY-SKILL-ACTION-001", + "assets/schemas/skill-actions.schema.json", + "action.properties.executionProfile missing", + ), + ) + self.assertEqual(set(profile.get("enum") or []), {"sync", "async"}) + + def test_actions_md_documents_execution_profile(self) -> None: + path = os.path.join(get_skill_root(), "references", "ACTIONS.md") + text = _read_text(path) + for marker in ("executionProfile", "async", "任务中心", "SKILL_ACTION_RUNTIME"): + self.assertIn( + marker, + text, + msg=_policy_msg( + "POLICY-SKILL-ACTION-001", + "references/ACTIONS.md", + f"missing marker {marker!r}", + ), + ) + + def test_actions_json_actions_declare_execution_profile(self) -> None: + skill_root = get_skill_root() + manifest = _load_actions_json(skill_root) + if manifest is None: + return + offenders: list[str] = [] + for action in manifest.get("actions") or []: + action_id = action.get("id", "?") + profile = action.get("executionProfile") + if profile not in ("sync", "async"): + offenders.append(f"{action_id}: executionProfile={profile!r}") + self.assertEqual( + offenders, + [], + msg=_policy_msg( + "POLICY-SKILL-ACTION-001", + "assets/actions.json", + "every action must set executionProfile to sync|async:\n" + "\n".join(offenders), + ), + ) + + def test_business_rpa_skill_has_async_agent_action(self) -> None: + from util.constants import SKILL_SLUG + + skill_root = get_skill_root() + if SKILL_SLUG == _TEMPLATE_PLACEHOLDER_SLUG: + self.skipTest("template placeholder slug skips business RPA action check") + if not _service_has_rpa_video_session(skill_root): + self.skipTest("no RpaVideoSession in scripts/service") + manifest = _load_actions_json(skill_root) + self.assertIsNotNone( + manifest, + msg=_policy_msg( + "POLICY-SKILL-ACTION-001", + "assets/actions.json", + "RPA skill must provide assets/actions.json", + ), + ) + found = False + for action in manifest.get("actions") or []: + placements = set(action.get("placements") or []) + if action.get("executionProfile") == "async" and "agent" in placements: + found = True + break + self.assertTrue( + found, + msg=_policy_msg( + "POLICY-SKILL-ACTION-001", + "assets/actions.json", + "RPA business skill needs at least one action with executionProfile=async and placements containing agent", + ), + ) + + class TestPolicyDocs001(unittest.TestCase): def test_policy_matrix_exists_and_lists_policy_ids(self) -> None: skill_root = get_skill_root()