Compare commits

...

5 Commits

Author SHA1 Message Date
a125b00285 Release v1.0.110: 沉淀壳子页与页面调用契约
All checks were successful
技能自动化发布 / release (push) Successful in 9s
2026-09-19 16:23:23 +08:00
2a3e971c76 docs: document JobStopped and lease cleanup contract for interrupt
All checks were successful
技能自动化发布 / release (push) Successful in 7s
2026-09-17 13:26:28 +08:00
db2ed089a3 Release v1.0.108: precipitate user-facing copy contract
All checks were successful
技能自动化发布 / release (push) Successful in 8s
2026-09-12 14:08:33 +08:00
c43c26ad15 Release v1.0.107: precipitate bind.selectionMode contract
All checks were successful
技能自动化发布 / release (push) Successful in 5s
2026-09-12 13:42:34 +08:00
fe4209b37f Release v1.0.106: document optional OEM brands metadata
All checks were successful
技能自动化发布 / release (push) Successful in 7s
2026-09-07 15:40:31 +08:00
28 changed files with 449 additions and 47 deletions

View File

@@ -9,6 +9,34 @@
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
## 1.0.110
- 数据管理侧栏除了生成出来的 HTML 快照,也会直接显示安装包里 `assets/html/pages/` 的页面,装完就有,不用点生成
- 这些页面可以调用本技能已经声明的命令:宿主代为执行 Python,把 JSON 交回页面
## 1.0.109
- 沉淀任务中断与账号租约契约:禁止用裸 `except Exception` 吞掉 `JobStopped`;`finally` 先释租约再关浏览器;宿主只通知不代收租约
- 补充 CHECKLIST A5-13(中断后租约须释放)及 ACCOUNT_MANAGER / LOGGING / ADAPTER / 样例 README
## 1.0.108
- 沉淀数据管理「用户可见文案」契约:弹窗 description / 确认黄条 / 参数说明 / 任务失败 message 须全中文产品用语
- 明确禁止把字段名、CLI 子命令、async/pending、Agent 禁令写进用户可见文案;Agent 路由仍放 SKILL.md
- 修正 `selection-mode-optional` 示例为口语文案;更新 ACTIONS / RUNTIME / CHECKLIST A4-10
## 1.0.107
- 沉淀数据管理 `bind.selectionMode` 契约(`required` / `none` / `optional`):同一按钮可「有勾选走 ids、无勾选走 pickCount」
- 明确省略时的宿主兼容推断、空选数量字段名约定,以及勿再拆同义双按钮
- 更新 ACTIONS.md / Schema / SKILL_ACTION_RUNTIME / CHECKLIST A4-9;新增 `selection-mode-optional.action.example.json`
## 1.0.106
- 文档契约:补充可选 `metadata.openclaw.brands`(OEM 市场编码 → `skill_market_relations`)
- 明确未写则仅挂 `jiangchang`;与 `developer_ids` 分工;勿用 Gitea 组织名推断市场
- 更新 `SKILL.md`、`DEVELOPMENT.md` §6、`development/README.md`、`REQUIREMENTS.md` 与发布检查清单
## 1.0.105
- 沉淀数据管理 form 与「已选择」摘要对齐契约:映射 `readOnly` 字段默认不占参数区(仍提交),与 confirm 信息架构一致

View File

@@ -1,7 +1,7 @@
---
name: 技能开发模板(通用业务版)
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
version: 1.0.105
version: 1.0.110
author: 深圳匠厂科技有限公司
metadata:
openclaw:
@@ -11,6 +11,9 @@ metadata:
category: "通用"
developer_ids:
- 12345 # 占位,请换成你的匠厂用户 ID
# brands: # 可选;未写则发布后仅挂匠厂市场(jiangchang)
# - jiangchang # 匠厂主站
# - huashang # 华商等 OEM:按目标市场填写,可多选
allowed-tools:
- bash
---
@@ -35,11 +38,13 @@ allowed-tools:
- **数据管理**:既是 **UI 执行面**(点按钮走与对话相同的闭环),也是数据与过程可见;不是「只读库」。
- **`placements`**:只决定 Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail)。其中 `row` 表示**行级入口**(宿主可呈现为操作列、行右键、行内「⋯」等;技能配什么就显示什么,无独立菜单 DSL)。数据管理技能按钮顺序见 `actions` **数组相对顺序**(详见 `references/ACTIONS.md`),无独立 sortOrder。
- **`bind.tables`**:只决定数据管理出现在哪些表;含 `toolbar` 时**必须**显式非空绑定。
- **`bind.selectionMode`(可选)**:表勾选如何驱动 Action(`required` / `none` / `optional`);省略由宿主推断。同一能力勿拆两个同义顶栏按钮,见 `references/ACTIONS.md`。
- **用户可见文案**:数据管理弹窗 / 任务中心文案须全中文产品用语;勿把 `pickCount`、`async`、`pending`、CLI 子命令写进弹窗(见 `references/ACTIONS.md`「用户可见文案」)。
- **`executionProfile`**:只决定执行方式——`sync` 当场返回结果,`async` 进**任务中心**。与 placements / 入口**无关**。心智上多数为 sync,仅长任务 / RPA 标 async;宿主省略该字段时按 sync。每个 Action **仍必须**显式写 `sync` 或 `async`。
- **`policy`(可选)**:需要按用户/角色控制数据管理按钮或行列时,在 `actions.json` 顶层启用 Data Policy,由技能 `policy` CLI 输出 Snapshot;**未启用 = 与今日一致(L0)**。详见 `references/POLICY.md`。用户/角色数据源(飞书/库/API)属技能内部,宿主不托管。
- **`cellPresentation`(可选)**:需要数据管理「某格特殊标色/强调」时启用;技能按当前数据写入呈现(语义 tone),宿主映射主题样式;**未启用 = P0 无标色**。用途不限(超时、预警、驳回等)。详见 `references/CELL_PRESENTATION.md`。
- **业务行生命周期(有处理队列时)**:对某一行做生成/触达/私信等处理时,默认只操作「可操作」角色状态;开始抢占「进行中」;结束回写成功或失败(失败带原因);重做靠人工改回可操作。状态名由本技能自定。详见 `references/WORK_ITEM_STATUS.md`。
- **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;文件型特殊报告才另做 `export-*`(落 `output/`);**HTML 整页报告**写 `html/` 并用固定 Action id `render-html`(见 `references/ACTIONS.md`)。
- **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;文件型特殊报告才另做 `export-*`(落 `output/`);**HTML**:壳子页放 `assets/html/pages/`,快照写 `html/` 并用固定 Action id `render-html`;页面用 `jiangchang.call` 调已声明命令(见 `references/ACTIONS.md`)。
- **准备 vs 执行(适用则拆)**:若技能存在「先写入/准备本地数据」与「再对外产生副作用」两段,Agent 必须能分开触发;禁止默认绑成不可拆的一步。一键无准备态的技能可跳过本条。
### 如何触发(能力类型)
@@ -167,3 +172,9 @@ python {baseDir}/scripts/main.py init-db
- 开发期技能常为不公开:不加本人 ID,开发者自己也无法在市场安装自测。取 ID 与写入步骤见 `development/DEVELOPMENT.md` §6。
- 当 `access_scope = 0`(不公开)时,平台会把 `developer_ids` 中的用户自动补写到 `skill_user_access`。
- `developer_ids` 建议写为正整数数组;第一个 ID 会作为主开发者同步到 `skills.developer_id`。
- `metadata.openclaw.brands`(**可选**):OEM 技能市场编码列表,写入平台 `skill_market_relations.market_code`。兼容别名 `oem_clients` / `client_keys`。
- **未写或为空**:发布时仅幂等补默认 `jiangchang`(匠厂主站),兼容老技能。
- **显式列表**(如 `jiangchang`、`huashang`):发布时幂等补写对应市场(**只补不删**);客户端靠 Header `X-Market-Code` 过滤目录。
- **不是** Gitea 组织名推断:仓库在 `client-huashang` 下 ≠ 自动出现在华商市场,必须以 `brands` 声明。
- 与 `developer_ids` 分工:`brands` 管「进哪个 OEM 市场目录」;`developer_ids` 管「不公开时谁能看见/安装」。
- 完整步骤与验收见 `development/DEVELOPMENT.md` §6「关于 brands」。

View File

@@ -2,11 +2,12 @@
- `actions.json`:Skill Action manifest 最小示例(按入口定位表决定是否保留/扩展;见 `development/PRODUCT_SURFACES.md`)。每个 action **必须**显式声明 `executionProfile`(`sync` \| `async`)。默认仅 `health` / `version` / `config-path`,不放 toolbar,故无 `bind` 示例。
- **业务技能注意:** 若主路径在数据管理,准备/载入/导入类 Action 应挂 `toolbar` + `bind.tables`(用户看得见),**不要**只挂 `skill-detail` 却在教程里写成「技能详情点按钮」。见 `development/TUTORIAL_SURFACES.md` §5、`references/ACTIONS.md`「用户可见性」。
- `html/example-report.html`:HTML 报告页最小壳(`{{TITLE}}` / `{{GENERATED_AT}}` / `{{BODY}}`);含数据管理 iframe 预览布局基线(流体铺满、禁横向滚动);渲染后须写入数据目录 `html/`,见 `references/ACTIONS.md`「HTML 页面报告」。
- `examples/`:CLI 成功输出形状示例、Action 形状参考(含 `path-root-file-action.example.json`、`selection-label-column.action.example.json`、`render-html.action.example.json` 等;虚构路径与数据)。
- `html/example-report.html`:HTML 快照页最小壳(`{{TITLE}}` / `{{GENERATED_AT}}` / `{{BODY}}`);含数据管理 iframe 预览布局基线(流体铺满、禁横向滚动);渲染后须写入数据目录 `html/`。不要放进 `pages/`。
- `html/pages/*.html`:装完就出现在侧栏的壳子页(宿主直接读安装包,不经生成)。页面用 `jiangchang.call` 调本技能已声明的命令。见 `references/ACTIONS.md`「HTML 页面报告」。模板仓库不要在这里放示例页。
- `examples/`:CLI 成功输出形状示例、Action 形状参考(含 `path-root-file-action.example.json`、`selection-label-column.action.example.json`、`selection-mode-optional.action.example.json`、`render-html.action.example.json` 等;虚构路径与数据)。
- `schemas/`:轻量 JSON Schema(`skill-actions.schema.json` 为**新技能严格规范**)。
`skill-actions.schema.json` 使用 `additionalProperties: false`。正式支持 `bind.tables`(toolbar Action 必填)与可选 `bind.inputMapping` / `columnMapping` / **`labelColumn`**(选择摘要标签列);placements 含 `toolbar` / `row` / `batch` / `cron` / `agent` / `skill-detail`(对齐匠厂宿主)。数据管理 confirm/form 弹层默认展示「已选择」摘要;**form + 有摘要时映射 `readOnly` 默认不占参数区(仍提交)**,可选 **`revealInForm`** 强制显示。可选 `hoverHint` 为数据管理工具栏悬停(省略不显示,不回退)。`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` / `columnMapping` / **`labelColumn`**(选择摘要标签列)/ **`selectionMode`**(`required` \| `none` \| `optional`:表勾选如何驱动 Action;省略则宿主兼容推断)。placements 含 `toolbar` / `row` / `batch` / `cron` / `agent` / `skill-detail`(对齐匠厂宿主)。数据管理 confirm/form 弹层在会应用勾选时展示「已选择」摘要;**form + 有摘要时映射 `readOnly` 默认不占参数区(仍提交)**,可选 **`revealInForm`** 强制显示。弹窗 `description` / 确认黄条 / 参数说明须**用户可见中文**(勿写 `pickCount`/`async`/`pending`/CLI 子命令;见 ACTIONS.md「用户可见文案」)。可选 `hoverHint` 为数据管理工具栏悬停(省略不显示,不回退)。`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);写法契约见 [`development/TUTORIAL_SURFACES.md`](../development/TUTORIAL_SURFACES.md)
- Agent 调用/编排参考见 [`references/`](../references/)(含 [`ACTIONS.md`](../references/ACTIONS.md))

View File

@@ -6,4 +6,6 @@
- `render-html.action.example.json`:HTML 报告「重新生成」Action 形状参考(固定 id `render-html`;见 `references/ACTIONS.md`)。
- `lookup-action.example.json`:Action 内联 Lookup 形状参考(单选 + `multiple: true` 多选并列;跨表引用勿填编号;见 `references/ACTIONS.md`「跨表引用:Lookup」)。
- `visible-when-action.example.json`:Action 参数字段条件显隐 `visibleWhen` 形状参考(互斥模式联动;见 `references/ACTIONS.md`「字段条件显隐」;≠ Policy L3)。
- `selection-label-column.action.example.json`:选择摘要 `bind.labelColumn` 形状参考(见 `references/ACTIONS.md`「选择摘要」)。
- `selection-mode-optional.action.example.json`:`bind.selectionMode: "optional"` 智能勾选/按数量合并形状参考;用户可见文案为口语中文(见 `references/ACTIONS.md`「勾选驱动模式」「用户可见文案」)。
- `replaces-create.action.example.json` / `replaces-update.action.example.json` / `replaces-delete.action.example.json` / `replaces-import.action.example.json`:数据管理默认按钮接管(`replaces`)形状参考;**import 时 UI 仍用宿主「导入数据」面板**,确认后调技能写库(见 `references/ACTIONS.md`「`replaces`」)。

View File

@@ -0,0 +1,46 @@
{
"id": "process-queue",
"label": "处理队列",
"description": "从业务队列处理待处理记录。有勾选则处理勾选的;没有勾选则按下方数量从队列领取。已成功或失败的会自动跳过。进度可在任务中心查看。",
"placements": ["toolbar", "batch", "cron", "agent"],
"executionProfile": "async",
"interaction": "form",
"confirmation": {
"message": "将按规则处理记录(有勾选则处理勾选的;未勾选则按数量从队列领取),是否继续?"
},
"bind": {
"tables": ["business_items"],
"selectionMode": "optional",
"labelColumn": "display_name",
"inputMapping": {
"itemIds": "$selection.ids"
}
},
"inputSchema": {
"type": "object",
"required": ["itemIds", "pickCount"],
"properties": {
"itemIds": {
"type": "string",
"title": "编号列表",
"description": "有勾选时自动填入,一般无需修改",
"readOnly": true
},
"pickCount": {
"type": "number",
"title": "处理数量",
"description": "没有勾选时:从待处理队列里领取的条数",
"default": 1,
"minimum": 1
}
}
},
"entrypoint": {
"type": "cli",
"command": "process",
"args": ["--ids", "{{itemIds}}", "--pick", "{{pickCount}}"]
},
"hostEffects": {
"onSuccess": ["reloadTable", "clearSelection"]
}
}

View File

@@ -11,3 +11,4 @@ lianlian
single-window
export-rebate
etax
colipu

View File

@@ -311,9 +311,14 @@
"minLength": 1,
"pattern": "^[a-z][a-z0-9_]*$",
"description": "可选。数据管理 Action 确认/表单弹层「已选择」摘要用的可读标签列(#id + 标签)。仅影响展示,不改变提交 payload。省略时宿主按常见列名启发(title/name 等),再不行只显示编号。"
},
"selectionMode": {
"type": "string",
"enum": ["required", "none", "optional"],
"description": "可选。数据管理勾选如何驱动本 Action:required=必须有勾选(无勾选禁用);none=忽略表勾选(不出现假「已选择」);optional=有勾选走 $selection.ids,无勾选走 pickCount/maxLeads 等空选数量字段。省略时宿主推断:有 $selection.ids 或 batch → required,否则 → none。与 placements/executionProfile 正交。"
}
},
"description": "表级绑定;toolbar 必填。可选 inputMapping / columnMapping / labelColumn。concurrency/locks 勿写入。"
"description": "表级绑定;toolbar 必填。可选 inputMapping / columnMapping / labelColumn / selectionMode。concurrency/locks 勿写入。"
},
"hostEffectId": {
"type": "string",

View File

@@ -173,7 +173,9 @@ python scripts/main.py account open <account_id>
- 租约持有者应使用技能 slug 或可追踪任务标识,`purpose` 写明用途。
- 租约 TTL 应覆盖正常任务时长;长任务需要平台支持的续租策略,不能依赖永不过期。
- `finally release_lease(...)` 是最低要求,浏览器关闭失败也不能跳过释放尝试。
- 进程被强制结束后可能留下租约;先用 `lease list --active` 核对,确认没有真实任务仍在使用后再释放或清理。
- **清理顺序**:`finally` 内**先 `release_lease`,再关浏览器**。关浏览器(含 CDP / 强杀 Profile 进程)可能耗时或卡住;先释租约可避免「窗还没关完、账号已被下一任务抢走失败」的空窗拉长。
- **任务中心中断**:宿主只下发 `stop` 并等待技能退出;技能须在闸门抛出 `JobStopped` 后进入 `finally` 自清。禁止用 `except Exception` 吞掉 `JobStopped`(详见 [`LOGGING.md`](LOGGING.md) §2.5)。不要指望宿主代收租约。
- 进程被强制结束后可能留下租约;先用 `lease list --active` 核对,确认没有真实任务仍在使用后再释放或清理;亦可等待 TTL 过期。
- 禁止看到租约冲突就直接删数据库记录、删 Profile、换账号或新建重复账号。
## 9. 工业级测试
@@ -184,6 +186,7 @@ python scripts/main.py account open <account_id>
- 账号不存在、平台不匹配、Profile 缺失、返回格式异常;
- 同一账号并发抢占和 `LEASE_CONFLICT`;
- 业务成功、业务失败、用户取消、浏览器崩溃后的租约释放;
- **NEED_MANUAL** 任务中心运行中点「中断」:租约变为 `released`(或无 active),马上再采不因残留租约 `LEASE_CONFLICT`;
- 登录过期、验证码 / 二次验证、账号风险和权限不足;
- 两个以上账号轮换且 Profile、登录态、结果互不污染;
- 关闭重开后登录保持;

View File

@@ -102,7 +102,7 @@ def get_adapter():
2. **普通兄弟技能调用**,优先走统一 **`service.sibling_bridge`**(`call_sibling_json`),**不要**在 `task_service.py`、`task_rpa.py` 等业务流程文件中到处散落 `subprocess.run`。
3. **account-manager 账号/租约能力**是例外:可参考 `examples/real_browser_rpa/scripts/service/account_client.py` 与 `examples/simulator_browser_rpa/scripts/service/account_client.py`,封装为**单一** `account_client.py`;允许在该文件内部集中通过 subprocess 调 account-manager CLI。**禁止** import `rpa_helpers` 等 account-manager 内部模块;`simulator_rpa` 与 `real_rpa` 均走同一 `pick_web_account` / `release_lease` 模式。
4. **不允许**直接 `import account-manager` 的内部 Python 模块(如 `service/`、`util/`、`db/`)。
5. **所有选择路径都必须取得 lease**:包括纯数字 `DEFAULT_ACCOUNT_ID`。`account get <id>` 只能读取账号,不能单独作为运行结果;必须再通过可定位该账号的 pick 流程取得非空 `lease_token`。随后必须 `finally release lease`;进程被 kill 后可能残留 lease,需按 [`ACCOUNT_MANAGER.md`](ACCOUNT_MANAGER.md) 排查。
5. **所有选择路径都必须取得 lease**:包括纯数字 `DEFAULT_ACCOUNT_ID`。`account get <id>` 只能读取账号,不能单独作为运行结果;必须再通过可定位该账号的 pick 流程取得非空 `lease_token`。随后必须 `finally` **先 `release_lease`、再关浏览器**;进程被 kill 后可能残留 lease,需按 [`ACCOUNT_MANAGER.md`](ACCOUNT_MANAGER.md) 排查。宿主中断只通知技能,**不**代收租约;禁止 `except Exception` 吞掉 `JobStopped`(见 [`LOGGING.md`](LOGGING.md) §2.5)。
6. **网页 RPA 硬规则**(详见 [`RPA.md`](RPA.md) §0.2):必须先经 `pick_web_account`(底层 **`ensure-web` / `pick-web --ensure`**)拿到 `profile_dir` 再开浏览器;拿不到则失败退出;生产有头;**是否站点登录**按平台约定,但**不等于**可以跳过 account-manager。登记账号 ≠ 站点已登录。
7. **账号选择优先级**:纯数字 `DEFAULT_ACCOUNT_ID` → 校验指定账号后带租约 pick;`DEFAULT_LOGIN_ID` / 非数字标识 → `ensure-web --login-id --lease`;皆空 → 按平台 `ensure-web --lease`。人读展示优先 `login_id` / `account_label`,不要只用数字 id。多条匹配或无法确保拿到指定 ID → `ERROR:AMBIGUOUS_ACCOUNT`。
8. **运行前校验**:`profile_dir` 和 `lease_token` 均须非空;账号平台必须匹配;缺任一字段立即失败,不打开浏览器。
@@ -119,10 +119,12 @@ result = call_sibling_json("account-manager", ["list", "--limit", "10"])
from service.account_client import pick_web_account, release_lease
account = pick_web_account(platform="target_platform", login_id=None)
lease_token = account.get("lease_token")
try:
...
finally:
release_lease(account.get("lease_token"))
release_lease(lease_token) # 先释租约
# 再关浏览器 / Playwright
```
## 相关文档

View File

@@ -60,7 +60,7 @@ checklist_version: 1.0
- [ ] S0-9 **入口定位表**(见 `PRODUCT_SURFACES.md` / `REQUIREMENTS` §0.1):已填写 / 未填写
- [ ] S0-10 本技能宣称的主用户路径涉及:对话 / 数据管理(执行或仅展示) / 定时(可多选;与定位表一致)
- [ ] S0-11 **业务行生命周期**(见 `references/WORK_ITEM_STATUS.md` / `REQUIREMENTS` §0.3):有处理队列且已声明状态→角色映射 / 不适用
- [ ] S0-12 **HTML 页面报告**(见 `references/ACTIONS.md`「HTML 页面报告」):有侧栏可预览的 `html/*.html` + `render-html` / 不适用
- [ ] S0-12 **HTML 页面报告**(见 `references/ACTIONS.md`「HTML 页面报告」):壳子页 `assets/html/pages/*.html` / 快照 `html/*.html` + `render-html` / 不适用
---
@@ -111,6 +111,8 @@ checklist_version: 1.0
- [ ] A4-6 **P0** 若 Action 需引用同库另一张业务表(话术/平台/账号等):参数为 Lookup 下拉(内联 `lookup` 或绑列),**不是**手填内部编号;可选「自动」可留空提交
- [ ] A4-7 **P1** 若 Action 有互斥采集/操作模式(如「按关键词 / 按主页」):用 `inputSchema` 字段级 **`visibleWhen`** 联动显隐,勿只靠 `description` 灰字说明「仅某某时填写」(见 `references/ACTIONS.md`;勿与 Policy L3 行态 `visibleWhen` 混淆)
- [ ] A4-8 **P1** 行内/勾选打开的 `form`:有「已选择」时参数区不应再重复映射主键(`inputMapping` + `readOnly`);需要强制露出时用 `revealInForm`,**禁止**用 `visibleWhen` 藏主键(会不提交)
- [ ] A4-9 **P1** 若同一能力既支持「勾选处理」又支持「按数量从队列领取」:用一个 Action + `bind.selectionMode: "optional"`(勿拆两个同义顶栏按钮);无勾选可点、有勾选显示「已选择」、空选数量字段用 `pickCount` 等约定名(见 `references/ACTIONS.md`)
- [ ] A4-10 **P0** 数据管理弹窗与任务失败文案:全中文产品用语;禁止在 `description` / `confirmation.message` / 参数 `description` / 用户向 `message` 写英文字段名、CLI 子命令、`async`/`pending`/`status=` 等(Agent 路由放 `SKILL.md`;见 `references/ACTIONS.md`「用户可见文案」)
---
## 5. 账号与浏览器会话(网页 RPA 适用;其他形态标 N/A 或改测等价项)
@@ -123,10 +125,11 @@ checklist_version: 1.0
- [ ] A5-6 **P0** **NEED_MANUAL** 真实站或约定仿真站主路径至少完整跑通 1 次;仅模拟数据通过不算交活
- [ ] A5-7 **P0** 账号创建后可在宿主数据管理和 `account list/get` 查到,返回的 `profile_dir` 在当前用户数据目录真实存在
- [ ] A5-8 **P0** 每个账号使用独立且稳定的 Profile;不同账号不共用,本机日常浏览器目录不参与运行
- [ ] A5-9 **P0** 指定账号 ID、指定登录标识和自动选择均取得非空租约;成功、失败、取消和异常路径最终释放
- [ ] A5-9 **P0** 指定账号 ID、指定登录标识和自动选择均取得非空租约;成功、失败、取消和异常路径最终释放;`finally` 先释租约再关浏览器
- [ ] A5-10 **P0** **NEED_MANUAL** 登录后关闭浏览器并释放租约,再取同一账号重开,登录状态仍有效;失效时明确提示重新登录
- [ ] A5-11 **P1** 至少两个账号连续或轮换测试通过,Profile、登录态、结果和失败互不污染
- [ ] A5-12 **P1** 租约冲突、残留租约和账号登录失效能按文档自助定位,不通过删库、删 Profile 或自动换号处理
- [ ] A5-13 **P0** **NEED_MANUAL** 任务中心运行中点「中断」:技能响应 `JobStopped` 并释放租约(数据管理「账号租约」为 `released` / 无 active);立即再跑不因残留租约失败;进度 `emit` 不得用裸 `except Exception` 吞掉停止
---
@@ -156,7 +159,7 @@ checklist_version: 1.0
- [ ] A7-3 **P0** 故意失败一条时,文件日志先有 `task_failed`/`warning`,再有 `ERROR:CODE` 或任务中心失败态
- [ ] A7-4 **P1** 关键步骤有据可查(能对应到开始/关键节点/结束;RPA 宜有多个 `stage=`)
- [ ] A7-5 **P1** 若宣称支持录屏或截图存证:打开开关后关键步骤或失败能留下可查看产物;宣称支持却完全无产物 → **FAIL**
- [ ] A7-6 **P2** 用户可见文案偏业务语言,而非大段开发黑话
- [ ] A7-6 **P2** 用户可见文案偏业务语言,而非大段开发黑话(数据管理弹窗 / 任务失败见 **A4-10**,更高优先级)
- [ ] A7-7 **P2** 数据管理技能按钮(toolbar / row / batch)中文名对照 `references/ACTIONS.md`「Action 按钮中文名」;不与宿主「查看 / 编辑 / 删除」抢名;打开类成功不弹成功条。行业专有按钮名可自创
---

View File

@@ -70,7 +70,7 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
- 工具栏「导入」**UI 一律**打开宿主「导入数据」面板(默认从本技能 `input/` 选文件);「导出」默认写到 `output/`
- **Action 表单**里 `display_type`/`format` 为 `file` 或 `directory` 时:宿主原生对话框默认打开**当前技能** `{skill_data}/input`;若字段声明可选 **`pathRoot`**(相对技能数据根,如 `input/库存数据`),则打开该子目录。禁止在技能里写用户 ID 或绝对盘符。契约见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「本地路径选择与 pathRoot」
- 若表声明了 `replaces: import`,确认导入后由技能 CLI 写库(见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「`replaces`」);仅有 `replaces: create` 时宿主可按行调 create(兼容)
- 数据管理扫描 `{skill_data}/html/*.html`(一层、非递归);`<title>` → 侧栏显示名;「重新生成」调用 Action id **`render-html`**(见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「HTML 页面报告」)
- 数据管理扫描两处 HTML(都只扫一层):安装包 `assets/html/pages/*.html` 是装完就有的壳子页;`{skill_data}/html/*.html` 是快照,「重新生成」调用 Action id **`render-html`**。页面里用 `jiangchang.call` 调本技能已声明的命令。详见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「HTML 页面报告」
---

View File

@@ -335,7 +335,7 @@ git remote -v
应该重点写:
- YAML frontmatter:`name`、`description`、`version`、`metadata.openclaw`(含 `slug`、`platform_kit_min_version`、`developer_ids` 等)
- YAML frontmatter:`name`、`description`、`version`、`metadata.openclaw`(含 `slug`、`platform_kit_min_version`、`developer_ids`、可选 `brands` 等)
- 何时触发本技能、CLI 入口摘要、运行契约要点
- 指向 `references/`(Agent 调用)与 `development/`(开发规范)的分工说明
@@ -407,6 +407,51 @@ metadata:
- 一期只做“补授权”,不会因为你 later 修改数组而自动撤销旧授权
- **首次正式 release 前**就应配好;配错或漏配时,CI 可能成功,但你在技能市场仍找不到技能
### 关于 `metadata.openclaw.brands`(OEM 市场,按需)
#### 目的
匠厂与华商、Lili、GDCM 等 OEM 客户端共用同一技能库,但技能市场目录按 **`market_code`** 过滤(客户端 Header `X-Market-Code`;缺省为 `jiangchang`)。
发布接口 `POST /api/skill/update` 会把 `metadata.openclaw.brands` 幂等写入 `skill_market_relations`(**只补不删**)。**未声明 `brands` 时仅补默认 `jiangchang`**,因此只写在 Gitea 的 `client-huashang` 组织下、却不写 `brands: [huashang]`,华商市场仍然搜不到该技能。
`brands` 与 `developer_ids` 正交:
| 字段 | 管什么 |
|------|--------|
| `brands` | 技能出现在哪些 OEM 市场目录 |
| `developer_ids` | 技能不公开(`access_scope = 0`)时,哪些用户仍可见并可安装 |
#### 何时填写
- **仅匠厂验收 / 通用技能**:可省略(默认匠厂)。
- **须在华商或其他 OEM 客户端出现**:必须显式列出对应编码(通常同时保留 `jiangchang` 以便主站也能看到)。
常见编码(小写机器码,非中文展示名):`jiangchang`、`huashang`、`lili`、`gdcm` 等。非法编码会导致发布接口 400,CI 失败。
#### 在 `SKILL.md` 中声明
```yaml
metadata:
openclaw:
slug: your-skill-slug
category: 通用
developer_ids:
- 12345
brands:
- jiangchang
- huashang
```
兼容别名(平台按优先级取第一个出现的键):`brands` → `oem_clients` → `client_keys`。模板与文档统一推荐写 **`brands`**。
约定:
- **可选字段**:未写或空列表 → 行为与历史一致,只挂 `jiangchang`
- **一期只补不删**:从 `brands` 去掉某市场后,不会自动删除库中已有关联;若需下架请走运营/后台或后续平台能力
- **不要**指望 Gitea 组织名、目录名 `client-*` 自动映射市场
- 发布后在对应 OEM 客户端(须带正确 `X-Market-Code`)搜索验收;也可查库 `skill_market_relations`
## 7. 文档目录分工
当前规范 skill 的文档按读者拆分:
@@ -590,7 +635,7 @@ cmd_stats() # 只读 sync:JSON,无 job_context
- 批量数量用 **`pickCount` / `--pick N` 参数**(任意正整数),不写死条数。
- **顺序执行、不并发**同一浏览器 Profile / account lease。
- **同步数据**与**宿主表导出**职责分离:同步不写 CSV/Excel;表导出交给数据管理。
- **HTML 页面报告**(可选):需要侧栏预览整页时,写入 `html/` + Action `render-html`;契约见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「HTML 页面报告」(含**宿主预览布局**底线)、[`DATA_PATHS.md`](DATA_PATHS.md);薄写入 helper 见 `scripts/service/html_page_service.py`。建议从 `assets/html/example-report.html` 复制壳;`{{BODY}}` 内数据须转义;样式升级后提醒用户重新生成。
- **HTML 页面报告**(可选):壳子页放 `assets/html/pages/*.html`,装完即进侧栏;快照才写入 `html/` + Action `render-html`。页面用 `jiangchang.call` 调已声明命令。契约见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「HTML 页面报告」(含**宿主预览布局**底线)、[`DATA_PATHS.md`](DATA_PATHS.md)。快照可从 `assets/html/example-report.html` 复制壳,不要把该文件放进 `pages/`。
- 详情见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md);契约样例见 `tests/samples/test_sync_contract.py.sample`、`tests/samples/test_action_core_contract.py.sample`。
### 队列 + pick(可选)
@@ -849,7 +894,7 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
|-------------------------|----------------|--------------|
| **新建任务**(对话 Agent) | `placements` 含 `agent`;或短查询走共享 Python CLI | 自然语言能触发主流程;长任务 / RPA 须走 `run_skill_action`,禁止 bash 干等 |
| **数据管理 · 表顶栏** | `placements` 含 `toolbar`(须合法非空 `bind.tables`) | 左侧能看到本技能库表;表顶栏出现按钮;点按行为符合预期 |
| **数据管理 · HTML 报告** | 写出 `html/*.html`;提供 Action id `render-html` | 侧栏出现报告项;预览正常;右键「重新生成」成功且时间/内容更新 |
| **数据管理 · HTML 报告** | 壳子页放 `assets/html/pages/*.html`;快照写出 `html/*.html` 并提供 `render-html` | 壳子装完即现、右键无「重新生成」;快照可重新生成;页面 `jiangchang.call` 能拿到 JSON |
| **数据管理 · 行级** | `placements` 含 `row`(建议 `bind.tables` + `bind.inputMapping` 预填主键) | 操作列和/或行右键(及可选「⋯」)出现本技能行级 Action(需有主键列);点按后入参/行为与壳无关、均正确 |
| **数据管理 · 批量** | `placements` 含 `batch` | 勾选行后批量按钮可用;无勾选时禁用;批量行为正确 |
| **定时任务** | `placements` 含 `cron`(创建时选「技能直调」) | 能选到本技能 Action、保存并触发;到点或「立即运行」行为正确 |
@@ -884,6 +929,7 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
- [ ] 目录名、`SKILL.md` slug、`constants.SKILL_SLUG` 三者一致
- [ ] `SKILL.md` 中 slug、名称、描述都已替换
- [ ] `SKILL.md` 的 `developer_ids` 已换成**本人**匠厂用户 ID(设置 → 用户信息 → 复制),不是模板示例 ID
- [ ] 若须在华商等 OEM 市场出现:`SKILL.md` 的 `brands` 已声明(如 `jiangchang` + `huashang`);未写则仅匠厂;不要用 Gitea 组织名代替
- [ ] `scripts/util/constants.py` 已修改
- [ ] `../references/CLI.md` 示例命令已改成真实命令
- [ ] `service` 下的核心业务文件(如 `task_service.py`)已按领域改名并实现

View File

@@ -122,6 +122,26 @@ RPA 等待用 `interruptible_sleep`,**不要**裸 `asyncio.sleep`(否则暂
Journal 会写入 `type: lifecycle` 事件(`paused` / `resumed` / `stopping`),供宿主任务中心显示状态。
**宿主中断(`stop`)与 `JobStopped`(必读):**
- 用户在任务中心点「中断」时,宿主写入 `control.json` `command=stop`,并**等待技能进程协作退出**(超时后才可能强杀)。
- 技能在 `emit` / `checkpoint` / `interruptible_sleep` / `rpa_step` 等闸门处会抛出 `JobStopped`;业务代码须让其冒泡到 `job_context` / 外层 `finally`,由技能**自己**释放租约、关闭浏览器等资源。
- **禁止**用裸 `except Exception:` 包住上述闸门调用。`JobStopped` 是 `Exception` 子类,会被吞掉,导致 UI 已停、进程仍占账号租约,下次采集必 `LEASE_CONFLICT`。
- 若必须兜底「emit 写 Journal 失败」,正确写法:
```python
from jiangchang_skill_core.activity import JobStopped, emit
try:
emit("正在处理…", skill=SKILL_SLUG, stage="fetch")
except JobStopped:
raise
except Exception:
log.debug("emit_failed", exc_info=True)
```
- 宿主**不会**按技能类型统一代收 account-manager 租约(许多技能根本不用账号管理)。强杀后的残留靠租约 TTL / 数据管理手工 force-release;主路径必须是技能自清。
### 2.3 可观测性三分法(禁止 task_logs)
| 信息类型 | 去哪里 | 技能怎么做 |

View File

@@ -15,6 +15,7 @@
| 4 | 按 [`NAMING.md`](NAMING.md) 确认 slug;**先填** [`REQUIREMENTS.md`](REQUIREMENTS.md)(含入口定位表),再写业务代码 | [`REQUIREMENTS.md`](REQUIREMENTS.md)、[`NAMING.md`](NAMING.md) |
| 5 | 替换标识与占位,按四象限 / examples 实现 `scripts/service/` | [`DEVELOPMENT.md`](DEVELOPMENT.md) §4~§14 |
| 6 | **配置 `developer_ids`(开发自测必做)**:开发期技能在匠厂默认不公开;不加则开发者自己也装测不了。从宿主设置复制用户 ID 写入 `SKILL.md` | [`DEVELOPMENT.md`](DEVELOPMENT.md) §6「关于 developer_ids」 |
| 6b | **按需配置 `brands`(OEM 市场)**:须在华商等 OEM 客户端出现时声明 `brands`(如 `jiangchang` + `huashang`);未写则仅挂匠厂。Gitea 组织名不会自动映射市场 | [`DEVELOPMENT.md`](DEVELOPMENT.md) §6「关于 brands」 |
| 7 | 本地单测(mock)通过后,按业务默认 **`real_rpa`** 真实跑通;再 `release.ps1`;看 Gitea CI。**mock 通 ≠ 完成** | [`ADAPTER.md`](ADAPTER.md);[`DEVELOPMENT.md`](DEVELOPMENT.md) §15 |
| 8 | 匠厂安装后,按**入口定位表已挂**的入口自测,并看**用户体验**(进度、失败提示、有头 RPA 等) | [`DEVELOPMENT.md`](DEVELOPMENT.md) §15;[`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md);[`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) |
| 9 | **交活验收(第三方)**:对照 [`CHECKLIST.md`](CHECKLIST.md) 验收;报告写入仓库根目录 [`acceptance-reports/`](../acceptance-reports/) | [`CHECKLIST.md`](CHECKLIST.md) |

View File

@@ -23,6 +23,7 @@
- 中文 description(一句话):
- account-manager platform_key(如有):
- 命名形态:标准型 / scope 型
- **目标 OEM 市场 `brands`(可选)**:未写则发布后仅挂匠厂(`jiangchang`)。须在华商等 OEM 客户端可见时填写,例如 `jiangchang`、`huashang`(见 `DEVELOPMENT.md` §6「关于 brands」)。**不要**用 Gitea 组织名代替本字段。
- **网页 RPA(如有)— 登录策略(三选一,须写明):**
- `required`:目标站必须登录才能闭环(用户须完成/保持站点登录;技能每轮主路径须有登录门禁 + 验证码/人工等待)
- `optional`:登录可提升能力,但不登录也能完成主路径(**仅当**公开页确可闭环时选用;会话依赖型采集/评论/私信**禁止**默认抄成 optional)
@@ -251,7 +252,9 @@
- `tests/test_display_metadata.py` 通过
- 真实联调样例仍放 `tests/integration/`,默认套件不包含真实外联(与业务 `.env` 默认 `real_rpa` 不冲突)
- `SKILL.md` 的 `developer_ids` 已配置为开发者本人匠厂用户 ID(开发期不公开时,否则本人无法在市场安装自测;取 ID 步骤见 `development/DEVELOPMENT.md` §6)
- 若目标含华商等 OEM 客户端:`SKILL.md` 的 `brands` 已声明对应 `market_code`(通常含 `jiangchang` + 目标 OEM);未写则仅匠厂市场可见
- 发布后 Gitea 工作流成功;匠厂技能市场对开发者账号可见最新版本并可安装
- 若声明了 OEM `brands`:在对应 OEM 客户端(`X-Market-Code` 正确)也能检索到该技能
- 安装后按本技能入口定位表与 `actions.json` 声明完成宿主多入口验收(见 `development/DEVELOPMENT.md` §15、`PRODUCT_SURFACES.md`);**须看用户体验**(进度可见、失败可读、RPA 有头、文案与真实行为一致等),后台通了不算完
- 已人工核验授权与技术路线:无破解、逆向、私有协议、验证码绕过、Hook / 注入、自动换号或其他规避平台限制的实现
- RPA 真实主路径已验证状态等待、`1~3` 秒正常操作节奏、有限重试、账号 / 设备串行、人工介入、结果核验和结束存证

View File

@@ -9,7 +9,7 @@
| 文档 | 职责 |
|------|------|
| 本文 | sync/async、任务中心、Agent 禁令、单内核编排、队列 pick、同步 vs 导出 |
| [`../references/ACTIONS.md`](../references/ACTIONS.md) | `actions.json` 字段、placements、`bind.tables`、inputSchema、跨表 Lookup、字段条件显隐 `visibleWhen`、本地路径 `file`/`directory` + 可选 **`pathRoot`**、选择摘要「已选择」与映射 **`readOnly` / `revealInForm`**、**按钮中文名 `label` 参考** |
| [`../references/ACTIONS.md`](../references/ACTIONS.md) | `actions.json` 字段、placements、`bind.tables`、`bind.selectionMode`、**用户可见文案**、inputSchema、跨表 Lookup、字段条件显隐 `visibleWhen`、本地路径 `file`/`directory` + 可选 **`pathRoot`**、选择摘要「已选择」与映射 **`readOnly` / `revealInForm`**、**按钮中文名 `label` 参考** |
| [`LOGGING.md`](LOGGING.md) | `job_context` / `emit` / `finish` / Run Journal |
| [`DATA_PATHS.md`](DATA_PATHS.md) | 下载/导入等落盘路径 |
@@ -21,7 +21,7 @@
1. **宿主管调度,技能管业务** — 不自行实现任务中心 / Job 轮询 / 后台守护进程。
2. **单业务内核、多入口** — 数据管理、定时任务、Agent、技能详情与直接 CLI 复用同一 domain service;禁止按入口复制业务逻辑。
3. **五职责正交** — `placements` / `bind.tables` / `executionProfile` / `entrypoint` / domain service 各管一事(见 ACTIONS.md)。可选第六轴 **policy**(见 `references/POLICY.md`):未启用 = L0 与今日一致。可选 **`hostEffects` / `steps`**(见 ACTIONS.md):数据管理交互前后的 UI/级联编排;省略 = 不额外改 UI(匠厂宿主 ≥2.8.5)。可选 **`hoverHint`**(见 ACTIONS.md):数据管理工具栏悬停;省略 = 不显示,不回退说明或确认黄条。可选 **`visibleWhen`**(见 ACTIONS.md):Action 参数表单字段条件显隐;省略 = 字段始终平铺;**勿与** Policy L3 行态 `visibleWhen` 混淆;**勿**用其藏映射主键。可选 **`pathRoot`**(见 ACTIONS.md):`file`/`directory` 原生选路对话框起始目录(相对技能数据根;省略默认 `{skill_data}/input`)。可选 **`bind.labelColumn`**(见 ACTIONS.md):选择摘要可读标签列;省略 = 宿主常见列名启发。可选 **`revealInForm`**(见 ACTIONS.md):有「已选择」时仍强制显示某 `readOnly` 字段;省略 = 映射只读默认不占 form 参数区(仍提交)。可选 **`cellPresentation`**(见 `references/CELL_PRESENTATION.md`):单元格语义标色;省略 = P0。
3. **五职责正交** — `placements` / `bind.tables` / `executionProfile` / `entrypoint` / domain service 各管一事(见 ACTIONS.md)。可选第六轴 **policy**(见 `references/POLICY.md`):未启用 = L0 与今日一致。可选 **`hostEffects` / `steps`**(见 ACTIONS.md):数据管理交互前后的 UI/级联编排;省略 = 不额外改 UI(匠厂宿主 ≥2.8.5)。可选 **`hoverHint`**(见 ACTIONS.md):数据管理工具栏悬停;省略 = 不显示,不回退说明或确认黄条。可选 **`visibleWhen`**(见 ACTIONS.md):Action 参数表单字段条件显隐;省略 = 字段始终平铺;**勿与** Policy L3 行态 `visibleWhen` 混淆;**勿**用其藏映射主键。可选 **`pathRoot`**(见 ACTIONS.md):`file`/`directory` 原生选路对话框起始目录(相对技能数据根;省略默认 `{skill_data}/input`)。可选 **`bind.labelColumn`**(见 ACTIONS.md):选择摘要可读标签列;省略 = 宿主常见列名启发。可选 **`bind.selectionMode`**(见 ACTIONS.md):表勾选如何驱动 Action(`required` / `none` / `optional`);省略 = 宿主按 `$selection.ids` / `batch` 推断。可选 **`revealInForm`**(见 ACTIONS.md):有「已选择」时仍强制显示某 `readOnly` 字段;省略 = 映射只读默认不占 form 参数区(仍提交)。可选 **`cellPresentation`**(见 `references/CELL_PRESENTATION.md`):单元格语义标色;省略 = P0。**用户可见文案**(见 ACTIONS.md「用户可见文案」):弹窗 `description` / 确认黄条 / 参数说明 / 任务失败 `message` 须全中文产品用语;Agent 路由勿塞进弹窗。
4. **长耗时建议 async** — 开浏览器、RPA、HTTP 大文件下载等建议 `executionProfile: "async"`,进**任务中心**;这是对**方法**的选择,**不**限制可以出现在哪些 placement / 入口。
5. **Agent:短 CLI / 长 Action** — 短查询可用共享 Python bash CLI;长任务 / RPA **禁止** `exec` + `process poll`,必须 `run_skill_action`。
6. **进度走 Journal** — `emit` / `video.add_step` → Run Journal;UI / Skill Run Card / 任务中心展示;**不靠 stdout 流**。
@@ -167,7 +167,7 @@ cmd_stats() # 只读 sync:JSON stdout,无 job_context
| 同步 / refresh / collect | 外部来源 → 技能本地 SQLite | 技能 domain service |
| 导出当前数据表 | 本地库 → CSV/Excel | **宿主**数据管理统一导出 |
| 特殊业务报告(文件) | 多表合并 / 特殊格式 → `output/` | 仅系统导出不足时提供 `export-*` |
| HTML 页面报告 | 写入 `{skill_data}/html/*.html` | 技能生成;宿主扫描预览;重新生成 Action id 固定为 **`render-html`** |
| HTML 页面报告 | 壳子页 `assets/html/pages/*.html`;快照写入 `{skill_data}/html/*.html` | 壳子装完即现;快照由技能生成,重新生成 Action id 固定为 **`render-html`**。页面用 `jiangchang.call` 调已声明命令 |
同步 Action 默认写库、不写导出文件。HTML 报告契约见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「HTML 页面报告」与 [`DATA_PATHS.md`](DATA_PATHS.md)。通用幂等 / 事务 / 空结果保护见 [`../references/SCHEMA.md`](../references/SCHEMA.md)。

View File

@@ -35,7 +35,7 @@
- **发布打包守护**:[`tests/test_release_packaging_constraints.py`](../tests/test_release_packaging_constraints.py) ——`scripts/**/*.py` 单文件 < 1000 行、文本文件 UTF-8 without BOM。
- **开发规范守护(hard policy)**:[`tests/test_development_policy_guard.py`](../tests/test_development_policy_guard.py) ——规则索引见 [`development/POLICY_MATRIX.md`](POLICY_MATRIX.md),只执行可机器检测的 hard 规则。
- **slug 语义**:[`tests/test_slug_naming.py`](../tests/test_slug_naming.py) ——verb-noun-platform 命名规范校验。
- **HTML 报告壳**:[`tests/test_html_page_service.py`](../tests/test_html_page_service.py) 守护 `assets/html/example-report.html` 的数据管理 iframe 预览布局基线;业务技能若有自定义 `assets/html/*.html`,建议在自有仓库加同类静态断言(见 `references/ACTIONS.md`「宿主预览布局」)。
- **HTML 报告壳**:[`tests/test_html_page_service.py`](../tests/test_html_page_service.py) 守护 `assets/html/example-report.html` 的数据管理 iframe 预览布局基线,并守护 ACTIONS.md 里的壳子页目录 `assets/html/pages` 与 `jiangchang.call`。业务技能若有自定义 `assets/html/*.html` 或 `pages/*.html`,建议在自有仓库加同类静态断言(见 `references/ACTIONS.md`「宿主预览布局」)。不要把 `example-report.html` 放进 `pages/`。
**原则:`tests/test_*.py` 不允许隐形访问外网、不允许拉起真实浏览器、不允许读写开发者机器的真实数据根**。如果需要仿真服务器,也应仅在 integration(并由档位变量放行)。

View File

@@ -52,7 +52,7 @@ real_browser_rpa/
8. **等待结果容器** — `#search-result-container`(示例 selector)
9. **滚动采集循环** — 采集可见条目 → 判断 stop_reason → 滚动加载
10. **返回结果** — `ScrapeRunResult` + `format_stop_reason_for_user()`
11. **释放租约** — `finally` 中 `release_lease()`
11. **释放租约** — `finally` 中**先** `release_lease()`,**再**关浏览器;进度 `emit` 不得用裸 `except Exception` 吞掉 `JobStopped`
## 开发新 skill 时如何复制
@@ -150,7 +150,7 @@ real_browser_rpa/
- **不自动破解验证码** — 滑块/短信只检测 + 等待人工完成
- **不对抗平台风控** — 频控、账号风险、封禁或权限不足时停止并交由人工处理
- **日志脱敏** — 不输出完整手机号/账号
- **租约释放** — `ensure-web` / `pick-web --lease` 后必须在 `finally` 释放
- **租约释放** — `ensure-web` / `pick-web --lease` 后必须在 `finally` **先**释放租约再关浏览器;任务中心中断靠 `JobStopped` 进 `finally`,禁止吞掉停止信号
- **失败留痕** — 真实 skill 应在关键失败点截图(示例中已留注释位)
## 常见坑

View File

@@ -55,7 +55,7 @@ simulator_browser_rpa/
| `browser_session.py` | async 系统 Chrome/Edge + launch args;**不在此 goto** |
| `account_client.py` | **唯一** account-manager subprocess 封装(`pick_web_account`→ensure-web / `release_lease`) |
| `simulator_playwright.py` | 门户轮询、登录、批量表单、PIN、解析 `batch_id`、截图 |
| `adapter/simulator_rpa.py` | **薄** adapter:pick 账号、lease、委托 RPA、`finally release_lease` |
| `adapter/simulator_rpa.py` | **薄** adapter:pick 账号、lease、委托 RPA、`finally` 先 `release_lease` 再关浏览器 |
| `adapter/mock.py` | 不启浏览器,供 unit/mock/CI |
| `task_service.py` | async 校验输入 → 选 adapter → `await submit_batch` |
| `sandbox/demo_app.html` | 可控 DOM;含可选门户门闩 + 原批量提交流程 |

View File

@@ -107,7 +107,7 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
| entrypoint | `type = cli`、`command`、`args`(**必填** JSON 数组,项为 `string` / `number` / `boolean`) |
| placements | `toolbar`、`row`、`batch`、`skill-detail`、`agent`、`cron`;仅 `replaces` 时可为空数组 |
| **replaces** | 可选 `"create"` \| `"update"` \| `"delete"` \| `"import"` — 接管默认新增/编辑/删除/导入(须 `bind.tables`) |
| **bind** | `{ "tables": ["business_table"] }`;可选 `inputMapping`、`columnMapping`、**`labelColumn`**(行内/批量预填与选择摘要标签,见下节) |
| **bind** | `{ "tables": ["business_table"] }`;可选 `inputMapping`、`columnMapping`、**`labelColumn`**、**`selectionMode`**(行内/批量预填、选择摘要标签、勾选驱动模式,见下节) |
| **policy** | 可选;`enabled` + `entrypoint`(推荐 `policy` CLI);见 POLICY.md。未启用 = L0 |
| **cellPresentation** | 可选;`{ "enabled": true }` 时宿主应加载单元格呈现;见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)。未启用 = P0 |
| inputSchema | 根 `type: object`;properties 使用 `string` / `number` / `boolean`;支持 `enum`、`default`、`required`、`sensitive`、`readOnly`、可选 **`revealInForm`**;可选 `display_type` / `format`(含 `file` / `directory` 等);可选 **`pathRoot`**(file/directory 起始目录,相对技能数据根);可选 **`visibleWhen`**(字段条件显隐,见专节) |
@@ -133,7 +133,7 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
规则:
- `bind` 为 object,`additionalProperties: false`;当前允许 `tables`(必填键当存在 bind 时)与可选 `inputMapping`、`columnMapping`、**`labelColumn`**。
- `bind` 为 object,`additionalProperties: false`;当前允许 `tables`(必填键当存在 bind 时)与可选 `inputMapping`、`columnMapping`、**`labelColumn`**、**`selectionMode`**。
- `tables` 非空数组、元素唯一、英文 **snake_case**、不得为空字符串。
- **`placements` 含 `toolbar` 时必须显式声明非空 `bind.tables`**(模板 Schema `if/then` + 自动测试强制)。
- `row` / `batch` **建议**同样声明 `bind.tables`,避免在无关表上露出按钮;未声明时宿主可能按兼容逻辑在更多表上展示。
@@ -142,14 +142,79 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
- **`inputMapping`(可选)**:把行/勾选映射到 `inputSchema` 字段,例如 `"leadId": "$row.$pk"`、`"ids": "$selection.ids"`。适合 `row` / `batch`。宿主仍会提交这些字段。映射字段建议标 `readOnly: true`:无「已选择」时只读展示;**有「已选择」时默认不占参数区**(仍提交,见下节)。不要用 `visibleWhen` 去「藏」映射主键。
- **`columnMapping`(可选)**:把 inputSchema 属性名映射到物理列名,供表单复用列展示名 / lookup。
- **`labelColumn`(可选)**:物理列名,供数据管理 Action 弹层「已选择」摘要展示 `#id + 标签`。**仅影响展示,不改变 CLI/提交 payload,也不决定业务是否跳过某些状态。** 省略时宿主用常见列名启发(如 `title` / `name` / `nickname` 等);再没有则只显示编号。列名不常见、或启发会选错列时再显式声明。
- **`selectionMode`(可选)**:数据管理**表勾选**如何驱动本 Action(与 `placements` / `executionProfile` 正交)。见下节「勾选驱动模式」。
- `concurrency` / `locks` 仍未稳定,**不要**写入新技能 manifest。
### 数据管理:勾选驱动模式 `bind.selectionMode`
同一业务能力应尽量用**一个** Action(多个 `placements`),避免「勾选下载」与「按数量处理队列」拆成两个同义顶栏按钮、却让用户误以为勾选生效。
| 值 | 含义 | 典型场景 |
|----|------|----------|
| `required` | 必须有表勾选;无勾选时宿主**禁用**按钮;弹层可显示「已选择」 | 经典批量:仅处理勾选行 |
| `none` | **忽略**表勾选;即使勾选了也不显示「已选择」、也不把 `$selection.ids` 带进提交 | 经典工具栏「按数量从队列领取」 |
| `optional` | **智能合并**:有勾选 → 走 `$selection.ids`(显示「已选择」);无勾选 → 走空选数量字段(如 `pickCount`) | 统一「视频下载 / 处理队列」一类按钮 |
| **省略** | 宿主兼容推断:`inputMapping` 含 `$selection.ids` 或 `placements` 含 `batch` → `required`;否则 → `none` | 旧技能零改动 |
**`optional` 写法要点:**
1. `bind.inputMapping` 声明 `"itemIds": "$selection.ids"`(字段名自定),并 `selectionMode: "optional"`。
2. `inputSchema` 同时声明勾选字段与空选数量字段;两者都可写进 `required`——宿主只校验**当前分支**。
3. 空选数量字段名约定(宿主按字段名识别,保守列表):`pickCount` / `maxLeads` / `maxCount` / `takeCount` / `pick`。
4. `entrypoint.args` 可同时写 `--ids {{itemIds}}` 与 `--pick {{pickCount}}`;缺值的 `--flag` + 空串由宿主压缩掉,不会留下脏 argv。
5. 有勾选时隐藏数量字段;无勾选时隐藏 `$selection.ids` 映射字段(`$row.*` 行上下文不受影响)。
6. **不要**为同一能力再挂第二个 toolbar 按钮。
示例:[`assets/examples/selection-mode-optional.action.example.json`](../assets/examples/selection-mode-optional.action.example.json)。
弹窗上的 `description` / 参数说明须用**普通用户中文**(见下节「用户可见文案」);`pickCount` 等只作 **property 名 / CLI 参数名**,不要写进用户可读句子。
### 用户可见文案(双受众,必读)
宿主会把下列字段**原样展示给普通用户**(数据管理弹窗、任务中心时间线、表说明等):
| 字段 | 出现位置 |
|------|----------|
| Action `description` | 弹窗顶部说明 |
| `confirmation.message` | 确认/表单黄条 |
| `inputSchema` 各字段 `title` / `description` | 参数区标签与帮助 |
| CLI / `finish` 返回的用户向 `message` | 任务中心、失败摘要 |
| 表/列 `display_name`、表 `description` | 数据管理侧栏与表头帮助 |
**总原则:** 以上文案 = **产品用语、全中文**。Agent 路由禁令、实现细节、契约自述放在 **`SKILL.md` / `references/`**,**不要**塞进弹窗 `description`。
**允许写:** 待下载、任务中心、打开浏览器、有勾选 / 没有勾选、下载数量、按钮中文名(如「导入待下载链接」)。
**禁止写进用户文案(常见反例):**
| 类型 | 反例 |
|------|------|
| 字段名 / CLI 子命令 | `pickCount`、`itemIds`、`import-urls`、`enqueue` |
| 英文状态 / 条件式 | `pending`、`status=pending`、`failed` |
| 执行剖面黑话 | `async`、`sync`、单独甩 `Job`(可说「任务中心」) |
| Agent 禁令 | 「不是明确 URL 入口」「不要先 download-enqueue…」 |
| 契约自述 | 「示例:bind.selectionMode=optional」 |
**对照:**
| 差(开发者备注) | 好(普通用户) |
|------------------|----------------|
| 按 `pickCount` 从 `pending` 领取(async) | 没有勾选则按下方数量从待下载队列领取;进度可在任务中心查看 |
| 请先 import-urls | 请先在数据管理录入链接,或使用「导入待下载链接」 |
**边界:**
- `inputSchema` **属性名**、`entrypoint` 参数、库表状态枚举值可以仍是英文;变的是 **title / description / message**。
- `label` 仍遵循上文「Action 按钮中文名」;本节约束的是说明与帮助文字。
- 与 `bind.selectionMode` 正交:行为对了,文案仍须对人说人话。
### 数据管理:选择摘要(宿主默认能力)
当用户在数据管理**勾选行**(或行内打开带行上下文的动作)后打开 `confirm` / `form` 弹层时,宿主会展示通用 **「已选择 · N 条」** 摘要(`#主键` + 可读标签;超过约 3 条可展开查看全部)。这是**宿主 UX**,对所有技能默认生效:
当用户在数据管理**勾选行**(或行内打开带行上下文的动作)后打开 `confirm` / `form` 弹层时,若当前 Action 的 `selectionMode` 会**应用**表勾选(`required`,或 `optional` 且已勾选),宿主会展示通用 **「已选择 · N 条」** 摘要(`#主键` + 可读标签;超过约 3 条可展开查看全部)。这是**宿主 UX**,对适用技能默认生效:
- **技能零改动即可受益**;不要在 `confirmation.message` 里手写「已选 id 列表」,也不要为了面板去拼业务状态跳过文案。
- **技能零改动即可受益**(省略 `selectionMode` 时的 `required` 推断路径);不要在 `confirmation.message` 里手写「已选 id 列表」,也不要为了面板去拼业务状态跳过文案。
- 摘要与执行解耦:勾选了谁由 UI 说明;**实际处理哪些行、是否跳过某些状态,仍由技能 CLI / domain service 决定**。
- `selectionMode: "none"` 时即使表上有勾选,也**不**展示假「已选择」、不把勾选 id 预填进本 Action。
- 需要稳定标签列时,在 `bind` 上声明可选 `labelColumn`(见上)。示例:[`assets/examples/selection-label-column.action.example.json`](../assets/examples/selection-label-column.action.example.json)。
**与 `confirm` / `form` 的信息架构对齐(映射只读字段):**
@@ -662,7 +727,7 @@ HINT: <next step>
|----|------|
| `toolbar` | 数据管理表顶栏技能按钮(须配合 `bind.tables`) |
| `row` | 数据管理**行级入口**(建议 `bind.tables` + `inputMapping`;表需有主键)。技能声明后,由宿主呈现为操作列链接、**行右键菜单**和/或行内「⋯」等壳;**不**新增 `row-context-menu` 等 placement |
| `batch` | 数据管理批量操作(需勾选行;无勾选时宿主禁用按钮) |
| `batch` | 数据管理批量操作入口。默认(`selectionMode` 省略或 `required`)需勾选行,无勾选时宿主禁用按钮;`optional` / `none` 时无勾选也可点(见 `bind.selectionMode`) |
| `skill-detail` | 技能详情页直调(与安装/四 Tab 同属技能市场详情;已声明则须自测) |
| `agent` | Agent 可直接调用(`run_skill_action`) |
| `cron` | 定时任务「技能直调」 |
@@ -736,28 +801,55 @@ HINT: <next step>
| **同步数据**(sync / refresh / collect) | 外部来源 → 技能本地数据库 | 技能 Action / domain service |
| **导出当前数据表** | 技能本地数据库 → CSV/Excel 等 | 数据管理页**宿主统一导出** |
| **生成特殊业务报告(文件)** | 多表合并、特殊排版或附加计算的 **xlsx/csv/json 等文件** | 仅当系统导出无法满足时,才提供独立 `export-*` Action;文件落 `output/` |
| **HTML 页面报告** | 可在数据管理侧栏预览的整页 HTML | 技能写入 `{skill_data}/html/*.html` + Action id **`render-html`**(见下节) |
| **HTML 页面报告** | 可在数据管理侧栏预览的整页 HTML | 壳子页放安装包 `assets/html/pages/`;快照写入 `{skill_data}/html/*.html` + Action id **`render-html`**(见下节) |
同步型 Action 默认:读外部数据、写技能自己的库、**不**生成 CSV/Excel/JSON 文件、不重复实现宿主表导出。兼容旧 export 命令若需要刷新数据,必须调用同一个 sync 业务内核。通用同步语义见 [`SCHEMA.md`](SCHEMA.md) 与 [`../development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md)。
## HTML 页面报告(数据管理预览)
> 一句话:**技能负责生成并覆盖 `html/*.html`;宿主负责发现、预览、用 `render-html` 触发重新生成。**
> 一句话:**壳子页跟安装包走,装完就在侧栏;快照由技能写入 `html/*.html`,宿主用 `render-html` 重新生成。** 页面上的按钮通过宿主调用本技能已声明的命令,不自己起 Python。
### 何时用
- 需要**可浏览的整页报告**(KPI + 表格 + 自带样式),而不是再导出一张表
- 与 `export-*` / 宿主表导出**并列**:HTML 走 `html/`;文件型特殊报告仍走 `output/` + `export-*`
- 需要**可浏览的整页**(KPI + 表格 + 自带样式),而不是再导出一张表
- **壳子页**:样式和脚本随技能安装就有,数字在打开后向宿主要。不要为了让它出现而点「生成 HTML」,也不要在 `init-db` 里拷进数据目录
- **快照**:把算好的报告烘进文件(往来对账单一类)。与 `export-*` / 宿主表导出**并列**:快照走 `html/`;文件型特殊报告仍走 `output/` + `export-*`
### 目录与发现(宿主契约)
| 项 | 约定 |
|----|------|
| 路径 | `{skill_data}/html/*.html`,**只扫一层**(子目录忽略) |
| helper | `util.runtime_paths.get_html_dir()`;写入用 `service.html_page_service.write_html_page` |
| 壳子页 | 安装包 `assets/html/pages/*.html`,**只扫一层**(子目录忽略)。装完即进侧栏,不经 `render-html` |
| 快照 | `{skill_data}/html/*.html`,**只扫一层**。helper:`get_html_dir()` / `write_html_page` |
| 侧栏名 | 取文件内 `<title>`(缺省则用文件名) |
| 预览 | 宿主 iframe 预览;右键:重新生成 / 浏览器打开 / 刷新 |
| 模板资源 | 建议放 `assets/html/*.html`(安装包内),渲染后**写出**到数据目录 `html/` |
| 预览 | 宿主 iframe 预览。壳子页右键:打开 / 打开目录 / 刷新,**没有**「重新生成」。快照右键另有「重新生成」 |
| 同名 | 壳子和快照文件名相同则留下快照。两边不要用同一个文件名 |
| 快照模板 | `assets/html/example-report.html` 是写出快照时用的壳,**不要**放进 `pages/` |
### 壳子页
1. 把自包含 HTML 放到 `assets/html/pages/`(一层,不要子目录)。`<title>` 就是侧栏名字。
2. 布局底线与下面「宿主预览布局」相同:铺满、禁横向滚动、禁外链 CSS/JS。
3. 不要在初始化数据库时把这些文件拷进 `{skill_data}/html/`。升级技能后宿主读的是新安装包里的文件。
### 页面调用
宿主在数据管理预览里注入 `jiangchang.call`。页面脚本把它当成普通函数调用;宿主只在**当前技能** `actions.json` 已声明的方法里查找,命中后走和表按钮同一条 Python CLI,把 CLI 打出的 JSON 交回。
```javascript
const data = await jiangchang.call("方法名", { date: "2026-09-19" });
```
| 项 | 约定 |
|----|------|
| 方法名 | `actions.json` 里该条的 `id`。没声明则调用失败。不能传任意命令,也不能调别的技能 |
| 参数 | 一个 JSON 对象,字段必须落在该条 `inputSchema` 里 |
| 入口 | 可以不挂 `toolbar`。页面调用不要求表按钮。`placements` 仍不能为空(例如只挂 `agent`) |
| 成功 | CLI 退出码 0,并打印**一个** JSON 对象。页面拿到的就是这个对象 |
| 失败 | 退出码非 0,JSON 里带可读的 `message`(`type` 为 `result`、`status` 为 `failed` 时宿主会把 `message` 交给页面) |
| 范围 | 只在数据管理预览里有这个函数。用浏览器单独打开该文件时没有,页面应说明「请在数据管理中打开」 |
异步命令当场只返回任务号,不在这一节展开。写入和只读是同一种调用,差在技能声明了哪条命令。
### Action:`render-html`(固定 id)
@@ -779,7 +871,7 @@ HINT: <next step>
### 宿主预览布局
数据管理在 **iframe** 内预览 `html/*.html`;宿主 iframe 已占满右侧预览区,**技能页不应再人为收窄正文**(尤其数据管理窗口通常较窄)。
数据管理在 **iframe** 内预览壳子页和快照;宿主 iframe 已占满右侧预览区,**技能页不应再人为收窄正文**(尤其数据管理窗口通常较窄)。
| 级别 | 约定 |
|------|------|
@@ -805,7 +897,9 @@ HINT: <next step>
### 验收(复制后自测)
- [ ] `html/` 下出现 `.html`,数据管理侧栏能看到报告项,预览正常
- [ ] 壳子页:安装后侧栏能看到 `assets/html/pages/` 里的标题,无需点生成;右键没有「重新生成」
- [ ] 页面调用:`jiangchang.call` 能拿到 CLI 的 JSON;未声明的方法名失败且有可读错误
- [ ] 快照:`html/` 下出现 `.html`,数据管理侧栏能看到报告项,预览正常
- [ ] 预览区**横向铺满**、**无横向滚动条**;拖窄侧栏后内容自适应(见上节「宿主预览布局」)
- [ ] 表顶栏(若声明 toolbar)或报告右键「重新生成」能跑通;成功后预览刷新
- [ ] 未登录/无权限用户被拒绝,且有可读错误
@@ -1099,6 +1193,8 @@ CRUD 侧宿主义务与禁止事项见 [`SCHEMA.md`](SCHEMA.md)「表单与展
若还需要默认篇数,可在同一 `properties` 里为 `pickCount` 增加 `"default": 1`、`"minimum": 1`。
与 **`bind.selectionMode: "optional"`** 联用时:无勾选弹层展示 `pickCount`;有勾选则隐藏 `pickCount`、提交 `$selection.ids`。不要再拆第二个「只 pick / 只勾选」toolbar 按钮。见上文「勾选驱动模式」。
约定:
- `boolean` 在宿主中渲染为开关
@@ -1109,7 +1205,7 @@ CRUD 侧宿主义务与禁止事项见 [`SCHEMA.md`](SCHEMA.md)「表单与展
- `required` 字段在提交前校验
- `sensitive` 字段不得出现在低信任入口
- `readOnly: true`:数据管理表单中标记为只读;**有「已选择」且该字段来自 `inputMapping`(或无 mapping 时的约定 id 字段)时,宿主默认不渲染该控件,预设值仍随提交传入**(与 `confirm` 无参数区对齐信息架构)。无「已选择」时仍只读展示。需要强制露出时加 `revealInForm: true`。**不要**用 `visibleWhen` 藏映射主键(会不提交)。
- `placements` 含 `batch` 的动作在数据管理表顶常驻,无勾选时禁用(与表删除一致)
- `placements` 含 `batch` 的动作:默认(`selectionMode` 为 `required` 或省略推断为 required)无勾选时禁用;`selectionMode` 为 `optional` / `none` 时无勾选也可点(见「勾选驱动模式」)
- 需要确认的副作用操作使用 `confirmation.message`
- 短名看不懂或会覆盖数据时,用独立的 `hoverHint`;不要把确认黄条复用成悬停
- 模板参数使用 `{{fieldName}}`,且必须在 `inputSchema.properties` 中声明

View File

@@ -27,7 +27,7 @@ python {baseDir}/scripts/main.py <command> …
| **短 CLI 白名单** | (例)`health` / `version` / `render-html` / `<your-read-or-prep>` | 可用 bash/exec + 共享 `python` |
| **Action 强制黑名单** | (例)`<your-long-or-rpa-command>` | **禁止** bash + `process poll`;必须 `run_skill_action` |
细节与 Action 清单见 [`ACTIONS.md`](ACTIONS.md)。若提供 HTML 报告,「重新生成」须有 Action id **`render-html`**(可同时进短 CLI 白名单);见 ACTIONS.md「HTML 页面报告」。
细节与 Action 清单见 [`ACTIONS.md`](ACTIONS.md)。快照「重新生成」须有 Action id **`render-html`**;壳子页与页面调用 `jiangchang.call` 见 ACTIONS.md「HTML 页面报告」。
## 最小命令

View File

@@ -143,7 +143,7 @@ metadata:
| **同步数据** | 外部来源 → 技能本地数据库 | 技能 Action / domain service |
| **导出当前数据表** | 本地库 → CSV/Excel 等 | 数据管理页**宿主统一导出** |
| **生成特殊业务报告(文件)** | 多表合并、特殊排版或附加计算的文件 | 仅当系统导出无法满足时,才提供独立 `export-*`;落 `output/` |
| **HTML 页面报告** | 侧栏可预览的整页 HTML | 写入 `html/` + Action id **`render-html`**(见 [`ACTIONS.md`](ACTIONS.md)) |
| **HTML 页面报告** | 侧栏可预览的整页 HTML | 壳子页 `assets/html/pages/`;快照写入 `html/` + Action id **`render-html`**(见 [`ACTIONS.md`](ACTIONS.md)) |
同步型 Action 默认只写库、不生成导出文件。兼容旧 export 命令需要刷新时,必须调用同一 sync 内核。可复用测试范式见 `tests/samples/test_sync_contract.py.sample`。

View File

@@ -1,9 +1,10 @@
"""技能 HTML 报告页:写入 `{skill_data}/html/*.html`(宿主扫描预览)。
"""技能 HTML 快照:写入 `{skill_data}/html/*.html`(宿主扫描预览)。
契约要点(权威说明见 `references/ACTIONS.md`「HTML 页面报告」):
- 目录:仅一层 `html/*.html`(非递归)
- 重新生成 Action id 必须为 `render-html`(宿主硬编码)
- 本模块只写快照。装完就有的壳子页放安装包 `assets/html/pages/*.html`,不要从这里拷过去
- 快照目录:仅一层 `html/*.html`(非递归)
- 重新生成 Action id 必须为 `render-html`(宿主硬编码;只针对快照)
- 页面布局底线见 `references/ACTIONS.md`「宿主预览布局」;模板壳 `assets/html/example-report.html`
- 成功后建议 `steps: refreshCatalog` 以重扫侧栏
- 本模块只提供路径/模板/原子写;业务 KPI/表格由各技能自行填充

View File

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

View File

@@ -134,7 +134,7 @@ def get_videos_dir(*, create: bool = True) -> str:
def get_html_dir(*, create: bool = True) -> str:
"""HTML 报告页目录:宿主扫描 `{skill_data}/html/*.html`(一层、非递归)。"""
"""HTML 快照目录:宿主扫描 `{skill_data}/html/*.html`(一层、非递归)。壳子页不在这里。"""
path = os.path.join(get_skill_data_dir(), STANDARD_SUBDIR_HTML)
if create:
os.makedirs(path, exist_ok=True)

View File

@@ -74,7 +74,7 @@ def _validate_bind_tables(action_id: str, bind: object) -> list[str]:
errors: list[str] = []
if not isinstance(bind, dict):
return [f"{action_id}: bind must be object"]
allowed = {"tables", "inputMapping", "columnMapping", "labelColumn"}
allowed = {"tables", "inputMapping", "columnMapping", "labelColumn", "selectionMode"}
unknown = set(bind.keys()) - allowed
if unknown:
errors.append(f"{action_id}: bind only allows {sorted(allowed)}, got {sorted(bind.keys())}")
@@ -98,6 +98,11 @@ def _validate_bind_tables(action_id: str, bind: object) -> list[str]:
errors.append(f"{action_id}: bind.labelColumn must be non-empty string")
elif not _TABLE_NAME_PATTERN.fullmatch(label_column):
errors.append(f"{action_id}: bind.labelColumn {label_column!r} must be snake_case")
selection_mode = bind.get("selectionMode")
if selection_mode is not None and selection_mode not in ("required", "none", "optional"):
errors.append(
f"{action_id}: bind.selectionMode must be required|none|optional, got {selection_mode!r}"
)
return errors
@@ -470,6 +475,106 @@ class TestActionsManifest(unittest.TestCase):
}
jsonschema.validate(instance=manifest, schema=schema)
def test_schema_allows_bind_selection_mode(self) -> None:
try:
import jsonschema
except ImportError:
self.fail("jsonschema is required")
schema = _load_actions_schema()
bind_props = schema["$defs"]["bind"]["properties"]
self.assertIn("selectionMode", bind_props)
self.assertEqual(
bind_props["selectionMode"].get("enum"),
["required", "none", "optional"],
)
manifest = {
"schemaVersion": 1,
"skill": "your-skill-slug",
"actions": [
{
"id": "probe-selection-mode",
"label": "处理队列",
"description": "验证 bind.selectionMode 可通过 Schema",
"placements": ["toolbar", "batch"],
"executionProfile": "async",
"interaction": "form",
"bind": {
"tables": ["demo_items"],
"selectionMode": "optional",
"inputMapping": {"itemIds": "$selection.ids"},
"labelColumn": "display_name",
},
"inputSchema": {
"type": "object",
"required": ["itemIds", "pickCount"],
"properties": {
"itemIds": {"type": "string", "readOnly": True},
"pickCount": {"type": "number", "default": 1, "minimum": 1},
},
},
"entrypoint": {
"type": "cli",
"command": "process",
"args": ["--ids", "{{itemIds}}", "--pick", "{{pickCount}}"],
},
}
],
}
jsonschema.validate(instance=manifest, schema=schema)
def test_action_examples_user_facing_copy_avoids_jargon(self) -> None:
"""示例 Action 的用户可见字符串不得含常见开发黑话(属性名本身除外)。"""
examples_dir = os.path.join(get_skill_root(), "assets", "examples")
# 故意拆开,避免本测试源码触发自身扫描。
forbidden = (
"pickCount",
"itemIds",
"status=",
"import-urls",
"download-enqueue",
"bind.selectionMode",
"selectionMode=",
)
# async/pending 作为整词出现在 description 才禁止;executionProfile 值除外。
word_forbidden = re.compile(r"\b(async|pending|sync)\b", re.I)
for name in os.listdir(examples_dir):
if not name.endswith(".action.example.json"):
continue
path = os.path.join(examples_dir, name)
with open(path, encoding="utf-8") as f:
data = json.load(f)
actions = data["actions"] if isinstance(data, dict) and "actions" in data else [data]
for action in actions:
if not isinstance(action, dict):
continue
texts: list[str] = []
for key in ("description",):
if action.get(key):
texts.append(str(action[key]))
conf = action.get("confirmation") or {}
if isinstance(conf, dict) and conf.get("message"):
texts.append(str(conf["message"]))
props = ((action.get("inputSchema") or {}).get("properties")) or {}
for prop in props.values():
if not isinstance(prop, dict):
continue
for key in ("title", "description"):
if prop.get(key):
texts.append(str(prop[key]))
blob = "\n".join(texts)
for token in forbidden:
self.assertNotIn(
token,
blob,
msg=f"{name} user-facing copy must not contain {token!r}",
)
match = word_forbidden.search(blob)
if match is not None:
self.fail(
f"{name} user-facing copy must not contain {match.group(0)!r}"
)
def test_no_execution_profile_placement_coupling_in_tests(self) -> None:
"""守护:本文件不得引入入口位置与 sync/async 的错误硬耦合断言。"""
path = os.path.join(get_skill_root(), "tests", "test_actions_manifest.py")
@@ -632,6 +737,10 @@ class TestActionsManifest(unittest.TestCase):
"cellPresentation",
"CELL_PRESENTATION.md",
"labelColumn",
"selectionMode",
"勾选驱动",
"用户可见文案",
"双受众",
"已选择",
"revealInForm",
"映射只读",

View File

@@ -415,14 +415,24 @@ class TestDocsStandards(unittest.TestCase):
def test_visible_when_example_is_listed(self) -> None:
text = self._read("assets/examples/README.md")
self.assertIn("visible-when-action.example.json", text)
self.assertIn("selection-mode-optional.action.example.json", text)
checklist = self._read("development/CHECKLIST.md")
self.assertIn("A4-7", checklist)
self.assertIn("visibleWhen", checklist)
self.assertIn("A4-8", checklist)
self.assertIn("revealInForm", checklist)
self.assertIn("A4-9", checklist)
self.assertIn("selectionMode", checklist)
self.assertIn("A4-10", checklist)
self.assertIn("用户可见文案", checklist)
runtime = self._read("development/SKILL_ACTION_RUNTIME.md")
self.assertIn("visibleWhen", runtime)
self.assertIn("revealInForm", runtime)
self.assertIn("selectionMode", runtime)
self.assertIn("用户可见文案", runtime)
actions = self._read("references/ACTIONS.md")
self.assertIn("用户可见文案", actions)
self.assertIn("双受众", actions)
def test_guarded_docs_do_not_reference_amazon_implementation(self) -> None:
forbidden = (

View File

@@ -70,6 +70,20 @@ class TestHtmlPageService(unittest.TestCase):
self.assertIn("overflow-wrap: anywhere", template)
self.assertNotIn("max-width:", template)
def test_actions_contract_keeps_shell_pages_and_call(self) -> None:
from util.runtime_paths import get_skill_root
root = get_skill_root()
actions_path = os.path.join(root, "references", "ACTIONS.md")
with open(actions_path, encoding="utf-8") as f:
actions = f.read()
self.assertIn("assets/html/pages", actions)
self.assertIn("jiangchang.call", actions)
self.assertTrue(os.path.isfile(os.path.join(root, "assets", "html", "example-report.html")))
self.assertFalse(
os.path.isfile(os.path.join(root, "assets", "html", "pages", "example-report.html"))
)
if __name__ == "__main__":
unittest.main()