Compare commits

...

5 Commits

Author SHA1 Message Date
4ed49495cf chore: default OPENCLAW_TEST_TARGET to real_rpa; host UX acceptance
All checks were successful
技能自动化发布 / release (push) Successful in 9s
2026-07-20 11:15:20 +08:00
8101b5ac01 docs: add extensible SHARED_REPOS catalog for public commons
All checks were successful
技能自动化发布 / release (push) Successful in 11s
2026-07-20 10:30:36 +08:00
c15854c20d docs: align host placements (row/batch), Gitea clone, developer_ids QA
All checks were successful
技能自动化发布 / release (push) Successful in 16s
2026-07-20 09:54:44 +08:00
8570d5803d docs(development): clarify onboarding, developer_ids self-test, host multi-entry QA
All checks were successful
技能自动化发布 / release (push) Successful in 20s
2026-07-20 09:20:38 +08:00
1ff70e26a5 Release v1.0.51: precipitate ensure-web, config empty-value, and login-required standards
All checks were successful
技能自动化发布 / release (push) Successful in 7s
2026-07-18 16:30:06 +08:00
34 changed files with 809 additions and 318 deletions

View File

@@ -1,11 +1,14 @@
# 运行模式:决定本技能用模拟数据还是真实浏览器/接口去执行 # 运行模式:决定本技能用模拟数据还是真实浏览器/接口去执行
OPENCLAW_TEST_TARGET=mock # 可选mock=本地模拟simulator_rpa=仿真站点real_api=真实接口real_rpa=真实浏览器;模板默认 mock日常正式使用请改为 real_rpa OPENCLAW_TEST_TARGET=real_rpa # 可选mock=本地模拟simulator_rpa=仿真站点real_api=真实接口real_rpa=真实浏览器;模板默认真实浏览器;本地单测/CI 请用 mock
# 目标网站地址:技能要访问的网站或服务地址 # 目标网站地址:技能要访问的网站或服务地址
TARGET_BASE_URL=https://sandbox.jc2009.com # 填写完整网址(含 https://);换环境时再改,一般保持默认即可 TARGET_BASE_URL=https://sandbox.jc2009.com # 填写完整网址(含 https://);换环境时再改,一般保持默认即可
# 默认登录账号:仅填写非敏感的账号标识(如工号/登录名) # 默认登录账号:仅填写非敏感的账号标识(如工号/登录名/手机号
DEFAULT_LOGIN_ID=04110001 # 只填账号标识,不要填密码;密码请在「账号管理」中登记 DEFAULT_LOGIN_ID= # 留空=按平台自动选用或登记;有值则按该标识筛选;不要填密码
# 默认账号编号:仅填账号管理里的纯数字主键(一般留空)
DEFAULT_ACCOUNT_ID= # 留空=自动;只填纯数字;手机号请填上方登录账号
# 是否显示浏览器窗口:网页自动化时是否弹出可见浏览器 # 是否显示浏览器窗口:网页自动化时是否弹出可见浏览器
OPENCLAW_BROWSER_HEADLESS=0 # 0=显示窗口必须便于介入与排查1=后台静默(勿作日常用法) OPENCLAW_BROWSER_HEADLESS=0 # 0=显示窗口必须便于介入与排查1=后台静默(勿作日常用法)

View File

@@ -9,6 +9,32 @@
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节 - 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段 - 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
## 1.0.55
- 模板 `.env.example` 默认 `OPENCLAW_TEST_TARGET=real_rpa`;四档保留;明确 mock 只保单测/CImock 通 ≠ 交活
- 验收要求 release 到匠厂并看用户体验(进度、失败提示、有头 RPA 等)
## 1.0.54
- 新增 `development/SHARED_REPOS.md`可扩展的公开公共仓目录skill-template / account-manager / jiangchang-platform-kit含类型、Gitea 地址、clone 与红线
- 开发入口与 RUNTIME / RPA / tools 文档挂接该目录,便于技术人员与 AI 定位公共依赖
## 1.0.53
- 宿主入口对齐:`row` / `batch` 升格为正式 placements支持 `bind.inputMapping``readOnly`;已声明入口均须自测(含技能详情直调)
- 开发文档:自建 Giteagit.jc2009.com与 scaffold 双来源;`developer_ids` 示例改为占位「你的用户ID」压缩 AI 工具章节
## 1.0.52
- 开发文档补齐全流程Gitea 克隆与本地模板复制两种拿仓方式;`developer_ids` 作为开发期自测通行证(下载安装注册 → 设置复制用户 ID
- 发布后宿主验收明确覆盖新建任务、数据管理、定时任务与任务中心;`development/README` 增加步骤索引
## 1.0.51
- 沉淀网页 RPA 首启标准:业务技能默认 `ensure-web`(有则用、无则登记);登记账号 ≠ 已登录目标站;租约全忙返回占用错误且不重复建号
- 明确会话依赖型采集须声明登录 `required` 并做登录门禁;配置留空 + 行尾说明不得当成假默认值;账号可用登录标识选择,展示优先登录名
- 最低共享库版本声明上调至 1.2.2(含配置空值解析修复);说明宿主按需升级、不因源上有更新自动追新
## 1.0.50 ## 1.0.50
- 沉淀网页 RPA 硬规则:必须先经账号管理取得 `profile_dir` 再开浏览器;生产有头;站点登录按平台约定(不全局强制);新增 `POLICY-RPA-004` 自动检测 - 沉淀网页 RPA 硬规则:必须先经账号管理取得 `profile_dir` 再开浏览器;生产有头;站点登录按平台约定(不全局强制);新增 `POLICY-RPA-004` 自动检测

View File

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

View File

@@ -1,12 +1,12 @@
--- ---
name: 技能开发模板(通用业务版) name: 技能开发模板(通用业务版)
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。" description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
version: 1.0.50 version: 1.0.55
author: 深圳匠厂科技有限公司 author: 深圳匠厂科技有限公司
metadata: metadata:
openclaw: openclaw:
slug: your-skill-slug slug: your-skill-slug
platform_kit_min_version: "1.2.0" platform_kit_min_version: "1.2.2"
emoji: "📦" emoji: "📦"
category: "通用" category: "通用"
developer_ids: developer_ids:
@@ -25,7 +25,7 @@ allowed-tools:
### 架构铁律(先读) ### 架构铁律(先读)
- **单业务内核、多入口**:数据管理 / 定时任务 / Agent / 技能详情 / 直接 CLI 必须复用同一 domain service禁止按入口复制业务逻辑禁止按 `source` 分叉业务行为。 - **单业务内核、多入口**:数据管理 / 定时任务 / Agent / 技能详情 / 直接 CLI 必须复用同一 domain service禁止按入口复制业务逻辑禁止按 `source` 分叉业务行为。
- **`placements`**:只决定 Action **出现在哪里**toolbar / cron / agent / skill-detail - **`placements`**:只决定 Action **出现在哪里**toolbar / row / batch / cron / agent / skill-detail
- **`bind.tables`**:只决定数据管理出现在哪些表;含 `toolbar` 时**必须**显式非空绑定。 - **`bind.tables`**:只决定数据管理出现在哪些表;含 `toolbar` 时**必须**显式非空绑定。
- **`executionProfile`**:只决定执行方式——`sync` 当场返回结果,`async` 进**任务中心**。与 placements / 入口**无关**。心智上多数为 sync仅长任务 / RPA 标 async宿主省略该字段时按 sync。每个 Action **仍必须**显式写 `sync``async` - **`executionProfile`**:只决定执行方式——`sync` 当场返回结果,`async` 进**任务中心**。与 placements / 入口**无关**。心智上多数为 sync仅长任务 / RPA 标 async宿主省略该字段时按 sync。每个 Action **仍必须**显式写 `sync``async`
- **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;特殊业务报告才另做 `export-*` - **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;特殊业务报告才另做 `export-*`
@@ -74,7 +74,7 @@ python {baseDir}/scripts/main.py config-path
python {baseDir}/scripts/main.py init-db python {baseDir}/scripts/main.py init-db
``` ```
- 公共库来自共享 venv 的 `jiangchang-platform-kit>=1.2.0`(包名路径 `jiangchang_skill_core`**不要 vendor** `scripts/jiangchang_skill_core/` - 公共库来自共享 venv 的 `jiangchang-platform-kit>=1.2.2`(包名路径 `jiangchang_skill_core`**不要 vendor** `scripts/jiangchang_skill_core/`
- 根目录 `requirements.txt` **只声明技能特有**依赖;不要写 `jiangchang-platform-kit` / `playwright` - 根目录 `requirements.txt` **只声明技能特有**依赖;不要写 `jiangchang-platform-kit` / `playwright`
- `metadata.openclaw.platform_kit_min_version` 是宿主兼容声明,**不是** pip 依赖。 - `metadata.openclaw.platform_kit_min_version` 是宿主兼容声明,**不是** pip 依赖。
- `health``collect_runtime_diagnostics` 做只读诊断。 - `health``collect_runtime_diagnostics` 做只读诊断。
@@ -150,6 +150,7 @@ python {baseDir}/scripts/main.py init-db
## 平台元数据 ## 平台元数据
- `metadata.openclaw.developer_ids`:技能发布后的默认开发者可见用户 ID 列表。 - `metadata.openclaw.developer_ids`:技能发布后的默认开发者可见用户 ID 列表ID 来自匠厂宿主「设置 → 用户信息」)
- 开发期技能常为不公开:不加本人 ID开发者自己也无法在市场安装自测。取 ID 与写入步骤见 `development/DEVELOPMENT.md` §6。
-`access_scope = 0`(不公开)时,平台会把 `developer_ids` 中的用户自动补写到 `skill_user_access` -`access_scope = 0`(不公开)时,平台会把 `developer_ids` 中的用户自动补写到 `skill_user_access`
- `developer_ids` 建议写为正整数数组;第一个 ID 会作为主开发者同步到 `skills.developer_id` - `developer_ids` 建议写为正整数数组;第一个 ID 会作为主开发者同步到 `skills.developer_id`

View File

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

View File

@@ -4,7 +4,7 @@
- `examples/`CLI 成功输出形状示例(虚构路径与数据)。 - `examples/`CLI 成功输出形状示例(虚构路径与数据)。
- `schemas/`:轻量 JSON Schema`skill-actions.schema.json` 为**新技能严格规范**`task-log-record.schema.json` 等)。 - `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` 正交——进任务中心只看 `async`,与入口无关。表级 Action 与 Agent 契约见 [`references/ACTIONS.md`](../references/ACTIONS.md);开发深度规范见 [`development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md)。 `skill-actions.schema.json` 使用 `additionalProperties: false`。正式支持 `bind.tables`toolbar Action 必填)与可选 `bind.inputMapping`placements 含 `toolbar` / `row` / `batch` / `cron` / `agent` / `skill-detail`(对齐匠厂宿主)。`concurrency`/`locks` 仍为预留。`placements``executionProfile` 正交——进任务中心只看 `async`,与入口无关。表级 Action 与 Agent 契约见 [`references/ACTIONS.md`](../references/ACTIONS.md);开发深度规范见 [`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) - 用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.md)
- Agent 调用/编排参考见 [`references/`](../references/)(含 [`ACTIONS.md`](../references/ACTIONS.md) - Agent 调用/编排参考见 [`references/`](../references/)(含 [`ACTIONS.md`](../references/ACTIONS.md)

View File

@@ -26,7 +26,7 @@
"placement": { "placement": {
"type": "string", "type": "string",
"enum": ["toolbar", "row", "batch", "cron", "agent", "skill-detail"], "enum": ["toolbar", "row", "batch", "cron", "agent", "skill-detail"],
"description": "稳定支持 toolbar/cron/agent/skill-detailrow/batch 为预留入口,新技能示例不得使用。placements 与 executionProfile 正交,不做条件限制。" "description": "稳定支持 toolbar/row/batch/cron/agent/skill-detail(对齐匠厂宿主)。placements 与 executionProfile 正交,不做条件限制。"
}, },
"scalarArg": { "scalarArg": {
"type": ["string", "number", "boolean"] "type": ["string", "number", "boolean"]
@@ -48,7 +48,11 @@
"type": "array", "type": "array",
"minItems": 1 "minItems": 1
}, },
"sensitive": { "type": "boolean" } "sensitive": { "type": "boolean" },
"readOnly": {
"type": "boolean",
"description": "宿主数据管理表单只读展示该字段(仍会随提交传入 entrypoint用于行内/勾选预填的主键等"
}
} }
}, },
"inputSchema": { "inputSchema": {
@@ -110,10 +114,15 @@
"pattern": "^[a-z][a-z0-9_]*$", "pattern": "^[a-z][a-z0-9_]*$",
"description": "英文 snake_case 业务表名;须真实存在于技能 SQLite / _jiangchang_tables" "description": "英文 snake_case 业务表名;须真实存在于技能 SQLite / _jiangchang_tables"
}, },
"description": "数据管理表级绑定:决定 toolbar Action 出现在哪些表。宿主兼容旧技能时,缺失 bind 可视为全表展示;新技能 toolbar Action 必须显式声明。" "description": "数据管理表级绑定:决定 toolbar/row/batch Action 出现在哪些表。宿主兼容旧技能时,缺失 bind 可视为全表展示;新技能 toolbar Action 必须显式声明。"
},
"inputMapping": {
"type": "object",
"additionalProperties": { "type": "string", "minLength": 1 },
"description": "可选。将 $row.$pk / $row.col / $selection.ids 等映射到 inputSchema 字段名,供 row/batch 预填。"
} }
}, },
"description": "Phase 1 仅沉淀 bind.tables不包含 selection/inputMapping/columns/row/concurrency/locks" "description": "表级绑定toolbar 必填。可选 inputMapping 供行内/批量预填。concurrency/locks 勿写入。"
}, },
"action": { "action": {
"type": "object", "type": "object",

View File

@@ -1,6 +1,6 @@
# 适配器标准:真实/仿真 × API/RPA 四档模式 # 适配器标准:真实/仿真 × API/RPA 四档模式
> 凡是"连接三方系统"ERP、CRM、SaaS、银行等的 skill都应采用本文的 **adapter 四档模式**。这样同一套业务逻辑可在不同档位间切换:开发用 mock、半集成用 simulator、上线用真实互不影响 > 凡是"连接三方系统"ERP、CRM、SaaS、银行等的 skill都应采用本文的 **adapter 四档模式**。同一套业务逻辑可在不同档位间切换**四档都保留**
## 为什么要分档 ## 为什么要分档
@@ -15,17 +15,17 @@
| 档位 | 含义 | 默认策略 | | 档位 | 含义 | 默认策略 |
|------|------|----------| |------|------|----------|
| **`mock`** | 纯内存或 fixture**默认单测/CI** | 模板 `.env.example` 默认 `OPENCLAW_TEST_TARGET=mock` | | **`mock`** | 纯内存或 fixture离线 | **单测 / CI 必过**mock 不通不要往下走 |
| **`simulator_rpa`** | 仿真站点或桌面仿真,可半集成 | 开发联调可选 | | **`simulator_rpa`** | 仿真站点或桌面仿真,可半集成 | 按业务场景选用 |
| **`real_api`** | 真实 API | 生产 / 集成测试显式设 `OPENCLAW_TEST_TARGET=real_api` | | **`real_api`** | 真实 API | 按业务场景选用(有官方接口时) |
| **`real_rpa`** | 真实浏览器/真实系统 | 生产 / 集成测试显式设 `OPENCLAW_TEST_TARGET=real_rpa` | | **`real_rpa`** | 真实浏览器/真实系统 | **模板 `.env.example` 默认**;业务自测与交活用此档 |
- **mock**:纯离线、不联网,给单测 / CI / 开发自测**保证可重复**。 - **mock**:纯离线、不联网,给单测 / CI**保证可重复**。**mock 通 ≠ 开发完成**。
- **simulator_rpa**:操作仿真平台(如 `sandbox.jc2009.com`跑端到端流程但不碰生产 - **simulator_rpa**:操作仿真平台(如 `sandbox.jc2009.com`按场景选用
- **real_api**:有官方接口时**首选**(最稳、最快、最易维护) - **real_api**:有官方接口时按场景选用
- **real_rpa**没有 API 只能操作生产界面,**风险最高、放最后** - **real_rpa**真实浏览器路径;模板默认,须真实跑通后再交活
> 推荐优先级:**real_api > simulator_rpa > real_rpa**mock 永远保留做 CI > **口径**:业务配置默认 `real_rpa`;单测/CI 用 `mock`。不得只 mock 通就交活;须 release 到匠厂宿主验收(含用户体验)
配置读取见 `CONFIG.md`**bootstrap 之后业务代码只通过 `config.get*()``OPENCLAW_TEST_TARGET` 等项**(进程 env > 用户 `.env` > `.env.example`)。 配置读取见 `CONFIG.md`**bootstrap 之后业务代码只通过 `config.get*()``OPENCLAW_TEST_TARGET` 等项**(进程 env > 用户 `.env` > `.env.example`)。
@@ -72,7 +72,7 @@ scripts/service/
from jiangchang_skill_core import config from jiangchang_skill_core import config
def get_adapter(): def get_adapter():
target = (config.get("OPENCLAW_TEST_TARGET") or "mock").lower() target = (config.get("OPENCLAW_TEST_TARGET") or "real_rpa").lower()
if target in ("unit", "mock"): if target in ("unit", "mock"):
return MockAdapter() return MockAdapter()
if target == "real_api": if target == "real_api":
@@ -100,8 +100,9 @@ def get_adapter():
2. **普通兄弟技能调用**,优先走统一 **`service.sibling_bridge`**`call_sibling_json`**不要**在 `task_service.py``task_rpa.py` 等业务流程文件中到处散落 `subprocess.run` 2. **普通兄弟技能调用**,优先走统一 **`service.sibling_bridge`**`call_sibling_json`**不要**在 `task_service.py``task_rpa.py` 等业务流程文件中到处散落 `subprocess.run`
3. **account-manager 账号/租约能力**是例外:可参考 `examples/real_browser_rpa/scripts/service/account_client.py` 与 `examples/simulator_browser_rpa/scripts/service/account_client.py`,封装为**单一** `account_client.py`;允许在该文件内部集中通过 subprocess 调 account-manager CLI。**禁止** import `rpa_helpers` 等 account-manager 内部模块;`simulator_rpa``real_rpa` 均走同一 `pick_web_account` / `release_lease` 模式。 3. **account-manager 账号/租约能力**是例外:可参考 `examples/real_browser_rpa/scripts/service/account_client.py` 与 `examples/simulator_browser_rpa/scripts/service/account_client.py`,封装为**单一** `account_client.py`;允许在该文件内部集中通过 subprocess 调 account-manager CLI。**禁止** import `rpa_helpers` 等 account-manager 内部模块;`simulator_rpa``real_rpa` 均走同一 `pick_web_account` / `release_lease` 模式。
4. **不允许**直接 `import account-manager` 的内部 Python 模块(如 `service/``util/``db/`)。 4. **不允许**直接 `import account-manager` 的内部 Python 模块(如 `service/``util/``db/`)。
5. **pick lease 后必须 `finally release lease`**;进程被 kill 后可能残留 lease需在运维文档说明排查方式查 account-manager lease 列表 / 手动释放)。 5. **pick/ensure lease 后必须 `finally release lease`**;进程被 kill 后可能残留 lease需在运维文档说明排查方式查 account-manager lease 列表 / 手动释放)。
6. **网页 RPA 硬规则**(详见 [`RPA.md`](RPA.md) §0.2):必须先 `pick_web_account` 拿到 `profile_dir` 再开浏览器;拿不到则失败退出;生产有头;**是否站点登录**按平台约定,但**不等于**可以跳过 account-manager。 6. **网页 RPA 硬规则**(详见 [`RPA.md`](RPA.md) §0.2):必须先 `pick_web_account`(底层 **`ensure-web` / `pick-web --ensure`**拿到 `profile_dir` 再开浏览器;拿不到则失败退出;生产有头;**是否站点登录**按平台约定,但**不等于**可以跳过 account-manager。登记账号 ≠ 站点已登录。
7. **账号选择优先级**:纯数字 `DEFAULT_ACCOUNT_ID``get <id>``DEFAULT_LOGIN_ID` / 非数字标识 → `ensure-web --login-id`;皆空 → 按平台 `ensure-web`。人读展示优先 `login_id` / `account_label`,不要只用数字 id。多条匹配 → `ERROR:AMBIGUOUS_ACCOUNT`
```python ```python
# 普通兄弟技能 — 走 sibling_bridge # 普通兄弟技能 — 走 sibling_bridge
@@ -111,10 +112,10 @@ result = call_sibling_json("account-manager", ["list", "--limit", "10"])
``` ```
```python ```python
# account-manager 账号/租约 — 集中在 account_client.py # account-manager 账号/租约 — 集中在 account_client.pyensure-web
from service.account_client import pick_web_account, release_lease from service.account_client import pick_web_account, release_lease
account = pick_web_account(platform="target_platform") account = pick_web_account(platform="target_platform", login_id=None)
try: try:
... ...
finally: finally:

View File

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

View File

@@ -1,6 +1,6 @@
# 技能开发教程 # 技能开发教程
这份文档是给**技术人员**看的,目标不是解释概念,而是让你拿到 `skill-template` 后,可以**一步一步开发出一个新的 skill**。 这份文档是给**技术人员**看的,目标不是解释概念,而是让你拿到**业务技能仓库**后,可以**一步一步开发出一个新的 skill**。
本文默认你开发的是当前最常见的一类业务 skill 本文默认你开发的是当前最常见的一类业务 skill
@@ -13,77 +13,38 @@
## 推荐 AI 开发工具 ## 推荐 AI 开发工具
当前 skill 开发建议尽量配合 AI 编程工具使用。这样做不是为了替代技术人员,而是为了提升以下环节的效率: 建议用 AI 编程工具辅助搭目录、补样板、写测试与排错。团队宜统一 12 个主力,避免协作习惯发散。
- 搭建标准目录结构 | 场景 | 建议 |
- 生成样板代码 |------|------|
- 理解旧项目代码 | 一体化 IDE | Cursor 或 Windsurf |
- 批量补文档、注释和测试 | 终端深度代理 | Claude Code 或 Aider |
- 辅助排查报错与重构代码 | 国内生态 | Trae / 通义灵码 |
| VS Code 插件 | GitHub Copilot 或 Cline |
建议团队统一选择 1 到 2 个主力工具长期使用,避免每个人工具链差异太大,导致协作方式不一致 官方下载与文档以各产品站点为准;技能开发步骤以本文与同目录规范为准,不依赖某一家工具的专有流程
下面先列国外主流工具,再列国内主流工具。链接优先使用官方站点、官方文档或官方安装入口 开发中常用的**公开公共仓**skill-template / account-manager / jiangchang-platform-kit 等,可 `git clone` 学习,目录可扩展)见 [`SHARED_REPOS.md`](SHARED_REPOS.md)
### 国外主流工具
| 工具 | 类型 | 适合场景 | 官方入口 |
|------|------|----------|----------|
| Cursor | 独立 AI IDE | 代码编辑、Agent 开发、整仓理解 | <a href="https://www.cursor.com/" target="_blank" rel="noopener noreferrer">官网</a> / <a href="https://www.cursor.com/downloads" target="_blank" rel="noopener noreferrer">下载</a> |
| Windsurf | 独立 AI IDE | Agent 编程、项目生成、连续开发流 | <a href="https://docs.codeium.com/windsurf" target="_blank" rel="noopener noreferrer">文档</a> / <a href="https://windsurf.com/download" target="_blank" rel="noopener noreferrer">下载</a> |
| GitHub Copilot | IDE 插件 / 编程助手 | 日常补全、解释代码、生成函数、配合 VS Code 或 JetBrains 使用 | <a href="https://github.com/copilot" target="_blank" rel="noopener noreferrer">官网</a> |
| Claude Code | 终端 / IDE 编程代理 | 命令行开发、代码库分析、自动改代码、运行命令 | <a href="https://www.anthropic.com/claude-code" target="_blank" rel="noopener noreferrer">官网</a> / <a href="https://docs.anthropic.com/en/docs/claude-code/" target="_blank" rel="noopener noreferrer">文档</a> |
| Codex | 终端 / IDE / Web 编程代理 | OpenAI 官方编码代理,适合代码生成、理解、调试、评审 | <a href="https://developers.openai.com/codex/" target="_blank" rel="noopener noreferrer">官网</a> / <a href="https://developers.openai.com/codex/quickstart" target="_blank" rel="noopener noreferrer">快速开始</a> |
| Aider | 终端 AI 编程工具 | 已有代码仓库的增量开发、终端协作、快速提交 | <a href="https://www.aider.chat/" target="_blank" rel="noopener noreferrer">官网</a> / <a href="https://aider.chat/docs/" target="_blank" rel="noopener noreferrer">文档</a> |
| Cline | VS Code / JetBrains 插件 | 编辑器内 Agent 开发、命令执行、浏览器联动调试 | <a href="https://cline.bot/" target="_blank" rel="noopener noreferrer">官网</a> / <a href="https://docs.cline.bot/introduction/welcome" target="_blank" rel="noopener noreferrer">文档</a> / <a href="https://marketplace.visualstudio.com/items?itemName=saoudrizwan.claude-dev" target="_blank" rel="noopener noreferrer">VS Code 插件</a> |
### 国内主流工具
| 工具 | 类型 | 适合场景 | 官方入口 |
|------|------|----------|----------|
| Trae | 独立 AI IDE | AI 辅助写代码、项目搭建、对话式开发 | <a href="https://www.trae.ai/home" target="_blank" rel="noopener noreferrer">官网</a> / <a href="https://www.trae.ai/download" target="_blank" rel="noopener noreferrer">下载</a> |
| 通义灵码 | 独立 IDE / IDE 插件 | 国内团队日常编码、问答、补全、代码生成 | <a href="https://tongyi.aliyun.com/lingma/?channel=yy_AiBot" target="_blank" rel="noopener noreferrer">官网</a> / <a href="https://tongyi.aliyun.com/lingma/download" target="_blank" rel="noopener noreferrer">下载</a> |
| CodeGeeX | IDE 插件 / 开源助手 | 代码补全、生成、注释、跨语言辅助 | <a href="https://github.com/zai-org/CodeGeeX" target="_blank" rel="noopener noreferrer">GitHub</a> / <a href="https://marketplace.visualstudio.com/items?itemName=aminer.codegeex" target="_blank" rel="noopener noreferrer">VS Code 插件</a> |
| 腾讯 CodeBuddy | IDE 插件 | 代码补全、测试生成、智能问答、腾讯云开发体系协作 | <a href="https://www.codebuddy.ai/" target="_blank" rel="noopener noreferrer">官网</a> / <a href="https://www.tencentcloud.com/document/product/1256" target="_blank" rel="noopener noreferrer">文档</a> / <a href="https://marketplace.visualstudio.com/items?itemName=Tencent-Cloud.coding-copilot" target="_blank" rel="noopener noreferrer">VS Code 插件</a> |
| 百度文心快码Baidu Comate | IDE 插件 | 国内研发团队辅助编码、解释、测试、优化 | <a href="https://comate.baidu.com/zh" target="_blank" rel="noopener noreferrer">官网</a> |
### 选型建议
如果你们团队主要做这类 Python skill 开发,我建议这样选:
- 想要一体化最强体验:优先试 `Cursor``Windsurf`
- 想要命令行深度协作:优先试 `Claude Code``Aider`
- 想继续基于 VS Code 插件体系:优先试 `GitHub Copilot``Cline``通义灵码``CodeBuddy`
- 想优先使用国内生态与中文支持:优先试 `Trae``通义灵码``CodeGeeX``CodeBuddy``文心快码`
### 团队落地建议
为了减少培训成本,建议内部至少统一一套主工具方案:
- 国外方案:`Cursor` + `Claude Code`
- 国内方案:`Trae` + `通义灵码`
- VS Code 插件方案:`GitHub Copilot` + `Cline`
不建议每位技术人员完全自由发挥,否则后续在:
- 提示词写法
- 代码修改习惯
- 调试方式
- 提交节奏
这些方面会越来越不统一。
## 1. 先理解模板的定位 ## 1. 先理解模板的定位
`skill-template` 不是业务 skill它只是一个**新 skill 仓库模板**。 `skill-template` 不是业务 skill它只是一个**新 skill 仓库模板**。
你不应该直接在这个仓库里开发业务,而应该 你不应该直接在这个仓库里开发业务。新技能仓库有**两种合法来源**(详见 §4「第一步」
0. 按 [`NAMING.md`](NAMING.md) 确定 slug`{verb}-{noun-phrase}-{platform}` | 来源 | 适用情况 | 怎么做 |
1. **优先**用 [`tools/scaffold_skill.ps1`](../tools/scaffold_skill.ps1) 创建新目录(见 [`tools/README.md`](../tools/README.md) |------|----------|--------|
2. 在新目录内 `git init` 并绑定**本技能**远端(**不**保留模板 `.git` | **A. Gitea 克隆** | 项目经理已在自建 Gitea[https://git.jc2009.com/](https://git.jc2009.com/)**不** Gitea 官网)开好业务仓并给你权限 | `git clone` 到本地后按 [`REQUIREMENTS.md`](REQUIREMENTS.md) 开发 |
3. 把占位内容替换掉 | **B. 本地模板复制** | 你本机已有 `skill-template` 源码,要新建尚未灌仓的技能目录 | **优先** [`tools/scaffold_skill.ps1`](../tools/scaffold_skill.ps1)(见 [`tools/README.md`](../tools/README.md));也可手工复制但必须清掉模板 `.git` |
4. 再开始写业务逻辑
拿到仓库后的推荐顺序:
0. 按 [`NAMING.md`](NAMING.md) 确定 / 核对 slug`{verb}-{noun-phrase}-{platform}`
1. **先填写**本仓 [`REQUIREMENTS.md`](REQUIREMENTS.md)(范围与验收),再写业务代码
2. 来源 B在新目录内 `git init` 并绑定**本技能**远端(**不得**保留模板 `.git`
3. 把占位内容替换掉;**尽早**配置 `developer_ids`(见 §6开发期宿主自测必做
4. 再实现 `scripts/service/` 等业务逻辑
5. 本地测试通过 → `release.ps1` → Gitea CI → 匠厂多入口验收§15
> **Git 红线**:禁止资源管理器整文件夹复制后保留模板 `.git``git remote -v` 必须指向新技能仓库,不能仍是 skill-template。 > **Git 红线**:禁止资源管理器整文件夹复制后保留模板 `.git``git remote -v` 必须指向新技能仓库,不能仍是 skill-template。
@@ -164,7 +125,7 @@ scripts/
作用:常量、日志、路径、时间工具、通用帮助函数 作用:常量、日志、路径、时间工具、通用帮助函数
`util/logging_config.py``jiangchang_skill_core.unified_logging` 的**薄封装**,业务代码应通过它获取 logger `util/logging_config.py``jiangchang_skill_core.unified_logging` 的**薄封装**,业务代码应通过它获取 logger
公共能力config、logging、runtime_env、rpa、media_assets、video_session、runtime_diagnostics、activity/SRCP从共享 runtime 的 `jiangchang-platform-kit>=1.2.0` import**不得**在 `scripts/` 下保留 `jiangchang_skill_core/` 副本。 公共能力config、logging、runtime_env、rpa、media_assets、video_session、runtime_diagnostics、activity/SRCP从共享 runtime 的 `jiangchang-platform-kit>=1.2.2` import**不得**在 `scripts/` 下保留 `jiangchang_skill_core/` 副本。
## 3.2 开发 RPA 类 skill ## 3.2 开发 RPA 类 skill
@@ -192,7 +153,7 @@ scripts/
from jiangchang_skill_core.rpa import launch_persistent_browser, anti_detect, wait_for_captcha_pass from jiangchang_skill_core.rpa import launch_persistent_browser, anti_detect, wait_for_captcha_pass
``` ```
上述 import 来自宿主共享 runtime 安装的 `jiangchang-platform-kit`,不是技能目录副本。 上述 import 来自宿主共享 runtime 安装的 `jiangchang-platform-kit`,不是技能目录副本。
4. **mock 档必须离线可**`OPENCLAW_TEST_TARGET=mock`sim_rpa / real_* 按需单独测 4. **单测/CI 须 mock 离线可**;业务 `.env` 默认 `real_rpa`,须真实跑通。**不得只 mock 通就交活**;须 release 到匠厂宿主验收(见 §15含用户体验
5. **桌面/手机**:本期标准见 RPA.md 第 2/3 节,复用 `jiangchang_desktop_sdk` / `screencast`**不要在新 skill 里重复造包**(尚待实战验证)。 5. **桌面/手机**:本期标准见 RPA.md 第 2/3 节,复用 `jiangchang_desktop_sdk` / `screencast`**不要在新 skill 里重复造包**(尚待实战验证)。
--- ---
@@ -202,7 +163,7 @@ scripts/
技能根目录的 `requirements.txt` 是**标准文件**,用于声明本技能**特有** Python 三方依赖。 技能根目录的 `requirements.txt` 是**标准文件**,用于声明本技能**特有** Python 三方依赖。
- **公共依赖**`jiangchang-platform-kit`、`playwright`、config、runtime diagnostics、RPA 公共能力等)由**宿主共享 runtime** 提供,**不要**写入技能 `requirements.txt`。 - **公共依赖**`jiangchang-platform-kit`、`playwright`、config、runtime diagnostics、RPA 公共能力等)由**宿主共享 runtime** 提供,**不要**写入技能 `requirements.txt`。
- `SKILL.md` 的 `metadata.openclaw.platform_kit_min_version`(当前 `1.2.0`)是运行契约/兼容性声明,供宿主安装与启用时校验,**不是** pip 依赖声明。 - `SKILL.md` 的 `metadata.openclaw.platform_kit_min_version`(当前 `1.2.2`)是运行契约/兼容性声明,供宿主安装与启用时校验,**不是** pip 依赖声明。
- 匠厂宿主安装/更新技能后,会将技能 `requirements.txt` 安装到共享 venv`{JIANGCHANG_DATA_ROOT}/python-runtime/.venv`。 - 匠厂宿主安装/更新技能后,会将技能 `requirements.txt` 安装到共享 venv`{JIANGCHANG_DATA_ROOT}/python-runtime/.venv`。
- **不要**在业务代码中 `subprocess` / `pip install`;缺依赖由 `health` 报错,由宿主负责安装。 - **不要**在业务代码中 `subprocess` / `pip install`;缺依赖由 `health` 报错,由宿主负责安装。
- **版本约束尽量收窄**,降低多技能共享 venv 时的冲突风险。推荐范围写法: - **版本约束尽量收窄**,降低多技能共享 venv 时的冲突风险。推荐范围写法:
@@ -256,13 +217,26 @@ release workflow 会对 `scripts/` 下的 Python 源码做加密/打包。当前
下面这套顺序建议严格按步骤做,不要一上来就直接写 `service`。 下面这套顺序建议严格按步骤做,不要一上来就直接写 `service`。
### 第一步:复制模板并改目录名 ### 第一步:拿到新技能仓库(两种来源)
例如你要开发 `disburse-payroll-icbc` 一类领域 skill目录名 = slug 例如你要开发 `disburse-payroll-icbc` 一类领域 skill目录名 = slug。下列两种来源**同等合法**,按你实际拿到仓库的方式选一条。
#### 推荐方式(首选 #### 来源 A从 Gitea 克隆(项目经理已开仓
在 **skill-template 仓库根目录**执行: 1. 确认项目经理已在自建 Gitea[https://git.jc2009.com/](https://git.jc2009.com/),公司基于开源 Gitea 自建的代码仓库,**不要**访问 Gitea 官网当作业务仓地址)创建**本技能**仓库,并给你拉取/推送权限。
2. 克隆到本地(示例):
```powershell
git clone https://git.jc2009.com/<org>/<your-skill-slug>.git
cd <your-skill-slug>
git remote -v # 确认 origin 指向本技能仓,不是 skill-template
```
3. 若仓内已是从本模板 scaffold 好的结构,直接进入「第二步」与 [`REQUIREMENTS.md`](REQUIREMENTS.md)**不要**再整仓复制 `skill-template` 覆盖,以免冲掉已有提交或串 Git 历史。
#### 来源 B本地已有 skill-template 时复制 / scaffold
在 **skill-template 仓库根目录**执行(推荐):
```powershell ```powershell
.\tools\scaffold_skill.ps1 -Slug disburse-payroll-icbc -Destination D:\OpenClaw\client-gdcm\disburse-payroll-icbc .\tools\scaffold_skill.ps1 -Slug disburse-payroll-icbc -Destination D:\OpenClaw\client-gdcm\disburse-payroll-icbc
@@ -276,7 +250,7 @@ git remote -v # 确认 origin 不是 skill-template
脚手架会排除 `.git`、缓存与 `.env`,并删除 `.openclaw-skill-template` 标记;**不会**自动 `git init`。 脚手架会排除 `.git`、缓存与 `.env`,并删除 `.openclaw-skill-template` 标记;**不会**自动 `git init`。
#### 禁止方式 ##### 禁止方式(来源 B
| 做法 | 后果 | | 做法 | 后果 |
|------|------| |------|------|
@@ -284,7 +258,7 @@ git remote -v # 确认 origin 不是 skill-template
| ❌ 保留模板 `.git` 只改 `remote url` | 历史、分支、对象库仍属 template | | ❌ 保留模板 `.git` 只改 `remote url` | 历史、分支、对象库仍属 template |
| ❌ 未删 `.git` 就 `git init` | 嵌套/混乱仓库,难以排查 | | ❌ 未删 `.git` 就 `git init` | 嵌套/混乱仓库,难以排查 |
#### 若已手工复制(补救) ##### 若已手工复制(补救)
```powershell ```powershell
cd <新技能目录> cd <新技能目录>
@@ -296,7 +270,7 @@ git remote add origin <新技能仓库 URL>
git remote -v git remote -v
``` ```
#### AI / 编程代理复制红线 ##### AI / 编程代理复制红线
| 禁止 | 说明 | | 禁止 | 说明 |
|------|------| |------|------|
@@ -307,9 +281,9 @@ git remote -v
目录名要和 skill slug 对齐,后面很多地方都依赖这个命名。 目录名要和 skill slug 对齐,后面很多地方都依赖这个命名。
### 第二步:先改 4 个最关键标识 ### 第二步:先改关键标识与占位
复制后优先改下面这些地方: 拿到仓库并填好 / 更新 [`REQUIREMENTS.md`](REQUIREMENTS.md) 后,优先改下面这些地方:
1. `SKILL.md` 1. `SKILL.md`
2. 根目录市场四 Tab`README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md` 2. 根目录市场四 Tab`README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md`
@@ -325,7 +299,7 @@ git remote -v
- 平台内部键 - 平台内部键
- 日志 logger 名 - 日志 logger 名
此外,如果该技能发布后默认不公开(`access_scope = 0`),建议一开始就把 `SKILL.md` 中的 `metadata.openclaw.developer_ids` 配好。这样后续发布到平台时,开发者本人仍能在技能市场看到并验证该技能 **开发 / 联调阶段务必尽早**配置 `SKILL.md` 中的 `metadata.openclaw.developer_ids`(完整目的与取 ID 步骤见 §6「关于 developer_ids」。开发期技能在匠厂常为不公开不加本人用户 ID**技术人员自己也无法在技能市场看到并安装自测**
## 5. 哪些占位内容必须替换 ## 5. 哪些占位内容必须替换
@@ -385,14 +359,28 @@ git remote -v
- `references/`CLI / SCHEMA - `references/`CLI / SCHEMA
- 代码注释与 `service/` 实现 - 代码注释与 `service/` 实现
### 关于 `metadata.openclaw.developer_ids` ### 关于 `metadata.openclaw.developer_ids`(开发自测必做)
这是一个平台发布元数据字段,用于解决下面这个问题: #### 目的(先理解再填)
- 技能发布后若平台记录中的 `access_scope = 0`,技能默认不公开 开发 / 测试阶段,技能 `release` 到匠厂后,平台侧**默认不公开**`access_scope = 0`
- 如果不额外授权,连开发者自己也可能在技能市场里看不到这个技能
因此可以在 `SKILL.md` 中声明: - 技能市场里对普通人**不可见**
- **若不额外授权,连开发该技能的技术人员自己也看不见、装不了**
- 看不见 → 无法安装 → 无法在宿主做 §15 的多入口验收(新建任务、数据管理、定时任务等)
因此 `developer_ids` 不是可有可无的装饰字段,而是**开发期自测通行证**:把本人(及需要一起验收的同事)的匠厂**用户 ID** 写进 `SKILL.md`,发布时平台将这些用户补进可见 / 可访问范围,使开发者能在「对其他人仍不可见」的前提下完成安装与测试。
#### 如何获取匠厂用户 ID完整步骤
1. **下载并安装**匠厂客户端:[https://jc2009.com/product.html](https://jc2009.com/product.html)
2. **注册并登录**(使用将用于开发自测的账号)
3. 打开客户端左下角头像旁的**设置**(齿轮)
4. 在 **用户信息** 中查看 **用户 ID**(正整数),点击旁边的 **复制**
5. 将该 ID 写入本技能 `SKILL.md` 的 `metadata.openclaw.developer_ids`(见下例)
6. **必须替换**模板里的示例 ID如 `10032`、`12428` 等占位),不要原样留着模板作者的 ID 却指望自己账号能看见技能
#### 在 `SKILL.md` 中声明
```yaml ```yaml
metadata: metadata:
@@ -400,17 +388,17 @@ metadata:
slug: your-skill-slug slug: your-skill-slug
category: 通用 category: 通用
developer_ids: developer_ids:
- 1032 - 12345 # 换成「你的用户ID」匠厂「设置 → 用户信息」复制的正整数)
- 12428
``` ```
约定如下: 约定如下(原有规则保留)
- 只允许填写正整数用户 ID - 只允许填写正整数用户 ID(来自匠厂宿主,不是 Gitea / Git 账号名)
- 推荐使用数组,即使当前只有 1 个开发者 - 推荐使用数组,即使当前只有 1 个开发者
- 发布时平台会把这些用户自动补写到 `skill_user_access` - 发布时平台会把这些用户自动补写到 `skill_user_access`
- 第一个 ID 会同步到 `skills.developer_id` - 第一个 ID 会同步到 `skills.developer_id`
- 一期只做“补授权”,不会因为你 later 修改数组而自动撤销旧授权 - 一期只做“补授权”,不会因为你 later 修改数组而自动撤销旧授权
- **首次正式 release 前**就应配好配错或漏配时CI 可能成功,但你在技能市场仍找不到技能
## 7. 文档目录分工 ## 7. 文档目录分工
@@ -744,7 +732,7 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
.\release.ps1 .\release.ps1
``` ```
如果你的技能使用了 `metadata.openclaw.developer_ids`,那么这一步触发的发布工作流除了同步 `skills` / `skill_versions` 外,还会在平台侧自动补开发者可见权限。测试非公开技能时,建议重点验证这部分是否生效 发布前 **`developer_ids` 应已配置**(见 §6。本步触发的发布工作流除了同步 `skills` / `skill_versions` 外,还会在平台侧自动补开发者可见权限;发布后应重点确认:用本人账号能在技能市场看到并安装该技能
这一步会自动完成标准发布动作,包括: 这一步会自动完成标准发布动作,包括:
@@ -789,9 +777,11 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
- 工作流文件是否存在 - 工作流文件是否存在
- 发布包结构是否符合模板规范 - 发布包结构是否符合模板规范
### 第四步:进入匠厂平台下载安装包 ### 第四步:进入匠厂客户端(用于安装验收)
当工作流成功后,就可以进入匠厂平台验证最终安装效果。 当工作流成功后,就可以进入匠厂客户端验证最终安装效果。
若你已按 §6 为 `developer_ids` 安装并登录过匠厂,**直接使用同一客户端、同一账号**即可,无需重复下载。若尚未安装:
匠厂产品下载地址: 匠厂产品下载地址:
@@ -803,11 +793,13 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
匠厂产品页可从这里进入:[产品下载 - 匠厂](https://jc2009.com/product.html) 匠厂产品页可从这里进入:[产品下载 - 匠厂](https://jc2009.com/product.html)
### 第五步:安装匠厂后,在技能市场检查最新 skill > 取用户 ID 写入 `developer_ids` 的完整步骤见 §6此处侧重发布后的安装验收。
安装并启动匠厂后,进入左侧“技能市场”,搜索或查找刚刚发布的 skill确认以下内容 ### 第五步:在技能市场检查最新 skill
- 技能可以被正常检索到 使用已写入 `developer_ids` 的账号登录并启动匠厂后,进入左侧“技能市场”,搜索或查找刚刚发布的 skill确认以下内容
- 技能可以被正常检索到(开发期不公开时,**仅** `developer_ids` 内账号可见)
- 技能名称、说明、版本信息正确 - 技能名称、说明、版本信息正确
- 最新版本已经同步出来 - 最新版本已经同步出来
- 可以正常安装或更新 - 可以正常安装或更新
@@ -826,11 +818,35 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
- 安装后状态正常 - 安装后状态正常
- 不会出现缺文件、缺入口或安装失败的问题 - 不会出现缺文件、缺入口或安装失败的问题
### 第七步:在“新建任务”中实际使用该 skill ### 第七步:按声明做宿主多入口验收(已声明的入口都要测;须看用户体验)
安装完成后,不要只停留在“已安装”状态,还需要进入“新建任务”页面,真正调用一次该 skill完成最终验证 安装完成后,不要只停留在“已安装”状态,也**不要**只在本地 mock 通就认为完成。宿主侧栏有多条与技能相关的入口;**按本技能 `assets/actions.json` 的 `placements` / `executionProfile` 声明逐项自测**(契约见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md)、[`../references/ACTIONS.md`](../references/ACTIONS.md))。未声明的入口可以跳过;**已声明却测不到,视为验收失败**。不要只测对话、忽略其它入口
建议至少验证: **用户体验(与功能入口并列,后台通了不算完):**
- 失败时用户能否看懂原因(对话 / 任务中心 / 提示),不是只有技术日志
- 长任务async是否有可见进度能否取消或等待说明
- 网页 RPA 是否有头运行;需登录/验证码时是否停下等人,而不是静默失败
- 数据管理:表名/字段中文、按钮文案是否清楚;结果是否写回用户看得见的地方
- 市场说明 / 教程是否与真实行为一致
| 宿主入口(侧栏 / 界面) | 技能侧如何挂上 | 建议验收什么 |
|-------------------------|----------------|--------------|
| **新建任务**(对话 Agent | `placements` 含 `agent`;或短查询走共享 Python CLI | 自然语言能触发主流程;长任务 / RPA 须走 `run_skill_action`,禁止 bash 干等 |
| **数据管理 · 表顶栏** | `placements` 含 `toolbar`(须合法非空 `bind.tables` | 左侧能看到本技能库表;表顶栏出现按钮;点按行为符合预期 |
| **数据管理 · 行内** | `placements` 含 `row`(建议 `bind.tables` + `bind.inputMapping` 预填主键) | 行内操作链接可见(需有主键列);点按后入参/行为正确 |
| **数据管理 · 批量** | `placements` 含 `batch` | 勾选行后批量按钮可用;无勾选时禁用;批量行为正确 |
| **定时任务** | `placements` 含 `cron`(创建时选「技能直调」) | 能选到本技能 Action、保存并触发到点或「立即运行」行为正确 |
| **任务中心** | 不是 placement由 `executionProfile: "async"` 决定是否进 Job | 长任务出现进度 / 可取消;来源标签与触发入口一致 |
| **技能市场 → 技能详情** | `placements` 含 `skill-detail`(详情直调) | **必验**安装与四 Tab。详情页「技能直调」按钮随宿主版本上线**已声明则须在具备该 UI 的宿主上完成按钮自测**(与其它入口同等对待,不要只靠 Agent |
补充说明:
- **进不进任务中心只看 `executionProfile`**,与从哪个入口触发无关(既有正交规则不变)。
- 数据管理还依赖库表元数据(`_jiangchang_*` 等,见 [`../references/SCHEMA.md`](../references/SCHEMA.md));仅有 Action、没有可展示库表时侧栏可能看不到表。
- `row` / `batch` / `toolbar` / `cron` / `agent` / `skill-detail` 均以**当前匠厂宿主已实现能力**为准;字段与示例见 [`../references/ACTIONS.md`](../references/ACTIONS.md)。
**新建任务(对话)最小检查:**
- 新任务中可以正常选择或触发该 skill - 新任务中可以正常选择或触发该 skill
- skill 能被正确唤起 - skill 能被正确唤起
@@ -841,6 +857,8 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
![新建任务中使用技能](../assets/screenshots/new-task-usage.png) ![新建任务中使用技能](../assets/screenshots/new-task-usage.png)
若第五步在技能市场**搜不到**本技能:先核对登录账号的用户 ID 是否已写入 `developer_ids` 并随本次 release 发布(见 §6不要只反复重装客户端。
## 16. 发布前检查清单 ## 16. 发布前检查清单
每个新 skill 发布前,建议技术人员逐条确认: 每个新 skill 发布前,建议技术人员逐条确认:
@@ -849,6 +867,7 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
- [ ] slug 符合 [`NAMING.md`](NAMING.md)verb-noun-platform - [ ] slug 符合 [`NAMING.md`](NAMING.md)verb-noun-platform
- [ ] 目录名、`SKILL.md` slug、`constants.SKILL_SLUG` 三者一致 - [ ] 目录名、`SKILL.md` slug、`constants.SKILL_SLUG` 三者一致
- [ ] `SKILL.md` 中 slug、名称、描述都已替换 - [ ] `SKILL.md` 中 slug、名称、描述都已替换
- [ ] `SKILL.md` 的 `developer_ids` 已换成**本人**匠厂用户 ID设置 → 用户信息 → 复制),不是模板示例 ID
- [ ] `scripts/util/constants.py` 已修改 - [ ] `scripts/util/constants.py` 已修改
- [ ] `../references/CLI.md` 示例命令已改成真实命令 - [ ] `../references/CLI.md` 示例命令已改成真实命令
- [ ] `service` 下的核心业务文件(如 `task_service.py`)已按领域改名并实现 - [ ] `service` 下的核心业务文件(如 `task_service.py`)已按领域改名并实现
@@ -868,8 +887,10 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
- [ ] 如有 integration 测试需求,已写在 `tests/integration/` 下并保持 `.sample` 后缀 - [ ] 如有 integration 测试需求,已写在 `tests/integration/` 下并保持 `.sample` 后缀
- [ ] 本仓库**不是** skill-template 的误复制(根目录**无** `.openclaw-skill-template` - [ ] 本仓库**不是** skill-template 的误复制(根目录**无** `.openclaw-skill-template`
- [ ] `git remote -v` 指向**本技能**远端URL 不含 skill-template 仓库名 - [ ] `git remote -v` 指向**本技能**远端URL 不含 skill-template 仓库名
- [ ] `git log` 首条提交属于本技能(非模板历史) - [ ] `git log` 首条提交属于本技能(非模板历史)(来源 A 从 Gitea 克隆的已有业务仓,以该仓历史为准)
- [ ] 网页 RPA先 account-manager `profile_dir` 再开浏览器;`.env` 默认有头REQUIREMENTS 写明登录策略required/optional/not_needed用户 README/TUTORIAL 强调先登记账号(`RPA.md` §0.2 / `POLICY-RPA-004` - [ ] 业务 `.env` / `.env.example` 默认 `OPENCLAW_TEST_TARGET=real_rpa`(单测/CI 仍用 mock**未只靠 mock 交活**
- [ ] 发布后计划在宿主按 §15 第七步验收:已声明入口 + **用户体验**(进度、失败提示、有头 RPA、文案一致等
- [ ] 网页 RPA`ensure-web` 取得 `profile_dir` 再开浏览器;`.env` 默认有头REQUIREMENTS 写明登录策略required/optional/not_needed会话依赖型勿抄 optional用户 README/TUTORIAL 区分「登记账号」与「站点登录」(`RPA.md` §0.2 / `POLICY-RPA-004`);依赖 kit 空值解析等修复时上调 `platform_kit_min_version`
## 17. 常见错误 ## 17. 常见错误

View File

@@ -19,7 +19,7 @@
| POLICY-RPA-001 | 不得 import/use account-manager 内部 `rpa_helpers` / `inject_account_manager_scripts_path` / `get_account_credential` | development/ADAPTER.md §兄弟依赖development/RPA.md §1.7 | hard | 扫描 `scripts/**/*.py` 禁止模式 | `tests/test_development_policy_guard.py::TestPolicyRpa001`(另见 `tests/test_no_rpa_helpers_import.py` | | POLICY-RPA-001 | 不得 import/use account-manager 内部 `rpa_helpers` / `inject_account_manager_scripts_path` / `get_account_credential` | development/ADAPTER.md §兄弟依赖development/RPA.md §1.7 | hard | 扫描 `scripts/**/*.py` 禁止模式 | `tests/test_development_policy_guard.py::TestPolicyRpa001`(另见 `tests/test_no_rpa_helpers_import.py` |
| POLICY-RPA-002 | RPA 交付代码不得直接执行 ffmpeg 拼 MP4应使用 `RpaVideoSession`(允许 `ffmpeg_path` 等诊断字段) | development/RPA.md §5.3 录屏成片标准 | hard | 扫描 `scripts/**/*.py` 禁止 `subprocess` / `os.system` / `os.popen` 直接调用 ffmpeg`task_run_support.py` 保留豁免) | `tests/test_development_policy_guard.py::TestPolicyRpa002` | | POLICY-RPA-002 | RPA 交付代码不得直接执行 ffmpeg 拼 MP4应使用 `RpaVideoSession`(允许 `ffmpeg_path` 等诊断字段) | development/RPA.md §5.3 录屏成片标准 | hard | 扫描 `scripts/**/*.py` 禁止 `subprocess` / `os.system` / `os.popen` 直接调用 ffmpeg`task_run_support.py` 保留豁免) | `tests/test_development_policy_guard.py::TestPolicyRpa002` |
| POLICY-RPA-003 | 即使默认 `OPENCLAW_RECORD_VIDEO=0`RPA / 长任务模板也必须保留录屏能力:`RpaVideoSession``video.add_step`、video artifact merge 到 `result_summary` | development/RPA.md §5.3development/LOGGING.mddevelopment/CONFIG.md | hard | 扫描 `scripts/service/*.py`service 层整体须含上述接入) | `tests/test_development_policy_guard.py::TestPolicyRpa003` | | POLICY-RPA-003 | 即使默认 `OPENCLAW_RECORD_VIDEO=0`RPA / 长任务模板也必须保留录屏能力:`RpaVideoSession``video.add_step`、video artifact merge 到 `result_summary` | development/RPA.md §5.3development/LOGGING.mddevelopment/CONFIG.md | hard | 扫描 `scripts/service/*.py`service 层整体须含上述接入) | `tests/test_development_policy_guard.py::TestPolicyRpa003` |
| POLICY-RPA-004 | 网页 RPA`.env.example` 若声明 `OPENCLAW_BROWSER_HEADLESS` 则默认必须为 `0``scripts/` 若使用 `launch_persistent_*` 则必须同时存在 `pick_web_account`(先 Profile 再开浏览器);登录不全局强制,但不得跳过 account-manager | development/RPA.md §0.2development/ADAPTER.md §兄弟依赖 | hard | 解析 `.env.example` + 扫描 `scripts/**/*.py` | `tests/test_development_policy_guard.py::TestPolicyRpa004` | | POLICY-RPA-004 | 网页 RPA`.env.example` 若声明 `OPENCLAW_BROWSER_HEADLESS` 则默认必须为 `0``scripts/` 若使用 `launch_persistent_*` 则必须同时存在 `pick_web_account`(先 Profile 再开浏览器;实现上应走 ensure-web);登录不全局强制,但不得跳过 account-manager | development/RPA.md §0.2development/ADAPTER.md §兄弟依赖 | hard | 解析 `.env.example` + 扫描 `scripts/**/*.py` | `tests/test_development_policy_guard.py::TestPolicyRpa004` |
| POLICY-PACKAGING-001 | `scripts/**/*.py` 单文件 < 1000 行 | development/DEVELOPMENT.md §3.4 发布打包约束development/RUNTIME.md §发布打包约束 | hard | 行数统计 | **已有测试覆盖**`tests/test_release_packaging_constraints.py::test_scripts_py_files_under_pyarmor_line_limit` | | POLICY-PACKAGING-001 | `scripts/**/*.py` 单文件 < 1000 行 | development/DEVELOPMENT.md §3.4 发布打包约束development/RUNTIME.md §发布打包约束 | hard | 行数统计 | **已有测试覆盖**`tests/test_release_packaging_constraints.py::test_scripts_py_files_under_pyarmor_line_limit` |
| POLICY-PACKAGING-002 | 文本文件 UTF-8 without BOM`U+FEFF` | development/DEVELOPMENT.md §3.4development/RUNTIME.md §编码与输出 | hard | BOM / 解码检查 | **已有测试覆盖**`tests/test_release_packaging_constraints.py::test_text_files_are_utf8_without_bom` | | POLICY-PACKAGING-002 | 文本文件 UTF-8 without BOM`U+FEFF` | development/DEVELOPMENT.md §3.4development/RUNTIME.md §编码与输出 | hard | BOM / 解码检查 | **已有测试覆盖**`tests/test_release_packaging_constraints.py::test_text_files_are_utf8_without_bom` |
| POLICY-DOCS-001 | 本矩阵存在且包含上述全部 `policy_id` | 本次规范化约定 | hard | 解析 `development/POLICY_MATRIX.md` | `tests/test_development_policy_guard.py::TestPolicyDocs001` | | POLICY-DOCS-001 | 本矩阵存在且包含上述全部 `policy_id` | 本次规范化约定 | hard | 解析 `development/POLICY_MATRIX.md` | `tests/test_development_policy_guard.py::TestPolicyDocs001` |
@@ -35,7 +35,7 @@
| 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.mdreferences/ACTIONS.md | hard | 文档/schema/manifest 检查 | `tests/test_development_policy_guard.py::TestPolicySkillAction001` | | 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.mdreferences/ACTIONS.md | hard | 文档/schema/manifest 检查 | `tests/test_development_policy_guard.py::TestPolicySkillAction001` |
| POLICY-SKILL-ACTION-002 | 每个 Action 必须显式声明 `executionProfile`,且只能是 `sync``async` | development/SKILL_ACTION_RUNTIME.mdreferences/ACTIONS.mdassets/schemas/skill-actions.schema.json | hard | schema required + manifest 扫描 | `tests/test_development_policy_guard.py::TestPolicySkillAction002``tests/test_actions_manifest.py` | | POLICY-SKILL-ACTION-002 | 每个 Action 必须显式声明 `executionProfile`,且只能是 `sync``async` | development/SKILL_ACTION_RUNTIME.mdreferences/ACTIONS.mdassets/schemas/skill-actions.schema.json | hard | schema required + manifest 扫描 | `tests/test_development_policy_guard.py::TestPolicySkillAction002``tests/test_actions_manifest.py` |
| POLICY-SKILL-ACTION-003 | toolbar Action 必须声明合法非空 `bind.tables`snake_case、唯一 | references/ACTIONS.mdassets/schemas/skill-actions.schema.json | hard | schema if/then + manifest 扫描 | `tests/test_development_policy_guard.py::TestPolicySkillAction003``tests/test_actions_manifest.py` | | POLICY-SKILL-ACTION-003 | toolbar Action 必须声明合法非空 `bind.tables`snake_case、唯一 | references/ACTIONS.mdassets/schemas/skill-actions.schema.json | hard | schema if/then + manifest 扫描 | `tests/test_development_policy_guard.py::TestPolicySkillAction003``tests/test_actions_manifest.py` |
| POLICY-SKILL-ACTION-004 | `placements``executionProfile` 正交文档不得把数据管理入口与异步执行方式硬绑定Schema 须接受 toolbar/cron/agent/skill-detail × sync/async 全部组合 | development/SKILL_ACTION_RUNTIME.mdreferences/ACTIONS.mdassets/schemas/skill-actions.schema.json | hard | Schema 4×2 矩阵校验 + 文档禁止短语扫描 | `tests/test_actions_manifest.py::test_schema_allows_all_placement_execution_profile_combinations``tests/test_development_policy_guard.py::TestPolicySkillAction004` | | POLICY-SKILL-ACTION-004 | `placements``executionProfile` 正交文档不得把数据管理入口与异步执行方式硬绑定Schema 须接受 toolbar/row/batch/cron/agent/skill-detail × sync/async 全部组合 | development/SKILL_ACTION_RUNTIME.mdreferences/ACTIONS.mdassets/schemas/skill-actions.schema.json | hard | Schema 6×2 矩阵校验 + 文档禁止短语扫描 | `tests/test_actions_manifest.py::test_schema_allows_all_placement_execution_profile_combinations``tests/test_development_policy_guard.py::TestPolicySkillAction004` |
| POLICY-ARCH-CORE-001 | 同一业务能力的 Action、CLI、Agent、cron、数据管理入口复用同一业务内核文档要求已声明 | development/DEVELOPMENT.mddevelopment/SKILL_ACTION_RUNTIME.md | hard文档标记 / soft语义 | 文档关键表述存在性;真实复用靠 sample/review | `tests/test_development_policy_guard.py::TestPolicyArchCore001``tests/samples/test_action_core_contract.py.sample` | | POLICY-ARCH-CORE-001 | 同一业务能力的 Action、CLI、Agent、cron、数据管理入口复用同一业务内核文档要求已声明 | development/DEVELOPMENT.mddevelopment/SKILL_ACTION_RUNTIME.md | hard文档标记 / soft语义 | 文档关键表述存在性;真实复用靠 sample/review | `tests/test_development_policy_guard.py::TestPolicyArchCore001``tests/samples/test_action_core_contract.py.sample` |
| POLICY-DATA-SYNC-001 | 同步型业务须遵循幂等、事务、完整性与空结果保护(文档已声明) | references/SCHEMA.mddevelopment/SKILL_ACTION_RUNTIME.md | hard文档标记 / soft语义 | 文档关键表述存在性;完整语义靠 sample/review | `tests/test_development_policy_guard.py::TestPolicyDataSync001``tests/samples/test_sync_contract.py.sample` | | POLICY-DATA-SYNC-001 | 同步型业务须遵循幂等、事务、完整性与空结果保护(文档已声明) | references/SCHEMA.mddevelopment/SKILL_ACTION_RUNTIME.md | hard文档标记 / soft语义 | 文档关键表述存在性;完整语义靠 sample/review | `tests/test_development_policy_guard.py::TestPolicyDataSync001``tests/samples/test_sync_contract.py.sample` |
@@ -59,5 +59,5 @@
| RPA 拟人操作、选择器纪律、HITL 超时 | development/RPA.md §0§1 | 行为与 DOM 质量,无法静态扫描 | | RPA 拟人操作、选择器纪律、HITL 超时 | development/RPA.md §0§1 | 行为与 DOM 质量,无法静态扫描 |
| adapter 四档契约测试覆盖 timeout/unauthorized 等 | development/ADAPTER.md §contract tests | 需业务实现后人工补测 | | adapter 四档契约测试覆盖 timeout/unauthorized 等 | development/ADAPTER.md §contract tests | 需业务实现后人工补测 |
| `SKILL.md` / `constants.SKILL_SLUG` 一致性 | development/DEVELOPMENT.md §16 | 已有 `tests/test_skill_metadata.py` | | `SKILL.md` / `constants.SKILL_SLUG` 一致性 | development/DEVELOPMENT.md §16 | 已有 `tests/test_skill_metadata.py` |
| platform_kit_min_version >= 1.2.0 | development/RUNTIME.md | 已有 `tests/test_platform_import.py` | | platform_kit_min_version >= 1.2.2 | development/RUNTIME.md | 已有 `tests/test_platform_import.py` |
| 宿主按 `bind.tables` 过滤 toolbar 按钮 | references/ACTIONS.md | 待宿主升级后完整生效;模板侧已强制显式绑定 | | 宿主按 `bind.tables` 过滤 toolbar 按钮 | references/ACTIONS.md | 待宿主升级后完整生效;模板侧已强制显式绑定 |

View File

@@ -1,20 +1,40 @@
# 开发资料入口 # 开发资料入口
本目录面向**人类开发者**与 **AI 编程代理**开始定制 skill 前,建议按以下顺序阅读: 本目录面向**人类开发者**与 **AI 编程代理**技术人员与编程 AI **以本目录为主读路径**`references/` 是 CLI / Action / Schema 契约细节,根目录市场四 Tab 面向最终用户。
1. [`REQUIREMENTS.md`](REQUIREMENTS.md) — 需求文档模板与验收标准 ## 新技能全流程(先看这张表)
2. [`NAMING.md`](NAMING.md) — slug / 仓库名命名规范(复制模板前必读)
3. [`DEVELOPMENT.md`](DEVELOPMENT.md) — 完整开发步骤与目录规范
4. [`TESTING.md`](TESTING.md) — 测试分层、隔离数据根与档位开关
5. [`LOGGING.md`](LOGGING.md) — 日志分层、必打节点、敏感信息红线(**涉及长任务、RPA、外部系统时必读**
6. [`ADAPTER.md`](ADAPTER.md) — 涉及外部系统对接时
7. [`RPA.md`](RPA.md) — 涉及浏览器 / 桌面 / 手机自动化时
8. [`CONFIG.md`](CONFIG.md) — `.env` 规范与 bootstrap 机制
9. [`DATA_PATHS.md`](DATA_PATHS.md) — 下载/导入/导出等本地文件路径标准(涉及文件读写时必读)
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`)。 按顺序做;细节与约束见 [`DEVELOPMENT.md`](DEVELOPMENT.md) 对应章节(**不替代**下文深度规范,只作导航)。
| 步骤 | 做什么 | 详见 |
|------|--------|------|
| 1 | **拿到仓库**:① 项目经理在 [自建 Gitea](https://git.jc2009.com/)`git.jc2009.com`,非 Gitea 官网)开仓并授权后 `git clone`;或 ② 本地已有 `skill-template` 时用 scaffold / 复制出新目录(遵守 Git 红线) | [`DEVELOPMENT.md`](DEVELOPMENT.md) §1、§4 |
| 2 | 按 [`NAMING.md`](NAMING.md) 确认 slug**先填** [`REQUIREMENTS.md`](REQUIREMENTS.md),再写业务代码 | [`REQUIREMENTS.md`](REQUIREMENTS.md)、[`NAMING.md`](NAMING.md) |
| 3 | 替换标识与占位,按四象限 / examples 实现 `scripts/service/` | [`DEVELOPMENT.md`](DEVELOPMENT.md) §4§14 |
| 4 | **配置 `developer_ids`(开发自测必做)**:开发期技能在匠厂默认不公开;不加则开发者自己也装测不了。从宿主设置复制用户 ID 写入 `SKILL.md` | [`DEVELOPMENT.md`](DEVELOPMENT.md) §6「关于 developer_ids」 |
| 5 | 本地单测mock通过后按业务默认 **`real_rpa`** 真实跑通;再 `release.ps1`;看 Gitea CI。**mock 通 ≠ 完成** | [`ADAPTER.md`](ADAPTER.md)[`DEVELOPMENT.md`](DEVELOPMENT.md) §15 |
| 6 | 匠厂安装后,按已声明入口自测,并看**用户体验**(进度、失败提示、有头 RPA 等) | [`DEVELOPMENT.md`](DEVELOPMENT.md) §15[`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) |
脚手架与 Git 防串库:[`../tools/README.md`](../tools/README.md)`scaffold_skill.ps1`)。业务仓也可直接从自建 Gitea clone见 [`DEVELOPMENT.md`](DEVELOPMENT.md) §4 来源 A。
**公开公共仓(可 clone 学习,可扩展登记):** [`SHARED_REPOS.md`](SHARED_REPOS.md) — 当前含 skill-template、account-manager、jiangchang-platform-kit。
## 深度规范阅读顺序
开始定制 skill 前,建议按以下顺序阅读:
1. [`SHARED_REPOS.md`](SHARED_REPOS.md) — **公共仓库目录**(模板 / 账号管理 / platform-kit 等,可扩展)
2. [`REQUIREMENTS.md`](REQUIREMENTS.md) — 需求文档模板与验收标准
3. [`NAMING.md`](NAMING.md) — slug / 仓库名命名规范(复制模板前必读)
4. [`DEVELOPMENT.md`](DEVELOPMENT.md) — 完整开发步骤与目录规范
5. [`TESTING.md`](TESTING.md) — 测试分层、隔离数据根与档位开关
6. [`LOGGING.md`](LOGGING.md) — 日志分层、必打节点、敏感信息红线(**涉及长任务、RPA、外部系统时必读**
7. [`ADAPTER.md`](ADAPTER.md) — 涉及外部系统对接时
8. [`RPA.md`](RPA.md) — 涉及浏览器 / 桌面 / 手机自动化时
9. [`CONFIG.md`](CONFIG.md) — `.env` 规范与 bootstrap 机制
10. [`DATA_PATHS.md`](DATA_PATHS.md) — 下载/导入/导出等本地文件路径标准(涉及文件读写时必读)
11. [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) — Skill Action sync/async、任务中心、Agent 禁令、队列 pick涉及 RPA / 长任务 / 数据管理按钮 / Cron / Agent 时必读)
12. [`RUNTIME.md`](RUNTIME.md) — 共享 runtime、数据路径、发布打包与编码约定
Agent 调用契约见 [`../references/CLI.md`](../references/CLI.md)、[`../references/SCHEMA.md`](../references/SCHEMA.md)、[`../references/ACTIONS.md`](../references/ACTIONS.md)。 Agent 调用契约见 [`../references/CLI.md`](../references/CLI.md)、[`../references/SCHEMA.md`](../references/SCHEMA.md)、[`../references/ACTIONS.md`](../references/ACTIONS.md)。
用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.md),不要写进本目录。 用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.md),不要写进本目录。

View File

@@ -23,10 +23,12 @@
- account-manager platform_key如有 - account-manager platform_key如有
- 命名形态:标准型 / scope 型 - 命名形态:标准型 / scope 型
- **网页 RPA如有— 登录策略(三选一,须写明):** - **网页 RPA如有— 登录策略(三选一,须写明):**
- `required`:目标站必须登录才能闭环(用户须先登记账号,并在有头浏览器中完成/保持登录 - `required`:目标站必须登录才能闭环(用户须完成/保持站点登录;技能每轮主路径须有登录门禁 + 验证码/人工等待
- `optional`:登录可提升能力,但不登录也能完成主路径 - `optional`:登录可提升能力,但不登录也能完成主路径**仅当**公开页确可闭环时选用;会话依赖型采集/评论/私信**禁止**默认抄成 optional
- `not_needed`:公开页即可,不要求站点登录 - `not_needed`:公开页即可,不要求站点登录
- 无论上列哪一种:**都必须**经 account-manager 取得 `profile_dir`(见 `development/RPA.md` §0.2);不得使用系统默认浏览器用户目录 - 无论上列哪一种:**都必须**经 account-manager **`ensure-web`**(或等价)取得 `profile_dir`(见 `development/RPA.md` §0.2);不得使用系统默认浏览器用户目录
- **登记 ≠ 登录**ensure/add-web 只建账号与 Profile站点登录由本技能在有头浏览器中完成
- **反模式**:把「需要登录态才能采到数据」的需求写成 `optional`,再省略 `ensure_logged_in`
## 1. 文档目标 ## 1. 文档目标
@@ -108,7 +110,7 @@
- Windows 环境下需保证 UTF-8 输出兼容 - Windows 环境下需保证 UTF-8 输出兼容
- 必须具备基本日志能力;敏感字段脱敏 - 必须具备基本日志能力;敏感字段脱敏
- RPA 类 skillPlaywright 由宿主共享 runtime 提供;技能侧不要 `playwright install`URL 不要放进 `launch_persistent_context``args` - RPA 类 skillPlaywright 由宿主共享 runtime 提供;技能侧不要 `playwright install`URL 不要放进 `launch_persistent_context``args`
- 网页 RPA启动前必须已 pick `profile_dir`;生产有头;站点登录策略见 §0required / optional / not_needed - 网页 RPA启动前必须经 ensure-web 拿`profile_dir`;生产有头;站点登录策略见 §0required / optional / not_needed`required` 时每轮须登录门禁
## 6. 输入输出要求 ## 6. 输入输出要求
@@ -162,21 +164,23 @@
- 代码结构符合模板规范;`SKILL.md` slug 与 `constants.SKILL_SLUG` 一致 - 代码结构符合模板规范;`SKILL.md` slug 与 `constants.SKILL_SLUG` 一致
- `health``version``init-db` 命令执行正常 - `health``version``init-db` 命令执行正常
- 主命令(如 `run`)在 mock / simulator 档位可重复验证 - `python tests/run_tests.py -v` 必跑测试全部通过mock / 离线门禁;**mock 通 ≠ 完成**
- `python tests/run_tests.py -v` 必跑测试全部通过 - 主命令在业务默认档(模板为 `real_rpa`)真实跑通;不得只 mock 通就交活
- `task_logs` 写入和查询符合 `references/SCHEMA.md`(含 `created_at` / `updated_at` Unix 秒级规范) - `task_logs` 写入和查询符合 `references/SCHEMA.md`(含 `created_at` / `updated_at` Unix 秒级规范)
- `init_db()` 已写入 `_jiangchang_tables` / `_jiangchang_columns`;用户可见表/字段具备中文 `display_name` - `init_db()` 已写入 `_jiangchang_tables` / `_jiangchang_columns`;用户可见表/字段具备中文 `display_name`
- 字段展示顺序与 `PRAGMA table_info(task_logs)` 的 cid 一致;不依赖 `display_order` - 字段展示顺序与 `PRAGMA table_info(task_logs)` 的 cid 一致;不依赖 `display_order`
- `tests/test_display_metadata.py` 通过 - `tests/test_display_metadata.py` 通过
- 真实联调(如有)放在 `tests/integration/`默认套件不包含真实外联 - 真实联调样例仍放 `tests/integration/`,默认套件不包含真实外联(与业务 `.env` 默认 `real_rpa` 不冲突)
- 发布后 Gitea 工作流成功;匠厂技能市场可见最新版本;安装后可在“新建任务”中调用 - `SKILL.md``developer_ids` 已配置为开发者本人匠厂用户 ID开发期不公开时否则本人无法在市场安装自测取 ID 步骤见 `development/DEVELOPMENT.md` §6
- 发布后 Gitea 工作流成功;匠厂技能市场对开发者账号可见最新版本并可安装
- 安装后按本技能 `actions.json` 声明完成宿主多入口验收(见 `development/DEVELOPMENT.md` §15**须看用户体验**进度可见、失败可读、RPA 有头、文案与真实行为一致等),后台通了不算完
## 10. 开发注意事项 ## 10. 开发注意事项
- 只修改当前 skill 仓库,不要改动无关兄弟项目 - 只修改当前 skill 仓库,不要改动无关兄弟项目
- 先判断四象限类型(`real_browser_rpa` / `real_api` / `simulator_browser_rpa` / `simulator_api`),再读对应 `examples/*/README.md` - 先判断四象限类型(`real_browser_rpa` / `real_api` / `simulator_browser_rpa` / `simulator_api`),再读对应 `examples/*/README.md`
- `cli` 只做参数解析;核心逻辑在 `service`;兄弟 skill 调用集中封装(见 `ADAPTER.md` - `cli` 只做参数解析;核心逻辑在 `service`;兄弟 skill 调用集中封装(见 `ADAPTER.md`
- 发布前完成本地验证、工作流验证和正式环境安装验证 - 发布前完成本地验证、工作流验证和正式环境安装验证(含 `developer_ids` 与宿主多入口)
## 11. 变更记录 ## 11. 变更记录
@@ -188,10 +192,11 @@
## 建议使用方式 ## 建议使用方式
1. 先写 `REQUIREMENTS.md` 1. 拿到业务技能仓库Gitea clone或本地从 `skill-template` scaffold / 复制;见 `DEVELOPMENT.md` §4
2. 再按 `DEVELOPMENT.md` 进入开发 2. 先写 / 填全本文件 `REQUIREMENTS.md`
3. 开发过程中补充 `references/CLI.md``references/SCHEMA.md``development/` 技术规范 3. 再按 `DEVELOPMENT.md` 进入开发(含尽早配置 `developer_ids`
4. 发布前对照第 9 节验收标准逐项检查 4. 开发过程中补充 `references/CLI.md``references/SCHEMA.md``development/` 技术规范
5. 发布前对照第 9 节验收标准逐项检查release 后按 `DEVELOPMENT.md` §15 做宿主多入口验收
## 最小模板示例 ## 最小模板示例
@@ -252,8 +257,8 @@
- 命令可运行tests/run_tests.py -v 通过 - 命令可运行tests/run_tests.py -v 通过
- task_logs 符合 SCHEMA - task_logs 符合 SCHEMA
- 正式环境安装验证通过 - `developer_ids` 已配置为「你的用户ID」匠厂设置中复制的正整数
- 正式环境安装验证通过;已声明的宿主入口(新建任务 / 数据管理 / 定时任务 / 任务中心 / 技能详情等)按 DEVELOPMENT §15 自测
## 10. 开发注意事项 ## 10. 开发注意事项
- 不修改无关项目;不引入旧模板结构 - 不修改无关项目;不引入旧模板结构

View File

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

View File

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

128
development/SHARED_REPOS.md Normal file
View File

@@ -0,0 +1,128 @@
# 公共仓库目录(可扩展)
本页是技能开发用到的**公开公共仓唯一登记处**,面向技术人员与 AI 编程代理。
- 仓均在公司**自建 Gitea**[https://git.jc2009.com/](https://git.jc2009.com/)(基于开源 Gitea 自建,**不是** Gitea 官网)。
- 技术人员一般**可直接访问、随时 `git clone` 到本地**,用于研究、学习、对照与跟进更新。
- **当前登记 3 项**;以后新增公开公共仓时,**只在本页按类型加行**,不必改散落长文。
深度规范仍在 [`RPA.md`](RPA.md)、[`RUNTIME.md`](RUNTIME.md)、[`ADAPTER.md`](ADAPTER.md)、[`DEVELOPMENT.md`](DEVELOPMENT.md);本页只做**指路 + 边界**。
---
## 1. 类型(可扩展)
| 类型 id | 含义 | 典型消费方式 |
|---------|------|--------------|
| `skill-template` | 业务技能作者脚手架与规范源 | clone 学习;`scaffold` / 复制出新业务仓;**不要**在模板仓写业务 |
| `sibling-skill` | 与业务技能并列安装的公开基础设施技能 | clone 学契约;运行时经 **CLI / 封装客户端** 调用 |
| `python-sdk` | 共享 Python 库(源码仓 + 宿主共享 venv 安装) | clone 读实现 / 贡献;运行时 **`import`****禁止 vendor** 进技能仓 |
| `asset-bundle` | (预留)共享媒体/资源包等 | 按该仓说明拉取或由 kit/宿主解析 |
| `tooling` | 预留发布脚本、脚手架、CI 辅助等 | clone 使用或引用其 workflow |
新增类型时:先在本表加一行类型说明,再在 §2 登记表增加实例。
---
## 2. 当前登记表
| 短名 | 类型 | Gitea唯一入口 | 一句话职责 |
|------|------|-------------------|------------|
| **skill-template** | `skill-template` | [https://git.jc2009.com/client-commons/skill-template](https://git.jc2009.com/client-commons/skill-template) | 新业务技能的开发模板、目录规范与发布约定 |
| **account-manager** | `sibling-skill` | [https://git.jc2009.com/client-commons/account-manager](https://git.jc2009.com/client-commons/account-manager) | 账号 / 凭据 / 浏览器 Profile / 租约的统一入口 |
| **jiangchang-platform-kit** | `python-sdk` | [https://git.jc2009.com/client-jiangchang/jiangchang-platform-kit](https://git.jc2009.com/client-jiangchang/jiangchang-platform-kit) | 共享 SDK`jiangchang_skill_core` 等);技能发布可复用 CI 也在此仓 |
本地常见路径(仅作对照,以你机器为准):
- `D:\OpenClaw\client-commons\skill-template`
- `D:\OpenClaw\client-commons\account-manager`
- `D:\OpenClaw\client-jiangchang\jiangchang-platform-kit`
### 2.1 skill-template
| 项 | 说明 |
|----|------|
| **Clone 为了什么** | 读 `development/`、跟模板版本、用 `tools/scaffold_skill` 开新技能 |
| **怎么用** | 来源 Bscaffold或学完后在业务仓开发。规范以本仓为准 |
| **红线** | 不要在模板仓写业务;业务仓 `origin` 不得指向本仓;不得保留 `.openclaw-skill-template` |
| **深入** | 本目录 [`README.md`](README.md)、[`DEVELOPMENT.md`](DEVELOPMENT.md) |
```powershell
git clone https://git.jc2009.com/client-commons/skill-template.git
```
### 2.2 account-manager
| 项 | 说明 |
|----|------|
| **Clone 为了什么** | 读 CLI / `references/INTEGRATION.md`、平台表、租约与错误码;排 `NO_ACCOUNT` / `LEASE_CONFLICT` 等 |
| **怎么用(业务技能)** | 经 `scripts/service/account_client.py`(或等价)**subprocess 调 CLI**;网页 RPA 须先拿到 `profile_dir``ensure-web` / `pick-web` |
| **红线** | **禁止**业务技能 `import` 其内部 `rpa_helpers` / 散落存密码(见 `POLICY-RPA-001`);对方文档若写「同进程 rpa_helpers」**以本模板约束为准** |
| **深入** | 对方仓 `references/INTEGRATION.md``CLI.md`;本模板 [`RPA.md`](RPA.md) §0.2、[`ADAPTER.md`](ADAPTER.md) |
```powershell
git clone https://git.jc2009.com/client-commons/account-manager.git
```
### 2.3 jiangchang-platform-kit
| 项 | 说明 |
|----|------|
| **Clone 为了什么** | 读 `jiangchang_skill_core`config / logging / activity·SRCP / rpa / sibling_bridge 等)、跟版本、贡献共享 API |
| **怎么用(业务技能)** | 运行时由**宿主共享 venv** 提供;技能内 `import jiangchang_skill_core...``SKILL.md``platform_kit_min_version` 是兼容声明 |
| **红线** | **不要**把 `jiangchang-platform-kit` / `playwright` 写入技能 `requirements.txt`**不要**在技能仓 vendor `jiangchang_skill_core/``POLICY-RUNTIME-002` |
| **深入** | 对方仓 `README.md`;本模板 [`RUNTIME.md`](RUNTIME.md)、[`LOGGING.md`](LOGGING.md) |
```powershell
git clone https://git.jc2009.com/client-jiangchang/jiangchang-platform-kit.git
```
私有 PyPI 索引(维护/排障用,日常技能开发通常不必手装):
`https://git.jc2009.com/api/packages/client-jiangchang/pypi/simple/`
---
## 3. 三者关系(别当成三个无关收藏夹)
```text
规范 / 脚手架 ──────────► skill-template本仓
业务技能 scripts/ ──import──► 宿主共享 venv 中的 platform-kit
└──CLI──► account-manager兄弟技能要先拿到 profile_dir 等)
```
- **写规范、开新仓** → 打开 **skill-template**
- **少写公共 Python、进度/RPA 原语** → 用 **kit**(运行靠宿主;源码可 clone
- **账号与浏览器 Profile** → 用 **account-manager**(运行靠 CLI源码可 clone
---
## 4. 何时该打开哪个仓
| 场景 | 优先打开 |
|------|----------|
| 不知道新技能目录/发布/developer_ids 怎么做 | **skill-template** `development/` |
| 定 `platform_key`、ensure-web、租约、凭证存储 | **account-manager** + 本模板 RPA/ADAPTER |
| 看 `emit` / `finish` / `@rpa_step` / `launch_persistent_browser` 实现 | **jiangchang-platform-kit** |
| 技能发布 CI / reusable workflow 从哪来 | **jiangchang-platform-kit** `.github/workflows/` |
| 「公共东西到底在哪」 | **本页登记表** |
---
## 5. 以后如何新增一条公共仓(维护约定)
1. 确认公开可访问URL 使用 `https://git.jc2009.com/...`(勿写 Gitea 官网)。
2. 选定或新增 §1 **类型**
3. 在 §2 **登记表**加一行;必要时增加 `§2.x` 短节(消费方式 + 红线 + clone 示例)。
4. 若改变业务技能硬约束,同步 [`POLICY_MATRIX.md`](POLICY_MATRIX.md) / 相关深度文档。
5. **不要**只在某个业务技能 README 里私藏链接而不登记本页。
---
## 6. 相关链接
- 开发入口:[`README.md`](README.md)
- 运行时:[`RUNTIME.md`](RUNTIME.md)
- 网页 RPA / 账号:[`RPA.md`](RPA.md)、[`ADAPTER.md`](ADAPTER.md)
- 脚手架:[`../tools/README.md`](../tools/README.md)

View File

@@ -52,7 +52,7 @@
| `health` / `version` / `config-path` / 纯统计 / 秒级查询 | `sync` | | `health` / `version` / `config-path` / 纯统计 / 秒级查询 | `sync` |
| 任意 placement含 toolbar | 允许 sync 或 async以 manifest 为准 | | 任意 placement含 toolbar | 允许 sync 或 async以 manifest 为准 |
合法:`sync|async` × `toolbar|cron|agent|skill-detail` 合法:`sync|async` × `toolbar|row|batch|cron|agent|skill-detail`
历史说明:旧文档曾错误地把数据管理按钮入口与异步执行方式绑死。该约束**已废止**;模板测试与 Schema **不得**再建立「入口位置决定 sync/async」一类硬耦合。旧版宿主曾「省略 → async」**当前宿主为「省略 → sync」**。新技能仍须显式声明,不得依赖省略行为。 历史说明:旧文档曾错误地把数据管理按钮入口与异步执行方式绑死。该约束**已废止**;模板测试与 Schema **不得**再建立「入口位置决定 sync/async」一类硬耦合。旧版宿主曾「省略 → async」**当前宿主为「省略 → sync」**。新技能仍须显式声明,不得依赖省略行为。
@@ -62,14 +62,18 @@
多个入口统一走 `POST /api/skill-actions/run`,可能带 `source.kind` 多个入口统一走 `POST /api/skill-actions/run`,可能带 `source.kind`
| 入口 | 常见 `source.kind` | 说明 | | 入口(匠厂侧栏 / 界面) | 常见 `source.kind` | 技能侧 | 开发者怎么测 |
|------|-------------------|------| |-------------------------|-------------------|--------|--------------|
| **Agent** | `agent` | `run_skill_action`;按 manifest 的 executionProfile 执行 | | **新建任务**(对话 Agent | `agent` | `placements``agent``run_skill_action` | 侧栏「新建任务」→ 对话触发 |
| **数据管理** | `data-management` | toolbar 按钮;须 `bind.tables` | | **数据管理 · 顶栏** | `data-management` | `toolbar` + 必填 `bind.tables` | 侧栏「数据管理」→ 表顶栏按钮 |
| **定时任务** | `cron` | Cron 配置参数后调用同一 Action | | **数据管理 · 行内** | `data-management` | `row`(建议 `bind.tables` + `inputMapping` | 同行操作链接(需主键) |
| **技能详情** | (依宿主) | 与上同一 CLI / 同一业务内核 | | **数据管理 · 批量** | `data-management` | `batch` | 勾选行后点批量按钮 |
| **定时任务** | `cron` | `placements``cron` | 侧栏「定时任务」→「技能直调」 |
| **任务中心** | (各入口触发的 Job | `executionProfile: "async"` | 侧栏「任务中心」查看进度 / 取消;**不是** placement |
| **技能详情** | `skill-detail` | `placements``skill-detail` | 市场详情安装/四 Tab详情直调按钮随宿主上线后**同等自测** |
技能 **不要** 在 Python 里按 `source` / placement 写业务分叉;宿主读 manifest 分流展示与等待策略。 技能 **不要** 在 Python 里按 `source` / placement 写业务分叉;宿主读 manifest 分流展示与等待策略。
**已声明的入口都要自测**,不要只测对话。操作清单见 [`DEVELOPMENT.md`](DEVELOPMENT.md) §15 第七步。
--- ---
@@ -81,7 +85,8 @@
|-------------|-------------------------|-----------------|----------------| |-------------|-------------------------|-----------------|----------------|
| 运行检查 / 版本 / 配置路径 | `sync` | `skill-detail`, `agent` | 无 | | 运行检查 / 版本 / 配置路径 | `sync` | `skill-detail`, `agent` | 无 |
| 库 / 队列统计 | `sync` | `skill-detail`, `agent` | 无 | | 库 / 队列统计 | `sync` | `skill-detail`, `agent` | 无 |
| 外源 → 本地库同步 | `sync``async` | 可含 `toolbar`(须 `bind.tables`)、`cron``agent` | 可选 | | 外源 → 本地库同步 | `sync``async` | 可含 `toolbar`(须 `bind.tables`)、`cron``agent``skill-detail` | 可选 |
| 单行 / 批量数据管理动作 | `sync``async` | `row` / `batch`(建议 `bind.tables`;行内常用 `inputMapping` | 可选 |
| 数据导入 / 失败重置 | `sync``async` | `skill-detail`, `agent` | 可选 | | 数据导入 / 失败重置 | `sync``async` | `skill-detail`, `agent` | 可选 |
| **单条**副作用 RPA | `async`(建议) | `agent`, `skill-detail` | **建议** | | **单条**副作用 RPA | `async`(建议) | `agent`, `skill-detail` | **建议** |
| **批量顺序** RPApick | `async`(建议) | `toolbar`, `cron`, `agent` | **建议** | | **批量顺序** RPApick | `async`(建议) | `toolbar`, `cron`, `agent` | **建议** |

View File

@@ -81,7 +81,7 @@ def test_whatever():
未设置环境变量 ⇒ 等价 `unit` 未设置环境变量 ⇒ 等价 `unit`
默认策略摘要:**不要在 unittest 必跑路径误设 `OPENCLAW_TEST_TARGET=real_*`**。 默认策略摘要:**不要在 unittest 必跑路径误设 `OPENCLAW_TEST_TARGET=real_*`**CI 仍离线)。业务 `.env` 模板默认是 `real_rpa`,与「单测用 mock」不冲突**mock 通 ≠ 交活**
档位读取与业务代码一致:经 `jiangchang_skill_core.config.get("OPENCLAW_TEST_TARGET")`(见 `CONFIG.md`)。 档位读取与业务代码一致:经 `jiangchang_skill_core.config.get("OPENCLAW_TEST_TARGET")`(见 `CONFIG.md`)。
@@ -137,7 +137,7 @@ Golden fixture 流程同理([`tests/samples/test_golden_cases.py.sample`](../t
浏览器 RPA 走 `simulator_rpa` 档位联调前,逐项确认: 浏览器 RPA 走 `simulator_rpa` 档位联调前,逐项确认:
- [ ] 数据目录 `.env``OPENCLAW_TEST_TARGET=simulator_rpa`(非 `mock` / `unit` - [ ] 数据目录 `.env``OPENCLAW_TEST_TARGET=simulator_rpa`(非 `mock` / `unit`
- [ ] account-manager`platform ensure` + 账号 `active` + 有 `profile_dir`(无 profile 禁止开浏览器,见 `RPA.md` §0.2 - [ ] account-manager`ensure-web`(或 `pick-web --ensure`+ 账号`profile_dir`(无 profile 禁止开浏览器,见 `RPA.md` §0.2
- [ ] `OPENCLAW_BROWSER_HEADLESS=0`(联调有头;勿默认无头) - [ ] `OPENCLAW_BROWSER_HEADLESS=0`(联调有头;勿默认无头)
- [ ] 目标 sandbox UI 已部署(跨团队;本地可用 `sandbox/demo_app.html` - [ ] 目标 sandbox UI 已部署(跨团队;本地可用 `sandbox/demo_app.html`
- [ ] 失败先查 `rpa-artifacts/` 截图,再查 Chrome profile 缓存(`--user-data-dir` 手工打开清站点数据) - [ ] 失败先查 `rpa-artifacts/` 截图,再查 Chrome profile 缓存(`--user-data-dir` 手工打开清站点数据)
@@ -262,7 +262,7 @@ Golden fixture 流程同理([`tests/samples/test_golden_cases.py.sample`](../t
- [ ] `requirements.txt` **不含** `jiangchang-platform-kit` / `playwright` - [ ] `requirements.txt` **不含** `jiangchang-platform-kit` / `playwright`
- [ ]`scripts/jiangchang_skill_core/` vendored 副本 - [ ]`scripts/jiangchang_skill_core/` vendored 副本
- [ ] `platform_kit_min_version` **>= 1.2.0**`SKILL.md` + `constants.py` - [ ] `platform_kit_min_version` **>= 1.2.2**`SKILL.md` + `constants.py`
- [ ] `health` 能输出 `platform_kit_version_ok`(或等价诊断行) - [ ] `health` 能输出 `platform_kit_version_ok`(或等价诊断行)
- [ ] `config-path` 可输出用户 `.env` 路径 JSON - [ ] `config-path` 可输出用户 `.env` 路径 JSON
- [ ] `pytest.ini` 存在且 `python_files` 只收集 `test_*.py` / `*_test.py` - [ ] `pytest.ini` 存在且 `python_files` 只收集 `test_*.py` / `*_test.py`

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -80,7 +80,7 @@ Action 是**可选能力**
| 字段 / 层 | 只决定什么 | 不决定什么 | | 字段 / 层 | 只决定什么 | 不决定什么 |
|-----------|------------|------------| |-----------|------------|------------|
| **`placements`** | Action **出现在哪里**toolbar / cron / agent / skill-detail | 不决定 sync/async | | **`placements`** | Action **出现在哪里**toolbar / row / batch / cron / agent / skill-detail | 不决定 sync/async |
| **`bind.tables`** | 数据管理里出现在**哪些业务表** | 不决定业务实现 | | **`bind.tables`** | 数据管理里出现在**哪些业务表** | 不决定业务实现 |
| **`executionProfile`** | 宿主是**同步等待**还是**异步 Job** | 不限制可展示入口 | | **`executionProfile`** | 宿主是**同步等待**还是**异步 Job** | 不限制可展示入口 |
| **`entrypoint`** | 实际执行哪个 CLI 命令与参数 | 不写业务逻辑 | | **`entrypoint`** | 实际执行哪个 CLI 命令与参数 | 不写业务逻辑 |
@@ -95,9 +95,9 @@ Action 是**可选能力**
| 顶层 | `schemaVersion``skill``actions` | | 顶层 | `schemaVersion``skill``actions` |
| action | `id``label``description``placements``entrypoint`、**`executionProfile`(必填)**;可选 `inputSchema``confirmation`、**`bind`** | | action | `id``label``description``placements``entrypoint`、**`executionProfile`(必填)**;可选 `inputSchema``confirmation`、**`bind`** |
| entrypoint | `type = cli``command``args`**必填** JSON 数组,项为 `string` / `number` / `boolean` | | entrypoint | `type = cli``command``args`**必填** JSON 数组,项为 `string` / `number` / `boolean` |
| placements | `toolbar``skill-detail``agent``cron` | | placements | `toolbar``row``batch``skill-detail``agent``cron` |
| **bind** | `{ "tables": ["business_table"] }`见下节) | | **bind** | `{ "tables": ["business_table"] }`;可选 `inputMapping`(行内/批量预填,见下节) |
| inputSchema | 根 `type: object`properties 使用 `string` / `number` / `boolean`;支持 `enum``default``required``sensitive` | | inputSchema | 根 `type: object`properties 使用 `string` / `number` / `boolean`;支持 `enum``default``required``sensitive``readOnly` |
| confirmation | `{ "message": "..." }` | | confirmation | `{ "message": "..." }` |
| **executionProfile** | `"sync"` \| `"async"`**必须显式写出**,禁止依赖缺省) | | **executionProfile** | `"sync"` \| `"async"`**必须显式写出**,禁止依赖缺省) |
@@ -113,14 +113,16 @@ Action 是**可选能力**
规则: 规则:
- `bind` 为 object`additionalProperties: false`;当前允许 `tables` - `bind` 为 object`additionalProperties: false`;当前允许 `tables`(必填键当存在 bind 时)与可选 `inputMapping`
- `tables` 非空数组、元素唯一、英文 **snake_case**、不得为空字符串。 - `tables` 非空数组、元素唯一、英文 **snake_case**、不得为空字符串。
- **`placements``toolbar` 时必须显式声明非空 `bind.tables`**(模板 Schema `if/then` + 自动测试强制)。 - **`placements``toolbar` 时必须显式声明非空 `bind.tables`**(模板 Schema `if/then` + 自动测试强制)。
- `row` / `batch` **建议**同样声明 `bind.tables`,避免在无关表上露出按钮;未声明时宿主可能按兼容逻辑在更多表上展示。
- 表名须真实存在于技能 SQLite 与 `_jiangchang_tables` - 表名须真实存在于技能 SQLite 与 `_jiangchang_tables`
- 希望出现在多张表时,显式列出所有表;**不要**依赖「无 bind 则全表展示」——那是宿主对**旧技能**的兼容,不是新技能标准。 - 希望出现在多张表时,显式列出所有表;**不要**依赖「无 bind 则全表展示」——那是宿主对**旧技能**的兼容,不是新技能标准。
- 本阶段**不**扩展 `selection` / `inputMapping` / `columns` / row binding / concurrency / locks - **`inputMapping`(可选)**:把行/勾选映射到 `inputSchema` 字段,例如 `"leadId": "$row.$pk"``"ids": "$selection.ids"`。适合 `row` / `batch`。宿主仍会提交这些字段;可用 `inputSchema.properties.*.readOnly: true` 在表单中只读展示
- `concurrency` / `locks` 仍未稳定,**不要**写入新技能 manifest。
默认模板 `actions.json` 仅含 `health` / `version` / `config-path`,不放在 toolbar故无需 `bind` 示例污染默认 manifest。业务示例 默认模板 `actions.json` 仅含 `health` / `version` / `config-path`,不放在 toolbar/row/batch,故无需 `bind` 示例污染默认 manifest。业务示例
```json ```json
{ {
@@ -134,16 +136,42 @@ Action 是**可选能力**
} }
``` ```
行内示例:
```json
{
"id": "retry-one",
"label": "重试本行",
"description": "对当前行执行一次业务重试。",
"placements": ["row"],
"executionProfile": "async",
"bind": {
"tables": ["business_records"],
"inputMapping": { "recordId": "$row.$pk" }
},
"inputSchema": {
"type": "object",
"required": ["recordId"],
"properties": {
"recordId": { "type": "string", "title": "记录 ID", "readOnly": true }
}
},
"entrypoint": {
"type": "cli",
"command": "retry",
"args": ["--id", "{{recordId}}"]
}
}
```
## 预留能力(当前不可当作已实现能力) ## 预留能力(当前不可当作已实现能力)
| 类别 | 预留项 | 说明 | | 类别 | 预留项 | 说明 |
|------|--------|------| |------|--------|------|
| placements | `row``batch` | 行内 / 批量入口,未来扩展;新技能示例不得使用 |
| action 字段 | `concurrency``locks` | 并发与锁控制,未稳定开放 | | action 字段 | `concurrency``locks` | 并发与锁控制,未稳定开放 |
| inputSchema | `array`、nested `object``oneOf` 等 | 复杂表单,不属于当前推荐范围 | | inputSchema | `array`、nested `object``oneOf` 等 | 复杂表单,不属于当前推荐范围 |
`bind` **不是**保留字段;已作为 Phase 1 表级绑定正式能力 `bind``row``batch` **不是**保留能力;已按匠厂宿主正式支持写入本规范
## 执行模型 ## 执行模型
宿主将 action 转为 argv再用**共享 Python**(用户数据目录下 `python-runtime/.venv`)执行: 宿主将 action 转为 argv再用**共享 Python**(用户数据目录下 `python-runtime/.venv`)执行:
@@ -168,7 +196,7 @@ python {skill_root}/scripts/main.py <command> <args...>
| `sync`(或省略时的宿主缺省) | 调用方等待结束,直接返回结构化结果或结构化错误;**不**创建后台 Job | 不进 | | `sync`(或省略时的宿主缺省) | 调用方等待结束,直接返回结构化结果或结构化错误;**不**创建后台 Job | 不进 |
| `async`(须显式声明) | 宿主创建后台 Job立即返回 `jobId`;用 `emit` / `checkpoint` / `finish` 报告进度与终态 | **进入** | | `async`(须显式声明) | 宿主创建后台 Job立即返回 `jobId`;用 `emit` / `checkpoint` / `finish` 报告进度与终态 | **进入** |
合法组合(全部允许):`sync|async` × `toolbar|cron|agent|skill-detail` 合法组合(全部允许):`sync|async` × `toolbar|row|batch|cron|agent|skill-detail`
经验建议(**不是** placement 硬限制):耗时不可预测、需要暂停/停止/进度展示的任务,用 `async`;其余默认写 `sync` 经验建议(**不是** placement 硬限制):耗时不可预测、需要暂停/停止/进度展示的任务,用 `async`;其余默认写 `sync`
@@ -199,26 +227,27 @@ HINT: <next step>
| 值 | 含义 | | 值 | 含义 |
|----|------| |----|------|
| `toolbar` | 数据管理第一排技能按钮(须配合 `bind.tables` | | `toolbar` | 数据管理表顶栏技能按钮(须配合 `bind.tables` |
| `skill-detail` | 技能详情或技能市场详情页 | | `row` | 数据管理行内操作(建议 `bind.tables` + `inputMapping`;表需有主键) |
| `batch` | 数据管理批量操作(需勾选行;无勾选时宿主禁用按钮) |
| `skill-detail` | 技能详情页直调(与安装/四 Tab 同属技能市场详情;已声明则须自测) |
| `agent` | Agent 可直接调用(`run_skill_action` | | `agent` | Agent 可直接调用(`run_skill_action` |
| `cron` | 定时任务可直接调用 | | `cron` | 定时任务「技能直调」 |
**预留(模板示例请勿使用):** `row``batch`
### Action 类型 × placements常见示例不是限制 ### Action 类型 × placements常见示例不是限制
下表只表示**常见用法示例**。实际 `placements` 以 manifest 配置为准;**`sync` / `async` 不影响展示入口**。 下表只表示**常见用法示例**。实际 `placements` 以 manifest 配置为准;**`sync` / `async` 不影响展示入口**。已声明的入口都要在宿主自测,不要只测其中一条。
| Action 类型 | 常见 executionProfile | 常见 placements | confirmation | | Action 类型 | 常见 executionProfile | 常见 placements | confirmation |
|-------------|----------------------|-----------------|--------------| |-------------|----------------------|-----------------|--------------|
| health / version / config-path | sync | skill-detail, agent | 无 | | health / version / config-path | sync | skill-detail, agent | 无 |
| 统计 / 只读查询 | sync | skill-detail, agent | 无 | | 统计 / 只读查询 | sync | skill-detail, agent | 无 |
| 数据同步(外源 → 本地库) | sync 或 async | toolbar + cron + agent + skill-detail按需 | 可选 | | 数据同步(外源 → 本地库) | sync 或 async | toolbar + cron + agent + skill-detail按需 | 可选 |
| 单行重试 / 行内处理 | sync 或 async | row可加 agent | 可选 |
| 批量处理勾选行 | sync 或 async | batch可加 toolbar/cron | 可选 |
| 导入 / 重置 | sync 或 async | skill-detail, agent | 可选 | | 导入 / 重置 | sync 或 async | skill-detail, agent | 可选 |
| 单条副作用 RPA | async建议 | agent, skill-detail | **建议** | | 单条副作用 RPA | async建议 | agent, skill-detail | **建议** |
| 批量顺序 pick RPA | async建议 | toolbar, cron, agent | **建议** | | 批量顺序 pick RPA | async建议 | toolbar, cron, agent | **建议** |
## 同步数据 vs 导出当前表 vs 特殊业务报告 ## 同步数据 vs 导出当前表 vs 特殊业务报告
不要混用「导出」一词: 不要混用「导出」一词:
@@ -236,7 +265,7 @@ HINT: <next step>
当前宿主稳定支持: 当前宿主稳定支持:
- `object` / `string` / `number` / `boolean` - `object` / `string` / `number` / `boolean`
- `enum` / `default` / `required` / `sensitive` - `enum` / `default` / `required` / `sensitive` / `readOnly`
- 简单模板替换(`{{fieldName}}` - 简单模板替换(`{{fieldName}}`
示例: 示例:
@@ -281,6 +310,8 @@ HINT: <next step>
- `enum` 在宿主中渲染为选择框 - `enum` 在宿主中渲染为选择框
- `required` 字段在提交前校验 - `required` 字段在提交前校验
- `sensitive` 字段不得出现在低信任入口 - `sensitive` 字段不得出现在低信任入口
- `readOnly: true` 时数据管理表单只读展示该字段(仍随提交传入);适合行内/勾选预填的主键、编号列表
- `placements``batch` 的动作在数据管理表顶常驻,无勾选时禁用(与表删除一致)
- 需要确认的副作用操作使用 `confirmation.message` - 需要确认的副作用操作使用 `confirmation.message`
- 模板参数使用 `{{fieldName}}`,且必须在 `inputSchema.properties` 中声明 - 模板参数使用 `{{fieldName}}`,且必须在 `inputSchema.properties` 中声明
- `entrypoint.args` 项当前只允许 `string``number``boolean`;不允许对象或 `null` - `entrypoint.args` 项当前只允许 `string``number``boolean`;不允许对象或 `null`

View File

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

View File

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

View File

@@ -15,7 +15,6 @@ from util.constants import SKILL_SLUG
_TEMPLATE_PLACEHOLDER_SLUG = "your-skill-slug" _TEMPLATE_PLACEHOLDER_SLUG = "your-skill-slug"
_ALLOWED_PLACEMENTS = frozenset({"toolbar", "row", "batch", "cron", "agent", "skill-detail"}) _ALLOWED_PLACEMENTS = frozenset({"toolbar", "row", "batch", "cron", "agent", "skill-detail"})
_RESERVED_PLACEMENTS = frozenset({"row", "batch"})
_RESERVED_ACTION_FIELDS = frozenset({"concurrency", "locks"}) _RESERVED_ACTION_FIELDS = frozenset({"concurrency", "locks"})
_ALLOWED_EXECUTION_PROFILES = frozenset({"sync", "async"}) _ALLOWED_EXECUTION_PROFILES = frozenset({"sync", "async"})
_REQUIRED_MANIFEST_KEYS = frozenset({"schemaVersion", "skill", "actions"}) _REQUIRED_MANIFEST_KEYS = frozenset({"schemaVersion", "skill", "actions"})
@@ -275,13 +274,13 @@ class TestActionsManifest(unittest.TestCase):
self.assertEqual(missing, [], msg="\n".join(missing)) self.assertEqual(missing, [], msg="\n".join(missing))
def test_schema_allows_all_placement_execution_profile_combinations(self) -> None: def test_schema_allows_all_placement_execution_profile_combinations(self) -> None:
"""POLICY-SKILL-ACTION-0044 placements × 2 profiles 必须全部通过 Schema。""" """POLICY-SKILL-ACTION-0046 placements × 2 profiles 必须全部通过 Schema。"""
try: try:
import jsonschema import jsonschema
except ImportError: except ImportError:
self.fail("jsonschema is required to validate placement×executionProfile orthogonality") self.fail("jsonschema is required to validate placement×executionProfile orthogonality")
schema = _load_actions_schema() schema = _load_actions_schema()
placements = ("toolbar", "cron", "agent", "skill-detail") placements = ("toolbar", "row", "batch", "cron", "agent", "skill-detail")
profiles = ("sync", "async") profiles = ("sync", "async")
for placement in placements: for placement in placements:
for profile in profiles: for profile in profiles:
@@ -346,16 +345,6 @@ class TestActionsManifest(unittest.TestCase):
for snippet in forbidden_snippets: for snippet in forbidden_snippets:
self.assertNotIn(snippet, text) self.assertNotIn(snippet, text)
def test_template_example_does_not_use_reserved_placements(self) -> None:
manifest = _load_actions_manifest()
for action in manifest["actions"]:
placements = set(action.get("placements") or [])
overlap = placements & _RESERVED_PLACEMENTS
self.assertFalse(
overlap,
msg=f"{action['id']} uses reserved placements not yet supported in template: {overlap}",
)
def test_template_variables_have_input_schema(self) -> None: def test_template_variables_have_input_schema(self) -> None:
manifest = _load_actions_manifest() manifest = _load_actions_manifest()
for action in manifest["actions"]: for action in manifest["actions"]:

View File

@@ -84,8 +84,8 @@ class TestConfigBootstrap(unittest.TestCase):
config.reset_cache() config.reset_cache()
with open(example, encoding="utf-8") as f: with open(example, encoding="utf-8") as f:
example_text = f.read() example_text = f.read()
self.assertIn(_env_line(_TEST_TARGET_KEY, "mock").strip(), example_text) self.assertIn(_env_line(_TEST_TARGET_KEY, "real_" + "rpa").strip(), example_text)
self.assertEqual(config.get(_TEST_TARGET_KEY), "mock") self.assertEqual(config.get(_TEST_TARGET_KEY), "real_" + "rpa")
def test_config_path_outputs_json(self) -> None: def test_config_path_outputs_json(self) -> None:
with IsolatedDataRoot(user_id="_cfg_path"): with IsolatedDataRoot(user_id="_cfg_path"):

View File

@@ -1211,7 +1211,7 @@ class TestPolicySkillAction004(unittest.TestCase):
msg=_policy_msg( msg=_policy_msg(
"POLICY-SKILL-ACTION-004", "POLICY-SKILL-ACTION-004",
"tests/test_actions_manifest.py", "tests/test_actions_manifest.py",
"missing Schema 4×2 orthogonality matrix test", "missing Schema 6×2 orthogonality matrix test",
), ),
) )

View File

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

View File

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

View File

@@ -61,7 +61,7 @@ class TestEnvExampleVideoDefaults(unittest.TestCase):
with open(path, encoding="utf-8") as f: with open(path, encoding="utf-8") as f:
text = f.read() text = f.read()
for key in ( for key in (
"OPENCLAW_" + "TEST_TARGET=mock", "OPENCLAW_" + "TEST_TARGET=real_" + "rpa",
"OPENCLAW_RECORD_VIDEO=0", "OPENCLAW_RECORD_VIDEO=0",
"OPENCLAW_ARTIFACTS_ON_FAILURE=1", "OPENCLAW_ARTIFACTS_ON_FAILURE=1",
"OPENCLAW_BROWSER_HEADLESS=0", "OPENCLAW_BROWSER_HEADLESS=0",

View File

@@ -1,5 +1,15 @@
# skill-template 工具 # skill-template 工具
## 业务仓也可以直接 clone来源 A
若项目经理已在**公司自建 Gitea**[https://git.jc2009.com/](https://git.jc2009.com/),基于开源 Gitea 自建,**不是** Gitea 官网)开好业务技能仓库并授权,技术人员应直接:
```powershell
git clone https://git.jc2009.com/<org>/<your-skill-slug>.git
```
完整说明见 [`../development/DEVELOPMENT.md`](../development/DEVELOPMENT.md) §4「来源 A」。公开公共仓含本模板、account-manager、platform-kit登记见 [`../development/SHARED_REPOS.md`](../development/SHARED_REPOS.md)。下面脚手架用于**本地已有 skill-template、要新建尚未灌仓目录**的场景(来源 B
## 推荐:脚手架创建新技能 ## 推荐:脚手架创建新技能
**不要**用资源管理器整文件夹复制 `skill-template`(会带走隐藏 `.git`,导致 push 串库)。 **不要**用资源管理器整文件夹复制 `skill-template`(会带走隐藏 `.git`,导致 push 串库)。