17 KiB
数据存储与数据管理展示规范
本文是 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)
---
name: 中文业务名称
description: 中文技能说明
metadata:
openclaw:
slug: english-kebab-slug
---
name:用户可见的中文业务名称。description:能力说明,不是技能名称。metadata.openclaw.slug:机器标识,kebab-case 英文;目录名与默认数据库文件名使用 slug。slug 语义规范见../development/NAMING.md。- 复制模板后不得将
your-skill-slug、My Skill等占位内容作为正式技能发布名称。
数据库路径与命名
{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 真实表名或字段名。
外部来源只读与本地数据可写
某些技能会从外部系统读取数据并写入技能自己的本地数据库。需要区分:
外部来源数据库:
可能只读,不应被修改
技能本地数据库:
可以根据业务语义允许新增、编辑或删除
不要把「外部来源只读」错误理解成「技能本地数据库一定只读」。技能通过 _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 标记、或保留历史——必须三选一并写清 |
| 部分失败 | 不得伪装成整体成功 |
| 半成品 | 事务失败不得留下可见不一致状态 |
建议统一返回统计:
{
"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 这一份字段权限声明:
skill metadata:
editable
host runtime:
createable / editable
宿主根据以下因素计算字段是否能够出现在新增或编辑表单中:
- 表级
readonly - 字段
editable - 主键
- generated 字段
- sensitive 字段
- BLOB 类型
- not-null 约束
- default value
通用规则:
新增表单:
editable = 1
且不是主键
且不是 generated
且不是敏感字段
且不是 BLOB
编辑表单:
editable = 1
且不是主键
且不是 generated
且不是敏感字段
且不是 BLOB
若一张表:
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明确处理
推荐顺序(可按业务裁剪,但已有字段应遵守相对位置):
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,不限制技能代码写库 |
物理定义(权威顺序):
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 参考)
以下表结构用于模板测试演示数据管理契约,不是模板默认业务表:
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:
[
{"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 后自动 pruneprune_stale_column_metadata— 清理已删除物理字段的元数据
新增业务表时如何初始化
- 在
connection.py(或迁移脚本)中CREATE TABLE真实物理表。 - 用
PRAGMA table_info或list_physical_column_names取得真实物理字段列表(顺序以cid为准)。 - 调用
upsert_table_metadata写入表级元数据。 - 调用
seed_column_metadata_batch,并传入valid_column_names=physical_column_names:
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 行。
字段删除或重命名时如何清理
- 在 DDL / 迁移中完成物理字段删除或重命名。
- 重新执行
seed_column_metadata_batch(..., valid_column_names=当前物理字段列表),自动 prune 旧字段元数据。 - 或单独调用
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或给出可理解中文名。
元数据同步规则
- 新增用户可见业务表 → 同步
_jiangchang_tables。 - 新增用户可见字段 → 同步
_jiangchang_columns。 - 删除/重命名表或字段 → 同步清理或迁移元数据。
- 初始化脚本必须幂等。
- 元数据中的表、字段必须在库中真实存在;可见项不得缺少中文
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为单一权威来源。 - 模板定义「如何声明能力」;具体技能定义「声明什么业务能力」;宿主负责「如何通用渲染和执行」。