diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b540d5..df53ebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ - 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节 - 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段 +## 1.0.98 + +- 沉淀数据管理**列筛选与全局搜索**契约:`searchable` 同时控制列筛选字段列表与顶栏全局搜索;存库 value / 展示 label 分工;筛选栏枚举/boolean/lookup 用下拉 Select(与表单开关/分段单选区分) +- 全局搜索:options label 映射、boolean 别名、lookup 多选 token、`datetime_unix_seconds` 日期区间 +- `ACTIONS.md` 表单对照表补充「列筛选」脚注;`CHECKLIST` A6-11 / A6-12;`display_metadata_validator` 对未开 searchable 的枚举/boolean 列软 warning + ## 1.0.97 - 沉淀数据管理 HTML 预览**布局契约**:`references/ACTIONS.md` 新增「宿主预览布局」(流体铺满 iframe、禁横向滚动、表格断行等) diff --git a/SKILL.md b/SKILL.md index 6716908..94cdc27 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 技能开发模板(通用业务版) description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。" -version: 1.0.97 +version: 1.0.98 author: 深圳匠厂科技有限公司 metadata: openclaw: diff --git a/development/CHECKLIST.md b/development/CHECKLIST.md index eb83fb4..9182fe5 100644 --- a/development/CHECKLIST.md +++ b/development/CHECKLIST.md @@ -136,6 +136,8 @@ checklist_version: 1.0 - [ ] A6-8 **P0** 同上适用时:开始处理后业务表进入**进行中**;成功回写成功类终态;失败回写失败类终态且**失败原因**可读(任务成功但业务行仍停在可操作/进行中 → **FAIL**) - [ ] A6-9 **P1** 同上适用时:`REQUIREMENTS` §0.3(或等价)已填写本技能状态→角色映射;处理类 Action 确认文案写清仅可操作态 - [ ] A6-10 **P1** 有 HTML 侧栏预览时(S0-12):预览区横向铺满、无横向滚动条;重新生成后内容与页头时间更新;布局符合 `references/ACTIONS.md`「宿主预览布局」。无 HTML 报告 → **N/A** +- [ ] A6-11 **P1** 业务**状态 / 启用类**列若用户需要筛表或顶栏搜中文:`searchable: 1` 且 `options_json`(或 `boolean` + `options_json`)的 **value 与存库一致**;仅 `searchable: 0` 则宿主不提供列筛与全局搜索。见 `references/SCHEMA.md`「列筛选与全局搜索」 +- [ ] A6-12 **P2** `display_type: boolean` 的展示列推荐同时配置 `options_json` 中文 label(如 停用/启用),避免仅靠宿主默认「是/否」;无 boolean 列 → **N/A** --- diff --git a/references/ACTIONS.md b/references/ACTIONS.md index 83e22d4..16763ea 100644 --- a/references/ACTIONS.md +++ b/references/ACTIONS.md @@ -871,8 +871,9 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件 | 默认值 | `default` | SQLite 列 `DEFAULT` | | 只读 | `readOnly: true` | 列 `editable: 0` / 宿主只读规则 | | **条件显隐** | **`visibleWhen`**(见下节) | **本版 CRUD 无对称**;仅 Action / 定时任务参数表单 | +| **列筛选「值」** | **无对称**(Action 无表格筛选) | 见 [`SCHEMA.md`](SCHEMA.md)「数据管理:列筛选与全局搜索」:枚举 / boolean / lookup 在筛选栏用**下拉 Select**(非开关、非分段单选);存库仍用 value,展示与搜索认 label | -**本版不做(勿宣传):** 文件上传、复杂 `oneOf` / nested object 专用表单;`visibleWhen` 多条件 AND/OR 树、跨 Action 依赖、CRUD 新增/编辑对称显隐。 +**本版不做(勿宣传):** 文件上传、复杂 `oneOf` / nested object 专用表单;`visibleWhen` 多条件 AND/OR 树、跨 Action 依赖、CRUD 新增/编辑对称显隐;Action 参数表单的列筛选对称。 (**Lookup 多选**已支持:同一 `lookup` + `multiple: true`,见上文;不是任意标签数组。) 日期时间约定: diff --git a/references/SCHEMA.md b/references/SCHEMA.md index 2131734..be6065c 100644 --- a/references/SCHEMA.md +++ b/references/SCHEMA.md @@ -287,7 +287,7 @@ upsert_column_metadata( | `grid_help` | 否 | 数据管理表头「?」;空/未写则不出。与表单 `description` 独立,禁止互拷 | | `display_order` | 否 | 兼容字段,默认保持 `NULL` | | `visible` | 是 | 是否进入数据管理 schema /「列」面板(`0` = **硬隐藏**,用户无法勾回;默认不展示但可勾回请用表级 `default_visible_columns_json`) | -| `searchable` | 是 | 是否参与搜索 | +| `searchable` | 是 | 是否参与**列筛选**与**顶栏全局搜索**(`0` = 两者均不参与);详见下节「数据管理:列筛选与全局搜索」 | | `editable` | 是 | `1` 业务字段允许宿主用于新增和编辑;`0` 系统/只读字段 | | `display_type` | 否 | 字段渲染类型;稳定取值见下节「表单与展示:稳定字段类型」 | | `options_json` | 否 | 枚举选项 JSON | @@ -382,6 +382,76 @@ seed_column_metadata_batch( Action 参数表单的 `title` / `description` / `default` / `display_type` 见 [`ACTIONS.md`](ACTIONS.md);勿与本节 CRUD 配置入口混用。 +#### 数据管理:列筛选与全局搜索(宿主支持) + +数据管理表格提供两类与 `searchable` 相关的检索能力,技能通过 `_jiangchang_columns.searchable` 声明是否参与: + +| 能力 | 用户入口 | `searchable = 1` 时 | `searchable = 0` 时 | +|------|----------|---------------------|---------------------| +| **列筛选** | 表格「筛选」→ 字段 / 操作符 / 值 | 出现在「字段」下拉,可按类型选值与操作符 | 不出现在列筛选字段列表 | +| **全局搜索** | 顶栏搜索框 | 参与 OR 模糊匹配(宿主对参与列数有上限,如最多 8 列) | 不参与全局搜索 | + +**存库 vs 展示(核心契约)** + +- **列表**:有 `options_json` 时展示 **label**(如「启用」「停用」) +- **筛选 / 查询 / 全局搜索**:比较与匹配 **value**(如 `0`/`1`、`active`、`toutiaohao`) +- 宿主负责 label ↔ value 映射;**禁止**为迁就 UI 把存库改成中文枚举,也**禁止**在 `description` 里写「筛选请填 0/1」 + +**筛选栏控件 ≠ 新增/编辑表单(勿混用)** + +列筛选「值」与表单使用同一套**类型语义**,但控件形态不同:筛选栏须与「字段」「操作符」同一行工具栏对齐(如统一 `h-9` 下拉),**枚举 / boolean / lookup 在筛选栏一律用下拉 Select**,不用新增/编辑表单里的开关或横向分段单选。表单侧仍按上表「新增/编辑控件」配置。 + +| 列类型 | 筛选「值」控件 | 常用操作符(宿主按类型裁剪) | 技能须配置 | +|--------|----------------|------------------------------|------------| +| 文本 / `url` / `email` | 文本框 | 等于 / 包含 / 开头 / 结尾 / 为空… | 通常 `searchable: 1` | +| 数字 | 数字框 | 等于 / 大于 / 小于… | `display_type: number` 或 INT/REAL | +| 枚举(`options_json` **数组**) | 下拉(展示 label) | 等于 / 不等于 / 为空 | **`options_json` 必填** + `searchable: 1` | +| `boolean` | 下拉(有 `options_json` 用其 label;否则宿主默认可选「是/否」) | 等于 / 不等于 / 为空 | `display_type: boolean`;**推荐同时写 `options_json`** 指定中文 label | +| `lookup` 单选 | 下拉 | 等于 / 不等于 / 为空 | `display_type: lookup` + lookup 对象 | +| `lookup` + `multiple: true` | 列筛选 UI 演进中;**全局搜索**已支持 | 等于时按逗号分隔 id 的 token 匹配 | 多选存 `TEXT` 逗号分隔 id | +| `date` / `datetime_unix_seconds` | 日期 / 日期时间 | 比较类 | 时间列通常 `searchable: 0` | +| 敏感列 | **不可筛** | — | 宿主强制排除 | + +**全局搜索扩展(用户可搜中文)** + +宿主在 `searchable = 1` 的列上除原文 `LIKE` 外,还应: + +- 有 `options_json` 枚举:**label 或 value** 均可命中 +- `boolean` 无 options:认「是/否」等常见别名 +- `lookup` 多选:按逗号分隔 id 的 **token 边界** 匹配;选项 label 亦可参与 +- `datetime_unix_seconds`:搜 `YYYY-MM-DD` 形态可匹配当天 Unix 秒区间 + +**技能配置建议** + +推荐: + +- 用户常筛的状态列(启用/停用、待生成/已生成、`status` 等)→ `searchable: 1` + `options_json`(value 与 DDL 存库一致) +- `boolean` 业务列:`display_type: boolean` **且** `options_json` 写中文 label(示例见下) + +禁止: + +- 仅为筛选把存库 value 改成中文 +- 在文档或 `description` 要求用户记内部 magic number / 英文 code + +示例(启用开关 + 中文筛选): + +```python +{ + "column_name": "enabled", + "display_name": "是否启用", + "visible": 1, + "searchable": 1, + "editable": 1, + "display_type": "boolean", + "options_json": json.dumps( + [{"value": 0, "label": "停用"}, {"value": 1, "label": "启用"}], + ensure_ascii=False, + ), +} +``` + +Action 参数表单**无**列筛选对称能力;筛选语义以本节为准,表单对照见 [`ACTIONS.md`](ACTIONS.md)「表单字段类型对照」。 + ## editable 与 createable 的关系 技能模板只维护 `editable` 这一份字段权限声明: diff --git a/scripts/db/display_metadata_validator.py b/scripts/db/display_metadata_validator.py index 0fe5ef3..42db9d1 100644 --- a/scripts/db/display_metadata_validator.py +++ b/scripts/db/display_metadata_validator.py @@ -235,6 +235,50 @@ def validate_options_json(options_json: str | None) -> tuple[list[str], list[str return errors, warnings +def validate_filter_search_metadata( + cursor, + table_name: str, +) -> tuple[list[str], list[str]]: + """对可见但未开启 searchable 的枚举 / boolean 列给出软建议。""" + warnings: list[str] = [] + cursor.execute( + """ + SELECT column_name, visible, searchable, display_type, options_json + FROM _jiangchang_columns + WHERE table_name = ? + """, + (table_name,), + ) + for column_name, visible, searchable, display_type, options_json in cursor.fetchall(): + if visible is not None and int(visible) == 0: + continue + if searchable is not None and int(searchable) != 0: + continue + + display_type_norm = str(display_type or "").strip() + is_boolean = display_type_norm == "boolean" + + has_enum = False + if options_json is not None and str(options_json).strip(): + opt_errors, _ = validate_options_json(options_json) + if not opt_errors: + try: + parsed = json.loads(str(options_json)) + except json.JSONDecodeError: + parsed = None + has_enum = isinstance(parsed, list) and len(parsed) > 0 + + if not has_enum and not is_boolean: + continue + + warnings.append( + f"{table_name}.{column_name}: 枚举/状态类列建议 searchable=1," + "以便宿主列筛选与按中文 label 全局搜索(见 references/SCHEMA.md)" + ) + + return [], warnings + + def validate_column_permissions( cursor, table_name: str, @@ -511,6 +555,9 @@ def validate_display_metadata(conn: sqlite3.Connection, *, check_column_order: b report.errors.extend(perm_errors) report.warnings.extend(perm_warnings) + _, filter_warnings = validate_filter_search_metadata(cur, table_name) + report.warnings.extend(filter_warnings) + cur.execute("SELECT table_name, column_name FROM _jiangchang_columns") for table_name, column_name in cur.fetchall(): if table_name in METADATA_TABLES: diff --git a/scripts/util/constants.py b/scripts/util/constants.py index a4d0006..2fcfc25 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.97" +SKILL_VERSION = "1.0.98" LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug" PLATFORM_KIT_MIN_VERSION = "1.2.11" diff --git a/tests/test_display_metadata.py b/tests/test_display_metadata.py index 92ebfc1..470b168 100644 --- a/tests/test_display_metadata.py +++ b/tests/test_display_metadata.py @@ -553,6 +553,41 @@ metadata: finally: conn.close() + def test_enum_column_without_searchable_warns(self) -> None: + conn = sqlite3.connect(":memory:") + try: + conn.executescript( + """ + CREATE TABLE filter_demo (id INTEGER PRIMARY KEY, enabled INTEGER NOT NULL DEFAULT 1); + """ + ) + from db.display_metadata import ( + init_display_metadata_tables, + upsert_column_metadata, + upsert_table_metadata, + ) + from db.display_metadata_validator import validate_filter_search_metadata + + cur = conn.cursor() + init_display_metadata_tables(cur) + upsert_table_metadata(cur, table_name="filter_demo", display_name="筛选演示", readonly=0) + upsert_column_metadata( + cur, + table_name="filter_demo", + column_name="enabled", + display_name="是否启用", + visible=1, + searchable=0, + editable=1, + display_type="boolean", + options_json='[{"value":0,"label":"停用"},{"value":1,"label":"启用"}]', + ) + conn.commit() + _, warnings = validate_filter_search_metadata(cur, "filter_demo") + self.assertTrue(any("searchable=1" in item for item in warnings)) + finally: + conn.close() + def test_schema_md_documents_readonly_and_seed_helpers(self) -> None: path = os.path.join(get_skill_root(), "references", "SCHEMA.md") with open(path, encoding="utf-8") as f: diff --git a/tests/test_docs_standards.py b/tests/test_docs_standards.py index c0a0ade..01347b3 100644 --- a/tests/test_docs_standards.py +++ b/tests/test_docs_standards.py @@ -356,6 +356,10 @@ class TestDocsStandards(unittest.TestCase): "跨表引用 lookup", "\"multiple\": true", "Lookup 多选", + "数据管理:列筛选与全局搜索", + "列筛选与全局搜索", + "searchable: 1", + "筛选栏控件", ): self.assertIn(marker, text, msg=f"SCHEMA.md missing {marker!r}") @@ -375,6 +379,7 @@ class TestDocsStandards(unittest.TestCase): "字段条件显隐", "visibleWhen", "grid_help", + "列筛选", ): self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}")