diff --git a/CHANGELOG.md b/CHANGELOG.md index 5aaebbf..a30d639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ - 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节 - 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段 +## 1.0.90 + +- 沉淀数据管理表格说明契约:`_jiangchang_tables.grid_help` / `_jiangchang_columns.grid_help` +- 有内容才显示表上方说明和列头「?」;不写则与今日一致 +- 与新增/编辑表单的 `description` 独立,禁止互拷(见 `references/SCHEMA.md`) + ## 1.0.89 - 仓库增加 MIT 许可证,版权归属深圳匠厂科技有限公司 diff --git a/SKILL.md b/SKILL.md index 80d0090..7c1f066 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 技能开发模板(通用业务版) description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。" -version: 1.0.89 +version: 1.0.90 author: 深圳匠厂科技有限公司 metadata: openclaw: diff --git a/references/ACTIONS.md b/references/ACTIONS.md index c387e21..35ff898 100644 --- a/references/ACTIONS.md +++ b/references/ACTIONS.md @@ -658,6 +658,7 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件 | URL(薄层) | `display_type` 或 `format`: `url` | `display_type: url` | | Email(薄层) | `display_type` 或 `format`: `email` | `display_type: email` | | 说明灰字 | `description` | `_jiangchang_columns.description` | +| 表格横幅 / 列头「?」 | (无;Action 不用) | `_jiangchang_tables.grid_help` / `_jiangchang_columns.grid_help` | | 默认值 | `default` | SQLite 列 `DEFAULT` | | 只读 | `readOnly: true` | 列 `editable: 0` / 宿主只读规则 | | **条件显隐** | **`visibleWhen`**(见下节) | **本版 CRUD 无对称**;仅 Action / 定时任务参数表单 | @@ -727,6 +728,8 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件 | Skill Action 弹窗 | `inputSchema.title` | `inputSchema.description` | `inputSchema.default` | `type` + `display_type`/`format` | | 数据管理新增/编辑 | `_jiangchang_columns.display_name` | `_jiangchang_columns.description` | 物理列 SQLite `DEFAULT` | `display_type` + 物理类型推断 | +数据管理**表格**上方说明与列头「?」用 `_jiangchang_tables.grid_help` / `_jiangchang_columns.grid_help`(空则不渲染),**不要**用 `description`。见 [`SCHEMA.md`](SCHEMA.md)「表格说明 `grid_help`」。 + CRUD 侧宿主义务与禁止事项见 [`SCHEMA.md`](SCHEMA.md)「表单与展示:稳定字段类型」「新增/编辑表单:字段说明与默认值」。同一业务含义两边文案与控件应对齐,但分别配置。 示例: diff --git a/references/SCHEMA.md b/references/SCHEMA.md index 2e912e0..fab5592 100644 --- a/references/SCHEMA.md +++ b/references/SCHEMA.md @@ -13,7 +13,7 @@ - 定义真实 SQLite 表结构 - 使用英文 `snake_case` 表名和字段名 - 使用 `_jiangchang_tables` 提供表级展示元数据 -- 使用 `_jiangchang_columns` 提供字段级展示元数据(含可选 `description` 字段说明) +- 使用 `_jiangchang_columns` 提供字段级展示元数据(含可选 `description` 表单说明、可选 `grid_help` 表格说明) - 需要「新增表单预填」时,在物理列上声明 SQLite `DEFAULT`(勿把默认值只写在文案里) - 定义哪些字段可见、可搜索、可创建、可编辑 - 定义字段展示类型和枚举选项 @@ -28,6 +28,7 @@ - 根据 schema 渲染数据管理页面 - 根据 `editable` 推导字段是否可创建或编辑 - 在**新增 / 编辑**表单中,若列有 `_jiangchang_columns.description`,在标签下方展示该说明(灰字;空则不显示) +- 在数据管理**表格**中,若表有 `_jiangchang_tables.grid_help` 则在表上方展示可折叠说明;若列有 `_jiangchang_columns.grid_help` 则在列头显示「?」;空/未写则不渲染。**不**把 `description` 当作表格说明 - 打开**新增**表单时,对可创建字段用 SQLite 列 `DEFAULT`(`PRAGMA table_info.dflt_value`)预填;无 DEFAULT 则留空 - 执行通用查询、新增、编辑、删除、导入、导出 - 对字段名、表名、类型和权限进行最终校验 @@ -138,7 +139,8 @@ metadata: |------|------|------| | `table_name` | 是 | 真实英文表名 | | `display_name` | 是 | 用户可见的中文业务名称 | -| `description` | 否 | 表说明 | +| `description` | 否 | 表说明(表单/备注用途);**不**用于表格上方横幅 | +| `grid_help` | 否 | 数据管理表格上方说明;空/未写则不出横幅。与 `description` 独立,禁止互拷(见下节) | | `sort_order` | 否 | 兼容保留字段,不用于替代物理字段顺序 | | `visible` | 是 | `1` 在宿主数据管理中展示 / `0` 隐藏 | | `readonly` | 是 | `1` 整张表只读;`0` 允许宿主根据字段权限执行写操作 | @@ -212,6 +214,43 @@ upsert_table_metadata( ) ``` +#### 表格说明 `grid_help`(宿主支持) + +技能可给数据管理**表格**写说明,与新增/编辑表单的 `description` **完全独立**: + +| 写在哪 | 宿主做什么 | 空/未写 | +|--------|------------|---------| +| `_jiangchang_tables.grid_help` | 表格上方可折叠横幅(首行摘要;有后续行才出展开) | 不出横幅 | +| `_jiangchang_columns.grid_help` | 列头「?」,点击查看该列说明 | 不出「?」 | + +规则: + +- **opt-in**:大多数表/列不需要写;不写则行为与今日一致 +- **禁止**把 `description` 拷进 `grid_help`,也禁止把表格规则写进 `description`(`description` 只给表单灰字) +- 多行文本时:第一行是摘要,其余行点「展开」后可见 +- 通过 `upsert_table_metadata(..., grid_help=...)` / `upsert_column_metadata(..., grid_help=...)` 写入;旧库由 `init_display_metadata_tables` 幂等 `ALTER` 补列 +- 需已支持该字段的匠厂宿主;更旧版本会忽略未知列,不写则无影响 + +示例(仅在确有表格规则时写;模板默认业务表不要预填): + +```python +upsert_table_metadata( + cursor, + table_name="timeliness_records", + display_name="时效登记", + readonly=1, + grid_help="超时按环节规则判定。\n交单:预计到港日前 3 天须交单", +) +upsert_column_metadata( + cursor, + table_name="timeliness_records", + column_name="submit_date", + display_name="交单日期", + grid_help="预计到港日前 3 天须交单", + editable=0, +) +``` + 禁止: - 使用中文真实表名 @@ -228,6 +267,7 @@ upsert_table_metadata( | `column_name` | 是 | 真实英文字段名 | | `display_name` | 是 | 用户可见的中文字段名(表头与表单标签);保持简短 | | `description` | 否 | 字段说明(给人看的短文案);**宿主在新增/编辑表单标签下方展示**(见下节);不要塞进 `display_name` | +| `grid_help` | 否 | 数据管理表头「?」;空/未写则不出。与表单 `description` 独立,禁止互拷 | | `display_order` | 否 | 兼容字段,默认保持 `NULL` | | `visible` | 是 | 是否进入数据管理 schema /「列」面板(`0` = **硬隐藏**,用户无法勾回;默认不展示但可勾回请用表级 `default_visible_columns_json`) | | `searchable` | 是 | 是否参与搜索 | @@ -277,7 +317,9 @@ upsert_table_metadata( | 字段说明 | `_jiangchang_columns.description`(可选) | 新增/编辑表单中,在标签下方以灰字展示;空则不显示 | | 创建默认值 | 物理列 SQLite `DEFAULT`(如 `limit_n INTEGER NOT NULL DEFAULT 0`) | 打开「新增」时,对可创建字段预填该默认值;无 DEFAULT 则留空 | | 编辑已有行 | — | **不**用 DEFAULT 覆盖已有行数据 | -| 表格列头 | 只用 `display_name` | 长说明**不**进入列头 | +| 表格列头标题 | `display_name` | 短名;不要把长说明塞进列头文字 | +| 表格列头「?」 | `_jiangchang_columns.grid_help`(可选) | 有内容才显示「?」;**不**回退 `description` | +| 表格上方说明 | `_jiangchang_tables.grid_help`(可选) | 有内容才显示横幅;**不**回退表级 `description` | 规则与禁止: @@ -552,7 +594,7 @@ Action 弹窗内联 Lookup 与上表 **同构**(字段名一致),见 [`ACT - `_jiangchang_tables` 和 `_jiangchang_columns` 必须通过 upsert 初始化 - 多次执行初始化不能重复记录 -- 重新初始化可以更新展示名、描述、`visible`、`searchable`、`editable`、`display_type`、`options_json` +- 重新初始化可以更新展示名、描述、`grid_help`、`visible`、`searchable`、`editable`、`display_type`、`options_json` - 元数据不能残留旧字段(删除或重命名字段时必须同步清理) - 元数据不能引用不存在的表或字段 - 新增字段或表时必须同步增加元数据 diff --git a/scripts/db/display_metadata.py b/scripts/db/display_metadata.py index fcdf75d..f815733 100644 --- a/scripts/db/display_metadata.py +++ b/scripts/db/display_metadata.py @@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS _jiangchang_tables ( table_name TEXT PRIMARY KEY, display_name TEXT NOT NULL, description TEXT, + grid_help TEXT, sort_order INTEGER, visible INTEGER NOT NULL DEFAULT 1, readonly INTEGER NOT NULL DEFAULT 0, @@ -35,6 +36,7 @@ CREATE TABLE IF NOT EXISTS _jiangchang_columns ( column_name TEXT NOT NULL, display_name TEXT NOT NULL, description TEXT, + grid_help TEXT, display_order INTEGER, visible INTEGER NOT NULL DEFAULT 1, searchable INTEGER NOT NULL DEFAULT 1, @@ -45,22 +47,35 @@ CREATE TABLE IF NOT EXISTS _jiangchang_columns ( ) """ +def _pragma_column_names(cursor, table_name: str) -> set[str]: + cursor.execute(f"PRAGMA table_info({table_name!r})") + return {str(row[1]) for row in cursor.fetchall()} + + 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()} + cols = _pragma_column_names(cursor, "_jiangchang_tables") if "default_sort_json" not in cols: cursor.execute("ALTER TABLE _jiangchang_tables ADD COLUMN default_sort_json TEXT") if "default_visible_columns_json" not in cols: cursor.execute( "ALTER TABLE _jiangchang_tables ADD COLUMN default_visible_columns_json TEXT" ) + if "grid_help" not in cols: + cursor.execute("ALTER TABLE _jiangchang_tables ADD COLUMN grid_help TEXT") + + +def _ensure_jiangchang_columns_columns(cursor) -> None: + cols = _pragma_column_names(cursor, "_jiangchang_columns") + if "grid_help" not in cols: + cursor.execute("ALTER TABLE _jiangchang_columns ADD COLUMN grid_help TEXT") def init_display_metadata_tables(cursor) -> None: cursor.execute(_JIANGCHANG_TABLES_DDL) cursor.execute(_JIANGCHANG_COLUMNS_DDL) _ensure_jiangchang_tables_columns(cursor) + _ensure_jiangchang_columns_columns(cursor) def upsert_table_metadata( @@ -69,6 +84,7 @@ def upsert_table_metadata( table_name: str, display_name: str, description: str | None = None, + grid_help: str | None = None, visible: int = 1, readonly: int = 0, default_sort_json: str | None = None, @@ -83,17 +99,20 @@ def upsert_table_metadata( default_visible_columns_json: 宿主数据管理默认展示列,JSON 字符串数组,例如 '["stat_type","period","stage"]' 为空则全显;用户改过「列」后以本机偏好为准(软默认,区别于 visible=0 硬隐藏)。 + + grid_help: 数据管理表格上方说明;空则不出横幅。与表单用的 description 独立,禁止互拷。 """ cursor.execute( """ INSERT INTO _jiangchang_tables ( - table_name, display_name, description, visible, readonly, + table_name, display_name, description, grid_help, visible, readonly, default_sort_json, default_visible_columns_json ) - VALUES (?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(table_name) DO UPDATE SET display_name = excluded.display_name, description = excluded.description, + grid_help = excluded.grid_help, visible = excluded.visible, readonly = excluded.readonly, default_sort_json = excluded.default_sort_json, @@ -103,6 +122,7 @@ def upsert_table_metadata( table_name, display_name, description, + grid_help, visible, readonly, default_sort_json, @@ -118,6 +138,7 @@ def upsert_column_metadata( column_name: str, display_name: str, description: str | None = None, + grid_help: str | None = None, visible: int = 1, searchable: int = 1, editable: int = 1, @@ -125,16 +146,18 @@ def upsert_column_metadata( options_json: str | None = None, ) -> None: # display_order 保留为 NULL:宿主按 PRAGMA table_info cid 展示字段,不读 display_order。 + # grid_help: 表头「?」;空则不出。与表单用的 description 独立,禁止互拷。 cursor.execute( """ INSERT INTO _jiangchang_columns ( - table_name, column_name, display_name, description, + table_name, column_name, display_name, description, grid_help, display_order, visible, searchable, editable, display_type, options_json ) - VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?) ON CONFLICT(table_name, column_name) DO UPDATE SET display_name = excluded.display_name, description = excluded.description, + grid_help = excluded.grid_help, display_order = NULL, visible = excluded.visible, searchable = excluded.searchable, @@ -147,6 +170,7 @@ def upsert_column_metadata( column_name, display_name, description, + grid_help, visible, searchable, editable, @@ -171,6 +195,7 @@ def seed_column_metadata_batch( column_name=str(column["column_name"]), display_name=str(column["display_name"]), description=str(column["description"]) if column.get("description") else None, + grid_help=str(column["grid_help"]) if column.get("grid_help") else None, visible=int(column.get("visible", 1)), searchable=int(column.get("searchable", 1)), editable=int(column.get("editable", 1)), diff --git a/scripts/util/constants.py b/scripts/util/constants.py index 94c12a0..4ffaf17 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.89" +SKILL_VERSION = "1.0.90" 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 0a47f42..ffb9bb0 100644 --- a/tests/test_display_metadata.py +++ b/tests/test_display_metadata.py @@ -578,6 +578,100 @@ metadata: finally: conn.close() + def test_grid_help_upsert_and_migrate_from_legacy_schema(self) -> None: + conn = sqlite3.connect(":memory:") + try: + from db.display_metadata import ( + init_display_metadata_tables, + upsert_column_metadata, + upsert_table_metadata, + ) + + cur = conn.cursor() + cur.execute( + """ + CREATE TABLE _jiangchang_tables ( + table_name TEXT PRIMARY KEY, + display_name TEXT NOT NULL, + description TEXT, + sort_order INTEGER, + visible INTEGER NOT NULL DEFAULT 1, + readonly INTEGER NOT NULL DEFAULT 0, + default_sort_json TEXT, + default_visible_columns_json TEXT + ) + """ + ) + cur.execute( + """ + CREATE TABLE _jiangchang_columns ( + table_name TEXT NOT NULL, + column_name TEXT NOT NULL, + display_name TEXT NOT NULL, + description TEXT, + display_order INTEGER, + visible INTEGER NOT NULL DEFAULT 1, + searchable INTEGER NOT NULL DEFAULT 1, + editable INTEGER NOT NULL DEFAULT 1, + display_type TEXT, + options_json TEXT, + PRIMARY KEY (table_name, column_name) + ) + """ + ) + init_display_metadata_tables(cur) + cur.execute("PRAGMA table_info('_jiangchang_tables')") + self.assertIn("grid_help", {str(row[1]) for row in cur.fetchall()}) + cur.execute("PRAGMA table_info('_jiangchang_columns')") + self.assertIn("grid_help", {str(row[1]) for row in cur.fetchall()}) + + upsert_table_metadata( + cur, + table_name="demo_items", + display_name="示例记录", + description="表单备注,不应出现在表格横幅", + grid_help="表格横幅说明", + ) + upsert_column_metadata( + cur, + table_name="demo_items", + column_name="title", + display_name="标题", + description="填标题", + grid_help="列头问号说明", + ) + upsert_table_metadata( + cur, + table_name="empty_help", + display_name="无表格说明", + ) + conn.commit() + cur.execute( + """ + SELECT description, grid_help + FROM _jiangchang_tables + WHERE table_name = 'demo_items' + """ + ) + self.assertEqual(cur.fetchone(), ("表单备注,不应出现在表格横幅", "表格横幅说明")) + cur.execute( + """ + SELECT description, grid_help + FROM _jiangchang_columns + WHERE table_name = 'demo_items' AND column_name = 'title' + """ + ) + self.assertEqual(cur.fetchone(), ("填标题", "列头问号说明")) + cur.execute( + """ + SELECT grid_help FROM _jiangchang_tables + WHERE table_name = 'empty_help' + """ + ) + self.assertTrue(not cur.fetchone()[0]) + finally: + conn.close() + if __name__ == "__main__": unittest.main() diff --git a/tests/test_docs_standards.py b/tests/test_docs_standards.py index c6b7dbb..2ec1c86 100644 --- a/tests/test_docs_standards.py +++ b/tests/test_docs_standards.py @@ -348,6 +348,8 @@ class TestDocsStandards(unittest.TestCase): "文件 / 图片上传", "default_visible_columns_json", "默认展示列", + "grid_help", + "表格说明 `grid_help`", ): self.assertIn(marker, text, msg=f"SCHEMA.md missing {marker!r}") @@ -365,6 +367,7 @@ class TestDocsStandards(unittest.TestCase): "SCHEMA.md", "字段条件显隐", "visibleWhen", + "grid_help", ): self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}")