715 lines
33 KiB
Markdown
715 lines
33 KiB
Markdown
# 数据存储与数据管理展示规范
|
||
|
||
本文是 skill 本地 SQLite 与匠厂宿主「数据管理」页面展示契约的**权威说明**。
|
||
|
||
**产品定位:** 数据管理既是业务数据与过程的可见面,也是与对话**平级**的 **UI 执行面**(表上 Action 走同一 CLI 闭环)。何时挂执行入口见 [`../development/PRODUCT_SURFACES.md`](../development/PRODUCT_SURFACES.md);本文管表结构与展示元数据。
|
||
|
||
实现上的 DDL 与默认中文元数据种子见 `scripts/db/display_metadata.py`;`scripts/db/connection.py` 的 `init_db()` 会一并初始化。
|
||
|
||
## 职责边界
|
||
|
||
### 技能负责
|
||
|
||
- 定义真实 SQLite 表结构
|
||
- 使用英文 `snake_case` 表名和字段名
|
||
- 使用 `_jiangchang_tables` 提供表级展示元数据
|
||
- 使用 `_jiangchang_columns` 提供字段级展示元数据(含可选 `description` 表单说明、可选 `grid_help` 表格说明)
|
||
- 需要「新增表单预填」时,在物理列上声明 SQLite `DEFAULT`(勿把默认值只写在文案里)
|
||
- 定义哪些字段可见、可搜索、可创建、可编辑
|
||
- 定义字段展示类型和枚举选项
|
||
- (可选)按业务规则维护单元格呈现旁路数据(语义 tone;见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md))
|
||
- 保证元数据与真实数据库结构一致
|
||
- 保证元数据初始化幂等
|
||
|
||
### 宿主负责
|
||
|
||
- 读取 `_jiangchang_*` 元数据
|
||
- 读取 SQLite `PRAGMA table_info`
|
||
- 根据 schema 渲染数据管理页面
|
||
- 根据 `editable` 推导字段是否可创建或编辑
|
||
- 在**新增 / 编辑**表单中,若列有 `_jiangchang_columns.description`,在标签下方展示该说明(灰字;空则不显示)
|
||
- 在数据管理**表格**中,若表有 `_jiangchang_tables.grid_help` 则在表上方展示可折叠说明;若列有 `_jiangchang_columns.grid_help` 则在列头显示「?」;空/未写则不渲染。**不**把 `description` 当作表格说明
|
||
- 打开**新增**表单时,对可创建字段用 SQLite 列 `DEFAULT`(`PRAGMA table_info.dflt_value`)预填;无 DEFAULT 则留空
|
||
- 执行通用查询、新增、编辑、删除、导入、导出
|
||
- 对字段名、表名、类型和权限进行最终校验
|
||
- 不猜测技能业务含义
|
||
- 不根据具体技能名称写特殊逻辑
|
||
- 可在技能安装/更新成功后静默调用 `python scripts/main.py init-db` 做幂等 ensure(内部即 `init_db()`);失败不影响安装结果
|
||
- 若技能启用了单元格呈现(见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)),读取旁路呈现数据并为单元格映射语义样式(`tone`);不猜测业务规则
|
||
|
||
## 初始化入口
|
||
|
||
幂等建库/迁移:`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`、`created_at`。
|
||
- **禁止**用中文作为 SQLite 真实表名或字段名。
|
||
|
||
## 外部来源只读与本地数据可写
|
||
|
||
某些技能会从外部系统读取数据并写入技能自己的本地数据库。需要区分:
|
||
|
||
```text
|
||
外部来源数据库:
|
||
可能只读,不应被修改
|
||
|
||
技能本地数据库:
|
||
可以根据业务语义允许新增、编辑或删除
|
||
```
|
||
|
||
不要把「外部来源只读」错误理解成「技能本地数据库一定只读」。技能通过 `_jiangchang_tables.readonly` 与 `_jiangchang_columns.editable` 声明本地表的实际写权限。
|
||
|
||
若创建/更新/删除/导入带有领域副作用(自动建目录、加密、级联清理、归一化字段),不要依赖宿主默认直写:在 `assets/actions.json` 用可选字段 `replaces: create|update|delete|import` 接管默认按钮。其中 **import** 时用户仍使用宿主「导入数据」面板,确认后由技能写库。详见 [`ACTIONS.md`](ACTIONS.md) 与 `assets/examples/replaces-*.action.example.json`。
|
||
|
||
## 表与表之间:禁止 SQLite 外键
|
||
|
||
业务表之间的「属于哪条父记录 / 引用哪条来源」**只存业务 id**(整数或文本标量),**禁止**在 DDL 中写 SQLite `FOREIGN KEY` / `REFERENCES`,也**不要**指望 `PRAGMA foreign_keys = ON` 替你校验或级联。
|
||
|
||
| 要做的事 | 正确做法 | 禁止 |
|
||
|----------|----------|------|
|
||
| 子表指向父表 | 子表列如 `case_id INTEGER NOT NULL`;写入前在 repository/service 查父表是否存在 | `FOREIGN KEY (case_id) REFERENCES parent(id)` |
|
||
| 父表记「最新一版」 | 父表列如 `latest_draft_id INTEGER NOT NULL DEFAULT 0`;由 service 更新指针 | 同上 |
|
||
| 删除 / 覆盖 / 级联清理 | repository/service 内显式 `DELETE` / `UPDATE`;需要时用 Action `steps` 的 `deleteByFk` | 依赖 DB 级联或宿主 CRUD 被 FK 拦住 |
|
||
| 数据管理里显示名称 | `_jiangchang_columns.display_type = lookup` + `options_json` 对象(见下文「跨表引用 lookup」) | 把 lookup 当成 DDL 外键 |
|
||
|
||
**为什么:** 宿主数据管理对业务表执行通用增删改,会直接写 SQLite。一旦 DDL 带外键,删除、迁移 rename 父表、或父表 id 变更时极易出现「本地数据操作失败」且难排查。关联语义属于技能业务,必须在代码里写清。
|
||
|
||
**迁移注意:** 避免「`ALTER TABLE parent RENAME TO parent_old` → 建新 parent → 删 old」这类会隐性改写子表 FK 指向的套路;优先 `ADD COLUMN` + 拷列,或重建子表且**不带** FK。
|
||
|
||
`display_metadata_validator` 与 `validate_template_database` 会扫描业务表 DDL;含 `FOREIGN KEY` / `REFERENCES` 时报错(见 `development/CHECKLIST.md` A6-6)。
|
||
|
||
## 标准数据同步语义(外源 → 本地库)
|
||
|
||
适用于「同步 / 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 特殊业务报告 vs HTML 页面
|
||
|
||
| 能力 | 语义 | 负责方 |
|
||
|------|------|--------|
|
||
| **同步数据** | 外部来源 → 技能本地数据库 | 技能 Action / domain service |
|
||
| **导出当前数据表** | 本地库 → CSV/Excel 等 | 数据管理页**宿主统一导出** |
|
||
| **生成特殊业务报告(文件)** | 多表合并、特殊排版或附加计算的文件 | 仅当系统导出无法满足时,才提供独立 `export-*`;落 `output/` |
|
||
| **HTML 页面报告** | 侧栏可预览的整页 HTML | 写入 `html/` + Action id **`render-html`**(见 [`ACTIONS.md`](ACTIONS.md)) |
|
||
|
||
同步型 Action 默认只写库、不生成导出文件。兼容旧 export 命令需要刷新时,必须调用同一 sync 内核。可复用测试范式见 `tests/samples/test_sync_contract.py.sample`。
|
||
|
||
## 元数据表(宿主兼容)
|
||
|
||
宿主读取字段(与匠厂 `metadata-resolver` 一致):
|
||
|
||
### `_jiangchang_tables` 表级标准
|
||
|
||
| 字段 | 必填 | 说明 |
|
||
|------|------|------|
|
||
| `table_name` | 是 | 真实英文表名 |
|
||
| `display_name` | 是 | 用户可见的中文业务名称 |
|
||
| `description` | 否 | 表说明(表单/备注用途);**不**用于表格上方横幅 |
|
||
| `grid_help` | 否 | 数据管理表格上方说明;空/未写则不出横幅。与 `description` 独立,禁止互拷(见下节) |
|
||
| `sort_order` | 否 | 兼容保留字段,不用于替代物理字段顺序 |
|
||
| `visible` | 是 | `1` 在宿主数据管理中展示 / `0` 隐藏 |
|
||
| `readonly` | 是 | `1` 整张表只读;`0` 允许宿主根据字段权限执行写操作 |
|
||
| `default_sort_json` | 否 | 数据管理**默认行排序**(见下节);空则宿主用内置默认 |
|
||
| `default_visible_columns_json` | 否 | 数据管理**默认展示列**(见下节);空则全显 |
|
||
|
||
#### 表级默认排序 `default_sort_json`(宿主支持)
|
||
|
||
技能可声明该表在数据管理中的默认排序;宿主优先采用,未声明或声明无效时回退:
|
||
|
||
`updated_at DESC` → `created_at DESC` → 主键 DESC。
|
||
|
||
JSON 数组(可多字段,按顺序):
|
||
|
||
```json
|
||
[{"column":"sent_at","direction":"desc"},{"column":"id","direction":"desc"}]
|
||
```
|
||
|
||
规则:
|
||
|
||
- `column` 必须是该表真实列名;敏感列会被宿主忽略
|
||
- `direction` 仅 `asc` / `desc`(缺省按 `desc`)
|
||
- 用户在 UI 上手动改排序后,以用户选择为准;声明只影响未选择时的默认
|
||
- 通过 `upsert_table_metadata(..., default_sort_json=...)` 写入;旧库由 `init_display_metadata_tables` 幂等 `ALTER` 补列
|
||
|
||
示例(消息时间线):
|
||
|
||
```python
|
||
upsert_table_metadata(
|
||
cursor,
|
||
table_name="messages",
|
||
display_name="消息",
|
||
readonly=0,
|
||
default_sort_json='[{"column":"sent_at","direction":"desc"},{"column":"id","direction":"desc"}]',
|
||
)
|
||
```
|
||
|
||
#### 表级默认展示列 `default_visible_columns_json`(宿主支持)
|
||
|
||
技能可声明该表在数据管理表格中的**默认展示列**(软默认,不是硬隐藏):
|
||
|
||
- **已配置**:首次打开(用户尚未改过「列」)只显示名单中的列;其余列仍在「列」面板中,用户可勾回
|
||
- **未配置 / 空 / 无效**:显示全部 schema 可见列(与今日行为一致)
|
||
- **用户改过「列」或表头「隐藏此列」后**:以本机偏好为准;「恢复默认列布局」后重新回到技能默认(若有)
|
||
|
||
JSON 数组(物理列名;也可写 `{"column":"col"}` 对象,宿主同样接受):
|
||
|
||
```json
|
||
["stat_type","period","stage","ops_count","timeout_ratio"]
|
||
```
|
||
|
||
规则:
|
||
|
||
- 列名必须是该表真实列,且 `_jiangchang_columns.visible=1`(硬隐藏列本来就不进表格)
|
||
- 与 `visible=0` **不同**:`visible=0` = 列从数据管理 schema /「列」面板移除;本字段只影响默认勾选状态
|
||
- 名单为空或全部无效时,宿主 fail-open:全显
|
||
- 通过 `upsert_table_metadata(..., default_visible_columns_json=...)` 写入;旧库由 `init_display_metadata_tables` 幂等 `ALTER` 补列
|
||
|
||
示例(时效统计:默认不展示编号与时间戳列):
|
||
|
||
```python
|
||
upsert_table_metadata(
|
||
cursor,
|
||
table_name="timeliness_stats",
|
||
display_name="时效统计",
|
||
readonly=1,
|
||
default_visible_columns_json=(
|
||
'["stat_type","period","stage","ops_count",'
|
||
'"timeout_open","timeout_done","timeout_total","timeout_ratio"]'
|
||
),
|
||
)
|
||
```
|
||
|
||
#### 表格说明 `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,
|
||
)
|
||
```
|
||
|
||
禁止:
|
||
|
||
- 使用中文真实表名
|
||
- 使用 `_schema_meta` 作为新的正式契约
|
||
- 只写英文物理名而不写中文 `display_name`
|
||
- 让元数据引用不存在的表
|
||
- 用 `visible=0` 假装「默认不展示但用户还能勾出来」(那是硬隐藏,勾不回来)
|
||
|
||
### `_jiangchang_columns` 字段级标准
|
||
|
||
| 字段 | 必填 | 说明 |
|
||
|------|------|------|
|
||
| `table_name` | 是 | 所属物理表 |
|
||
| `column_name` | 是 | 真实英文字段名 |
|
||
| `display_name` | 是 | 用户可见的中文字段名(表头与表单标签);保持简短 |
|
||
| `description` | 否 | 字段说明(给人看的短文案);**宿主在新增/编辑表单标签下方展示**(见下节);不要塞进 `display_name` |
|
||
| `grid_help` | 否 | 数据管理表头「?」;空/未写则不出。与表单 `description` 独立,禁止互拷 |
|
||
| `display_order` | 否 | 兼容字段,默认保持 `NULL` |
|
||
| `visible` | 是 | 是否进入数据管理 schema /「列」面板(`0` = **硬隐藏**,用户无法勾回;默认不展示但可勾回请用表级 `default_visible_columns_json`) |
|
||
| `searchable` | 是 | 是否参与搜索 |
|
||
| `editable` | 是 | `1` 业务字段允许宿主用于新增和编辑;`0` 系统/只读字段 |
|
||
| `display_type` | 否 | 字段渲染类型;稳定取值见下节「表单与展示:稳定字段类型」 |
|
||
| `options_json` | 否 | 枚举选项 JSON |
|
||
|
||
这两张表是**数据字典**,不是业务数据;宿主不会把它们当作普通业务表展示。
|
||
|
||
#### 表单与展示:稳定字段类型(宿主支持,须与 Action 对称)
|
||
|
||
数据管理「新增 / 编辑」表单与 Skill Action 参数表单应使用**同一套控件语义**(配置入口不同,见 [`ACTIONS.md`](ACTIONS.md))。新技能只使用下表;宿主须两边都实现,避免「Action 有、CRUD 没有」。
|
||
|
||
**本版稳定支持(须齐):**
|
||
|
||
| `display_type`(CRUD) | 物理类型建议 | 列表展示 | 新增/编辑控件 | 存库约定 |
|
||
|------------------------|--------------|----------|---------------|----------|
|
||
| `text`(或缺省) | `TEXT` / 一般标量 | 原文 | 单行文本 | 原文 |
|
||
| `textarea` | `TEXT` | 原文(可截断) | 多行文本 | 原文 |
|
||
| `json` | `TEXT` | 原文 | 多行文本(JSON 字符串) | 原文;提交时宿主可做基础 JSON 合法性校验 |
|
||
| `number` | `INTEGER` / `REAL` / `NUMERIC` | 原文 | **数字输入**(`type=number`) | 数字;无 `display_type` 时,物理类型为 INT/REAL/NUMERIC 也应走数字控件 |
|
||
| `boolean` | `INTEGER` 0/1 或 BOOL | 开关语义展示 | 开关 | `0` / `1` |
|
||
| (枚举) | 任意 + `options_json` **数组** | 标签映射 | 下拉选择 | 选项 `value` |
|
||
| `lookup` | 跨表引用 id;`options_json` **对象**;可选 `multiple` | 单选:标签映射;多选:多标签(如「豆包、元宝」) | 单选下拉;多选见下文 | 单选:业务 id 标量;多选:`TEXT` 逗号分隔 id(非 SQLite 外键) |
|
||
| (敏感) | 任意;列 `sensitive=1` | 掩码/隐藏策略由宿主定 | 新建可用密码框;编辑策略与既有宿主一致 | 原文(勿日志明文) |
|
||
| `date` | `TEXT` 推荐 `YYYY-MM-DD`,或可安全归一的值 | 日期 | **日期选择**(日) | `YYYY-MM-DD` |
|
||
| `datetime_unix_seconds` | `INTEGER` | `YYYY-MM-DD HH:mm:ss` | **日期时间选择**;提交写回 **Unix 秒** | Unix **秒**整数;禁止改成 ISO 字符串 |
|
||
| `url` | `TEXT` | 原文(可作链接) | 单行输入 `type=url`(薄层;校验从宽) | 原文 |
|
||
| `email` | `TEXT` | 原文 | 单行输入 `type=email`(薄层;校验从宽) | 原文 |
|
||
|
||
说明灰字与创建默认值见下一小节;与 Action 的对照写在 [`ACTIONS.md`](ACTIONS.md)「表单字段类型对照」。
|
||
|
||
**明确不在本版稳定范围(勿在新技能宣传为已支持):**
|
||
|
||
- 文件 / 图片上传
|
||
- 复杂 `oneOf` / nested object 表单(任意 JSON 数组当业务多选控件)
|
||
- 把业务可编辑时间存成毫秒、ISO 文本或 SQLite `datetime` 文本(审计字段规范见「标准时间字段」)
|
||
|
||
> **Lookup 多选**已纳入本版契约(见下文 `multiple`),与「任意多选/标签数组」不是同一能力。
|
||
|
||
`datetime_unix_milliseconds`:仅兼容**列表展示**旧数据;**新技能不要用**;可编辑时间一律 `datetime_unix_seconds`。
|
||
|
||
#### 新增/编辑表单:字段说明与默认值(宿主支持)
|
||
|
||
数据管理「新增记录 / 编辑记录」与 Skill Action 弹窗是**两套表单**;CRUD 侧约定如下(交互对齐 Action 的 `inputSchema.description` / `default`,但**配置入口不同**)。
|
||
|
||
| 能力 | 技能写什么 | 宿主做什么 |
|
||
|------|------------|------------|
|
||
| 字段说明 | `_jiangchang_columns.description`(可选) | 新增/编辑表单中,在标签下方以灰字展示;空则不显示 |
|
||
| 创建默认值 | 物理列 SQLite `DEFAULT`(如 `limit_n INTEGER NOT NULL DEFAULT 0`) | 打开「新增」时,对可创建字段预填该默认值;无 DEFAULT 则留空 |
|
||
| 编辑已有行 | — | **不**用 DEFAULT 覆盖已有行数据 |
|
||
| 表格列头标题 | `display_name` | 短名;不要把长说明塞进列头文字 |
|
||
| 表格列头「?」 | `_jiangchang_columns.grid_help`(可选) | 有内容才显示「?」;**不**回退 `description` |
|
||
| 表格上方说明 | `_jiangchang_tables.grid_help`(可选) | 有内容才显示横幅;**不**回退表级 `description` |
|
||
|
||
规则与禁止:
|
||
|
||
- **推荐**对易填错、语义特殊的可编辑字段写 `description`(示例链接、`0` 表示不限制等);非强制每个字段都有
|
||
- **禁止**把说明硬塞进 `display_name`(如 `评论上限(0不限)`);表头会变脏,也无法与 Action 说明对齐
|
||
- 业务语义(如「0 = 不限制」)写在 `description` 里;宿主只展示,**不**解读业务含义
|
||
- 第一期**不**新增元数据「表单默认值」列;CRUD 默认值 = DDL `DEFAULT`,与 Action 的 `inputSchema.default` 分开配置
|
||
- 同一业务字段若同时出现在 Action 与新增表单,**文案应对齐**(短标题 + 说明),但分别写在 `actions.json` 与 `_jiangchang_columns` / DDL
|
||
- 数字类可编辑列推荐 `display_type: "number"`(或依赖 INT/REAL/NUMERIC 推断)并在 DDL 写清 `DEFAULT`(如条数上限默认 `0`)
|
||
|
||
示例(可编辑列种子 + DDL 默认):
|
||
|
||
```sql
|
||
-- 物理列:创建默认 0
|
||
comment_limit INTEGER NOT NULL DEFAULT 0
|
||
```
|
||
|
||
```python
|
||
seed_column_metadata_batch(
|
||
cursor,
|
||
table_name="videos",
|
||
columns=(
|
||
{
|
||
"column_name": "video_url",
|
||
"display_name": "视频链接",
|
||
"editable": 1,
|
||
"display_type": "url",
|
||
"description": "填平台视频页链接;需包含可识别的视频路径片段。",
|
||
},
|
||
{
|
||
"column_name": "comment_limit",
|
||
"display_name": "采集评论条数",
|
||
"editable": 1,
|
||
"display_type": "number",
|
||
"description": "最多采多少条。填 0 = 不限制;填正整数则按该数量封顶。",
|
||
},
|
||
),
|
||
valid_column_names=physical,
|
||
)
|
||
```
|
||
|
||
Action 参数表单的 `title` / `description` / `default` / `display_type` 见 [`ACTIONS.md`](ACTIONS.md);勿与本节 CRUD 配置入口混用。
|
||
|
||
## 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` |
|
||
|
||
### 业务可编辑时间 vs 审计时间
|
||
|
||
| 场景 | `editable` | 列表 | 新增/编辑表单 |
|
||
|------|------------|------|----------------|
|
||
| `created_at` / `updated_at` 等审计列 | `0` | 格式化为 `YYYY-MM-DD HH:mm:ss` | 不出现(或只读,由宿主既有规则) |
|
||
| 业务时间列(预约、截止、活动开始等) | `1` | 同上格式化 | **日期时间控件**;提交写入 Unix **秒**整数 |
|
||
|
||
宿主对 `display_type = datetime_unix_seconds`:
|
||
|
||
- **列表**:格式化为 `YYYY-MM-DD HH:mm:ss` 展示,**不**改库内原始整数
|
||
- **可编辑表单**:提供日期时间选择;保存时写回 Unix **秒**(不要写 ISO 字符串、不要写毫秒)
|
||
|
||
### 自动生成与维护
|
||
|
||
- **created_at**:`INTEGER NOT NULL DEFAULT (unixepoch())`;插入时可省略,由数据库生成。
|
||
- **updated_at**:`INTEGER NOT NULL DEFAULT (unixepoch())`;插入时同样可省略。
|
||
- **updated_at 更新**:业务表通过 SQLite trigger `{table}_set_updated_at` 在业务字段更新后自动刷新(`init_updated_at_trigger`)。
|
||
|
||
实现参考:
|
||
|
||
- DDL 与 trigger:`scripts/db/connection.py`、`scripts/db/timestamp_columns.py`
|
||
- 中文元数据种子:`scripts/db/display_metadata.py`
|
||
- 自检:`scripts/db/display_metadata_validator.py`
|
||
|
||
### 系统自动写入表的只读约定
|
||
|
||
技能内部**系统自动写入**、不供用户在宿主数据管理中手工维护的表,应声明:
|
||
|
||
| 层级 | 值 | 说明 |
|
||
|------|-----|------|
|
||
| 表级 `readonly` | `1` | 宿主数据管理只读展示,不提供新增/编辑/删除表单 |
|
||
| 相关业务字段 `editable` | `0` | 元数据权限只约束宿主 UI,不限制技能代码写库 |
|
||
|
||
模板**默认不建**此类审计表。过程看 `jiangchang.log`,进度/终态看任务中心,业务结果看业务表。
|
||
|
||
表级 `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` 支持两种形态:
|
||
|
||
### 1)静态枚举(数组)
|
||
|
||
每项包含 `value` 与 `label`:
|
||
|
||
```json
|
||
[
|
||
{"value": "draft", "label": "草稿"},
|
||
{"value": "published", "label": "已发布"},
|
||
{"value": "archived", "label": "已归档"}
|
||
]
|
||
```
|
||
|
||
宿主据此渲染选择框;技能侧负责保证 JSON 合法且与物理字段取值一致。
|
||
|
||
### 2)跨表引用 lookup(对象,用于关联 id 字段)
|
||
|
||
当字段取值依赖同库另一张业务表时,`display_type` 设为 `lookup`,`options_json` 写对象(**不是**数组)。这是**展示与表单选择**契约,**不是** SQLite 外键。
|
||
|
||
**单选(默认,与既有行为一致):**
|
||
|
||
```json
|
||
{
|
||
"ref_table": "platforms",
|
||
"value_column": "platform_key",
|
||
"label_column": "display_name",
|
||
"extra_column": "category",
|
||
"filter_column": "enabled",
|
||
"filter_value": 1
|
||
}
|
||
```
|
||
|
||
**多选(平行扩展,仅多一个开关):**
|
||
|
||
```json
|
||
{
|
||
"ref_table": "engines",
|
||
"value_column": "id",
|
||
"label_column": "display_name",
|
||
"filter_column": "enabled",
|
||
"filter_value": 1,
|
||
"multiple": true
|
||
}
|
||
```
|
||
|
||
| 字段 | 必填 | 说明 |
|
||
|---|---|---|
|
||
| `ref_table` | 是 | 同库被引用表名(snake_case 标识符) |
|
||
| `value_column` | 是 | 写入当前字段的值列(业务 id / 编号) |
|
||
| `label_column` | 是 | 选项主文案列(名称) |
|
||
| `extra_column` | 否 | 第三展示列(不必叫「备注」);有值时拼进括号 |
|
||
| `filter_column` / `filter_value` | 否 | 等值过滤(如只列出 `enabled=1`) |
|
||
| `multiple` | 否 | `true` = 多选 Lookup;省略或 `false` = **单选**(默认) |
|
||
|
||
宿主在加载表 schema 时查询 `ref_table`,把结果展开成 `{value,label}` 选项列表。
|
||
|
||
**存库约定:**
|
||
|
||
| 模式 | 物理列建议 | 存什么 |
|
||
|------|------------|--------|
|
||
| 单选 | 整数或文本标量 | 一个业务 id(如 `2`、`douyin`) |
|
||
| 多选 | **`TEXT`** | 逗号分隔 id,如 `1,2,5`(与 `$selection.ids` 风格一致);**不要**建联接子表当 UI 多选 |
|
||
|
||
**统一展示文案(宿主固定一种,技能只配列):**
|
||
|
||
- 单选默认:`{value} · {label}`,例如 `2 · 通用私信-留资`、`douyin · 抖音`
|
||
- 配置了 `extra_column` 且该行有非空值:`{value} · {label}({extra})`
|
||
- 未配置 `extra_column`、或额外列为空:不加 `()`
|
||
- `label` 为空或与 `value` 相同时:只显示 `value`(可再带额外括号)
|
||
- **多选列表格**:按 id 拆开后逐个映射为 label,再用顿号/逗号拼接(如 `豆包、元宝`);过长可截断并提示「+N」
|
||
|
||
**控件呈现(宿主策略,非第二套契约):**
|
||
|
||
- 单选:下拉 Select(现状)
|
||
- 多选且选项较少(建议 ≤6):勾选列表
|
||
- 多选且选项较多:字段内 Chip + 可搜索下拉(**不是**再开一层业务弹窗)
|
||
|
||
Action 弹窗内联 Lookup 与上表 **同构**(字段名一致,含 `multiple`),见 [`ACTIONS.md`](ACTIONS.md)「跨表引用:Lookup」。
|
||
|
||
示例:账号表的平台字段 → 平台信息表;凭据表的账号编号 → 账号信息表;私信 Action 选话术 → `message_templates`;抽检 Action 多选引擎 → `engines` + `multiple: true`。
|
||
|
||
## 幂等初始化标准
|
||
|
||
- `_jiangchang_tables` 和 `_jiangchang_columns` 必须通过 upsert 初始化
|
||
- 多次执行初始化不能重复记录
|
||
- 重新初始化可以更新展示名、描述、`grid_help`、`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**——模板不会猜测真实物理字段,避免误删;文档与实现均以此为准。
|
||
|
||
不要直接修改用户真实数据库作为迁移方式;元数据初始化必须使用真实物理字段列表。
|
||
|
||
## 中文命名质量
|
||
|
||
- 使用普通用户能理解的业务用语:`error_msg` → **错误信息**,`created_at` → **创建时间**。
|
||
- 禁止:`Task Logs`、`input编号`、`errormsg`、直接把物理名当 `display_name`。
|
||
- 不宜向用户暴露的实现细节(如 `payload_json`、`selector`)应设 `visible = 0` 或给出可理解中文名。
|
||
|
||
## 元数据同步规则
|
||
|
||
1. 新增用户可见业务表 → 同步 `_jiangchang_tables`。
|
||
2. 新增用户可见字段 → 同步 `_jiangchang_columns`。
|
||
3. 删除/重命名表或字段 → 同步清理或迁移元数据。
|
||
4. 初始化脚本必须幂等。
|
||
5. 元数据中的表、字段必须在库中真实存在;可见项不得缺少中文 `display_name`。
|
||
|
||
## 开发自检
|
||
|
||
- `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 与种子 helper 以 `scripts/db/display_metadata.py` 为单一权威来源。
|
||
- 模板定义「如何声明能力」;具体技能定义「声明什么业务能力」;宿主负责「如何通用渲染和执行」。
|