Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c9f610a8e | |||
| 2c9bf56054 | |||
| 633ecff4e2 | |||
| 46bc573777 | |||
| 0002e86729 | |||
| caf0191366 | |||
| ba284922e5 |
25
CHANGELOG.md
25
CHANGELOG.md
@@ -9,6 +9,31 @@
|
||||
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
|
||||
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
|
||||
|
||||
## 1.0.67
|
||||
|
||||
- 移除模板内嵌的 `tools/ui_inspector`;UI 录制器产品迁至独立仓库 `jiangchang-ui-recorder`
|
||||
- 相关开发文档改为指向 Gitea 产品仓库入口
|
||||
|
||||
## 1.0.66
|
||||
|
||||
- 数据管理支持表级默认排序:`_jiangchang_tables.default_sort_json`(多字段 JSON);宿主优先采用,未配置则仍按 updated_at → created_at → 主键
|
||||
- 更新 SCHEMA.md 与 `upsert_table_metadata`;旧库幂等补列
|
||||
|
||||
## 1.0.65
|
||||
|
||||
- `replaces` create/update 可复用表列 lookup:字段名对齐列名,或使用 `bind.columnMapping` / 字段 `column`
|
||||
- 更新 ACTIONS.md 与 skill-actions.schema.json
|
||||
|
||||
## 1.0.64
|
||||
|
||||
- Skill Action 新增可选 `replaces: create|update|delete`:接管数据管理默认新增/编辑/删除,无配置时宿主仍直写 SQLite
|
||||
- 更新 ACTIONS.md 与 skill-actions.schema.json;`replaces` 须配合 `bind.tables`,可与空 `placements` 并用
|
||||
|
||||
## 1.0.63
|
||||
|
||||
- 数据管理支持字段级 lookup:`options_json` 可声明同库关联表(`ref_table` / `value_column` / `label_column`),宿主新建/编辑时渲染下拉
|
||||
- 更新 SCHEMA.md 与元数据校验,兼容静态枚举数组与 lookup 对象两种形态
|
||||
|
||||
## 1.0.62
|
||||
|
||||
- Skill Action 新增 `interaction` / `interactionByPlacement`:声明数据管理点击是直通、轻确认还是参数表单(与 `executionProfile` 正交)
|
||||
|
||||
2
SKILL.md
2
SKILL.md
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: 技能开发模板(通用业务版)
|
||||
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
|
||||
version: 1.0.62
|
||||
version: 1.0.67
|
||||
author: 深圳匠厂科技有限公司
|
||||
metadata:
|
||||
openclaw:
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
"enum": ["toolbar", "row", "batch", "cron", "agent", "skill-detail"],
|
||||
"description": "稳定支持 toolbar/row/batch/cron/agent/skill-detail(对齐匠厂宿主)。placements 与 executionProfile 正交,不做条件限制。"
|
||||
},
|
||||
"mutationKind": {
|
||||
"type": "string",
|
||||
"enum": ["create", "update", "delete"],
|
||||
"description": "可选。接管数据管理默认按钮:create=新增、update=编辑、delete=删除。须配合非空 bind.tables;无配置时宿主仍直写 SQLite。"
|
||||
},
|
||||
"scalarArg": {
|
||||
"type": ["string", "number", "boolean"]
|
||||
},
|
||||
@@ -52,6 +57,12 @@
|
||||
"readOnly": {
|
||||
"type": "boolean",
|
||||
"description": "宿主数据管理表单只读展示该字段(仍会随提交传入 entrypoint);用于行内/勾选预填的主键等"
|
||||
},
|
||||
"column": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-z][a-z0-9_]*$",
|
||||
"description": "可选。绑定业务表物理列名;replaces create/update 时宿主可复用该列的中文名与 lookup 下拉"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -120,9 +131,18 @@
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string", "minLength": 1 },
|
||||
"description": "可选。将 $row.$pk / $row.col / $selection.ids 等映射到 inputSchema 字段名,供 row/batch 预填。"
|
||||
},
|
||||
"columnMapping": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-z][a-z0-9_]*$"
|
||||
},
|
||||
"description": "可选。将 inputSchema 属性名映射到业务表物理列名,供 replaces create/update 抽屉复用列 displayName / lookup options。"
|
||||
}
|
||||
},
|
||||
"description": "表级绑定;toolbar 必填。可选 inputMapping 供行内/批量预填。concurrency/locks 勿写入。"
|
||||
"description": "表级绑定;toolbar 必填。可选 inputMapping / columnMapping。concurrency/locks 勿写入。"
|
||||
},
|
||||
"action": {
|
||||
"type": "object",
|
||||
@@ -144,10 +164,11 @@
|
||||
},
|
||||
"placements": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"minItems": 0,
|
||||
"items": { "$ref": "#/$defs/placement" },
|
||||
"description": "决定 Action 出现在哪里;与 executionProfile 正交"
|
||||
"description": "决定 Action 出现在哪里;与 executionProfile 正交。仅 replaces 接管默认 CRUD 时可为空数组。"
|
||||
},
|
||||
"replaces": { "$ref": "#/$defs/mutationKind" },
|
||||
"entrypoint": { "$ref": "#/$defs/entrypoint" },
|
||||
"inputSchema": { "$ref": "#/$defs/inputSchema" },
|
||||
"confirmation": { "$ref": "#/$defs/confirmation" },
|
||||
@@ -170,9 +191,12 @@
|
||||
"batch": { "type": "string", "enum": ["direct", "confirm", "form"] },
|
||||
"cron": { "type": "string", "enum": ["direct", "confirm", "form"] },
|
||||
"agent": { "type": "string", "enum": ["direct", "confirm", "form"] },
|
||||
"skill-detail": { "type": "string", "enum": ["direct", "confirm", "form"] }
|
||||
"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"] }
|
||||
},
|
||||
"description": "按 placement 覆盖 interaction(只能写 concrete 模式,不能写 auto)。常用于同一 Action 在 row 直通、toolbar 仍要表单。"
|
||||
"description": "按 placement / replaces 覆盖 interaction(只能写 concrete 模式,不能写 auto)。"
|
||||
},
|
||||
"bind": { "$ref": "#/$defs/bind" }
|
||||
},
|
||||
@@ -187,7 +211,17 @@
|
||||
"then": {
|
||||
"required": ["bind"]
|
||||
},
|
||||
"description": "concurrency/locks 为未稳定预留字段,勿写入新技能 manifest。placements 含 toolbar 时必须声明 bind.tables。"
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"required": ["replaces"]
|
||||
},
|
||||
"then": {
|
||||
"required": ["bind"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "concurrency/locks 为未稳定预留字段,勿写入新技能 manifest。placements 含 toolbar 或声明 replaces 时必须声明 bind.tables。无 replaces 时 placements 建议非空(模板测试强制)。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
|
||||
未完成第 2~6 步时,代码只能保留明确的 `TODO: NEED_REAL_UI_INSPECTION`,不得写“看起来可能正确”的 selector。
|
||||
|
||||
开发期推荐使用独立仓 [jiangchang-ui-recorder](https://git.jc2009.com/client-jiangchang/jiangchang-ui-recorder)(本机常见路径 `D:\OpenClaw\client-jiangchang\jiangchang-ui-recorder`,`python run.py`):以 **Raw Event Journal → Action → Element Catalog → 异步 Observation** 录制,优先把 `action_flow.json` 与 `element_catalog.json` 交给 AI 编程工具;原始事件可重新归并,tree/index 作为诊断证据。默认不再让完整页面 dump 阻塞动作采集。本模板仓内的 `tools/ui_inspector` 已移除,勿再从 skill-template 启动。
|
||||
|
||||
## 3. 元素清单
|
||||
|
||||
每个关键输入、按钮、列表、弹窗、成功状态和错误状态都必须登记。推荐 YAML:
|
||||
|
||||
@@ -81,9 +81,10 @@ Action 是**可选能力**:
|
||||
| 字段 / 层 | 只决定什么 | 不决定什么 |
|
||||
|-----------|------------|------------|
|
||||
| **`placements`** | Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail) | 不决定 sync/async,不决定点了是否弹窗 |
|
||||
| **`replaces`** | **可选**:接管数据管理默认「新增 / 编辑 / 删除」 | 不强制;无配置时宿主仍直写 SQLite |
|
||||
| **`bind.tables`** | 数据管理里出现在**哪些业务表** | 不决定业务实现 |
|
||||
| **`executionProfile`** | 宿主是**同步等待**还是**异步 Job** | 不限制可展示入口,不决定是否弹窗 |
|
||||
| **`interaction`** | 可点击入口(数据管理 toolbar/row/batch、未来 skill-detail)**点了怎么交互** | 不决定 sync/async;**cron 触发与 agent 忽略** |
|
||||
| **`interaction`** | 可点击入口(数据管理 toolbar/row/batch、默认 CRUD 接管、未来 skill-detail)**点了怎么交互** | 不决定 sync/async;**cron 触发与 agent 忽略** |
|
||||
| **`entrypoint`** | 实际执行哪个 CLI 命令与参数 | 不写业务逻辑 |
|
||||
| **domain service** | **真正完成什么业务** | 不感知 toolbar / cron / agent 来源 |
|
||||
|
||||
@@ -94,9 +95,10 @@ Action 是**可选能力**:
|
||||
| 层级 | 稳定字段 / 能力 |
|
||||
|------|----------------|
|
||||
| 顶层 | `schemaVersion`、`skill`、`actions` |
|
||||
| action | `id`、`label`、`description`、`placements`、`entrypoint`、**`executionProfile`(必填)**;可选 `inputSchema`、`confirmation`、**`interaction`**、**`interactionByPlacement`**、**`bind`** |
|
||||
| action | `id`、`label`、`description`、`placements`、`entrypoint`、**`executionProfile`(必填)**;可选 `inputSchema`、`confirmation`、**`interaction`**、**`interactionByPlacement`**、**`bind`**、**`replaces`** |
|
||||
| entrypoint | `type = cli`、`command`、`args`(**必填** JSON 数组,项为 `string` / `number` / `boolean`) |
|
||||
| placements | `toolbar`、`row`、`batch`、`skill-detail`、`agent`、`cron` |
|
||||
| placements | `toolbar`、`row`、`batch`、`skill-detail`、`agent`、`cron`;仅 `replaces` 时可为空数组 |
|
||||
| **replaces** | 可选 `"create"` \| `"update"` \| `"delete"` — 接管默认新增/编辑/删除(须 `bind.tables`) |
|
||||
| **bind** | `{ "tables": ["business_table"] }`;可选 `inputMapping`(行内/批量预填,见下节) |
|
||||
| inputSchema | 根 `type: object`;properties 使用 `string` / `number` / `boolean`;支持 `enum`、`default`、`required`、`sensitive`、`readOnly` |
|
||||
| confirmation | `{ "message": "..." }` — **风险文案**(confirm/form 弹窗黄条;agent 元数据);**不是**「要不要弹窗」的开关 |
|
||||
@@ -125,6 +127,52 @@ Action 是**可选能力**:
|
||||
- **`inputMapping`(可选)**:把行/勾选映射到 `inputSchema` 字段,例如 `"leadId": "$row.$pk"`、`"ids": "$selection.ids"`。适合 `row` / `batch`。宿主仍会提交这些字段;可用 `inputSchema.properties.*.readOnly: true` 在表单中只读展示。
|
||||
- `concurrency` / `locks` 仍未稳定,**不要**写入新技能 manifest。
|
||||
|
||||
## `replaces`(可选:接管默认新增 / 编辑 / 删除)
|
||||
|
||||
有领域副作用的表(建目录、加密、级联清理)不要依赖宿主直写 SQLite。此时可为该表声明:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "account-add-web",
|
||||
"label": "新增账号",
|
||||
"description": "登记网页账号并初始化 profile 目录。",
|
||||
"replaces": "create",
|
||||
"placements": [],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "form",
|
||||
"bind": { "tables": ["accounts"] },
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["platform_key", "account_label"],
|
||||
"properties": {
|
||||
"platform_key": { "type": "string", "title": "平台" },
|
||||
"account_label": { "type": "string", "title": "账号名称" }
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "account",
|
||||
"args": ["add-web", "--platform", "{{platform_key}}", "--label", "{{account_label}}"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- **全可选**:无 `replaces` 时,宿主默认按钮仍直写库,行为与以前一致。
|
||||
- 取值:`create`(新增)/ `update`(编辑)/ `delete`(删除)。
|
||||
- **必须**非空 `bind.tables`;同一技能同一表同一 `replaces` 至多一个 Action。
|
||||
- 与 `placements` 正交:可只接管默认按钮(`placements: []`),不必再挂 toolbar「第二个新增」。
|
||||
- 点默认按钮时走该 Action 的 `interaction`(create/update 常用 `form`;delete 常用 `confirm`)。
|
||||
- `update` / `delete` 可用 `inputMapping`(`$row.$pk`、`$selection.ids`)预填主键。
|
||||
- 宿主导入仍直写:有 `replaces:create` 的表慎用宿主导入,或另做 import Action。
|
||||
- 整表 `_jiangchang_tables.readonly=1` 时仍隐藏写按钮(优先于 `replaces`)。
|
||||
- **列元数据复用(lookup)**:create/update 抽屉可将 `inputSchema` 字段对齐业务列,复用中文名与 lookup 下拉:
|
||||
- 优先让属性名 = 物理列名(如 `platform_key`);
|
||||
- 或 `bind.columnMapping`:`{ "platform": "platform_key" }`;
|
||||
- 或字段上写 `"column": "platform_key"`。
|
||||
- **不要**在 actions.json 里写死会过期的全量平台 `enum`。
|
||||
|
||||
## `interaction`(点击交互方式)
|
||||
|
||||
与 `executionProfile` **正交**:一个 Action 可以是 `async` + `direct`(一点就进任务中心),也可以是 `sync` + `form`(填参后当场返回)。
|
||||
|
||||
@@ -78,6 +78,8 @@ metadata:
|
||||
|
||||
不要把「外部来源只读」错误理解成「技能本地数据库一定只读」。技能通过 `_jiangchang_tables.readonly` 与 `_jiangchang_columns.editable` 声明本地表的实际写权限。
|
||||
|
||||
若创建/更新/删除带有领域副作用(自动建目录、加密、级联清理),不要依赖宿主默认直写:在 `assets/actions.json` 用可选字段 `replaces: create|update|delete` 接管默认按钮,详见 [`ACTIONS.md`](ACTIONS.md)。
|
||||
|
||||
## 标准数据同步语义(外源 → 本地库)
|
||||
|
||||
适用于「同步 / refresh / collect」类能力。使用通用名称表述,勿把具体平台表名写进模板实现。
|
||||
@@ -131,6 +133,38 @@ metadata:
|
||||
| `sort_order` | 否 | 兼容保留字段,不用于替代物理字段顺序 |
|
||||
| `visible` | 是 | `1` 在宿主数据管理中展示 / `0` 隐藏 |
|
||||
| `readonly` | 是 | `1` 整张表只读;`0` 允许宿主根据字段权限执行写操作 |
|
||||
| `default_sort_json` | 否 | 数据管理**默认行排序**(见下节);空则宿主用内置默认 |
|
||||
|
||||
#### 表级默认排序 `default_sort_json`(宿主支持)
|
||||
|
||||
技能可声明该表在数据管理中的默认排序;宿主优先采用,未声明或声明无效时回退:
|
||||
|
||||
`updated_at DESC` → `created_at DESC` → 主键 DESC。
|
||||
|
||||
JSON 数组(可多字段,按顺序):
|
||||
|
||||
```json
|
||||
[{"column":"sent_at","direction":"desc"},{"column":"id","direction":"desc"}]
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- `column` 必须是该表真实列名;敏感列会被宿主忽略
|
||||
- `direction` 仅 `asc` / `desc`(缺省按 `desc`)
|
||||
- 用户在 UI 上手动改排序后,以用户选择为准;声明只影响未选择时的默认
|
||||
- 通过 `upsert_table_metadata(..., default_sort_json=...)` 写入;旧库由 `init_display_metadata_tables` 幂等 `ALTER` 补列
|
||||
|
||||
示例(消息时间线):
|
||||
|
||||
```python
|
||||
upsert_table_metadata(
|
||||
cursor,
|
||||
table_name="messages",
|
||||
display_name="消息",
|
||||
readonly=0,
|
||||
default_sort_json='[{"column":"sent_at","direction":"desc"},{"column":"id","direction":"desc"}]',
|
||||
)
|
||||
```
|
||||
|
||||
禁止:
|
||||
|
||||
@@ -354,7 +388,11 @@ CREATE TABLE demo_items (
|
||||
|
||||
## 枚举字段 options_json
|
||||
|
||||
`options_json` 为 JSON 数组,每项包含 `value` 与 `label`:
|
||||
`options_json` 支持两种形态:
|
||||
|
||||
### 1)静态枚举(数组)
|
||||
|
||||
每项包含 `value` 与 `label`:
|
||||
|
||||
```json
|
||||
[
|
||||
@@ -366,6 +404,31 @@ CREATE TABLE demo_items (
|
||||
|
||||
宿主据此渲染选择框;技能侧负责保证 JSON 合法且与物理字段取值一致。
|
||||
|
||||
### 2)表关联 lookup(对象,推荐用于外键式字段)
|
||||
|
||||
当字段取值依赖同库另一张业务表时,`display_type` 设为 `lookup`,`options_json` 写对象(**不是**数组):
|
||||
|
||||
```json
|
||||
{
|
||||
"ref_table": "platforms",
|
||||
"value_column": "platform_key",
|
||||
"label_column": "display_name",
|
||||
"filter_column": "enabled",
|
||||
"filter_value": 1
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|---|---|---|
|
||||
| `ref_table` | 是 | 同库被引用表名(snake_case 标识符) |
|
||||
| `value_column` | 是 | 写入当前字段的值列 |
|
||||
| `label_column` | 是 | 下拉展示名列 |
|
||||
| `filter_column` / `filter_value` | 否 | 等值过滤(如只列出 `enabled=1`) |
|
||||
|
||||
宿主在加载表 schema 时查询 `ref_table`,把结果展开成 `{value,label}` 下拉选项;**物理列仍存标量**(如 `platform_key` 文本、`account_id` 整数),不做嵌套子表。
|
||||
|
||||
示例:账号表的平台字段 → 平台信息表;凭据表的账号编号 → 账号信息表。
|
||||
|
||||
## 幂等初始化标准
|
||||
|
||||
- `_jiangchang_tables` 和 `_jiangchang_columns` 必须通过 upsert 初始化
|
||||
|
||||
@@ -24,7 +24,8 @@ CREATE TABLE IF NOT EXISTS _jiangchang_tables (
|
||||
description TEXT,
|
||||
sort_order INTEGER,
|
||||
visible INTEGER NOT NULL DEFAULT 1,
|
||||
readonly INTEGER NOT NULL DEFAULT 0
|
||||
readonly INTEGER NOT NULL DEFAULT 0,
|
||||
default_sort_json TEXT
|
||||
)
|
||||
"""
|
||||
|
||||
@@ -82,23 +83,50 @@ TASK_LOGS_COLUMN_METADATA: tuple[dict[str, object], ...] = (
|
||||
)
|
||||
|
||||
|
||||
def _ensure_jiangchang_tables_columns(cursor) -> None:
|
||||
"""旧库补列:CREATE IF NOT EXISTS 不会给已有表加新字段。"""
|
||||
cursor.execute("PRAGMA table_info('_jiangchang_tables')")
|
||||
cols = {str(row[1]) for row in cursor.fetchall()}
|
||||
if "default_sort_json" not in cols:
|
||||
cursor.execute("ALTER TABLE _jiangchang_tables ADD COLUMN default_sort_json TEXT")
|
||||
|
||||
|
||||
def init_display_metadata_tables(cursor) -> None:
|
||||
cursor.execute(_JIANGCHANG_TABLES_DDL)
|
||||
cursor.execute(_JIANGCHANG_COLUMNS_DDL)
|
||||
_ensure_jiangchang_tables_columns(cursor)
|
||||
|
||||
|
||||
def upsert_table_metadata(cursor, *, table_name: str, display_name: str, description: str | None = None, visible: int = 1, readonly: int = 0) -> None:
|
||||
def upsert_table_metadata(
|
||||
cursor,
|
||||
*,
|
||||
table_name: str,
|
||||
display_name: str,
|
||||
description: str | None = None,
|
||||
visible: int = 1,
|
||||
readonly: int = 0,
|
||||
default_sort_json: str | None = None,
|
||||
) -> None:
|
||||
"""写入表级元数据。
|
||||
|
||||
default_sort_json: 宿主数据管理默认排序,JSON 数组,例如
|
||||
'[{"column":"sent_at","direction":"desc"},{"column":"id","direction":"desc"}]'
|
||||
为空则宿主回退到 updated_at → created_at → 主键。
|
||||
"""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO _jiangchang_tables (table_name, display_name, description, visible, readonly)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
INSERT INTO _jiangchang_tables (
|
||||
table_name, display_name, description, visible, readonly, default_sort_json
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(table_name) DO UPDATE SET
|
||||
display_name = excluded.display_name,
|
||||
description = excluded.description,
|
||||
visible = excluded.visible,
|
||||
readonly = excluded.readonly
|
||||
readonly = excluded.readonly,
|
||||
default_sort_json = excluded.default_sort_json
|
||||
""",
|
||||
(table_name, display_name, description, visible, readonly),
|
||||
(table_name, display_name, description, visible, readonly, default_sort_json),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -196,7 +196,7 @@ def _default_uses_unixepoch(default_value: object) -> bool:
|
||||
|
||||
|
||||
def validate_options_json(options_json: str | None) -> tuple[list[str], list[str]]:
|
||||
"""校验枚举 options_json 可被解析为 [{value, label}, ...] 结构。"""
|
||||
"""校验 options_json:静态枚举数组,或 lookup 对象。"""
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
if options_json is None or str(options_json).strip() == "":
|
||||
@@ -206,8 +206,24 @@ def validate_options_json(options_json: str | None) -> tuple[list[str], list[str
|
||||
except json.JSONDecodeError as exc:
|
||||
errors.append(f"options_json 不是合法 JSON:{exc}")
|
||||
return errors, warnings
|
||||
|
||||
if isinstance(parsed, dict):
|
||||
for key in ("ref_table", "value_column", "label_column"):
|
||||
if not str(parsed.get(key) or "").strip():
|
||||
errors.append(f"options_json lookup 必须包含非空 {key}")
|
||||
ident = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$")
|
||||
for key in ("ref_table", "value_column", "label_column", "filter_column"):
|
||||
raw = parsed.get(key)
|
||||
if raw is None or raw == "":
|
||||
continue
|
||||
if not ident.match(str(raw)):
|
||||
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")
|
||||
return errors, warnings
|
||||
|
||||
if not isinstance(parsed, list):
|
||||
errors.append("options_json 必须是 JSON 数组")
|
||||
errors.append("options_json 必须是 JSON 数组(静态枚举)或对象(lookup)")
|
||||
return errors, warnings
|
||||
for index, item in enumerate(parsed):
|
||||
if not isinstance(item, dict):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。"""
|
||||
|
||||
SKILL_SLUG = "your-skill-slug"
|
||||
SKILL_VERSION = "1.0.61"
|
||||
SKILL_VERSION = "1.0.67"
|
||||
LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug"
|
||||
PLATFORM_KIT_MIN_VERSION = "1.2.2"
|
||||
|
||||
@@ -190,7 +190,21 @@ class TestActionsManifest(unittest.TestCase):
|
||||
|
||||
placements = action.get("placements")
|
||||
self.assertIsInstance(placements, list, msg=f"{action_id} placements must be array")
|
||||
self.assertGreater(len(placements), 0, msg=f"{action_id} placements empty")
|
||||
replaces = action.get("replaces")
|
||||
if replaces is not None:
|
||||
self.assertIn(
|
||||
replaces,
|
||||
{"create", "update", "delete"},
|
||||
msg=f"{action_id} invalid replaces {replaces!r}",
|
||||
)
|
||||
bind = action.get("bind") or {}
|
||||
tables = bind.get("tables") or []
|
||||
self.assertTrue(
|
||||
isinstance(tables, list) and len(tables) > 0,
|
||||
msg=f"{action_id} replaces requires non-empty bind.tables",
|
||||
)
|
||||
else:
|
||||
self.assertGreater(len(placements), 0, msg=f"{action_id} placements empty")
|
||||
for placement in placements:
|
||||
self.assertIn(placement, _ALLOWED_PLACEMENTS, msg=f"{action_id} invalid placement {placement!r}")
|
||||
|
||||
@@ -228,12 +242,18 @@ 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"):
|
||||
for key in ("toolbar", "row", "batch", "cron", "agent", "skill-detail", "create", "update", "delete"):
|
||||
self.assertIn(key, by_placement.get("properties") or {})
|
||||
self.assertEqual(
|
||||
set((by_placement["properties"][key].get("enum") or [])),
|
||||
{"direct", "confirm", "form"},
|
||||
)
|
||||
self.assertIn("replaces", props)
|
||||
self.assertEqual(props["replaces"].get("$ref"), "#/$defs/mutationKind")
|
||||
self.assertEqual(
|
||||
set(schema["$defs"]["mutationKind"].get("enum") or []),
|
||||
{"create", "update", "delete"},
|
||||
)
|
||||
|
||||
def test_manifest_validates_against_json_schema(self) -> None:
|
||||
try:
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# skill-template 工具
|
||||
|
||||
## UI 拾取(开发期)
|
||||
|
||||
`tools/ui_inspector` 已迁出本模板仓。请使用独立产品仓:
|
||||
|
||||
- Gitea:https://git.jc2009.com/client-jiangchang/jiangchang-ui-recorder
|
||||
- 本机常见路径:`D:\OpenClaw\client-jiangchang\jiangchang-ui-recorder`
|
||||
- 启动:`python run.py`
|
||||
|
||||
模板仓历史中仍保留删除前的归档提交(可按需从 Git 恢复旧副本);日常开发与运行以独立仓为准。
|
||||
|
||||
## 业务仓也可以直接 clone(来源 A)
|
||||
|
||||
若项目经理已在**公司自建 Gitea**([https://git.jc2009.com/](https://git.jc2009.com/),基于开源 Gitea 自建,**不是** Gitea 官网)开好业务技能仓库并授权,技术人员应直接:
|
||||
|
||||
Reference in New Issue
Block a user