# 数据存储与数据管理展示规范 本文是 skill 本地 SQLite 与匠厂宿主「数据管理」页面展示契约的**权威说明**。 实现上的 DDL 与默认中文元数据种子见 `scripts/db/display_metadata.py`;`scripts/db/connection.py` 的 `init_db()` 会一并初始化。 ## 职责边界 ### 技能负责 - 定义真实 SQLite 表结构 - 使用英文 `snake_case` 表名和字段名 - 使用 `_jiangchang_tables` 提供表级展示元数据 - 使用 `_jiangchang_columns` 提供字段级展示元数据 - 定义哪些字段可见、可搜索、可创建、可编辑 - 定义字段展示类型和枚举选项 - 保证元数据与真实数据库结构一致 - 保证元数据初始化幂等 ### 宿主负责 - 读取 `_jiangchang_*` 元数据 - 读取 SQLite `PRAGMA table_info` - 根据 schema 渲染数据管理页面 - 根据 `editable` 推导字段是否可创建或编辑 - 执行通用查询、新增、编辑、删除、导入、导出 - 对字段名、表名、类型和权限进行最终校验 - 不猜测技能业务含义 - 不根据具体技能名称写特殊逻辑 SQLite **没有**可靠的 `COMMENT ON TABLE/COLUMN`;SQL 文件里的 `-- 注释` 也不会成为可查询结构。 因此中文展示名称必须写入元数据表;字段顺序必须写在 `CREATE TABLE` 的列定义顺序中(`PRAGMA table_info` 的 `cid`)。 > **禁止**新技能使用宿主的旧版 `_schema_meta` 表。仅 `_jiangchang_tables` / `_jiangchang_columns` 为正式契约。 ## 技能名称(SKILL.md) ```yaml --- name: 中文业务名称 description: 中文技能说明 metadata: openclaw: slug: english-kebab-slug --- ``` - `name`:用户可见的中文业务名称。 - `description`:能力说明,不是技能名称。 - `metadata.openclaw.slug`:机器标识,kebab-case 英文;目录名与默认数据库文件名使用 slug。slug 语义规范见 [`../development/NAMING.md`](../development/NAMING.md)。 - 复制模板后不得将 `your-skill-slug`、`My Skill` 等占位内容作为正式技能发布名称。 ## 数据库路径与命名 ```text {JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/{skill-slug}/{skill-slug}.db ``` - 数据库文件:英文 slug,例如 `your-skill-slug.db`(默认 `{skill-slug}.db`)。 - 业务表、字段:**英文 snake_case** 物理名,例如 `demo_items`、`task_logs`、`created_at`。 - **禁止**用中文作为 SQLite 真实表名或字段名。 ## 外部来源只读与本地数据可写 某些技能会从外部系统读取数据并写入技能自己的本地数据库。需要区分: ```text 外部来源数据库: 可能只读,不应被修改 技能本地数据库: 可以根据业务语义允许新增、编辑或删除 ``` 不要把「外部来源只读」错误理解成「技能本地数据库一定只读」。技能通过 `_jiangchang_tables.readonly` 与 `_jiangchang_columns.editable` 声明本地表的实际写权限。 ## 元数据表(宿主兼容) 宿主读取字段(与匠厂 `metadata-resolver` 一致): ### `_jiangchang_tables` 表级标准 | 字段 | 必填 | 说明 | |------|------|------| | `table_name` | 是 | 真实英文表名 | | `display_name` | 是 | 用户可见的中文业务名称 | | `description` | 否 | 表说明 | | `sort_order` | 否 | 兼容保留字段,不用于替代物理字段顺序 | | `visible` | 是 | `1` 在宿主数据管理中展示 / `0` 隐藏 | | `readonly` | 是 | `1` 整张表只读;`0` 允许宿主根据字段权限执行写操作 | 禁止: - 使用中文真实表名 - 使用 `_schema_meta` 作为新的正式契约 - 只写英文物理名而不写中文 `display_name` - 让元数据引用不存在的表 ### `_jiangchang_columns` 字段级标准 | 字段 | 必填 | 说明 | |------|------|------| | `table_name` | 是 | 所属物理表 | | `column_name` | 是 | 真实英文字段名 | | `display_name` | 是 | 用户可见的中文字段名 | | `description` | 否 | 字段说明 | | `display_order` | 否 | 兼容字段,默认保持 `NULL` | | `visible` | 是 | 是否在数据管理表格中展示 | | `searchable` | 是 | 是否参与搜索 | | `editable` | 是 | `1` 业务字段允许宿主用于新增和编辑;`0` 系统/只读字段 | | `display_type` | 否 | 字段渲染类型(如 `text`、`textarea`、`datetime_unix_seconds`) | | `options_json` | 否 | 枚举选项 JSON | 这两张表是**数据字典**,不是业务数据;宿主不会把它们当作普通业务表展示。 ## editable 与 createable 的关系 技能模板只维护 `editable` 这一份字段权限声明: ```text skill metadata: editable host runtime: createable / editable ``` 宿主根据以下因素计算字段是否能够出现在新增或编辑表单中: - 表级 `readonly` - 字段 `editable` - 主键 - generated 字段 - sensitive 字段 - BLOB 类型 - not-null 约束 - default value 通用规则: ```text 新增表单: editable = 1 且不是主键 且不是 generated 且不是敏感字段 且不是 BLOB 编辑表单: editable = 1 且不是主键 且不是 generated 且不是敏感字段 且不是 BLOB ``` 若一张表: ```text readonly = 0 但所有字段 editable = 0 ``` 则: - 这张表技术上可写,但不适合手工新增或编辑 - 宿主不应打开空白新增表单 - 技能应通过自身业务流程产生数据 - 这不是宿主猜测业务,而是技能通过元数据声明的结果 ## 系统字段标准 通常保持 `editable = 0` 的字段类型包括: - 自增主键 - generated 字段 - `created_at` / `updated_at` - 同步时间戳 - 内容哈希 - 计算结果 - 内部状态 - 删除标记 - 审计字段 - 敏感凭据或密钥字段 不要把具体业务字段名写死;按字段类型和职责设置 `editable`。 ## 表结构标准 - 业务表和字段使用英文 `snake_case` - 可写表优先使用单字段主键 - 自增主键优先放在 `CREATE TABLE` 第一列 - `created_at` 和 `updated_at` 放在业务字段末尾 - 字段展示顺序以 `CREATE TABLE` 和 `PRAGMA table_info(cid)` 为准 - 不依赖 `display_order` 重新排列字段 - 不要让宿主通过英文字段名推断展示顺序 - 复杂字段需要通过 `display_type` 或 `visible` 明确处理 推荐顺序(可按业务裁剪,但已有字段应遵守相对位置): ```text 1. 主键 id 2. 关联标识与核心业务字段 3. 普通业务字段 4. 状态、结果、错误等辅助字段 5. created_at(如有) 6. updated_at(如有) ``` ## 标准时间字段(新技能统一规范) 新技能业务表若需要时间审计字段,**统一**采用 Unix **秒级**时间戳,不要使用毫秒时间戳、ISO 字符串或 SQLite 文本 datetime。 | 项目 | 规范 | |------|------| | 物理字段 | `created_at INTEGER`、`updated_at INTEGER` | | 存储格式 | Unix 秒级整数 | | 展示元数据 `display_type` | `datetime_unix_seconds` | | 中文名 | `created_at` → **创建时间**;`updated_at` → **更新时间** | | 用户编辑 | `editable = 0`(系统字段,宿主只展示格式化结果) | | 字段顺序 | 位于业务字段末尾:`… → created_at → updated_at` | | 排序 | **不使用** `display_order`;顺序仅来自 `PRAGMA table_info` | ### 自动生成与维护 - **created_at**:`INTEGER NOT NULL DEFAULT (unixepoch())`;插入时可省略,由数据库生成。 - **updated_at**:`INTEGER NOT NULL DEFAULT (unixepoch())`;插入时同样可省略。 - **updated_at 更新**:模板默认表 `task_logs` 通过 SQLite trigger `{table}_set_updated_at` 在业务字段更新后自动刷新。 实现参考: - DDL 与 trigger:`scripts/db/connection.py`、`scripts/db/timestamp_columns.py` - 中文元数据种子:`scripts/db/display_metadata.py` - 自检:`scripts/db/display_metadata_validator.py` 宿主只负责把 `display_type = datetime_unix_seconds` 格式化为 `YYYY-MM-DD HH:mm:ss` 展示,**不修改**数据库原始值。 ### 模板默认:`task_logs` 物理定义(权威顺序): ```sql CREATE TABLE IF NOT EXISTS task_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, task_type TEXT NOT NULL, target_id TEXT, input_id TEXT, input_title TEXT, status TEXT NOT NULL, error_msg TEXT, result_summary TEXT, created_at INTEGER NOT NULL DEFAULT (unixepoch()), updated_at INTEGER NOT NULL DEFAULT (unixepoch()) ); ``` 对应中文展示(由 `init_db()` 写入元数据表): | 物理字段 | 中文名 | editable | |----------|--------|----------| | id | 编号 | 0 | | task_type | 任务类型 | 1 | | target_id | 目标编号 | 1 | | input_id | 输入编号 | 1 | | input_title | 输入标题 | 1 | | status | 状态 | 1 | | error_msg | 错误信息 | 1 | | result_summary | 结果摘要 | 1 | | created_at | 创建时间 | 0 | | updated_at | 更新时间 | 0 | 表中文名:**任务日志**;表级 `readonly = 0`。 ### 通用示例:`demo_items`(测试 fixture 参考) 以下表结构用于模板测试演示数据管理契约,**不是**模板默认业务表: ```sql CREATE TABLE demo_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT, status TEXT NOT NULL DEFAULT 'draft', generated_value TEXT, created_at INTEGER NOT NULL DEFAULT (unixepoch()), updated_at INTEGER NOT NULL DEFAULT (unixepoch()) ); ``` 对应元数据要点: | 物理字段 | editable | 说明 | |----------|----------|------| | id | 0 | 自增主键 | | title | 1 | 业务字段 | | description | 1 | 业务字段 | | status | 1 | 枚举,`options_json` 演示 | | generated_value | 0 | 系统/计算字段 | | created_at | 0 | 系统时间 | | updated_at | 0 | 系统时间 | ## 枚举字段 options_json `options_json` 为 JSON 数组,每项包含 `value` 与 `label`: ```json [ {"value": "draft", "label": "草稿"}, {"value": "published", "label": "已发布"}, {"value": "archived", "label": "已归档"} ] ``` 宿主据此渲染选择框;技能侧负责保证 JSON 合法且与物理字段取值一致。 ## 幂等初始化标准 - `_jiangchang_tables` 和 `_jiangchang_columns` 必须通过 upsert 初始化 - 多次执行初始化不能重复记录 - 重新初始化可以更新展示名、描述、`visible`、`searchable`、`editable`、`display_type`、`options_json` - 元数据不能残留旧字段(删除或重命名字段时必须同步清理) - 元数据不能引用不存在的表或字段 - 新增字段或表时必须同步增加元数据 - 不要直接修改用户真实数据库作为迁移方式 实现 helper: - `upsert_table_metadata` / `upsert_column_metadata` — 幂等写入 - `prune_stale_column_metadata` — 清理已删除物理字段的元数据 ## 中文命名质量 - 使用普通用户能理解的业务用语:`task_logs` → **任务日志**,`error_msg` → **错误信息**。 - 禁止:`Task Logs`、`input编号`、`errormsg`、直接把物理名当 `display_name`。 - 不宜向用户暴露的实现细节(如 `payload_json`、`selector`)应设 `visible = 0` 或给出可理解中文名。 ## 元数据同步规则 1. 新增用户可见业务表 → 同步 `_jiangchang_tables`。 2. 新增用户可见字段 → 同步 `_jiangchang_columns`。 3. 删除/重命名表或字段 → 同步清理或迁移元数据。 4. 初始化脚本必须幂等。 5. 元数据中的表、字段必须在库中真实存在;可见项不得缺少中文 `display_name`。 ## 不同业务的 `task_logs` 映射 | 业务场景 | task_type | target_id | input_id | |----------|-----------|-----------|----------| | 发布类 | publish | 账号 ID | 内容 ID | | 工资代发 | disburse | 付款账户 | 批次 ID | | 对账 | reconcile | 平台标识 | 对账批次 ID | | 发票验真 | verify | 税务地区 | 发票批次 ID | 业务特有字段优先放入 `result_summary`(JSON 字符串),避免随意加列;若确需新列,必须同步元数据并保持合理物理顺序。 ## 开发自检 - `init_db()` 后运行 `tests/test_display_metadata.py` 与 `tests/test_data_management_contract.py`。 - 若提供宿主 Action 入口,运行 `tests/test_actions_manifest.py`。 - 复制为真实技能后,使用 `scripts/db/display_metadata_validator.py` 中的校验函数检查 SKILL 名称、slug 与元数据完整性。 ## 模板原则 - 模板不做复杂历史迁移框架;新 skill 从当前 schema 起步。 - 元数据 DDL 与 `task_logs` 中文种子以 `scripts/db/display_metadata.py` 为单一权威来源。 - 模板定义「如何声明能力」;具体技能定义「声明什么业务能力」;宿主负责「如何通用渲染和执行」。