Compare commits

...

2 Commits

Author SHA1 Message Date
225d5c0777 docs: add lookup.multiple multi-select contract (v1.0.93)
All checks were successful
技能自动化发布 / release (push) Successful in 5s
2026-08-21 16:19:13 +08:00
fbf326d525 Document replaces:import and add CRUD/import action examples
All checks were successful
技能自动化发布 / release (push) Successful in 7s
2026-08-21 11:57:38 +08:00
19 changed files with 438 additions and 52 deletions

View File

@@ -9,6 +9,24 @@
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
## 1.0.93
- 沉淀 Lookup **多选**契约:同一 `lookup` / `options_json` 对象上加 `multiple: true`(平行扩展单选,不另造 display_type)
- 多选存 `TEXT` 逗号分隔 id;列表展示映射为多个 label;控件由宿主按选项数量选择(短列表勾选 / 长列表 Chip+下拉)
- 更新 SCHEMA / ACTIONS / skill-actions.schema / lookup 示例与 options_json 校验
## 1.0.93
- 沉淀 Lookup **多选**契约:同一 `lookup` / `options_json` 对象加 `multiple: true`,不另造类型
- 多选存 `TEXT` 逗号分隔 id;短列表勾选 / 长列表 Chip+下拉为宿主渲染策略
- ACTIONS / SCHEMA / schema / 示例与文档守护对齐
## 1.0.92
- 沉淀数据管理 `replaces: import`:导入 UI 统一宿主面板,确认后由技能写库
- ACTIONS / schema / 守护测试对齐 create·update·delete·import;补充可抄示例 `replaces-*.action.example.json`
- 修正旧文档「宿主导入仍直写」的过时说明
## 1.0.91
- 沉淀「表与表之间禁止 SQLite 外键」契约:只存业务 id,关联与级联在 repository/service;界面用 `lookup`

View File

@@ -1,7 +1,7 @@
---
name: 技能开发模板(通用业务版)
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
version: 1.0.91
version: 1.0.93
author: 深圳匠厂科技有限公司
metadata:
openclaw:

View File

@@ -4,5 +4,6 @@
- `skill-policy-snapshot.*.example.json`:Data Policy Snapshot 形状参考(见 `references/POLICY.md`)。
- `skill-cell-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 下拉形状参考(跨表引用勿填编号;见 `references/ACTIONS.md`「跨表引用:Lookup」)。
- `lookup-action.example.json`:Action 内联 Lookup 形状参考(单选 + `multiple: true` 多选并列;跨表引用勿填编号;见 `references/ACTIONS.md`「跨表引用:Lookup」)。
- `visible-when-action.example.json`:Action 参数字段条件显隐 `visibleWhen` 形状参考(互斥模式联动;见 `references/ACTIONS.md`「字段条件显隐」;≠ Policy L3)。
- `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

@@ -1,12 +1,12 @@
{
"_comment": "形状参考:Action 参数引用同库另一张表时用 Lookup 下拉,勿让用户填内部编号。完整契约见 references/ACTIONS.md「跨表引用:Lookup」与 references/SCHEMA.md。",
"_comment": "形状参考:Action 参数引用同库另一张表时用 Lookup。单选与多选同一 lookup 对象,多选只加 multiple:true。完整契约见 references/ACTIONS.md「跨表引用:Lookup」与 references/SCHEMA.md。",
"schemaVersion": 1,
"skill": "your-skill-slug",
"actions": [
{
"id": "send-with-template",
"label": "按模板发送(示例)",
"description": "演示内联 lookup:从 message_templates 选模板;-- 表示自动挑选。",
"description": "演示内联 lookup 单选:从 message_templates 选模板;-- 表示自动挑选。",
"placements": ["toolbar"],
"executionProfile": "async",
"interaction": "form",
@@ -43,6 +43,41 @@
}
}
}
},
{
"id": "probe-with-engines",
"label": "按引擎抽检(多选示例)",
"description": "演示 lookup.multiple:从 engines 多选,提交为逗号分隔 id。",
"placements": ["toolbar"],
"executionProfile": "async",
"interaction": "form",
"bind": {
"tables": ["queries"]
},
"entrypoint": {
"type": "cli",
"command": "query",
"args": ["batch-probe", "--engine-ids", "{{engineIds}}"]
},
"inputSchema": {
"type": "object",
"properties": {
"engineIds": {
"type": "string",
"title": "将使用的引擎",
"description": "可多选;提交值为逗号分隔编号,如 1,2。",
"display_type": "lookup",
"lookup": {
"ref_table": "engines",
"value_column": "id",
"label_column": "display_name",
"filter_column": "enabled",
"filter_value": 1,
"multiple": true
}
}
}
}
}
]
}

View File

@@ -0,0 +1,28 @@
{
"id": "item-create",
"label": "新增记录",
"description": "新增业务记录并执行领域副作用(走技能 CLI,非宿主直写库)。",
"replaces": "create",
"placements": [],
"executionProfile": "sync",
"interaction": "form",
"bind": {
"tables": ["items"]
},
"hostEffects": {
"onSuccess": ["reloadTable"]
},
"inputSchema": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string", "title": "名称" },
"note": { "type": "string", "title": "备注" }
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["create", "--name", "{{name}}", "--note", "{{note}}"]
}
}

View File

@@ -0,0 +1,33 @@
{
"id": "item-delete",
"label": "删除记录",
"description": "删除业务记录并清理关联副作用。",
"replaces": "delete",
"placements": [],
"executionProfile": "sync",
"interaction": "confirm",
"confirmation": {
"message": "确定删除该记录?关联数据将一并清理。"
},
"bind": {
"tables": ["items"],
"inputMapping": {
"id": "$row.$pk"
}
},
"hostEffects": {
"onSuccess": ["reloadTable", "clearSelection"]
},
"inputSchema": {
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "number", "title": "编号", "readOnly": true }
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["delete", "--id", "{{id}}"]
}
}

View File

@@ -0,0 +1,38 @@
{
"id": "item-import",
"label": "导入记录",
"description": "由宿主「导入数据」面板调用:解析 Excel/CSV 并写入业务表(含领域校验与派生字段)。",
"replaces": "import",
"placements": [],
"executionProfile": "sync",
"interaction": "form",
"bind": {
"tables": ["items"]
},
"hostEffects": {
"onSuccess": ["reloadTable"]
},
"inputSchema": {
"type": "object",
"required": ["filePath"],
"properties": {
"filePath": {
"type": "string",
"title": "导入文件",
"description": "由宿主导入面板传入的 Excel/CSV 路径。",
"format": "file"
},
"onError": {
"type": "string",
"title": "导入规则(出错时)",
"enum": ["跳过该行,继续导入", "立即停止,不再导入后续行"],
"default": "跳过该行,继续导入"
}
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["import", "--file", "{{filePath}}", "--on-error", "{{onError}}"]
}
}

View File

@@ -0,0 +1,34 @@
{
"id": "item-update",
"label": "编辑记录",
"description": "编辑业务记录并重算派生字段。",
"replaces": "update",
"placements": [],
"executionProfile": "sync",
"interaction": "form",
"bind": {
"tables": ["items"],
"inputMapping": {
"id": "$row.$pk",
"name": "$row.name",
"note": "$row.note"
}
},
"hostEffects": {
"onSuccess": ["reloadTable"]
},
"inputSchema": {
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "number", "title": "编号", "readOnly": true },
"name": { "type": "string", "title": "名称" },
"note": { "type": "string", "title": "备注" }
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["update", "--id", "{{id}}", "--name", "{{name}}", "--note", "{{note}}"]
}
}

View File

@@ -1,4 +1,4 @@
{
"version": "1.0.91",
"version": "1.0.92",
"skill": "your-skill-slug"
}

View File

@@ -84,8 +84,8 @@
},
"mutationKind": {
"type": "string",
"enum": ["create", "update", "delete"],
"description": "可选。接管数据管理默认按钮:create=新增、update=编辑、delete=删除。须配合非空 bind.tables;无配置时宿主仍直写 SQLite。"
"enum": ["create", "update", "delete", "import"],
"description": "可选。接管数据管理默认按钮:create=新增、update=编辑、delete=删除、import=导入。须配合非空 bind.tables;无配置时宿主仍直写 SQLite。import 时 UI 仍用宿主「导入数据」面板,确认后调用本 Action 写库。"
},
"scalarArg": {
"type": ["string", "number", "boolean"]
@@ -120,12 +120,12 @@
},
"display_type": {
"type": "string",
"enum": ["textarea", "date", "datetime_unix_seconds", "url", "email", "lookup"],
"description": "可选。表单控件提示;与 format 二选一即可。lookup=关联下拉(须配 lookup 对象或绑到已有 lookup 列)。须与 CRUD display_type 语义对齐(见 ACTIONS.md / SCHEMA.md)。"
"enum": ["textarea", "date", "datetime_unix_seconds", "url", "email", "lookup", "directory", "file"],
"description": "可选。表单控件提示;与 format 二选一即可。lookup=关联下拉(须配 lookup 对象或绑到已有 lookup 列)。file/directory=本地路径选择。须与 CRUD display_type 语义对齐(见 ACTIONS.md / SCHEMA.md)。"
},
"format": {
"type": "string",
"enum": ["textarea", "date", "datetime_unix_seconds", "url", "email", "lookup"],
"enum": ["textarea", "date", "datetime_unix_seconds", "url", "email", "lookup", "directory", "file"],
"description": "可选。display_type 别名,语义相同。"
},
"lookup": {
@@ -214,9 +214,13 @@
"filter_value": {
"type": ["string", "number", "boolean", "null"],
"description": "可选。与 filter_column 配对的等值"
},
"multiple": {
"type": "boolean",
"description": "可选。true=多选 Lookup(存/提交 TEXT 逗号分隔 id);省略或 false=单选(默认)。与 CRUD options_json lookup 同构。"
}
},
"description": "同库 FK 式 Lookup。展示:value · label;有 extra 时 value · label(extra)。宿主亦接受 camelCase 键名。"
"description": "同库 FK 式 Lookup。展示:value · label;有 extra 时 value · label(extra)。multiple 时多选。宿主亦接受 camelCase 键名。"
},
"inputSchema": {
"type": "object",
@@ -420,7 +424,8 @@
"skill-detail": { "type": "string", "enum": ["direct", "confirm", "form"] },
"create": { "type": "string", "enum": ["direct", "confirm", "form"] },
"update": { "type": "string", "enum": ["direct", "confirm", "form"] },
"delete": { "type": "string", "enum": ["direct", "confirm", "form"] }
"delete": { "type": "string", "enum": ["direct", "confirm", "form"] },
"import": { "type": "string", "enum": ["direct", "confirm", "form"] }
},
"description": "按 placement / replaces 覆盖 interaction(只能写 concrete 模式,不能写 auto)。"
},

View File

@@ -67,7 +67,8 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
宿主约定(与本模板对齐):
- 工具栏「导入」默认从 `input/` 取文件;「导出」默认写到 `output/`
- 工具栏「导入」**UI 一律**打开宿主「导入数据」面板(默认从 `input/` 选文件);「导出」默认写到 `output/`
- 若表声明了 `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 页面报告」)
---

View File

@@ -56,7 +56,7 @@
数据管理有两层,**同等重要**:
1. **执行面**:工具栏 / 行级 / 批量 / `replaces` 接管 CRUD —— 与对话一样触发同一 CLI,走完整业务闭环(传统用户可不聊天)。
1. **执行面**:工具栏 / 行级 / 批量 / `replaces` 接管新增·编辑·删除·**导入** —— 与对话一样触发同一 CLI,走完整业务闭环(传统用户可不聊天)。导入按钮的 UX 始终是宿主「导入数据」面板;有 `replaces:import` 时确认后由技能写库。
2. **运营面**:业务表、状态流向、配置、input/output、日志、HTML —— 看清数据从哪来、到哪去、卡在哪。
**适合:**

View File

@@ -83,7 +83,7 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
| 字段 / 层 | 只决定什么 | 不决定什么 |
|-----------|------------|------------|
| **`placements`** | Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail;`row`=行级入口,壳由宿主呈现) | 不决定 sync/async,不决定点了是否弹窗,不决定行级用操作列还是右键,不决定谁能点 |
| **`replaces`** | **可选**:接管数据管理默认「新增 / 编辑 / 删除」 | 不强制;无配置时宿主仍直写 SQLite |
| **`replaces`** | **可选**:接管数据管理默认「新增 / 编辑 / 删除 / **导入**」 | 不强制;无配置时宿主仍直写 SQLite;**import 时 UI 仍用宿主「导入数据」面板** |
| **`bind.tables`** | 数据管理里出现在**哪些业务表** | 不决定业务实现 |
| **`executionProfile`** | 宿主是**同步等待**还是**异步 Job** | 不限制可展示入口,不决定是否弹窗 |
| **`interaction`** | 可点击入口(数据管理 toolbar/row/batch、默认 CRUD 接管、未来 skill-detail)**点了怎么交互** | 不决定 sync/async;**cron 触发与 agent 忽略** |
@@ -105,7 +105,7 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
| action | `id`、`label`、`description`、`placements`、`entrypoint`、**`executionProfile`(必填)**;可选 `hoverHint`、`inputSchema`、`confirmation`、**`interaction`**、**`interactionByPlacement`**、**`bind`**、**`replaces`**、**`hostEffects`**、**`steps`**、**`authz`**(仅文档提示) |
| entrypoint | `type = cli`、`command`、`args`(**必填** JSON 数组,项为 `string` / `number` / `boolean`) |
| placements | `toolbar`、`row`、`batch`、`skill-detail`、`agent`、`cron`;仅 `replaces` 时可为空数组 |
| **replaces** | 可选 `"create"` \| `"update"` \| `"delete"` — 接管默认新增/编辑/删除(须 `bind.tables`) |
| **replaces** | 可选 `"create"` \| `"update"` \| `"delete"` \| `"import"` — 接管默认新增/编辑/删除/导入(须 `bind.tables`) |
| **bind** | `{ "tables": ["business_table"] }`;可选 `inputMapping`(行内/批量预填,见下节) |
| **policy** | 可选;`enabled` + `entrypoint`(推荐 `policy` CLI);见 POLICY.md。未启用 = L0 |
| **cellPresentation** | 可选;`{ "enabled": true }` 时宿主应加载单元格呈现;见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)。未启用 = P0 |
@@ -140,32 +140,166 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
- **`inputMapping`(可选)**:把行/勾选映射到 `inputSchema` 字段,例如 `"leadId": "$row.$pk"`、`"ids": "$selection.ids"`。适合 `row` / `batch`。宿主仍会提交这些字段;可用 `inputSchema.properties.*.readOnly: true` 在表单中只读展示。
- `concurrency` / `locks` 仍未稳定,**不要**写入新技能 manifest。
## `replaces`(可选:接管默认新增 / 编辑 / 删除)
## `replaces`(可选:接管默认新增 / 编辑 / 删除 / 导入)
有领域副作用的表(建目录、加密、级联清理)不要依赖宿主直写 SQLite。此时可为该表声明:
有领域副作用的表(归一化字段、建目录、加密、级联清理)不要依赖宿主直写 SQLite。此时可为该表声明 `replaces`,**用户仍点宿主默认按钮**,由技能 CLI 完成写入。
### 怎么选
| 场景 | 建议 |
|------|------|
| 纯表字段、无副作用 | 不配 `replaces`,宿主直写即可 |
| 单条新增/编辑/删除有副作用 | `replaces: create` / `update` / `delete` |
| 批量导入也有副作用(推荐) | 另配 `replaces: import`(与 create 可同表并存) |
| 只有 create、暂无 import | 宿主「导入」面板确认后会**逐行调 create**(兼容);正式产品应补 `import` |
### 导入分流(必读)
点工具栏「导入」时:
1. **UI 一律**打开宿主「导入数据」面板(下载模板 → 选文件 → 映射 → 出错规则 → 预览)。
2. **写库**按表能力分流:
- 有 `replaces: import` → 把选中文件路径 + 出错规则交给该 Action(技能解析并写入)
- 仅有 `replaces: create` → 宿主按行调用 create(兼容)
- 都没有 → 宿主直写 SQLite
**不要**再做一张技能自己的「开始执行」导入表单来冒充导入体验。`placements` 对 import 保持 `[]`;`description` 写明「由宿主导入面板调用」。
可抄文件见 [`assets/examples/`](../assets/examples/):`replaces-*.action.example.json`。
### 示例:create
```json
{
"id": "account-add-web",
"label": "新增账号",
"description": "登记网页账号并初始化 profile 目录。",
"id": "item-create",
"label": "新增记录",
"description": "新增业务记录并执行领域副作用(走技能 CLI,非宿主直写库)。",
"replaces": "create",
"placements": [],
"executionProfile": "sync",
"interaction": "form",
"bind": { "tables": ["accounts"] },
"bind": { "tables": ["items"] },
"hostEffects": { "onSuccess": ["reloadTable"] },
"inputSchema": {
"type": "object",
"required": ["platform_key", "account_label"],
"required": ["name"],
"properties": {
"platform_key": { "type": "string", "title": "平台" },
"account_label": { "type": "string", "title": "账号名称" }
"name": { "type": "string", "title": "名称" },
"note": { "type": "string", "title": "备注" }
}
},
"entrypoint": {
"type": "cli",
"command": "account",
"args": ["add-web", "--platform", "{{platform_key}}", "--label", "{{account_label}}"]
"command": "item",
"args": ["create", "--name", "{{name}}", "--note", "{{note}}"]
}
}
```
### 示例:update
```json
{
"id": "item-update",
"label": "编辑记录",
"description": "编辑业务记录并重算派生字段。",
"replaces": "update",
"placements": [],
"executionProfile": "sync",
"interaction": "form",
"bind": {
"tables": ["items"],
"inputMapping": {
"id": "$row.$pk",
"name": "$row.name",
"note": "$row.note"
}
},
"hostEffects": { "onSuccess": ["reloadTable"] },
"inputSchema": {
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "number", "title": "编号", "readOnly": true },
"name": { "type": "string", "title": "名称" },
"note": { "type": "string", "title": "备注" }
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["update", "--id", "{{id}}", "--name", "{{name}}", "--note", "{{note}}"]
}
}
```
### 示例:delete
```json
{
"id": "item-delete",
"label": "删除记录",
"description": "删除业务记录并清理关联副作用。",
"replaces": "delete",
"placements": [],
"executionProfile": "sync",
"interaction": "confirm",
"confirmation": { "message": "确定删除该记录?关联数据将一并清理。" },
"bind": {
"tables": ["items"],
"inputMapping": { "id": "$row.$pk" }
},
"hostEffects": { "onSuccess": ["reloadTable", "clearSelection"] },
"inputSchema": {
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "number", "title": "编号", "readOnly": true }
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["delete", "--id", "{{id}}"]
}
}
```
### 示例:import
```json
{
"id": "item-import",
"label": "导入记录",
"description": "由宿主「导入数据」面板调用:解析 Excel/CSV 并写入业务表(含领域校验与派生字段)。",
"replaces": "import",
"placements": [],
"executionProfile": "sync",
"interaction": "form",
"bind": { "tables": ["items"] },
"hostEffects": { "onSuccess": ["reloadTable"] },
"inputSchema": {
"type": "object",
"required": ["filePath"],
"properties": {
"filePath": {
"type": "string",
"title": "导入文件",
"description": "由宿主导入面板传入的 Excel/CSV 路径。",
"format": "file"
},
"onError": {
"type": "string",
"title": "导入规则(出错时)",
"enum": ["跳过该行,继续导入", "立即停止,不再导入后续行"],
"default": "跳过该行,继续导入"
}
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["import", "--file", "{{filePath}}", "--on-error", "{{onError}}"]
}
}
```
@@ -173,12 +307,13 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
规则:
- **全可选**:无 `replaces` 时,宿主默认按钮仍直写库,行为与以前一致。
- 取值:`create`(新增)/ `update`(编辑)/ `delete`(删除)。
- 取值:`create`(新增)/ `update`(编辑)/ `delete`(删除)/ `import`(导入写库)。
- **必须**非空 `bind.tables`;同一技能同一表同一 `replaces` 至多一个 Action。
- 与 `placements` 正交:可只接管默认按钮(`placements: []`),不必再挂 toolbar「第二个新增」。
- 点默认按钮时走该 Action 的 `interaction`(create/update 常用 `form`;delete 常用 `confirm`)。
- 与 `placements` 正交:可只接管默认按钮(`placements: []`),不必再挂 toolbar「第二个新增/导入」。
- 点默认「新增/编辑/删除」时走该 Action 的 `interaction`(create/update 常用 `form`;delete 常用 `confirm`)。
- 点默认「导入」时**固定**打开宿主导入面板;确认后若有 `replaces:import` 则调用该 Action(`interaction` 不决定是否再弹技能表单)。
- `update` / `delete` 可用 `inputMapping`(`$row.$pk`、`$selection.ids`)预填主键。
- 宿主导入仍直写:有 `replaces:create` 的表慎用宿主导入,或另做 import Action。
- `import` 约定:`filePath`(或 `file`)+ 可选 `onError`(`skip`/`abort` 或上例中文枚举);技能自行解析文件与表头。
- 整表 `_jiangchang_tables.readonly=1` 时仍隐藏写按钮(优先于 `replaces`)。
- **列元数据复用(lookup)**:Action 表单(含 toolbar/batch 弹窗与 create/update 抽屉)可将 `inputSchema` 字段对齐业务列,复用中文名与 lookup 下拉:
- 优先让属性名 = 物理列名(如 `platform_key`);
@@ -598,7 +733,7 @@ HINT: <next step>
| 当前表已有同含义列,且列上已是 CRUD lookup | `column` / `bind.columnMapping`,或属性名 = 列名 | toolbar/batch 弹窗与 create/update 抽屉都会复用该列已解析的选项 |
| 当前表没有该列(如在「潜客」表上选「话术模板」) | `display_type: "lookup"` + 字段内联 `lookup` 对象 | 打开表单时宿主按当前技能库解析选项 |
内联 `lookup` 与列元数据 `options_json` **同构**(见 [`SCHEMA.md`](SCHEMA.md)「表关联 lookup」):
内联 `lookup` 与列元数据 `options_json` **同构**(见 [`SCHEMA.md`](SCHEMA.md)「跨表引用 lookup」):
```json
{
@@ -615,6 +750,20 @@ HINT: <next step>
"filter_column": "is_active",
"filter_value": 1
}
},
"engineIds": {
"type": "string",
"title": "监测引擎",
"description": "可多选;提交为逗号分隔编号。",
"display_type": "lookup",
"lookup": {
"ref_table": "engines",
"value_column": "id",
"label_column": "display_name",
"filter_column": "enabled",
"filter_value": 1,
"multiple": true
}
}
}
```
@@ -626,16 +775,22 @@ HINT: <next step>
| `label_column` | 是 | 主文案列 |
| `extra_column` | 否 | 第三展示列(任意列名,不限「备注」) |
| `filter_column` / `filter_value` | 否 | 等值过滤 |
| `multiple` | 否 | `true` = 多选;省略/`false` = 单选(默认) |
**下拉展示(宿主统一,技能只配列):** `{value} · {label}`;有非空 `extra_column` 时为 `{value} · {label}({extra})`。例:`2 · 通用私信-留资`、`2 · 通用私信-留资(dm)`。
**下拉/选项展示(宿主统一,技能只配列):** 单选为 `{value} · {label}`;有非空 `extra_column` 时为 `{value} · {label}({extra})`。例:`2 · 通用私信-留资`、`2 · 通用私信-留资(dm)`。
**可选「自动」:** 字段不要放进 `required`;不要用 `minimum: 1` 卡死留空。宿主会省略空的可选数字参数,避免「提交的数据无效」。
**多选存值与模板:** Action / CRUD 存 **`TEXT` 逗号分隔 id**(如 `1,2,5`);`{{engineIds}}` 原样替换进 CLI。多选字段在 `inputSchema` 里用 `type: "string"`(逗号串),不要用尚未稳定的 JSON array 当业务控件。
**多选控件(宿主策略):** 选项少(建议 ≤6)用勾选列表;选项多用字段内 Chip + 可搜索下拉(不是再开业务弹窗)。
**可选「自动」(单选常见):** 字段不要放进 `required`;不要用 `minimum: 1` 卡死留空。宿主会省略空的可选数字参数,避免「提交的数据无效」。
**反模式(勿做):**
- 标题写成「某某编号」,说明里教用户去左侧表抄 id
- 用静态 `enum` 罗列会增长/变更的业务行(会过期)
- 在技能里自己拼假 label 列代替宿主统一格式(难维护)
- 另造 `lookup_multi` / 第二套 display_type(应只加 `multiple: true`)
形状参考:[`assets/examples/lookup-action.example.json`](../assets/examples/lookup-action.example.json)。
@@ -651,7 +806,7 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件
| 数字 | `type: number` | `display_type: number` 或 INT/REAL/NUMERIC |
| 开关 | `type: boolean` | `display_type: boolean` |
| 枚举下拉 | `enum: [...]` | `options_json` 数组 |
| 关联下拉(Lookup) | 内联 `display_type: lookup` + `lookup`,或绑列 `column` / `columnMapping` | `display_type: lookup` + `options_json` 对象 |
| 关联下拉(Lookup) | 内联 `display_type: lookup` + `lookup`(可选 `multiple`),或绑列 | 同左;多选存 `TEXT` 逗号分隔 id |
| 密码 | `sensitive: true` | 列 `sensitive` |
| 日期(日) | `display_type` 或 `format`: `date` | `display_type: date` → 存 `YYYY-MM-DD` |
| 日期时间 | `display_type` 或 `format`: `datetime_unix_seconds` | 同左;**存/传 Unix 秒** |
@@ -663,7 +818,8 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件
| 只读 | `readOnly: true` | 列 `editable: 0` / 宿主只读规则 |
| **条件显隐** | **`visibleWhen`**(见下节) | **本版 CRUD 无对称**;仅 Action / 定时任务参数表单 |
**本版不做(勿宣传):** 文件上传、多选/标签数组、复杂 `oneOf` / nested object 专用表单;`visibleWhen` 多条件 AND/OR 树、跨 Action 依赖、CRUD 新增/编辑对称显隐。
**本版不做(勿宣传):** 文件上传、复杂 `oneOf` / nested object 专用表单;`visibleWhen` 多条件 AND/OR 树、跨 Action 依赖、CRUD 新增/编辑对称显隐。
(**Lookup 多选**已支持:同一 `lookup` + `multiple: true`,见上文;不是任意标签数组。)
日期时间约定:
@@ -676,7 +832,7 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件
- 根:`object`;属性:`string` / `number` / `boolean`(`object`/`array` 仅作 JSON 多行编辑兼容,新技能慎用)
- `enum` / `default` / `required` / `sensitive` / `readOnly`
- `display_type` / `format`:`textarea` | `date` | `datetime_unix_seconds` | `url` | `email` | **`lookup`**
- 字段内联 `lookup`(与 CRUD `options_json` lookup 对象同构;可选 `extra_column`)
- 字段内联 `lookup`(与 CRUD `options_json` lookup 对象同构;可选 `extra_column`、`multiple`)
- 属性级 `description`(表单标签下方灰字说明)
- 简单模板替换(`{{fieldName}}`)
- 可选空数字字段提交时由宿主省略,避免 `""` 触发 number 校验失败
@@ -802,7 +958,7 @@ CRUD 侧宿主义务与禁止事项见 [`SCHEMA.md`](SCHEMA.md)「表单与展
- 短名看不懂或会覆盖数据时,用独立的 `hoverHint`;不要把确认黄条复用成悬停
- 模板参数使用 `{{fieldName}}`,且必须在 `inputSchema.properties` 中声明
- `entrypoint.args` 项当前只允许 `string`、`number`、`boolean`;不允许对象或 `null`
- **不要**在当前模板中宣传文件上传、多选数组、`oneOf`、复杂 nested object 等尚未稳定支持的能力
- **不要**在当前模板中宣传文件上传、任意 JSON 多选数组、`oneOf`、复杂 nested object 等尚未稳定支持的能力;Lookup 多选请用 `lookup.multiple`
## 风险与入口配置
- 查询、诊断、版本检查可以放到 `skill-detail` 或 `agent`

View File

@@ -150,7 +150,7 @@
4. `mode=open`:等价「L1 不限制按钮」,可与 L2 行列限制并存。
5. **`denylist` 不是 v1 合法值**;出现则 §6 fail-closed。
`replaces` 接管的默认新增/编辑/删除:视为对应 Action 的 id 是否在 allowlist 中;不在则宿主隐藏该接管按钮。`mode=open` 时不按 id 限制 replaces。
`replaces` 接管的默认新增/编辑/删除/导入:视为对应 Action 的 id 是否在 allowlist 中;不在则宿主隐藏该接管按钮(导入按钮是否可用还受 `hostCapabilities` 含 `import` 影响)。`mode=open` 时不按 id 限制 replaces。
### 2.4 `hostCapabilities`(L1 可选冻)

View File

@@ -87,7 +87,7 @@ metadata:
不要把「外部来源只读」错误理解成「技能本地数据库一定只读」。技能通过 `_jiangchang_tables.readonly` 与 `_jiangchang_columns.editable` 声明本地表的实际写权限。
若创建/更新/删除带有领域副作用(自动建目录、加密、级联清理),不要依赖宿主默认直写:在 `assets/actions.json` 用可选字段 `replaces: create|update|delete` 接管默认按钮,详见 [`ACTIONS.md`](ACTIONS.md)。
若创建/更新/删除/导入带有领域副作用(自动建目录、加密、级联清理、归一化字段),不要依赖宿主默认直写:在 `assets/actions.json` 用可选字段 `replaces: create|update|delete|import` 接管默认按钮。其中 **import** 时用户仍使用宿主「导入数据」面板,确认后由技能写库。详见 [`ACTIONS.md`](ACTIONS.md) 与 `assets/examples/replaces-*.action.example.json`。
## 表与表之间:禁止 SQLite 外键
@@ -308,7 +308,7 @@ upsert_column_metadata(
| `number` | `INTEGER` / `REAL` / `NUMERIC` | 原文 | **数字输入**(`type=number`) | 数字;无 `display_type` 时,物理类型为 INT/REAL/NUMERIC 也应走数字控件 |
| `boolean` | `INTEGER` 0/1 或 BOOL | 开关语义展示 | 开关 | `0` / `1` |
| (枚举) | 任意 + `options_json` **数组** | 标签映射 | 下拉选择 | 选项 `value` |
| `lookup` | 跨表引用 id 标量 + `options_json` **对象** | 标签映射 | 下拉选择(查关联表) | 业务 id(非 SQLite 外键) |
| `lookup` | 跨表引用 id;`options_json` **对象**;可选 `multiple` | 单选:标签映射;多选:多标签(如「豆包、元宝」) | 单选下拉;多选见下文 | 单选:业务 id 标量;多选:`TEXT` 逗号分隔 id(非 SQLite 外键) |
| (敏感) | 任意;列 `sensitive=1` | 掩码/隐藏策略由宿主定 | 新建可用密码框;编辑策略与既有宿主一致 | 原文(勿日志明文) |
| `date` | `TEXT` 推荐 `YYYY-MM-DD`,或可安全归一的值 | 日期 | **日期选择**(日) | `YYYY-MM-DD` |
| `datetime_unix_seconds` | `INTEGER` | `YYYY-MM-DD HH:mm:ss` | **日期时间选择**;提交写回 **Unix 秒** | Unix **秒**整数;禁止改成 ISO 字符串 |
@@ -320,9 +320,11 @@ upsert_column_metadata(
**明确不在本版稳定范围(勿在新技能宣传为已支持):**
- 文件 / 图片上传
- 多选、标签数组、复杂 `oneOf` / nested object 表单
- 复杂 `oneOf` / nested object 表单(任意 JSON 数组当业务多选控件)
- 把业务可编辑时间存成毫秒、ISO 文本或 SQLite `datetime` 文本(审计字段规范见「标准时间字段」)
> **Lookup 多选**已纳入本版契约(见下文 `multiple`),与「任意多选/标签数组」不是同一能力。
`datetime_unix_milliseconds`:仅兼容**列表展示**旧数据;**新技能不要用**;可编辑时间一律 `datetime_unix_seconds`。
#### 新增/编辑表单:字段说明与默认值(宿主支持)
@@ -573,7 +575,9 @@ CREATE TABLE demo_items (
### 2)跨表引用 lookup(对象,用于关联 id 字段)
当字段取值依赖同库另一张业务表时,`display_type` 设为 `lookup`,`options_json` 写对象(**不是**数组)。这是**展示与表单下拉**契约,**不是** SQLite 外键:
当字段取值依赖同库另一张业务表时,`display_type` 设为 `lookup`,`options_json` 写对象(**不是**数组)。这是**展示与表单选择**契约,**不是** SQLite 外键。
**单选(默认,与既有行为一致):**
```json
{
@@ -586,26 +590,54 @@ CREATE TABLE demo_items (
}
```
**多选(平行扩展,仅多一个开关):**
```json
{
"ref_table": "engines",
"value_column": "id",
"label_column": "display_name",
"filter_column": "enabled",
"filter_value": 1,
"multiple": true
}
```
| 字段 | 必填 | 说明 |
|---|---|---|
| `ref_table` | 是 | 同库被引用表名(snake_case 标识符) |
| `value_column` | 是 | 写入当前字段的值列(业务 id / 编号) |
| `label_column` | 是 | 下拉主文案列(名称) |
| `label_column` | 是 | 选项主文案列(名称) |
| `extra_column` | 否 | 第三展示列(不必叫「备注」);有值时拼进括号 |
| `filter_column` / `filter_value` | 否 | 等值过滤(如只列出 `enabled=1`) |
| `multiple` | 否 | `true` = 多选 Lookup;省略或 `false` = **单选**(默认) |
宿主在加载表 schema 时查询 `ref_table`,把结果展开成 `{value,label}` 下拉选项;**物理列仍存标量**(如 `platform_key` 文本、`account_id` 整数),不做嵌套子表。
宿主在加载表 schema 时查询 `ref_table`,把结果展开成 `{value,label}` 选项列表。
**存库约定:**
| 模式 | 物理列建议 | 存什么 |
|------|------------|--------|
| 单选 | 整数或文本标量 | 一个业务 id(如 `2`、`douyin`) |
| 多选 | **`TEXT`** | 逗号分隔 id,如 `1,2,5`(与 `$selection.ids` 风格一致);**不要**建联接子表当 UI 多选 |
**统一展示文案(宿主固定一种,技能只配列):**
- 默认:`{value} · {label}`,例如 `2 · 通用私信-留资`、`douyin · 抖音`
- 单选默认:`{value} · {label}`,例如 `2 · 通用私信-留资`、`douyin · 抖音`
- 配置了 `extra_column` 且该行有非空值:`{value} · {label}({extra})`
- 未配置 `extra_column`、或额外列为空:不加 `()`
- `label` 为空或与 `value` 相同时:只显示 `value`(可再带额外括号)
- **多选列表格**:按 id 拆开后逐个映射为 label,再用顿号/逗号拼接(如 `豆包、元宝`);过长可截断并提示「+N」
Action 弹窗内联 Lookup 与上表 **同构**(字段名一致),见 [`ACTIONS.md`](ACTIONS.md)「跨表引用:Lookup」。
**控件呈现(宿主策略,非第二套契约):**
示例:账号表的平台字段 → 平台信息表;凭据表的账号编号 → 账号信息表;私信 Action 选话术 → `message_templates`。
- 单选:下拉 Select(现状)
- 多选且选项较少(建议 ≤6):勾选列表
- 多选且选项较多:字段内 Chip + 可搜索下拉(**不是**再开一层业务弹窗)
Action 弹窗内联 Lookup 与上表 **同构**(字段名一致,含 `multiple`),见 [`ACTIONS.md`](ACTIONS.md)「跨表引用:Lookup」。
示例:账号表的平台字段 → 平台信息表;凭据表的账号编号 → 账号信息表;私信 Action 选话术 → `message_templates`;抽检 Action 多选引擎 → `engines` + `multiple: true`。
## 幂等初始化标准

View File

@@ -219,6 +219,8 @@ def validate_options_json(options_json: str | None) -> tuple[list[str], list[str
errors.append(f"options_json.{key} 必须是安全标识符,当前为 {raw!r}")
if parsed.get("filter_column") not in (None, "") and "filter_value" not in parsed:
warnings.append("options_json lookup 声明了 filter_column 但未提供 filter_value")
if "multiple" in parsed and not isinstance(parsed.get("multiple"), bool):
errors.append("options_json lookup.multiple 必须是布尔值")
return errors, warnings
if not isinstance(parsed, list):

View File

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

View File

@@ -188,7 +188,7 @@ class TestActionsManifest(unittest.TestCase):
if replaces is not None:
self.assertIn(
replaces,
{"create", "update", "delete"},
{"create", "update", "delete", "import"},
msg=f"{action_id} invalid replaces {replaces!r}",
)
bind = action.get("bind") or {}
@@ -236,7 +236,7 @@ class TestActionsManifest(unittest.TestCase):
self.assertIn("interactionByPlacement", props)
by_placement = props["interactionByPlacement"]
self.assertIs(by_placement.get("additionalProperties"), False)
for key in ("toolbar", "row", "batch", "cron", "agent", "skill-detail", "create", "update", "delete"):
for key in ("toolbar", "row", "batch", "cron", "agent", "skill-detail", "create", "update", "delete", "import"):
self.assertIn(key, by_placement.get("properties") or {})
self.assertEqual(
set((by_placement["properties"][key].get("enum") or [])),
@@ -246,7 +246,7 @@ class TestActionsManifest(unittest.TestCase):
self.assertEqual(props["replaces"].get("$ref"), "#/$defs/mutationKind")
self.assertEqual(
set(schema["$defs"]["mutationKind"].get("enum") or []),
{"create", "update", "delete"},
{"create", "update", "delete", "import"},
)
def test_schema_defines_hover_hint(self) -> None:

View File

@@ -353,6 +353,8 @@ class TestDocsStandards(unittest.TestCase):
"表与表之间:禁止 SQLite 外键",
"禁止 SQLite 外键",
"跨表引用 lookup",
"\"multiple\": true",
"Lookup 多选",
):
self.assertIn(marker, text, msg=f"SCHEMA.md missing {marker!r}")
@@ -364,6 +366,7 @@ class TestDocsStandards(unittest.TestCase):
"跨表引用:Lookup",
"display_type: \"lookup\"",
"extra_column",
"multiple",
"数据管理新增/编辑",
"_jiangchang_columns.description",
"datetime_unix_seconds",