453 lines
17 KiB
Markdown
453 lines
17 KiB
Markdown
# 数据存储与数据管理展示规范
|
||
|
||
本文是 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` 推导字段是否可创建或编辑
|
||
- 执行通用查询、新增、编辑、删除、导入、导出
|
||
- 对字段名、表名、类型和权限进行最终校验
|
||
- 不猜测技能业务含义
|
||
- 不根据具体技能名称写特殊逻辑
|
||
- 可在技能安装/更新成功后静默调用 `python scripts/main.py init-db` 做幂等 ensure(内部即 `init_db()`);失败不影响安装结果
|
||
|
||
## 初始化入口
|
||
|
||
幂等建库/迁移:`python scripts/main.py init-db`。业务仓储也可懒触发 `init_db()`,与 CLI 结果一致。
|
||
|
||
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` 声明本地表的实际写权限。
|
||
|
||
## 标准数据同步语义(外源 → 本地库)
|
||
|
||
适用于「同步 / refresh / collect」类能力。使用通用名称表述,勿把具体平台表名写进模板实现。
|
||
|
||
| 要求 | 说明 |
|
||
|------|------|
|
||
| 外部来源默认只读 | 技能不得修改外源 |
|
||
| 本地 SQLite 可事务写入 | 写入走 repository + 事务 |
|
||
| 幂等 | 同一业务键重复同步结果稳定 |
|
||
| 稳定唯一业务键 | 明确 upsert / 匹配字段 |
|
||
| 全量或增量 | 在实现与文档中显式声明 |
|
||
| 完整快照才提交 | 完整性校验通过后再 commit;调用方**必须显式提供**完整性状态(如 keyword-only 必填的 `snapshot_complete`),**禁止**默认视为完整,**禁止**靠数据条数猜测 |
|
||
| 空结果 / 读取失败保护 | 读取失败、分页不完整或不可信空结果(`snapshot_complete=False`)**不得**清库或误删;仅当完整快照**显式确认**为空(`snapshot_complete=True` 且结果为空)时,才可按全量失效策略处理旧数据 |
|
||
| 全量失效策略 | 物理删除、`removed`/`is_active` 标记、或保留历史——必须三选一并写清 |
|
||
| 部分失败 | 不得伪装成整体成功 |
|
||
| 半成品 | 事务失败不得留下可见不一致状态 |
|
||
|
||
建议统一返回统计:
|
||
|
||
```json
|
||
{
|
||
"total": 100,
|
||
"inserted": 10,
|
||
"updated": 20,
|
||
"unchanged": 65,
|
||
"removed": 5
|
||
}
|
||
```
|
||
|
||
### 同步数据 vs 导出当前数据表 vs 特殊业务报告
|
||
|
||
| 能力 | 语义 | 负责方 |
|
||
|------|------|--------|
|
||
| **同步数据** | 外部来源 → 技能本地数据库 | 技能 Action / domain service |
|
||
| **导出当前数据表** | 本地库 → CSV/Excel 等 | 数据管理页**宿主统一导出** |
|
||
| **生成特殊业务报告** | 多表合并、特殊排版或附加计算 | 仅当系统导出无法满足时,才提供独立 `export-*` |
|
||
|
||
同步型 Action 默认只写库、不生成导出文件。兼容旧 export 命令需要刷新时,必须调用同一 sync 内核。可复用测试范式见 `tests/samples/test_sync_contract.py.sample`。
|
||
|
||
## 元数据表(宿主兼容)
|
||
|
||
宿主读取字段(与匠厂 `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`
|
||
|
||
`task_logs` 是技能内部**系统自动写入**的任务审计日志(由 `task_logs_repository` INSERT/UPDATE),不是供用户在宿主数据管理中手工维护的业务表。
|
||
|
||
权限设计:
|
||
|
||
| 层级 | 值 | 说明 |
|
||
|------|-----|------|
|
||
| 表级 `readonly` | `1` | 宿主数据管理只读展示,不提供新增/编辑/删除表单 |
|
||
| 所有业务字段 `editable` | `0` | 含 `task_type`、`status`、`error_msg`、`result_summary` 等 |
|
||
| 技能内部 repository | 正常 INSERT/UPDATE | 元数据权限只约束宿主 UI,不限制技能代码写库 |
|
||
|
||
物理定义(权威顺序):
|
||
|
||
```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 | 任务类型 | 0 |
|
||
| target_id | 目标编号 | 0 |
|
||
| input_id | 输入编号 | 0 |
|
||
| input_title | 输入标题 | 0 |
|
||
| status | 状态 | 0 |
|
||
| error_msg | 错误信息 | 0 |
|
||
| result_summary | 结果摘要 | 0 |
|
||
| created_at | 创建时间 | 0 |
|
||
| updated_at | 更新时间 | 0 |
|
||
|
||
表中文名:**任务日志**;表级 `readonly = 1`(宿主只读;技能 repository 仍可写入)。
|
||
|
||
### 通用示例:`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` — 幂等写入
|
||
- `seed_column_metadata_batch` — 批量 upsert;**可选**传入 `valid_column_names` 时在 upsert 后自动 prune
|
||
- `prune_stale_column_metadata` — 清理已删除物理字段的元数据
|
||
|
||
### 新增业务表时如何初始化
|
||
|
||
1. 在 `connection.py`(或迁移脚本)中 `CREATE TABLE` 真实物理表。
|
||
2. 用 `PRAGMA table_info` 或 `list_physical_column_names` 取得**真实物理字段列表**(顺序以 `cid` 为准)。
|
||
3. 调用 `upsert_table_metadata` 写入表级元数据。
|
||
4. 调用 `seed_column_metadata_batch`,并传入 `valid_column_names=physical_column_names`:
|
||
|
||
```python
|
||
from db.display_metadata import list_physical_column_names, seed_column_metadata_batch, upsert_table_metadata
|
||
|
||
physical = list_physical_column_names(cursor, "demo_items")
|
||
upsert_table_metadata(cursor, table_name="demo_items", display_name="示例记录", readonly=0)
|
||
seed_column_metadata_batch(
|
||
cursor,
|
||
table_name="demo_items",
|
||
columns=DEMO_ITEMS_COLUMN_METADATA,
|
||
valid_column_names=physical,
|
||
)
|
||
```
|
||
|
||
传入 `valid_column_names` 时:先 upsert,再删除不在物理表中的陈旧 `_jiangchang_columns` 行。
|
||
|
||
### 字段删除或重命名时如何清理
|
||
|
||
1. 在 DDL / 迁移中完成物理字段删除或重命名。
|
||
2. 重新执行 `seed_column_metadata_batch(..., valid_column_names=当前物理字段列表)`,自动 prune 旧字段元数据。
|
||
3. 或单独调用 `prune_stale_column_metadata(cursor, table_name, valid_column_names)`。
|
||
|
||
**不传入 `valid_column_names` 时不会自动 prune**——模板不会猜测真实物理字段,避免误删;文档与实现均以此为准。
|
||
|
||
不要直接修改用户真实数据库作为迁移方式;元数据初始化必须使用真实物理字段列表。
|
||
|
||
## 中文命名质量
|
||
|
||
- 使用普通用户能理解的业务用语:`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` 为单一权威来源。
|
||
- 模板定义「如何声明能力」;具体技能定义「声明什么业务能力」;宿主负责「如何通用渲染和执行」。
|