diff --git a/CHANGELOG.md b/CHANGELOG.md index 44cc31b..bb43ee3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ - 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节 - 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段 +## 1.0.122 + +- 沉淀 Action **静态枚举中文 label**:`enum` + **`enumNames`**,或绑列复用列 `options_json` 数组 +- 约定宿主优先序:**绑列 options → enumNames → 纯 enum**;固定小枚举不要造假 Lookup +- 更新 Schema / ACTIONS / SCHEMA / CHECKLIST A4-13;新增 `enum-names-action.example.json` + ## 1.0.121 - 沉淀 Lookup **分页勾选小表**:`display_type: lookup` + `presentation: "paged_table"` + 可选 `page_size`(仍是 Lookup,不要新造控件类型) diff --git a/SKILL.md b/SKILL.md index b47f4d6..fc53361 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 技能开发模板(通用业务版) description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。" -version: 1.0.121 +version: 1.0.122 author: 深圳匠厂科技有限公司 metadata: openclaw: diff --git a/assets/examples/enum-names-action.example.json b/assets/examples/enum-names-action.example.json new file mode 100644 index 0000000..50b1bd7 --- /dev/null +++ b/assets/examples/enum-names-action.example.json @@ -0,0 +1,51 @@ +{ + "_comment": "形状参考:Action 静态枚举用 enum + enumNames 显示中文;提交仍是 enum value。有对应表列时优先 column 绑列复用 options_json。完整契约见 references/ACTIONS.md「静态枚举 label」。", + "schemaVersion": 1, + "skill": "your-skill-slug", + "actions": [ + { + "id": "create-ticket", + "label": "新建工单(示例)", + "description": "演示 enumNames:界面显示中文,CLI 仍收到英文枚举值。", + "placements": ["toolbar", "agent"], + "executionProfile": "sync", + "interaction": "form", + "bind": { + "tables": ["tickets"] + }, + "entrypoint": { + "type": "cli", + "command": "ticket-create", + "args": [ + "--category", "{{category}}", + "--priority", "{{priority}}", + "--title", "{{title}}" + ] + }, + "inputSchema": { + "type": "object", + "required": ["title", "category"], + "properties": { + "title": { + "type": "string", + "title": "标题" + }, + "category": { + "type": "string", + "title": "类型", + "column": "category", + "enum": ["incident", "request", "consult", "other"], + "enumNames": ["故障", "需求", "咨询", "其他"] + }, + "priority": { + "type": "string", + "title": "优先级", + "enum": ["low", "medium", "high", "urgent"], + "enumNames": ["低", "中", "高", "紧急"], + "description": "无对应列时仅靠 enumNames 展示中文" + } + } + } + } + ] +} diff --git a/assets/schemas/skill-actions.schema.json b/assets/schemas/skill-actions.schema.json index 365e3f3..524f5e9 100644 --- a/assets/schemas/skill-actions.schema.json +++ b/assets/schemas/skill-actions.schema.json @@ -147,7 +147,13 @@ "default": {}, "enum": { "type": "array", - "minItems": 1 + "minItems": 1, + "description": "静态枚举取值(提交/存库用)。展示中文时配 enumNames,或绑列复用 options_json;勿用 Lookup 冒充固定小枚举。" + }, + "enumNames": { + "type": "array", + "minItems": 1, + "description": "可选。与 enum 一一对应的展示文案(JSON Schema / OpenAPI 风格)。缺项或空串时宿主回退为对应 enum 原文。有对应表列时优先绑列复用 options_json(见 ACTIONS.md「静态枚举 label」)。" }, "sensitive": { "type": "boolean" }, "readOnly": { @@ -162,7 +168,7 @@ "type": "string", "minLength": 1, "pattern": "^[a-z][a-z0-9_]*$", - "description": "可选。绑定业务表物理列名;Action 弹窗(toolbar/batch)与 replaces create/update 抽屉均可复用该列的中文名与 lookup 下拉" + "description": "可选。绑定业务表物理列名;Action 弹窗(toolbar/batch)与 replaces create/update 抽屉可复用该列的中文名与 options(静态枚举 options_json 数组,或已解析的 lookup)。" }, "display_type": { "type": "string", diff --git a/development/CHECKLIST.md b/development/CHECKLIST.md index 130bca9..df45707 100644 --- a/development/CHECKLIST.md +++ b/development/CHECKLIST.md @@ -115,6 +115,7 @@ checklist_version: 1.0 - [ ] 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`「分页勾选小表」) +- [ ] A4-13 **P1** 若 Action 有固定小枚举(状态/船司/优先级等)需中文展示:用 `enum` + **`enumNames`**,或绑列复用列 `options_json` 数组;宿主优先序为绑列 options → enumNames → 纯 enum;**禁止**为中文展示造假 Lookup(见 `references/ACTIONS.md`「静态枚举 label」) --- ## 5. 账号与浏览器会话(网页 RPA 适用;其他形态标 N/A 或改测等价项) diff --git a/references/ACTIONS.md b/references/ACTIONS.md index 41e3831..97c435f 100644 --- a/references/ACTIONS.md +++ b/references/ACTIONS.md @@ -110,7 +110,7 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表** | **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`**(字段条件显隐,见专节) | +| inputSchema | 根 `type: object`;properties 使用 `string` / `number` / `boolean`;支持 `enum`、可选 **`enumNames`**、`default`、`required`、`sensitive`、`readOnly`、可选 **`revealInForm`**;可选 `column`(绑列复用 options);可选 `display_type` / `format`(含 `file` / `directory` 等);可选 **`pathRoot`**(file/directory 起始目录,相对技能数据根);可选 **`visibleWhen`**(字段条件显隐,见专节) | | confirmation | `{ "message": "..." }` — **风险文案**(confirm/form 弹窗黄条;agent 元数据);**不是**「要不要弹窗」的开关 | | **hoverHint** | 可选字符串。数据管理**工具栏悬停**文案;与 `description` / `confirmation.message` **独立**。省略则不显示悬停,不回退。短名已够懂时不要写。需宿主已支持该字段;当前实现为顶栏技能按钮,不含宿主内置删除/刷新/新增/导入/导出 | | **visibleWhen** | 可选。写在 **property** 上(非 action 顶层):依赖同表单其它字段值决定是否展示;隐藏字段不提交、不校验 `required`。**勿与** [`POLICY.md`](POLICY.md) L3 行态 `visibleWhen` 混淆;也**勿**用它藏「已选择」已表达的映射主键(见「选择摘要」) | @@ -437,13 +437,14 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表** - 点默认「导入」时**固定**打开宿主导入面板;确认后若有 `replaces:import` 则调用该 Action(`interaction` 不决定是否再弹技能表单)。 - `update` / `delete` 可用 `inputMapping`(`$row.$pk`、`$selection.ids`)预填主键。 - `import` 约定:`filePath`(或 `file`)+ 可选 `onError`(`skip`/`abort` 或上例中文枚举);技能自行解析文件与表头。 -- 整表 `_jiangchang_tables.readonly=1` 时仍隐藏写按钮(优先于 `replaces`)。 -- **列元数据复用(lookup)**:Action 表单(含 toolbar/batch 弹窗与 create/update 抽屉)可将 `inputSchema` 字段对齐业务列,复用中文名与 lookup 下拉: - - 优先让属性名 = 物理列名(如 `platform_key`); - - 或 `bind.columnMapping`:`{ "platform": "platform_key" }`; - - 或字段上写 `"column": "platform_key"`。 - - 当前表没有对应列时,用字段内联 `lookup`(见下文「跨表引用:Lookup」)。 - - **不要**在 actions.json 里写死会过期的全量平台 `enum`,也**不要**让用户手填内部编号。 +- 整表 `_jiangchang_tables.readonly=1` 时仍隐藏宿主默认新增/编辑/导入与直写删除;**若声明了 `replaces: delete`,宿主仍显示「删除」并走技能 CLI**(不直写 SQLite)。 +- **列元数据复用**:Action 表单(含 toolbar/batch 弹窗与 create/update 抽屉)可将 `inputSchema` 字段对齐业务列,复用中文名与列上 **options**(静态枚举 `options_json` **数组**,或已解析的 lookup): + - 优先让属性名 = 物理列名(如 `carrier_code` / `platform_key`); + - 或 `bind.columnMapping`:`{ "carrierCode": "carrier_code" }`; + - 或字段上写 `"column": "carrier_code"`(推荐:驼峰属性名时显式写出,避免只靠启发式)。 + - 当前表没有对应列、且选项是**固定小枚举**时:用 `enum` + **`enumNames`**(见下文「静态枚举 label」),**不要**为中文展示去造 Lookup。 + - 当前表没有对应列、且选项是**跨表/跨技能业务行**时:用字段内联 `lookup`(见下文「跨表引用:Lookup」)。 + - **不要**在 actions.json 里写死会过期的全量业务行 `enum`,也**不要**让用户手填内部编号。 ## `interaction`(点击交互方式) @@ -1141,7 +1142,7 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件 | JSON 长文本 | `type: object`/`array`(以 JSON 字符串编辑)或 `textarea` | `display_type: json` / `textarea` | | 数字 | `type: number` | `display_type: number` 或 INT/REAL/NUMERIC | | 开关 | `type: boolean` | `display_type: boolean` | -| 枚举下拉 | `enum: [...]` | `options_json` 数组 | +| 枚举下拉 | `enum: [...]` + 可选 **`enumNames`**;有对应列时再加 `column` / 属性名对齐,复用列 `options_json` | `options_json` **数组** `{value,label}` | | 关联下拉(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` | @@ -1161,7 +1162,34 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件 **本版不做(勿宣传):** 浏览器内文件上传控件、复杂 `oneOf` / nested object 专用表单;`visibleWhen` 多条件 AND/OR 树、跨 Action 依赖、CRUD 新增/编辑对称显隐;Action 参数表单的列筛选对称。 (**Lookup 多选**已支持:同一 `lookup` + `multiple: true`,见上文;不是任意标签数组。**分页勾选小表**已支持:同一 `lookup` + `presentation: "paged_table"`,见「分页勾选小表」。) -(**本地路径选择**已支持:`file` / `directory` + 可选 `pathRoot`,见下节;提交值仍是本机绝对路径字符串,不是 multipart 上传。) +(**本地路径选择**已支持:`file` / `directory` + 可选 `pathRoot`,见下节;提交值仍是本机绝对路径字符串,不是 multipart 上传。) +(**静态枚举中文 label**已支持:Action `enumNames` 与列 `options_json` 数组对称,见下节「静态枚举 label」。) + +### 静态枚举 label(`enum` / `enumNames` / 绑列) + +固定小枚举(状态、船司代码、优先级等)**不要**走 Lookup。CRUD 用列 `options_json` 数组;Action 表单要与「新增记录」对齐中文展示时,按下面做。 + +**宿主解析优先序(高 → 低):** + +1. **绑列 options**:字段通过 `column` / `columnMapping` / 属性名(含 camelCase→snake_case)对齐到当前表列,且该列已有解析后的 `options`(来自 `options_json` 数组或 lookup)→ 用列上的 `{value,label}`。 +2. **`enumNames`**:与 `enum` 下标一一对应的展示文案;缺项或空串回退为该位 `enum` 原文。 +3. **纯 `enum`**:无 label 时宿主只能显示代码值(体验差,新技能避免)。 + +```json +"carrierCode": { + "type": "string", + "title": "船司", + "column": "carrier_code", + "enum": ["maersk", "hmm"], + "enumNames": ["MAERSK 马士基", "HMM 现代商船"] +} +``` + +推荐: + +- 表上已有同义枚举列 → **显式 `column`**(或属性名 = 列名)+ 列侧写好 `options_json`;`enum`/`enumNames` 可保留作无列上下文时的兜底(如 agent-only Action)。 +- 无对应列、仅 Action 表单需要 → 只写 `enum` + `enumNames`(见 [`assets/examples/enum-names-action.example.json`](../assets/examples/enum-names-action.example.json))。 +- **禁止**为了中文展示把固定枚举改成假 Lookup。 日期时间约定: @@ -1235,7 +1263,8 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件 ### 当前宿主稳定支持(摘要) - 根:`object`;属性:`string` / `number` / `boolean`(`object`/`array` 仅作 JSON 多行编辑兼容,新技能慎用) -- `enum` / `default` / `required` / `sensitive` / `readOnly` / 可选 `revealInForm` +- `enum` / 可选 **`enumNames`** / `default` / `required` / `sensitive` / `readOnly` / 可选 `revealInForm` +- 可选 **`column`**:绑列复用 displayName 与 options(静态枚举或 lookup) - `display_type` / `format`:`textarea` | **`year`** | **`year_month`** / `month` | `date` | `datetime_unix_seconds` | `url` | `email` | **`lookup`** | **`file`** | **`directory`** - 可选 **`pathRoot`**(仅 `file` / `directory`):相对技能数据根的起始目录;省略默认 `{skill_data}/input` - 字段内联 `lookup`(与 CRUD `options_json` lookup 对象同构;可选 `extra_column`、`multiple`) @@ -1365,7 +1394,7 @@ CRUD 侧宿主义务与禁止事项见 [`SCHEMA.md`](SCHEMA.md)「表单与展 约定: - `boolean` 在宿主中渲染为开关 -- `enum` 在宿主中渲染为选择框 +- `enum` 在宿主中渲染为选择框(或短列表分段控件);**展示文案**优先绑列 options,其次 `enumNames`,再次纯 `enum` 值 - `type: number` 渲染为数字输入 - `display_type`/`format: year` 渲染为年份选择;`year_month` / `month` 渲染为年月选择 - `display_type`/`format: date` 渲染为日期(日);`datetime_unix_seconds` 渲染为日期时间并按 **Unix 秒**提交 diff --git a/references/SCHEMA.md b/references/SCHEMA.md index a37f753..dc01a83 100644 --- a/references/SCHEMA.md +++ b/references/SCHEMA.md @@ -621,7 +621,7 @@ readonly = 0 | 层级 | 值 | 说明 | |------|-----|------| -| 表级 `readonly` | `1` | 宿主数据管理只读展示,不提供新增/编辑/删除表单 | +| 表级 `readonly` | `1` | 宿主数据管理只读展示,不提供新增/编辑/导入与直写删除;若技能声明 `replaces: delete` 仍可显示删除并走 CLI | | 相关业务字段 `editable` | `0` | 元数据权限只约束宿主 UI,不限制技能代码写库 | 模板**默认不建**此类审计表。过程看 `jiangchang.log`,进度/终态看任务中心,业务结果看业务表。 @@ -687,6 +687,8 @@ CREATE TABLE demo_items ( 宿主据此渲染选择框;技能侧负责保证 JSON 合法且与物理字段取值一致。 +**与 Action 表单对称:** 数据管理「新增/编辑」读列 `options_json`;Skill Action 弹窗用 `enum` + 可选 **`enumNames`**,或字段绑列(`column` / 属性名对齐)直接复用本列 options。宿主优先序:**绑列 options → enumNames → 纯 enum**。详见 [`ACTIONS.md`](ACTIONS.md)「静态枚举 label」。固定小枚举**不要**改成 Lookup。 + ### 2)跨表引用 lookup(对象,用于关联 id 字段) 当字段取值依赖同库另一张业务表时,`display_type` 设为 `lookup`,`options_json` 写对象(**不是**数组)。这是**展示与表单选择**契约,**不是** SQLite 外键。 diff --git a/scripts/util/constants.py b/scripts/util/constants.py index 8c21945..167be3b 100644 --- a/scripts/util/constants.py +++ b/scripts/util/constants.py @@ -1,6 +1,6 @@ """技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。""" SKILL_SLUG = "your-skill-slug" -SKILL_VERSION = "1.0.121" +SKILL_VERSION = "1.0.122" LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug" PLATFORM_KIT_MIN_VERSION = "1.2.11" diff --git a/tests/test_actions_manifest.py b/tests/test_actions_manifest.py index 27e8011..93b869f 100644 --- a/tests/test_actions_manifest.py +++ b/tests/test_actions_manifest.py @@ -290,6 +290,29 @@ class TestActionsManifest(unittest.TestCase): action_props = schema["$defs"]["action"]["properties"] self.assertNotIn("visibleWhen", action_props) + def test_schema_defines_enum_names_on_input_property(self) -> None: + schema = _load_actions_schema() + props = schema["$defs"]["inputProperty"]["properties"] + self.assertIn("enum", props) + self.assertIn("enumNames", props) + self.assertEqual(props["enumNames"].get("type"), "array") + self.assertEqual(props["enumNames"].get("minItems"), 1) + self.assertIn("column", props) + + def test_enum_names_example_exists_and_aligns_labels(self) -> None: + path = os.path.join( + get_skill_root(), "assets", "examples", "enum-names-action.example.json" + ) + self.assertTrue(os.path.isfile(path), msg="enum-names example must exist") + with open(path, encoding="utf-8") as f: + example = json.load(f) + props = example["actions"][0]["inputSchema"]["properties"] + category = props["category"] + self.assertEqual(len(category["enum"]), len(category["enumNames"])) + self.assertEqual(category.get("column"), "category") + priority = props["priority"] + self.assertEqual(len(priority["enum"]), len(priority["enumNames"])) + def test_default_manifest_omits_visible_when(self) -> None: for action in _load_actions_manifest()["actions"]: properties = ((action.get("inputSchema") or {}).get("properties")) or {} @@ -802,6 +825,9 @@ class TestActionsManifest(unittest.TestCase): "已选择", "revealInForm", "映射只读", + "enumNames", + "静态枚举 label", + "绑列 options", ): self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}") self.assertNotIn( diff --git a/tests/test_docs_standards.py b/tests/test_docs_standards.py index 579202a..3bb20ef 100644 --- a/tests/test_docs_standards.py +++ b/tests/test_docs_standards.py @@ -446,17 +446,27 @@ class TestDocsStandards(unittest.TestCase): ), msg="lookup-paged-table.action.example.json missing", ) + self.assertTrue( + os.path.isfile( + os.path.join(get_skill_root(), "assets/examples/enum-names-action.example.json") + ), + msg="enum-names-action.example.json missing", + ) checklist = self._read("development/CHECKLIST.md") self.assertIn("skill_cli", checklist) self.assertIn("A4-12", checklist) + self.assertIn("A4-13", checklist) + self.assertIn("enumNames", 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) + self.assertIn("enumNames", 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("enumNames", schema_json) self.assertIn("page_size", schema_json) self.assertIn('"skill"', schema_json)