Compare commits

...

4 Commits

Author SHA1 Message Date
cef7e366cf 沉淀 Lookup 分页勾选小表契约,避免再造一套控件
All checks were successful
技能自动化发布 / release (push) Successful in 7s
2026-09-30 11:21:37 +08:00
afe0788488 Release v1.0.120: document bind.visibleWhen for row menu visibility
All checks were successful
技能自动化发布 / release (push) Successful in 7s
2026-09-29 20:41:23 +08:00
c49fcba157 Docs: precipitate Action Lookup skill_cli contract (v1.0.119)
All checks were successful
技能自动化发布 / release (push) Successful in 10s
2026-09-28 11:56:12 +08:00
bf3bb9cd10 chore: auto release commit (2026-09-27 16:42:38)
All checks were successful
技能自动化发布 / release (push) Successful in 12s
2026-09-27 16:42:39 +08:00
24 changed files with 753 additions and 87 deletions

View File

@@ -9,6 +9,31 @@
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
## 1.0.121
- 沉淀 Lookup **分页勾选小表**:`display_type: lookup` + `presentation: "paged_table"` + 可选 `page_size`(仍是 Lookup,不要新造控件类型)
- 约定 `skill_cli` 追加 `--page` / `--limit`,stdout 为一行 `{"items","total"}`;0 条也是成功,界面显示「暂无可选项」
- 只读列表命令不要在入口加载整包 platform-kit 或 Playwright;同库一个过滤条件优先 `local_sql`
- 更新 Schema / ACTIONS / SCHEMA / CHECKLIST A4-12;新增 `lookup-paged-table.action.example.json`
## 1.0.120
- 正式沉淀行菜单显隐契约 **`bind.visibleWhen`**(宿主已支持:按当前行列值显示/隐藏行按钮)
- 与表单字段 `visibleWhen`、Policy L3 预留消歧;更新 Schema / ACTIONS / POLICY / CHECKLIST A4-11 / RUNTIME
- 新增示例 `bind-visible-when-row.action.example.json`
## 1.0.119
- 沉淀匠厂宿主 **Action Lookup 三源**:`local_sql`(同库)/ **`skill_cli`(兄弟技能 CLI JSON)** / `external_api`(预留)
- 明确列 Lookup 仍仅 `local_sql`;跨技能账号列表用 `skill_cli`,**禁止**本库镜像缓存表
- 更新 `ACTIONS.md` / `SCHEMA.md` / `skill-actions.schema.json` / CHECKLIST A4-6 / `ACCOUNT_MANAGER.md`;新增 `lookup-skill-cli.action.example.json`
## 1.0.118
- 沉淀浏览器 RPA 窗口/viewport 最佳实践(`RPA.md` §1.2.1):`no_viewport=True` + `--start-maximized`,禁止 `viewport=None` 与 CDP/JS 中途改窗
- `RPA_BROWSER.md` 补充 Shadow DOM 双 input 陷阱与 Cookie 可选 dismiss 模式
- 新增 `POLICY-RPA-007` 文档标记与 launch 禁止模式门禁;CHECKLIST A5-4 对齐
## 1.0.117
- 沉淀匠厂 Host ≥ 6.9.28 **多数据源**契约:可选 MySQL 云库 + `data-sources.json` 声明;默认仍为本地 SQLite

View File

@@ -1,7 +1,7 @@
---
name: 技能开发模板(通用业务版)
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
version: 1.0.117
version: 1.0.121
author: 深圳匠厂科技有限公司
metadata:
openclaw:
@@ -39,6 +39,7 @@ allowed-tools:
- **`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`。
- **`bind.visibleWhen`(可选)**:`row` 菜单/按钮按当前行列值显隐(如仅已采用可入队);省略则始终显示。≠ 表单字段 `visibleWhen`,≠ Policy L3。见 `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)属技能内部,宿主不托管。

View File

@@ -8,7 +8,7 @@
- `examples/`:CLI 成功输出形状示例、Action 形状参考(含 `data-sources.mysql.example.json`、`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`**(选择摘要标签列)/ **`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)。
`skill-actions.schema.json` 使用 `additionalProperties: false`。正式支持 `bind.tables`(toolbar Action 必填)与可选 `bind.inputMapping` / `columnMapping` / **`labelColumn`**(选择摘要标签列)/ **`selectionMode`**(`required` \| `none` \| `optional`:表勾选如何驱动 Action;省略则宿主兼容推断)/ **`visibleWhen`**(行菜单/行按钮按当前行列值显隐)。Action Lookup 可选 **`presentation: "paged_table"`** + **`page_size`**(分页勾选小表,仍是 `display_type: lookup`)。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,9 +6,12 @@
- `skill-cell-presentation.example.json`:数据管理单元格呈现文档形状参考(见 `references/CELL_PRESENTATION.md`)。
- `skill-grid-presentation.example.json`:数据管理整行 / 整列 / 区域呈现文档形状参考(见 `references/CELL_PRESENTATION.md`)。
- `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」)。
- `lookup-action.example.json`:Action 内联 Lookup(**local_sql**)形状参考(单选 + `multiple: true`;同库跨表勿填编号;见 `references/ACTIONS.md`「跨表引用:Lookup」)。
- `lookup-skill-cli.action.example.json`:Action Lookup **`skill_cli`** 形状参考(打开表单时宿主调兄弟技能 CLI 抽 JSON 选项;跨技能账号列表勿再造本库缓存表;见同节)。
- `lookup-paged-table.action.example.json`:Lookup **分页勾选小表** `presentation: "paged_table"` 形状参考(仍是 `display_type: lookup`;见 `references/ACTIONS.md`「分页勾选小表」)。
- `year-month-action.example.json`:Action / CRUD 账期月份字段 `display_type: year_month` 形状参考(见 `references/ACTIONS.md`「表单字段类型对照」)。
- `visible-when-action.example.json`:Action 参数字段条件显隐 `visibleWhen` 形状参考(互斥模式联动;见 `references/ACTIONS.md`「字段条件显隐」;≠ Policy L3)。
- `visible-when-action.example.json`:Action 参数字段条件显隐 `visibleWhen` 形状参考(互斥模式联动;见 `references/ACTIONS.md`「字段条件显隐」;≠ `bind.visibleWhen`,≠ Policy L3)。
- `bind-visible-when-row.action.example.json`:行菜单显隐 `bind.visibleWhen` 形状参考(按当前行状态显示/隐藏行按钮;见 `references/ACTIONS.md`「行菜单显隐」)。
- `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,39 @@
{
"id": "approve-item",
"label": "采用",
"description": "把当前记录改为已采用。仅待确认或已跳过的行显示此按钮。",
"placements": ["row", "agent"],
"executionProfile": "sync",
"interaction": "direct",
"bind": {
"tables": ["business_items"],
"visibleWhen": {
"field": "status",
"in": ["pending_review", "skipped"]
},
"labelColumn": "display_name",
"inputMapping": {
"itemId": "$row.$pk"
}
},
"inputSchema": {
"type": "object",
"required": ["itemId"],
"properties": {
"itemId": {
"type": "number",
"title": "编号",
"description": "行内触发时自动填入,一般无需修改",
"readOnly": true
}
}
},
"entrypoint": {
"type": "cli",
"command": "approve",
"args": ["--id", "{{itemId}}"]
},
"hostEffects": {
"onSuccess": ["reloadTable"]
}
}

View File

@@ -1,5 +1,5 @@
{
"_comment": "形状参考:Action 参数引用同库另一张表时用 Lookup。单选与多选同一 lookup 对象,多选只加 multiple:true。完整契约见 references/ACTIONS.md「跨表引用:Lookup」与 references/SCHEMA.md。",
"_comment": "形状参考:Action 参数引用同库另一张表时用 Lookup(local_sql)。单选与多选同一 lookup 对象,多选只加 multiple:true。跨技能列表见 lookup-skill-cli.action.example.json。完整契约见 references/ACTIONS.md「跨表引用:Lookup」。",
"schemaVersion": 1,
"skill": "your-skill-slug",
"actions": [

View File

@@ -0,0 +1,47 @@
{
"_comment": "形状参考:Action Lookup 分页勾选小表。仍是 display_type=lookup,加 presentation=paged_table。完整契约见 references/ACTIONS.md「分页勾选小表」。不要新造 display_type。skill_cli 的 stdout 须为一行 {items,total};0 条也成功退出。列表命令不要在 main.py 入口加载整包 jiangchang_skill_core 或 Playwright。",
"schemaVersion": 1,
"skill": "your-skill-slug",
"actions": [
{
"id": "import-picked",
"label": "导入文章",
"description": "从已生成文章里勾选要导入的,可翻页。",
"placements": ["toolbar"],
"executionProfile": "sync",
"interaction": "form",
"bind": {
"tables": ["articles"],
"selectionMode": "none"
},
"entrypoint": {
"type": "cli",
"command": "import-articles",
"args": ["--ids", "{{articleIds}}"]
},
"inputSchema": {
"type": "object",
"required": ["articleIds"],
"properties": {
"articleIds": {
"type": "string",
"title": "选择文章",
"description": "勾选要导入的文章。可翻页,可本页全选。",
"display_type": "lookup",
"lookup": {
"source": "skill_cli",
"skill": "geo-generate-article-all",
"command": "title",
"args": ["list-generated-json", "--platform", "toutiaohao"],
"value_column": "id",
"label_column": "title",
"multiple": true,
"presentation": "paged_table",
"page_size": 10
}
}
}
}
}
]
}

View File

@@ -0,0 +1,51 @@
{
"_comment": "形状参考:Action Lookup 的 skill_cli 源——打开表单时宿主 sync 调用兄弟技能 CLI,从 stdout JSON 抽选项。完整契约见 references/ACTIONS.md「跨表引用:Lookup」。本例对标 account-manager 遗留 list-json(stdout 为账号数组,字段 id / account_label);其它依赖技能请核对自己的命令与 JSON 形状。",
"schemaVersion": 1,
"skill": "your-skill-slug",
"actions": [
{
"id": "publish-with-account",
"label": "发布(示例)",
"description": "演示跨技能选账号:skill_cli 调 account-manager,勿在本技能维护账号缓存表。",
"placements": ["toolbar", "cron"],
"executionProfile": "async",
"interaction": "form",
"bind": {
"tables": ["articles"],
"selectionMode": "none"
},
"entrypoint": {
"type": "cli",
"command": "run",
"args": ["--account-id", "{{accountId}}", "--pick", "{{pickCount}}"]
},
"inputSchema": {
"type": "object",
"required": ["pickCount"],
"properties": {
"pickCount": {
"type": "number",
"title": "发布数量",
"description": "从未发布队列领取的条数",
"default": 1,
"minimum": 1
},
"accountId": {
"type": "string",
"title": "发布账号",
"description": "不选则由系统自动分配;选定后本次全部使用该号。",
"display_type": "lookup",
"lookup": {
"source": "skill_cli",
"skill": "account-manager",
"command": "list-json",
"args": ["toutiao"],
"value_column": "id",
"label_column": "account_label"
}
}
}
}
}
]
}

View File

@@ -90,6 +90,48 @@
"scalarArg": {
"type": ["string", "number", "boolean"]
},
"visibleWhenRule": {
"description": "条件显隐规则。支持 {field,equals} / {field,in:[...]},或单键简写 {status:\"approved\"} / {status:[\"a\",\"b\"]}。",
"oneOf": [
{
"type": "object",
"required": ["field", "equals"],
"additionalProperties": false,
"properties": {
"field": { "type": "string", "minLength": 1 },
"equals": { "type": ["string", "number", "boolean"] }
}
},
{
"type": "object",
"required": ["field", "in"],
"additionalProperties": false,
"properties": {
"field": { "type": "string", "minLength": 1 },
"in": {
"type": "array",
"minItems": 1,
"items": { "type": ["string", "number", "boolean"] }
}
}
},
{
"type": "object",
"minProperties": 1,
"maxProperties": 1,
"additionalProperties": {
"oneOf": [
{ "type": ["string", "number", "boolean"] },
{
"type": "array",
"minItems": 1,
"items": { "type": ["string", "number", "boolean"] }
}
]
}
}
]
},
"inputProperty": {
"type": "object",
"required": ["type"],
@@ -139,74 +181,45 @@
},
"lookup": {
"$ref": "#/$defs/lookupConfig",
"description": "可选。内联同库表关联 Lookup(与列 options_json lookup 对象同构)。用于当前表没有对应列时;推荐同时设 display_type=lookup。见 ACTIONS.md「跨表引用:Lookup」。"
"description": "可选。Action 内联 Lookup:local_sql(同库表)/ skill_cli(兄弟技能 CLI JSON)/ external_api(预留)。打开表单时由宿主解析。推荐同时设 display_type=lookup。见 ACTIONS.md「跨表引用:Lookup」。"
},
"visibleWhen": {
"description": "可选。表单字段条件显隐。宿主在数据管理/定时任务参数表单中:规则不满足则隐藏该字段、提交时省略、且不校验 required。支持 {field,equals} / {field,in:[...]},或单键简写 {collectMode:\"按关键词采集\"} / {collectMode:[\"a\",\"b\"]}。Agent/CLI 无 UI 时同样按当前 input 求值后剥离隐藏字段。",
"oneOf": [
{
"type": "object",
"required": ["field", "equals"],
"additionalProperties": false,
"properties": {
"field": { "type": "string", "minLength": 1 },
"equals": { "type": ["string", "number", "boolean"] }
}
},
{
"type": "object",
"required": ["field", "in"],
"additionalProperties": false,
"properties": {
"field": { "type": "string", "minLength": 1 },
"in": {
"type": "array",
"minItems": 1,
"items": { "type": ["string", "number", "boolean"] }
}
}
},
{
"type": "object",
"minProperties": 1,
"maxProperties": 1,
"additionalProperties": {
"oneOf": [
{ "type": ["string", "number", "boolean"] },
{
"type": "array",
"minItems": 1,
"items": { "type": ["string", "number", "boolean"] }
}
]
}
}
]
"description": "可选。表单字段条件显隐。宿主在数据管理/定时任务参数表单中:规则不满足则隐藏该字段、提交时省略、且不校验 required。Agent/CLI 无 UI 时同样按当前 input 求值后剥离隐藏字段。≠ bind.visibleWhen(行菜单),≠ Policy L3。",
"allOf": [{ "$ref": "#/$defs/visibleWhenRule" }]
}
}
},
"lookupConfig": {
"type": "object",
"required": ["ref_table", "value_column", "label_column"],
"additionalProperties": false,
"properties": {
"source": {
"type": "string",
"enum": ["local_sql", "skill_cli", "external_api"],
"description": "可选。显式数据源;省略时:有 ref_table→local_sql,有 skill→skill_cli,有 url→external_api。"
},
"lookup_source": {
"type": "string",
"enum": ["local_sql", "skill_cli", "external_api"],
"description": "可选。source 别名。"
},
"ref_table": {
"type": "string",
"minLength": 1,
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
"description": "同库被引用表名"
"description": "local_sql:同库被引用表名"
},
"value_column": {
"type": "string",
"minLength": 1,
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
"description": "提交值列(编号 / 外键)"
"description": "提交值列;local_sql 必填;skill_cli/external_api 默认可为 id"
},
"label_column": {
"type": "string",
"minLength": 1,
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
"description": "主文案列"
"description": "主文案列;local_sql 必填;skill_cli 默认可为 display_label"
},
"extra_column": {
"type": "string",
@@ -218,18 +231,101 @@
"type": "string",
"minLength": 1,
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
"description": "可选。等值过滤列"
"description": "可选。local_sql 等值过滤列"
},
"filter_value": {
"type": ["string", "number", "boolean", "null"],
"description": "可选。与 filter_column 配对的等值"
},
"skill": {
"type": "string",
"minLength": 1,
"description": "skill_cli:兄弟/依赖技能 slug"
},
"skill_slug": {
"type": "string",
"minLength": 1,
"description": "skill_cli:skill 别名"
},
"command": {
"type": "string",
"minLength": 1,
"description": "skill_cli:CLI 子命令(argv[0])"
},
"cli_command": {
"type": "string",
"minLength": 1,
"description": "skill_cli:command 别名"
},
"args": {
"oneOf": [
{
"type": "array",
"items": { "type": "string" }
},
{ "type": "string" }
],
"description": "skill_cli:追加 argv;数组或逗号分隔串"
},
"items_path": {
"type": "string",
"minLength": 1,
"description": "skill_cli / external_api:stdout JSON 内选项数组点路径,默认 items"
},
"url": {
"type": "string",
"minLength": 1,
"description": "external_api(预留):HTTP URL"
},
"endpoint": {
"type": "string",
"minLength": 1,
"description": "external_api:url 别名"
},
"method": {
"type": "string",
"enum": ["GET", "POST"],
"description": "external_api:HTTP 方法,默认 GET"
},
"multiple": {
"type": "boolean",
"description": "可选。true=多选 Lookup(存/提交 TEXT 逗号分隔 id);省略或 false=单选(默认)。与 CRUD options_json lookup 同构。"
"description": "可选。true=多选 Lookup(存/提交 TEXT 逗号分隔 id);省略或 false=单选(默认)。"
},
"presentation": {
"type": "string",
"enum": ["select", "paged_table"],
"description": "可选。省略或 select=下拉/勾选/Chip。paged_table=分页勾选小表(编号|文案,可翻页、可本页全选)。仍是 lookup,不要新造 display_type。"
},
"page_size": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"description": "可选。presentation=paged_table 时每页条数,默认 10,最大 100。"
}
},
"description": "同库 FK 式 Lookup。展示:value · label;有 extra 时 value · label(extra)。multiple 时多选。宿主亦接受 camelCase 键名。"
"oneOf": [
{
"required": ["ref_table", "value_column", "label_column"],
"description": "local_sql(同库表)"
},
{
"anyOf": [
{ "required": ["skill", "command"] },
{ "required": ["skill", "cli_command"] },
{ "required": ["skill_slug", "command"] },
{ "required": ["skill_slug", "cli_command"] }
],
"description": "skill_cli(兄弟技能 CLI JSON)"
},
{
"anyOf": [
{ "required": ["url"] },
{ "required": ["endpoint"] }
],
"description": "external_api(预留)"
}
],
"description": "Action Lookup:local_sql / skill_cli / external_api。展示:value · label;有 extra 时 value · label(extra)。宿主亦接受 camelCase。列 Lookup 仅 local_sql,见 SCHEMA.md。"
},
"inputSchema": {
"type": "object",
@@ -316,9 +412,13 @@
"type": "string",
"enum": ["required", "none", "optional"],
"description": "可选。数据管理勾选如何驱动本 Action:required=必须有勾选(无勾选禁用);none=忽略表勾选(不出现假「已选择」);optional=有勾选走 $selection.ids,无勾选走 pickCount/maxLeads 等空选数量字段。省略时宿主推断:有 $selection.ids 或 batch → required,否则 → none。与 placements/executionProfile 正交。"
},
"visibleWhen": {
"description": "可选。placements 含 row 时:按当前行原始列值决定是否出现在行菜单/行按钮(宿主已支持)。形状同表单字段 visibleWhen。省略=始终显示。≠ inputSchema 字段 visibleWhen,≠ Policy L3 预留。见 ACTIONS.md「行菜单显隐」。",
"allOf": [{ "$ref": "#/$defs/visibleWhenRule" }]
}
},
"description": "表级绑定;toolbar 必填。可选 inputMapping / columnMapping / labelColumn / selectionMode。concurrency/locks 勿写入。"
"description": "表级绑定;toolbar 必填。可选 inputMapping / columnMapping / labelColumn / selectionMode / visibleWhen。concurrency/locks 勿写入。"
},
"hostEffectId": {
"type": "string",

View File

@@ -93,6 +93,17 @@ finally:
## 5. 账号选择与多账号
### 5.0 数据管理 / 定时任务表单选账号(Action Lookup)
若 Action 参数需要用户**点选账号**(而非手填编号):
- 在 `actions.json` 用 **`display_type: lookup` + `lookup.source: skill_cli`**,指向 `account-manager` 能吐 JSON 选项列表的 CLI(字段名 / `items_path` 以 account-manager 文档与实际 stdout 为准)。
- **禁止**在本技能库再建「账号选项缓存表」再 `local_sql`——易过期,且定时任务等入口容易空下拉。
- 契约与示例见 [`references/ACTIONS.md`](../references/ACTIONS.md)「跨表引用:Lookup」、`assets/examples/lookup-skill-cli.action.example.json`。
表单 Lookup **只负责填参数**;真正开浏览器前的 ensure / pick / 租约仍按下文与 `account_client` 执行,二者不要混成「打开表单就 pick-web」。
### 5.1 运行时选择顺序
选择优先级:
1. 用户明确指定纯数字 `DEFAULT_ACCOUNT_ID`:先确认账号存在且平台一致,再通过可定位该账号的 pick 流程取得租约;不得只 `get` 后返回空租约。

View File

@@ -108,11 +108,13 @@ checklist_version: 1.0
- [ ] A4-3 **P0** 异步任务在任务中心能看到进行中与结束状态;结束有成功或失败结论
- [ ] A4-4 **P1** 进行中的进度/步骤信息对用户可理解
- [ ] A4-5 **P1** 已对外暴露的动作参数覆盖主路径关键输入(无“命令行能做、动作入口不能做”的关键缺口)
- [ ] 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-6 **P0** 若 Action 需引用业务行(话术/平台/账号等):参数为 Lookup 下拉(**不是**手填内部编号);同库用 `local_sql`(内联或绑列),跨技能用 **`skill_cli`**(勿在本库造镜像缓存表);可选「自动」可留空提交(见 `references/ACTIONS.md`)
- [ ] A4-7 **P1** 若 Action 有互斥采集/操作模式(如「按关键词 / 按主页」):用 `inputSchema` 字段级 **`visibleWhen`** 联动显隐,勿只靠 `description` 灰字说明「仅某某时填写」(见 `references/ACTIONS.md`;勿与 **`bind.visibleWhen`** 行菜单、Policy L3 混淆)
- [ ] 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`「用户可见文案」)
- [ ] A4-11 **P1** 若行操作应按业务状态显隐(如仅待确认可「采用」、仅已采用可「加入待写」):用 **`bind.visibleWhen`**(对当前行列值求值;存库原始值如 `approved`);勿指望 Policy L3;CLI 侧仍须校验状态(见 `references/ACTIONS.md`「行菜单显隐」)
- [ ] A4-12 **P1** 若要在表单里翻页勾选一批业务行(文章、平台等):用 Lookup **`presentation: "paged_table"`**(仍是 `display_type: lookup`,勿新造类型);`skill_cli` 的 stdout 为一行 `{"items","total"}`,0 条也成功退出;该列表命令不要在入口加载整包 platform-kit 或 Playwright(见 `references/ACTIONS.md`「分页勾选小表」)
---
## 5. 账号与浏览器会话(网页 RPA 适用;其他形态标 N/A 或改测等价项)
@@ -120,7 +122,7 @@ checklist_version: 1.0
- [ ] A5-1 **P0** 未准备好账号/浏览器档案时,主路径失败并提示,不会默默用本机默认浏览器身份跑起来
- [ ] A5-2 **P0** 登录策略为 required 时:未登录不能把做不成的事当成成功
- [ ] A5-3 **P0** 交付联调默认能看见浏览器窗口(有头);不能把无头当作唯一可验收形态
- [ ] A5-4 **P1** 浏览器窗口启动形态满足交付约定(如最大化或可视区域足够操作与人工介入)
- [ ] A5-4 **P1** 浏览器窗口启动形态满足交付约定(如最大化或可视区域足够操作与人工介入);`no_viewport=True` + `--start-maximized`,无「先小后大」或右侧留白/scrollbar 后遗症(见 `RPA.md` §1.2.1)
- [ ] A5-5 **P1** 验证码或人工确认:有等待与超时;超时后任务失败可理解,不无限挂死或假成功
- [ ] A5-6 **P0** **NEED_MANUAL** 真实站或约定仿真站主路径至少完整跑通 1 次;仅模拟数据通过不算交活
- [ ] A5-7 **P0** 账号创建后可在宿主数据管理和 `account list/get` 查到,返回的 `profile_dir` 在当前用户数据目录真实存在

View File

@@ -23,6 +23,7 @@
| POLICY-RPA-004 | 网页 RPA:`.env.example` 若声明 `OPENCLAW_BROWSER_HEADLESS` 则默认必须为 `0`;`scripts/` 若使用 `launch_persistent_*` 则必须同时存在 `pick_web_account`(先 Profile 再开浏览器;实现上应走 ensure-web);登录不全局强制,但不得跳过 account-manager | development/RPA.md §0.2;development/ADAPTER.md §兄弟依赖 | hard | 解析 `.env.example` + 扫描 `scripts/**/*.py` | `tests/test_development_policy_guard.py::TestPolicyRpa004` |
| POLICY-RPA-005 | 必须保留 AI + RPA 总纲,明确 Agent / RPA 职责、授权范围、禁止破解/逆向/私有协议/安全验证绕过、人工介入、状态等待;真实业务默认动作节奏为 1~3 秒 | development/AI_RPA.md §4–§6;development/RPA.md §0.0/§0.3 | hard(文档与默认配置) / soft(真实行为) | 检查总纲关键边界;解析 `.env.example` 的 `STEP_DELAY_MIN=1.0` / `STEP_DELAY_MAX=3.0` | `tests/test_development_policy_guard.py::TestPolicyRpa005` |
| POLICY-RPA-006 | 浏览器 RPA 必须保留 account-manager 生命周期标准;所有样例账号选择路径都须返回非空 `profile_dir + lease_token`,指定数字账号 ID 不得退化为无租约 `account get` | development/ACCOUNT_MANAGER.md;development/ADAPTER.md §兄弟依赖;development/RPA.md §0.2 | hard | 检查标准文档关键标记;扫描真实/仿真 `account_client.py` 的指定 ID 带租约实现并禁止空租约赋值 | `tests/test_development_policy_guard.py::TestPolicyRpa006` |
| POLICY-RPA-007 | 网页 RPA 启动须 `no_viewport=True` + `--start-maximized`;禁止 `viewport=None`、CDP/JS 中途改窗;`RPA.md` §1.2.1 须保留窗口标准 | development/RPA.md §1.2.1;development/RPA_BROWSER.md §4;examples/real_browser_rpa | hard | 文档标记 + 扫描含 `launch_persistent_*` 的 Python 禁止模式;校验 real_browser 示例 | `tests/test_development_policy_guard.py::TestPolicyRpa007` |
| POLICY-PACKAGING-001 | `scripts/**/*.py` 单文件 < 1000 行 | development/DEVELOPMENT.md §3.4 发布打包约束;development/RUNTIME.md §发布打包约束 | hard | 行数统计 | **已有测试覆盖**:`tests/test_release_packaging_constraints.py::test_scripts_py_files_under_pyarmor_line_limit` |
| POLICY-PACKAGING-002 | 文本文件 UTF-8 without BOM,无 `U+FEFF` | development/DEVELOPMENT.md §3.4;development/RUNTIME.md §编码与输出 | hard | BOM / 解码检查 | **已有测试覆盖**:`tests/test_release_packaging_constraints.py::test_text_files_are_utf8_without_bom` |
| POLICY-RELEASE-001 | 发布 tag(去掉前缀)必须等于 `SKILL.md` 的 `version:` 且等于 `scripts/util/constants.py` 的 `SKILL_VERSION`;`release.ps1` 打 tag 前硬失败,不自动改写字段 | development/DEVELOPMENT.md §15 发布;宿主市场「最新版本」来自 tag、「当前版本」来自包内 `SKILL.md` | hard | `release.ps1` 门禁 + 日常版本字段对齐测试 | `release.ps1`(`Assert-SkillReleaseVersionAlignment`);`tests/test_development_policy_guard.py::TestPolicyRelease001`;`tests/test_skill_metadata.py::test_skill_version_alignment` |

View File

@@ -189,18 +189,35 @@ BrowserType.launch_persistent_context: Arguments can not specify page to be open
**正确做法(生产路径):**
1. `launch_persistent_context(user_data_dir=profile_dir, executable_path=chrome, args=chrome_args, ...)`
1. `launch_persistent_context(user_data_dir=profile_dir, executable_path=chrome, args=chrome_args, locale="zh-CN", no_viewport=True, ...)`
2. `page = await context.new_page()`
3. `await page.goto(start_url, wait_until="domcontentloaded", timeout=60000)`
对强风控站点,如果需要更拟人,可以启动后通过**地址栏输入 / 真实点击进入**目标页,但**仍然不能把 URL 塞进 launch args**。
#### 1.2.1 窗口与 viewport(`POLICY-RPA-007`)
persistent profile 会记住上次窗口大小;**最大化与 layout 同步**须按下列标准,避免「先小后大、右侧留白、scrollbar 后遗症」:
| 必须 | 禁止 |
|------|------|
| `no_viewport=True`(页面 layout 跟随真实窗口) | `viewport=None` 或固定 `viewport={width,height}` 冒充全屏 |
| launch args 含 `--start-maximized` | CDP `Browser.setWindowBounds` / JS `resizeTo` **中途改窗** |
| 真实站:`browser_session` 内 **启动后尽快 `goto` 业务页** | 长时间停空白页再导航、goto 后再 CDP 最大化 |
| 优先复制 `examples/real_browser_rpa/scripts/service/browser_session.py` | 自造 maximize / CDP 窗口 hack |
说明:
- **`no_viewport=True` ≠ `viewport=None`**。后者在 Playwright 中不等于「跟窗口走」,易导致 OS 窗口已最大化但页面仍按窄 viewport 排版。
- **仿真站**(`examples/simulator_browser_rpa`)可在 `browser_session` 只做 launch、由上层 `goto`;仍须 **`no_viewport=True`**。
- 共享库 `jiangchang_skill_core.rpa.launch_persistent_browser` 已内置 `no_viewport=True`;自写 `browser_session` 时勿漏。
完整参考实现见:
- `examples/real_browser_rpa/README.md`(**开发真实浏览器 RPA 类 skill 时必须先读**)
- `examples/real_browser_rpa/scripts/service/browser_session.py`
**生产路径不要依赖 CDP 接管现有页面**;CDP 仅用于诊断或桌面宿主已打开浏览器的场景。
**生产路径不要依赖 CDP 接管现有页面**;CDP 仅用于诊断或桌面宿主已打开浏览器的场景(**含禁止用 CDP 最大化窗口**)。
### 1.3 HITL / 验证码

View File

@@ -90,6 +90,19 @@ button = row.get_by_role("button", name="添加")
Playwright CSS locator 默认可穿透 open Shadow DOM;XPath 不穿透。closed Shadow DOM 无法正常读取时,评估目标应用是否提供可访问性属性,不能注入破解。
**Web Component 双 input 陷阱**(常见于 `<mc-input>` 等宿主):
- 轻 DOM 内可能有 **hidden 同步用 input**(`aria-hidden="true"`、`tabindex="-1"`)。
- 裸写 `host >> input` 时 Playwright 常默认匹配**第一个** hidden input,等 `visible` 会超时。
- 做法:等 **host 可见**作 page ready;输入用 shadow 内真实字段(如 `>> #mc-input-track-input` 或 `>> input.input:not([aria-hidden="true"])`);或 click host + keyboard。
### Cookie / 同意条(可选出现)
Profile 已点过「允许全部」后,下次可能**不再出现** Cookie 条。不要无限等待:
- 短超时(约 2~3s)探测可见 → 点击;
- 超时或无元素 → **skip 继续**,不要阻塞主流程。
### 动态列表
等待列表完成加载后再读取;`locator.all()` 不会等待动态列表稳定。滚动或刷新后重新获取 locator。
@@ -109,6 +122,7 @@ Playwright CSS locator 默认可穿透 open Shadow DOM;XPath 不穿透。close
## 6. 禁止模式
- 用全页所有 `input` 的尺寸和位置猜搜索框。
- 对 Web Component 裸用 `host >> input` 且不等 shadow 内真实可编辑 input。
- 用 `.first` 绕过严格模式。
- 从 DevTools 复制深层 `nth-child` 或绝对 XPath。
- 只等 URL 含 `search` 就判定结果成功。

View File

@@ -9,7 +9,7 @@
| 文档 | 职责 |
|------|------|
| 本文 | sync/async、任务中心、Agent 禁令、单内核编排、队列 pick、同步 vs 导出 |
| [`../references/ACTIONS.md`](../references/ACTIONS.md) | `actions.json` 字段、placements、`bind.tables`、`bind.selectionMode`、**用户可见文案**、inputSchema、跨表 Lookup、字段条件显隐 `visibleWhen`、本地路径 `file`/`directory` + 可选 **`pathRoot`**、选择摘要「已选择」与映射 **`readOnly` / `revealInForm`**、**按钮中文名 `label` 参考** |
| [`../references/ACTIONS.md`](../references/ACTIONS.md) | `actions.json` 字段、placements、`bind.tables`、`bind.selectionMode`、`bind.visibleWhen`(行菜单显隐)、**用户可见文案**、inputSchema、跨表 Lookup(含分页勾选 `presentation: paged_table`)、字段条件显隐 `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):选择摘要可读标签列;省略 = 宿主常见列名启发。可选 **`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 路由勿塞进弹窗。
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 参数表单字段条件显隐;省略 = 字段始终平铺;**勿与** **`bind.visibleWhen`**(行菜单)或 Policy L3 混淆;**勿**用其藏映射主键。可选 **`pathRoot`**(见 ACTIONS.md):`file`/`directory` 原生选路对话框起始目录(相对技能数据根;省略默认 `{skill_data}/input`)。可选 **`bind.labelColumn`**(见 ACTIONS.md):选择摘要可读标签列;省略 = 宿主常见列名启发。可选 **`bind.selectionMode`**(见 ACTIONS.md):表勾选如何驱动 Action(`required` / `none` / `optional`);省略 = 宿主按 `$selection.ids` / `batch` 推断。可选 **`bind.visibleWhen`**(见 ACTIONS.md「行菜单显隐」):`row` 菜单按当前行列值显隐;省略 = 始终显示。可选 **`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 流**。

View File

@@ -91,6 +91,7 @@ real_browser_rpa/
- **不要**在技能内安装 Playwright;浏览器与 Python 包由宿主/runtime 提供
- `launch_persistent_context` 的 `args` **只放 Chrome 参数**;**不要把 URL 放进 `args`**
- 页面必须通过 `new_page()` + `goto()`,或通过真实地址栏/点击进入
- **必须** `no_viewport=True` + `--start-maximized`;**禁止** `viewport=None` 与 CDP/JS 中途改窗(见 `development/RPA.md` §1.2.1、`POLICY-RPA-007`)
## 复制边界:哪些能复制,哪些必须替换

View File

@@ -59,6 +59,7 @@ async def start_browser_session(
"""
启动持久化 Chrome profile,new_page + goto 入口页,返回 (playwright, context, page)。
窗口标准(POLICY-RPA-007):no_viewport=True + --start-maximized;勿用固定 viewport 或 CDP 改窗。
真实 skill 需要安装 playwright 与 jiangchang_skill_core。
"""
from playwright.async_api import async_playwright

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`**、**`selectionMode`**(行内/批量预填、选择摘要标签、勾选驱动模式,见下节) |
| **bind** | `{ "tables": ["business_table"] }`;可选 `inputMapping`、`columnMapping`、**`labelColumn`**、**`selectionMode`**、**`visibleWhen`**(行内/批量预填、选择摘要标签、勾选驱动模式、**行菜单显隐**,见下节) |
| **policy** | 可选;`enabled` + `entrypoint`(推荐 `policy` CLI);见 POLICY.md。未启用 = L0 |
| **cellPresentation** | 可选;`{ "enabled": true }` 时宿主应加载格子与 grid 呈现;见 [`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`**、**`selectionMode`**。
- `bind` 为 object,`additionalProperties: false`;当前允许 `tables`(必填键当存在 bind 时)与可选 `inputMapping`、`columnMapping`、**`labelColumn`**、**`selectionMode`**、**`visibleWhen`**。
- `tables` 非空数组、元素唯一、英文 **snake_case**、不得为空字符串。
- **`placements` 含 `toolbar` 时必须显式声明非空 `bind.tables`**(模板 Schema `if/then` + 自动测试强制)。
- `row` / `batch` **建议**同样声明 `bind.tables`,避免在无关表上露出按钮;未声明时宿主可能按兼容逻辑在更多表上展示。
@@ -143,8 +143,42 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
- **`columnMapping`(可选)**:把 inputSchema 属性名映射到物理列名,供表单复用列展示名 / lookup。
- **`labelColumn`(可选)**:物理列名,供数据管理 Action 弹层「已选择」摘要展示 `#id + 标签`。**仅影响展示,不改变 CLI/提交 payload,也不决定业务是否跳过某些状态。** 省略时宿主用常见列名启发(如 `title` / `name` / `nickname` 等);再没有则只显示编号。列名不常见、或启发会选错列时再显式声明。
- **`selectionMode`(可选)**:数据管理**表勾选**如何驱动本 Action(与 `placements` / `executionProfile` 正交)。见下节「勾选驱动模式」。
- **`visibleWhen`(可选)**:`placements` 含 `row` 时,按**当前行原始列值**决定是否出现在行菜单 / 行按钮。见下节「行菜单显隐」。
- `concurrency` / `locks` 仍未稳定,**不要**写入新技能 manifest。
### 数据管理:行菜单显隐 `bind.visibleWhen`
行级 Action 常需按业务状态显隐,例如:已采用不再出「采用」;仅已采用才出「加入待写」。**宿主已支持**写在 `bind.visibleWhen`(对当前行求值;不匹配则该行菜单不出现此项)。这是正式 Action 契约,**不是** Policy L3 预留,也**不是**表单字段 `inputSchema.properties.*.visibleWhen`。
| | `bind.visibleWhen` | 表单字段 `visibleWhen` | Policy L3(预留) |
|--|--------------------|------------------------|-------------------|
| 作用对象 | 行菜单 / 行按钮是否出现 | Action 参数表单里某个控件 | (未冻结)权限策略等 |
| 求值上下文 | **当前行列值** | 当前表单参数值 | — |
| 省略时 | 始终显示 | 字段始终显示 | — |
**写法(形状与表单字段相同):**
```json
"bind": {
"tables": ["long_tails"],
"visibleWhen": { "status": "approved" },
"inputMapping": { "longTailId": "$row.$pk" }
}
```
```json
"visibleWhen": { "field": "status", "in": ["pending_review", "skipped"] }
```
也支持 `{ "field": "status", "equals": "approved" }`。规则缺失或无法解析时宿主视为始终可见(兼容旧技能)。
**要点:**
1. 只影响 **UI 是否展示**;CLI / Agent 仍可直接调同一 entrypoint,技能侧仍应校验状态。
2. 比较用行的**存库原始值**(如 `approved`),不是界面中文「已采用」。
3. 顶栏 / batch 按钮不受 `bind.visibleWhen` 约束(无「当前行」);批量入口靠勾选 + 技能跳过非法状态。
4. 示例:[`assets/examples/bind-visible-when-row.action.example.json`](../assets/examples/bind-visible-when-row.action.example.json)。
### 数据管理:勾选驱动模式 `bind.selectionMode`
同一业务能力应尽量用**一个** Action(多个 `placements`),避免「勾选下载」与「按数量处理队列」拆成两个同义顶栏按钮、却让用户误以为勾选生效。
@@ -746,7 +780,7 @@ HINT: <next step>
### 行级入口 `row`(宿主呈现契约)
- **技能决定有什么**:`placements` 含 `"row"`(且通过 `bind.tables` / policy 等过滤)的 Action,构成该表行的技能菜单项集合;配什么就出现什么。
- **技能决定有什么**:`placements` 含 `"row"`(且通过 `bind.tables` / policy 等过滤)的 Action,构成该表行的技能菜单项集合;再用可选 **`bind.visibleWhen`** 按行状态过滤(见「行菜单显隐」)。
- **宿主决定怎么摆**:可用操作列、行右键、行内「⋯」等呈现**同一批** Action;点击均走同一 `launch`(`placement: "row"`),`interaction` / `executionProfile` 与壳无关。
- **顺序**:各壳内技能项顺序 = 下方「数据管理技能按钮顺序」契约(`actions` 数组相对顺序)。
- **宿主内置项**:查看 / 编辑 / 删除由宿主提供,**不是**技能 `actions.json` 配置出来的;若出现在行右键,须与技能项分组(例如分隔线后),且不得要求技能再配一份菜单 DSL。
@@ -910,14 +944,37 @@ const data = await jiangchang.call("方法名", { date: "2026-09-19" });
### 跨表引用:Lookup(宿主已支持;勿让用户填编号)
当 Action 参数要引用**同库另一张业务表**的一行(话术模板、平台、账号等)时,必须用 Lookup 下拉,**禁止**做成「请填写编号」的数字框(用户记不住内部 id,体验差)。
当 Action 参数要引用**一行业务实体**(话术模板、平台、账号等)时,必须用 Lookup 下拉,**禁止**做成「请填写编号」的数字框(用户记不住内部 id,体验差)。
| 场景 | 怎么配 | 说明 |
|------|--------|------|
| 当前表已有同含义列,且列上已是 CRUD lookup | `column` / `bind.columnMapping`,或属性名 = 列名 | toolbar/batch 弹窗与 create/update 抽屉都会复用该列已解析的选项 |
| 当前表没有该列(如在「潜客」表上选「话术模板」) | `display_type: "lookup"` + 字段内联 `lookup` 对象 | 打开表单时宿主按当前技能库解析选项 |
#### 两种 Lookup 入口(勿混)
内联 `lookup` 与列元数据 `options_json` **同构**(见 [`SCHEMA.md`](SCHEMA.md)「跨表引用 lookup」):
| 种类 | 声明位置 | 何时解析 | 允许的数据源 |
|------|----------|----------|--------------|
| **列 Lookup** | `_jiangchang_columns.display_type = lookup` + `options_json` 对象 | 拉表 schema 时 | **仅** `local_sql`(同库表) |
| **Action Lookup** | `inputSchema.properties.*.lookup`(推荐同时 `display_type: lookup`) | **打开动作表单** / 字段因 `visibleWhen` 变为可见时 | `local_sql` / **`skill_cli`** / `external_api`(预留) |
固定小枚举仍用 `options_json` **数组**或 Action `enum`,**不走** Lookup 解析器。
#### Action Lookup 三种 source(匠厂宿主已统一)
| `source` | 何时用 | 核心字段 | 推断(可省略显式 `source`) |
|----------|--------|----------|------------------------------|
| `local_sql` | 选项在**本技能库**另一张表 | `ref_table` + `value_column` + `label_column` | 有 `ref_table` |
| `skill_cli` | 选项在**兄弟/依赖技能**(如 account-manager 账号列表) | `skill` + `command`;可选 `args` / `items_path` / `value_column` / `label_column` | 有 `skill` / `skill_slug` |
| `external_api` | 第三方 HTTP(预留) | `url`(或 `endpoint`) | 有 `url` / `endpoint` |
**选型:**
| 场景 | 怎么配 |
|------|--------|
| 当前表已有同含义列,且列上已是 CRUD lookup | `column` / `bind.columnMapping`,或属性名 = 列名(复用列上已解析的 **local_sql** 选项) |
| 同库另一张表、当前表没有对应列 | Action 内联 `lookup` → `local_sql`(可只写 `ref_table`…) |
| 跨技能列表(账号、批次、其它技能业务行) | Action 内联 `lookup` → **`skill_cli`**(`skill` + `command`) |
| 固定三五个状态 | `enum` / 列 `options_json` 数组,**不要** Lookup |
**反模式(跨技能):** 在本技能再造一张「账号选项缓存表」再 `local_sql`——选项会过期,定时任务等入口也容易空下拉。应直接 `skill_cli` 调依赖技能。
#### `local_sql`(同库;与列 `options_json` 对象同构)
```json
{
@@ -954,29 +1011,124 @@ const data = await jiangchang.call("方法名", { date: "2026-09-19" });
| 字段 | 必填 | 说明 |
|------|------|------|
| `ref_table` | 是 | 同库被引用表 |
| `ref_table` | 是(local_sql) | 同库被引用表 |
| `value_column` | 是 | 提交给 CLI 的值(业务 id / 编号) |
| `label_column` | 是 | 主文案列 |
| `extra_column` | 否 | 第三展示列(任意列名,不限「备注」) |
| `filter_column` / `filter_value` | 否 | 等值过滤 |
| `multiple` | 否 | `true` = 多选;省略/`false` = 单选(默认) |
**下拉/选项展示(宿主统一,技能只配列):** 单选为 `{value} · {label}`;有非空 `extra_column` 时为 `{value} · {label}({extra})`。例:`2 · 通用私信-留资`、`2 · 通用私信-留资(dm)`。
列 Lookup 字段表与加载时机见 [`SCHEMA.md`](SCHEMA.md)「跨表引用 lookup」(**仅 local_sql**)。
#### `skill_cli`(兄弟/依赖技能 CLI)
打开表单时,宿主对声明的技能执行一次 **sync** CLI,从 **stdout JSON** 抽出选项列表(默认路径 `items`)。
```json
{
"accountId": {
"type": "string",
"title": "发布账号",
"description": "不选则由系统自动分配。",
"display_type": "lookup",
"lookup": {
"source": "skill_cli",
"skill": "account-manager",
"command": "list-json",
"args": ["toutiao"],
"value_column": "id",
"label_column": "account_label"
}
}
}
```
| 字段 | 必填 | 说明 |
|------|------|------|
| `skill` / `skill_slug` | 是(二选一) | 兄弟技能 slug |
| `command` / `cli_command` | 是(二选一) | CLI 子命令(argv 第一段) |
| `args` | 否 | 追加参数;数组或逗号串 |
| `value_column` | 否 | 默认 `id` |
| `label_column` | 否 | 默认 `display_label` |
| `extra_column` | 否 | 第三展示列 |
| `items_path` | 否 | JSON 内数组点路径,默认 `items`;若 stdout 本身就是数组可省略 |
| `multiple` | 否 | 同 local_sql |
**依赖技能侧约定:** 该命令须成功退出并打印可解析 JSON;宿主接受**顶层数组**,或对象内由 `items_path`(默认 `items`)指向的数组。元素须含 value/label 列。具体子命令与字段名以依赖技能文档为准(上例对标 account-manager 遗留 `list-json <platform>`,字段为 `id` / `account_label`;其它技能请核对 stdout)。
**与运行时分流:** 表单选账号用 `skill_cli` Lookup;真正开浏览器 / 占租约仍走本技能 `account_client`(见 [`development/ACCOUNT_MANAGER.md`](../development/ACCOUNT_MANAGER.md)),二者不要混成「表单打开就 pick-web」。
#### 展示、多选与可选空
**下拉/选项展示(宿主统一,技能只配列):** 单选为 `{value} · {label}`;有非空 `extra_column` 时为 `{value} · {label}({extra})`。列表/详情单元格优先 `listLabel`(不带 id 前缀)。例:`2 · 通用私信-留资`。
**多选存值与模板:** Action / CRUD 存 **`TEXT` 逗号分隔 id**(如 `1,2,5`);`{{engineIds}}` 原样替换进 CLI。多选字段在 `inputSchema` 里用 `type: "string"`(逗号串),不要用尚未稳定的 JSON array 当业务控件。
**多选控件(宿主策略):** 选项少(建议 ≤6)用勾选列表;选项多用字段内 Chip + 可搜索下拉(不是再开业务弹窗)。
**多选控件(宿主策略):** 选项少(建议 ≤6)用勾选列表;选项多用字段内 Chip + 可搜索下拉(不是再开业务弹窗)。需要编号+文案的小表、翻页、本页全选时,用下面的 `paged_table`,不要再做一套控件。
#### 分页勾选小表(`presentation: "paged_table"`)
仍是 **`display_type: "lookup"`**。在已有 `lookup` 上增加 `presentation` 与可选 `page_size`。提交值与 `multiple: true` 相同:**TEXT 逗号分隔 id**。`local_sql` 与 `skill_cli` 都能用。列 Lookup(`_jiangchang_columns`)**不要**声明 `paged_table`。
```json
{
"articleIds": {
"type": "string",
"title": "选择文章",
"description": "勾选要导入的文章。可翻页,可本页全选。",
"display_type": "lookup",
"lookup": {
"source": "skill_cli",
"skill": "geo-generate-article-all",
"command": "title",
"args": ["list-generated-json", "--platform", "toutiaohao"],
"value_column": "id",
"label_column": "title",
"multiple": true,
"presentation": "paged_table",
"page_size": 10
}
}
}
```
| 字段 | 必填 | 说明 |
|------|------|------|
| `presentation` | 用小表时必填 | 只认 `"paged_table"`。省略或 `"select"` = 现有下拉 / 勾选 / Chip |
| `page_size` | 否 | 每页条数,默认 10,限制 1–100 |
| `multiple` | 建议 `true` | 小表按多选提交;一个都不勾时把字段放进 `required`,宿主拒绝空提交 |
**`skill_cli` 分页:** 宿主在 argv 后追加 `--page`、`--limit`。stdout **只有一行 JSON 对象**:`{"items":[{"id":"1","title":"标题"}],"total":42}`。`total` 是过滤后的总条数,不是本页条数。命令可以自己切片;若一次返回全量且带 `total`,宿主也会按页切开。
**空列表:** 分页小表把 `{"items":[],"total":0}` 且退出码 0 当成成功,界面显示「暂无可选项」。普通下拉 Lookup 仍把 0 条当成命令失败。不要为「没有数据」退出码 1,也不要往 stdout 打日志。
**同库过滤只有一列:** `local_sql` 的 `filter_column` / `filter_value` 只支持一个等值条件。要「已启用并且提示词齐全」这类两个条件,用一条只读 `skill_cli`,或先做成同库视图再 `local_sql`。同库、一个条件就能列出来的,优先 `local_sql`(打开即查库)。`skill_cli` 每次打开和翻页都会再起进程。
**只读列表命令要轻:** 供这张表调用的命令,在 `scripts/main.py` 分到该命令之前,不要 `import jiangchang_skill_core` 整包(包初始化会带上授权客户端),也不要导入 Playwright / 发布栈。0 条也打印空 `items` 后退出码 0。健康检查、真正执行任务仍走 `cli/app.py` 的 `setup_skill_logging`,不要把那条路径拆掉。
**反模式(在上一节之外再加):**
- 新造 `display_type: "paged_lookup"` 或另一套分页控件(应只加 `lookup.presentation`)
- 列表命令在模块顶导入浏览器 / RPA / 整包 platform-kit,打开小表要等数秒
- 没有数据时让命令失败,小表里出现英文报错而不是「暂无可选项」
- 用两个 `filter_column` 表达 AND(宿主只认一个等值过滤)
形状参考:[`assets/examples/lookup-paged-table.action.example.json`](../assets/examples/lookup-paged-table.action.example.json)。
**可选「自动」(单选常见):** 字段不要放进 `required`;不要用 `minimum: 1` 卡死留空。宿主会省略空的可选数字参数,避免「提交的数据无效」。
**解析失败:** 单字段 Lookup 失败时该字段选项为空,表单仍可开(可留空提交的场景不受阻)。
**反模式(勿做):**
- 标题写成「某某编号」,说明里教用户去左侧表抄 id
- 用静态 `enum` 罗列会增长/变更的业务行(会过期)
- 在技能里自己拼假 label 列代替宿主统一格式(难维护)
- 另造 `lookup_multi` / 第二套 display_type(应只加 `multiple: true`)
- 另造 `lookup_multi` / `paged_lookup` / 第二套 display_type(多选只加 `multiple: true`;小表只加 `presentation: "paged_table"`)
- 为跨技能列表在本库维护镜像缓存表再 `local_sql`(应用 `skill_cli`)
- 在列 `options_json` 上声明 `skill_cli`(列 Lookup **只**支持同库)
形状参考:[`assets/examples/lookup-action.example.json`](../assets/examples/lookup-action.example.json)。
形状参考:[`assets/examples/lookup-action.example.json`](../assets/examples/lookup-action.example.json)、[`assets/examples/lookup-skill-cli.action.example.json`](../assets/examples/lookup-skill-cli.action.example.json)、[`assets/examples/lookup-paged-table.action.example.json`](../assets/examples/lookup-paged-table.action.example.json)。
### 表单字段类型对照(须与 CRUD 对称)
@@ -990,7 +1142,7 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件
| 数字 | `type: number` | `display_type: number` 或 INT/REAL/NUMERIC |
| 开关 | `type: boolean` | `display_type: boolean` |
| 枚举下拉 | `enum: [...]` | `options_json` 数组 |
| 关联下拉(Lookup) | 内联 `display_type: lookup` + `lookup`(可选 `multiple`),或绑列 | 同左;多选存 `TEXT` 逗号分隔 id |
| 关联下拉(Lookup) | 内联 `display_type: lookup` + `lookup`(`local_sql` / `skill_cli` / `external_api`;可选 `multiple`;可选 `presentation: "paged_table"`),或绑列 | 列侧仅同库 `local_sql`;多选存 `TEXT` 逗号分隔 id;分页小表只在 Action |
| 密码 | `sensitive: true` | 列 `sensitive` |
| 年份 | `display_type` 或 `format`: `year` | `display_type: year` → 存 `YYYY` |
| 年月 | `display_type` 或 `format`: `year_month`(别名 `month`) | `display_type: year_month` → 存 `YYYY-MM` |
@@ -1008,7 +1160,7 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件
| **列筛选「值」** | **无对称**(Action 无表格筛选) | 见 [`SCHEMA.md`](SCHEMA.md)「数据管理:列筛选与全局搜索」:枚举 / boolean / lookup 在筛选栏用**下拉 Select**(非开关、非分段单选);存库仍用 value,展示与搜索认 label |
**本版不做(勿宣传):** 浏览器内文件上传控件、复杂 `oneOf` / nested object 专用表单;`visibleWhen` 多条件 AND/OR 树、跨 Action 依赖、CRUD 新增/编辑对称显隐;Action 参数表单的列筛选对称。
(**Lookup 多选**已支持:同一 `lookup` + `multiple: true`,见上文;不是任意标签数组。)
(**Lookup 多选**已支持:同一 `lookup` + `multiple: true`,见上文;不是任意标签数组。**分页勾选小表**已支持:同一 `lookup` + `presentation: "paged_table"`,见「分页勾选小表」。)
(**本地路径选择**已支持:`file` / `directory` + 可选 `pathRoot`,见下节;提交值仍是本机绝对路径字符串,不是 multipart 上传。)
日期时间约定:
@@ -1097,8 +1249,10 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件
用于「先选模式,再出对应参数」类表单。写在 `inputSchema.properties.<field>` 上(**不是** action 顶层字段)。
> **消歧义:** 本文的 `visibleWhen` = **Action 参数表单控件显隐**。
> [`POLICY.md`](POLICY.md) L3 预留的 `visibleWhen` = **行态/权限策略**(未冻结),**不是**同一能力。勿混用、勿互相替代。
> **消歧义:**
> - **表单** `inputSchema.properties.*.visibleWhen` = Action **参数控件**显隐。
> - **`bind.visibleWhen`** = 数据管理 **行菜单 / 行按钮**显隐(见上文「行菜单显隐」)。
> - [`POLICY.md`](POLICY.md) L3 预留的行态 `visibleWhen` = **权限策略**(未冻结),**不是**以上二者。勿混用、勿互相替代。
```json
"keyword": {

View File

@@ -2,8 +2,10 @@
> **状态:正式契约 v1.0(`schemaVersion` 1)。**
> **目标:** 冻结「启用方式 + Snapshot 最小字段 + L2 scope 白名单」,使多数技能(L0)零配置,需要权限的技能(L1/L2)有稳定扩展面。
> **非目标:** 宿主实现业务用户/角色库;规定飞书/HTTP/本地表等权限数据源;本期不强制 L3(行态 `visibleWhen` 等)。
> **注意:** L3 预留的行态 `visibleWhen` **不是** Action 参数表单的 `inputSchema.properties.*.visibleWhen`(表单控件显隐,见 [`ACTIONS.md`](ACTIONS.md)「字段条件显隐」)。二者同名不同义,勿混用。
> **非目标:** 宿主实现业务用户/角色库;规定飞书/HTTP/本地表等权限数据源;本期不强制 L3(菜单级藏表、`rows.mode=delegate` 等)。
> **注意:**
> - 行菜单按业务状态显隐请用 Action 契约 **`bind.visibleWhen`**(见 [`ACTIONS.md`](ACTIONS.md)「行菜单显隐」)——宿主已支持,**不属于**本 Policy L3。
> - L3 预留的行态 `visibleWhen` **不是** Action 参数表单的 `inputSchema.properties.*.visibleWhen`,也**不是** `bind.visibleWhen`。三者同名不同义,勿混用。
> **配套 Schema:** [`../assets/schemas/skill-policy-snapshot.schema.json`](../assets/schemas/skill-policy-snapshot.schema.json);启用块见 [`../assets/schemas/skill-actions.schema.json`](../assets/schemas/skill-actions.schema.json)。
相关正交轴见 [`ACTIONS.md`](ACTIONS.md)。本契约增加第六轴:**policy(当前身份允许什么)**。
@@ -17,7 +19,7 @@
| **L0** | 无 `policy`,或 `policy.enabled !== true` | **与今日完全一致**:不拉 Snapshot,不按身份过滤按钮/行/列 |
| **L1** | `policy.enabled: true`,Snapshot 至少给出 Action 授权 | 数据管理展示 skill Action 前与 allowlist 求交;可选限制宿主内置能力 |
| **L2** | 在 L1 基础上 Snapshot 带 `data.tables[table]` | 列表查询应用声明式 `rows.scope`;列按 `columns` 投影 |
| **L3** | (预留,本契约不冻结行为) | 如**行态** `visibleWhen`、菜单级藏表、`rows.mode=delegate` 等(≠ Action 表单 `inputSchema.visibleWhen`) |
| **L3** | (预留,本契约不冻结行为) | 如菜单级藏表、`rows.mode=delegate`、更复杂的行态策略字段等(≠ `bind.visibleWhen` 行菜单,≠ 表单 `inputSchema.visibleWhen`) |
**80% 技能停留 L0。** 启用 policy 的技能必须实现约定 CLI,并在写路径自行再鉴权(见 §7)。

View File

@@ -728,6 +728,8 @@ CREATE TABLE demo_items (
宿主在加载表 schema 时查询 `ref_table`,把结果展开成 `{value,label}` 选项列表。
> **列 Lookup 仅支持同库 `local_sql`。** Action 表单若要从兄弟技能拉选项(如 account-manager 账号列表),须在 `actions.json` 的 `inputSchema.lookup` 使用 **`skill_cli`**,见 [`ACTIONS.md`](ACTIONS.md)「跨表引用:Lookup」。不要在列 `options_json` 上声明 `skill` / `command`。
**存库约定:**
| 模式 | 物理列建议 | 存什么 |
@@ -748,8 +750,9 @@ CREATE TABLE demo_items (
- 单选:下拉 Select(现状)
- 多选且选项较少(建议 ≤6):勾选列表
- 多选且选项较多:字段内 Chip + 可搜索下拉(**不是**再开一层业务弹窗)
- **分页勾选小表**(`lookup.presentation: "paged_table"`)只属于 **Action** 参数表单,列 Lookup 不声明。见 [`ACTIONS.md`](ACTIONS.md)「分页勾选小表」
Action 弹窗内联 Lookup 与上表 **同构**(字段名一致,含 `multiple`),见 [`ACTIONS.md`](ACTIONS.md)「跨表引用:Lookup」。
Action 弹窗内联 Lookup 与上表 **同构**(字段名一致,含 `multiple`;Action 另可加 `presentation` / `page_size`),见 [`ACTIONS.md`](ACTIONS.md)「跨表引用:Lookup」。
示例:账号表的平台字段 → 平台信息表;凭据表的账号编号 → 账号信息表;私信 Action 选话术 → `message_templates`;抽检 Action 多选引擎 → `engines` + `multiple: true`。

View File

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

View File

@@ -74,7 +74,14 @@ 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", "selectionMode"}
allowed = {
"tables",
"inputMapping",
"columnMapping",
"labelColumn",
"selectionMode",
"visibleWhen",
}
unknown = set(bind.keys()) - allowed
if unknown:
errors.append(f"{action_id}: bind only allows {sorted(allowed)}, got {sorted(bind.keys())}")
@@ -103,6 +110,9 @@ def _validate_bind_tables(action_id: str, bind: object) -> list[str]:
errors.append(
f"{action_id}: bind.selectionMode must be required|none|optional, got {selection_mode!r}"
)
visible_when = bind.get("visibleWhen")
if visible_when is not None and (not isinstance(visible_when, dict) or not visible_when):
errors.append(f"{action_id}: bind.visibleWhen must be a non-empty object")
return errors
@@ -522,6 +532,51 @@ class TestActionsManifest(unittest.TestCase):
}
jsonschema.validate(instance=manifest, schema=schema)
def test_schema_allows_bind_visible_when(self) -> None:
try:
import jsonschema
except ImportError:
self.fail("jsonschema is required")
schema = _load_actions_schema()
bind_props = schema["$defs"]["bind"]["properties"]
self.assertIn("visibleWhen", bind_props)
self.assertIn("visibleWhenRule", schema["$defs"])
manifest = {
"schemaVersion": 1,
"skill": "your-skill-slug",
"actions": [
{
"id": "probe-bind-visible-when",
"label": "采用",
"description": "验证 bind.visibleWhen 可通过 Schema",
"placements": ["row"],
"executionProfile": "sync",
"interaction": "direct",
"bind": {
"tables": ["demo_items"],
"visibleWhen": {
"field": "status",
"in": ["pending_review", "skipped"],
},
"inputMapping": {"itemId": "$row.$pk"},
},
"inputSchema": {
"type": "object",
"required": ["itemId"],
"properties": {
"itemId": {"type": "number", "readOnly": True},
},
},
"entrypoint": {
"type": "cli",
"command": "approve",
"args": ["--id", "{{itemId}}"],
},
}
],
}
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")
@@ -740,6 +795,8 @@ class TestActionsManifest(unittest.TestCase):
"labelColumn",
"selectionMode",
"勾选驱动",
"行菜单显隐",
"bind.visibleWhen",
"用户可见文案",
"双受众",
"已选择",

View File

@@ -39,6 +39,7 @@ POLICY_IDS = (
"POLICY-RPA-004",
"POLICY-RPA-005",
"POLICY-RPA-006",
"POLICY-RPA-007",
"POLICY-PACKAGING-001",
"POLICY-PACKAGING-002",
"POLICY-RELEASE-001",
@@ -205,6 +206,26 @@ RPA_006_EXAMPLE_CLIENTS = (
"examples/simulator_browser_rpa/scripts/service/account_client.py",
)
RPA_007_SOURCE = (
"development/RPA.md §1.2.1; development/RPA_BROWSER.md §4; "
"examples/real_browser_rpa; development/POLICY_MATRIX.md"
)
RPA_007_DOC_MARKERS = (
"no_viewport=True",
"viewport=None",
"setWindowBounds",
"POLICY-RPA-007",
)
RPA_007_LAUNCH_MARKERS = RPA_004_LAUNCH_MARKERS
RPA_007_FORBIDDEN_PATTERNS = (
"viewport=None",
"setWindowBounds",
"maximize_browser_window",
)
RPA_007_REAL_BROWSER_SESSION = (
"examples/real_browser_rpa/scripts/service/browser_session.py"
)
LOGGING_001_SOURCE = "development/LOGGING.md; development/RUNTIME.md"
LOGGING_002_SOURCE = "development/LOGGING.md; development/DEVELOPMENT.md"
LOGGING_003_SOURCE = "development/LOGGING.md"
@@ -837,6 +858,84 @@ class TestPolicyRpa005(unittest.TestCase):
)
class TestPolicyRpa007(unittest.TestCase):
def test_rpa_md_documents_viewport_window_standard(self) -> None:
skill_root = get_skill_root()
path = os.path.join(skill_root, "development", "RPA.md")
text = _read_text(path)
missing = [marker for marker in RPA_007_DOC_MARKERS if marker not in text]
self.assertEqual(
missing,
[],
msg=_policy_msg(
"POLICY-RPA-007",
RPA_007_SOURCE,
"RPA.md missing viewport/window markers: " + ", ".join(missing),
),
)
def test_real_browser_example_uses_no_viewport(self) -> None:
skill_root = get_skill_root()
path = os.path.join(skill_root, *RPA_007_REAL_BROWSER_SESSION.split("/"))
text = _read_text(path)
self.assertIn(
"no_viewport=True",
text,
msg=_policy_msg(
"POLICY-RPA-007",
RPA_007_SOURCE,
f"{RPA_007_REAL_BROWSER_SESSION} must use no_viewport=True",
),
)
self.assertNotIn(
"viewport=None",
text,
msg=_policy_msg(
"POLICY-RPA-007",
RPA_007_SOURCE,
f"{RPA_007_REAL_BROWSER_SESSION} must not use viewport=None",
),
)
def test_launch_persistent_forbids_bad_viewport_and_cdp_maximize(self) -> None:
skill_root = get_skill_root()
rel_paths: list[str] = []
for root_name in ("scripts", "examples"):
root = os.path.join(skill_root, root_name)
if not os.path.isdir(root):
continue
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in SKIP_DIR_NAMES]
rel_dir = os.path.relpath(dirpath, skill_root).replace("\\", "/")
if any(rel_dir == p or rel_dir.startswith(p + "/") for p in SKIP_REL_PREFIXES):
continue
for name in filenames:
if not name.endswith(".py"):
continue
rel = os.path.relpath(os.path.join(dirpath, name), skill_root).replace(
"\\", "/"
)
rel_paths.append(rel)
violations: list[str] = []
for rel in rel_paths:
text = _read_text(os.path.join(skill_root, *rel.split("/")))
if not any(marker in text for marker in RPA_007_LAUNCH_MARKERS):
continue
for pattern in RPA_007_FORBIDDEN_PATTERNS:
if pattern in text:
violations.append(f"{rel}: forbidden {pattern!r}")
self.assertEqual(
violations,
[],
msg=_policy_msg(
"POLICY-RPA-007",
RPA_007_SOURCE,
"launch_persistent files with forbidden viewport/window hacks: "
+ "; ".join(violations),
),
)
class TestPolicyRpa006(unittest.TestCase):
def test_account_manager_lifecycle_doc_has_required_markers(self) -> None:
skill_root = get_skill_root()

View File

@@ -413,9 +413,16 @@ class TestDocsStandards(unittest.TestCase):
"inputSchema 与参数表单",
"表单字段类型对照(须与 CRUD 对称)",
"跨表引用:Lookup",
"display_type: \"lookup\"",
'"display_type": "lookup"',
"extra_column",
"multiple",
"skill_cli",
"paged_table",
"page_size",
"暂无可选项",
"local_sql",
"external_api",
"items_path",
"数据管理新增/编辑",
"_jiangchang_columns.description",
"datetime_unix_seconds",
@@ -427,6 +434,31 @@ class TestDocsStandards(unittest.TestCase):
"列筛选",
):
self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}")
self.assertTrue(
os.path.isfile(
os.path.join(get_skill_root(), "assets/examples/lookup-skill-cli.action.example.json")
),
msg="lookup-skill-cli.action.example.json missing",
)
self.assertTrue(
os.path.isfile(
os.path.join(get_skill_root(), "assets/examples/lookup-paged-table.action.example.json")
),
msg="lookup-paged-table.action.example.json missing",
)
checklist = self._read("development/CHECKLIST.md")
self.assertIn("skill_cli", checklist)
self.assertIn("A4-12", checklist)
self.assertIn("paged_table", checklist)
account_mgr = self._read("development/ACCOUNT_MANAGER.md")
self.assertIn("skill_cli", account_mgr)
schema = self._read("references/SCHEMA.md")
self.assertIn("skill_cli", schema)
schema_json = self._read("assets/schemas/skill-actions.schema.json")
self.assertIn("skill_cli", schema_json)
self.assertIn("paged_table", schema_json)
self.assertIn("page_size", schema_json)
self.assertIn('"skill"', schema_json)
def test_actions_md_disambiguates_visible_when_from_policy(self) -> None:
actions = self._read("references/ACTIONS.md")
@@ -445,6 +477,7 @@ 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("bind-visible-when-row.action.example.json", text)
self.assertIn("selection-mode-optional.action.example.json", text)
self.assertIn("skill-cell-presentation.example.json", text)
self.assertIn("skill-grid-presentation.example.json", text)
@@ -456,15 +489,20 @@ class TestDocsStandards(unittest.TestCase):
self.assertIn("A4-9", checklist)
self.assertIn("selectionMode", checklist)
self.assertIn("A4-10", checklist)
self.assertIn("A4-11", checklist)
self.assertIn("bind.visibleWhen", 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("bind.visibleWhen", runtime)
self.assertIn("用户可见文案", runtime)
actions = self._read("references/ACTIONS.md")
self.assertIn("用户可见文案", actions)
self.assertIn("双受众", actions)
self.assertIn("行菜单显隐", actions)
self.assertIn("bind.visibleWhen", actions)
def test_guarded_docs_do_not_reference_amazon_implementation(self) -> None:
forbidden = (