# 数据存储与数据管理展示规范 本文是 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` - **数据管理侧栏/表列表**:当库中存在 `_jiangchang_tables` 时,**仅展示**在该表注册且 `visible=1` 的业务表;孤儿物理表(有表、无注册元数据)**不得**出现在侧栏,直接访问 schema API 应返回「表不存在」。极老库无 `_jiangchang_tables` 时,宿主可保留物理表枚举 fallback(兼容 `_schema_meta` 时代) - 根据 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` | 是 | 是否参与**列筛选**与**顶栏全局搜索**(`0` = 两者均不参与);详见下节「数据管理:列筛选与全局搜索」 | | `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 配置入口混用。 #### 数据管理:列筛选与全局搜索(宿主支持) 数据管理表格提供两类与 `searchable` 相关的检索能力,技能通过 `_jiangchang_columns.searchable` 声明是否参与: | 能力 | 用户入口 | `searchable = 1` 时 | `searchable = 0` 时 | |------|----------|---------------------|---------------------| | **列筛选** | 表格「筛选」→ 字段 / 操作符 / 值 | 出现在「字段」下拉,可按类型选值与操作符 | 不出现在列筛选字段列表 | | **全局搜索** | 顶栏搜索框 | 参与 OR 模糊匹配(宿主对参与列数有上限,如最多 8 列) | 不参与全局搜索 | **存库 vs 展示(核心契约)** - **列表**:有 `options_json` 时展示 **label**(如「启用」「停用」) - **筛选 / 查询 / 全局搜索**:比较与匹配 **value**(如 `0`/`1`、`active`、`toutiaohao`) - 宿主负责 label ↔ value 映射;**禁止**为迁就 UI 把存库改成中文枚举,也**禁止**在 `description` 里写「筛选请填 0/1」 **筛选栏控件 ≠ 新增/编辑表单(勿混用)** 列筛选「值」与表单使用同一套**类型语义**,但控件形态不同:筛选栏须与「字段」「操作符」同一行工具栏对齐(如统一 `h-9` 下拉),**枚举 / boolean / lookup 在筛选栏一律用下拉 Select**,不用新增/编辑表单里的开关或横向分段单选。表单侧仍按上表「新增/编辑控件」配置。 | 列类型 | 筛选「值」控件 | 常用操作符(宿主按类型裁剪) | 技能须配置 | |--------|----------------|------------------------------|------------| | 文本 / `url` / `email` | 文本框 | 等于 / 包含 / 开头 / 结尾 / 为空… | 通常 `searchable: 1` | | 数字 | 数字框 | 等于 / 大于 / 小于… | `display_type: number` 或 INT/REAL | | 枚举(`options_json` **数组**) | 下拉(展示 label) | 等于 / 不等于 / 为空 | **`options_json` 必填** + `searchable: 1` | | `boolean` | 下拉(有 `options_json` 用其 label;否则宿主默认可选「是/否」) | 等于 / 不等于 / 为空 | `display_type: boolean`;**推荐同时写 `options_json`** 指定中文 label | | `lookup` 单选 | 下拉 | 等于 / 不等于 / 为空 | `display_type: lookup` + lookup 对象 | | `lookup` + `multiple: true` | 列筛选 UI 演进中;**全局搜索**已支持 | 等于时按逗号分隔 id 的 token 匹配 | 多选存 `TEXT` 逗号分隔 id | | `date` / `datetime_unix_seconds` | 日期 / 日期时间 | 比较类 | 时间列通常 `searchable: 0` | | 敏感列 | **不可筛** | — | 宿主强制排除 | **全局搜索扩展(用户可搜中文)** 宿主在 `searchable = 1` 的列上除原文 `LIKE` 外,还应: - 有 `options_json` 枚举:**label 或 value** 均可命中 - `boolean` 无 options:认「是/否」等常见别名 - `lookup` 多选:按逗号分隔 id 的 **token 边界** 匹配;选项 label 亦可参与 - `datetime_unix_seconds`:搜 `YYYY-MM-DD` 形态可匹配当天 Unix 秒区间 **技能配置建议** 推荐: - 用户常筛的状态列(启用/停用、待生成/已生成、`status` 等)→ `searchable: 1` + `options_json`(value 与 DDL 存库一致) - `boolean` 业务列:`display_type: boolean` **且** `options_json` 写中文 label(示例见下) 禁止: - 仅为筛选把存库 value 改成中文 - 在文档或 `description` 要求用户记内部 magic number / 英文 code 示例(启用开关 + 中文筛选): ```python { "column_name": "enabled", "display_name": "是否启用", "visible": 1, "searchable": 1, "editable": 1, "display_type": "boolean", "options_json": json.dumps( [{"value": 0, "label": "停用"}, {"value": 1, "label": "启用"}], ensure_ascii=False, ), } ``` Action 参数表单**无**列筛选对称能力;筛选语义以本节为准,表单对照见 [`ACTIONS.md`](ACTIONS.md)「表单字段类型对照」。 ## 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`,进度/终态看任务中心,业务结果看业务表。 ### 业务行生命周期(处理队列) 若技能对某一业务行做「生成 / 触达 / 私信 / 发布」等有副作用处理,状态字段须遵守 **角色模型**(可操作 / 进行中 / 成功·失败·跳过类终态),**不要**把某一技能的状态名当成全平台唯一枚举。默认只处理可操作态;重做靠人工改回可操作。完整规则见 [`WORK_ITEM_STATUS.md`](WORK_ITEM_STATUS.md)。 建议: | 项 | 说明 | |----|------| | 状态列 | `options_json` 列出本技能全部业务状态(含进行中与各类终态) | | 失败原因 | 失败类终态应有用户可读的原因字段(勿写密钥) | | 可编辑 | 终态/进行中建议可改回可操作,或提供等价 Action | | `grid_help` | 可一句话说明「仅某某状态可处理」 | 表级 `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**——模板不会猜测真实物理字段,避免误删;文档与实现均以此为准。 不要直接修改用户真实数据库作为迁移方式;元数据初始化必须使用真实物理字段列表。 ## 表增删改与退役(技能 schema 迁移) 技能拥有 SQLite 物理 schema;宿主只渲染**已注册**的元数据。增删改表时技能须在 `init_db()` 中做幂等迁移,并与 `_jiangchang_*` 同步。 | 操作 | 技能必须做 | |------|------------| | **新增**用户可见表 | `CREATE TABLE` + `upsert_table_metadata` + 列元数据种子 | | **修改**表/列 | 物理迁移(`ALTER` / 重建)+ 更新或 `prune_stale_column_metadata` | | **临时隐藏**(表仍保留) | 元数据 `visible=0`(表与元数据行都保留) | | **永久废弃**用户可见表 | `drop_deprecated_user_table(...)`(见下) | ### 永久废弃用户可见表 使用 `scripts/db/display_metadata.py` 的 **`drop_deprecated_user_table`**(幂等): 1. `DROP TRIGGER`(若有 `{table}_set_updated_at` 等) 2. `DROP TABLE IF EXISTS` 3. `DELETE` 对应 `_jiangchang_columns` / `_jiangchang_tables` 行 ```python from db.display_metadata import drop_deprecated_user_table def init_db() -> None: ... drop_deprecated_user_table(cur, "task_logs") init_display_metadata(cur) ``` **反模式(禁止):** 只删 `_jiangchang_*` 元数据、保留物理表。宿主虽可能不展示孤儿表,但库内残留、自检失败,且违背「元数据与物理结构一致」。 **新技能禁止**创建或写入 `task_logs` 一类通用任务审计表。过程 → `jiangchang.log` / 运行日志;进度/终态 → 任务中心;业务结果 → 业务表(见 [`WORK_ITEM_STATUS.md`](WORK_ITEM_STATUS.md)、[`../development/LOGGING.md`](../development/LOGGING.md))。 ### 旧库迁移测试(废弃表时必做) 复制 `tests/samples/test_table_deprecation_migration.py.sample`,构造「旧库含废弃表 + 元数据 + 历史行」→ `init_db()` 后断言: - `sqlite_master` 无该表 - `_jiangchang_*` 无残留行 - `iter_user_tables` 不含该表 卸载技能**不删**用户数据目录;重装会复用旧库,迁移必须在 `init_db()` 中完成(见 [`../development/DATA_PATHS.md`](../development/DATA_PATHS.md))。 ## 中文命名质量 - 使用普通用户能理解的业务用语:`error_msg` → **错误信息**,`created_at` → **创建时间**。 - 禁止:`Task Logs`、`input编号`、`errormsg`、直接把物理名当 `display_name`。 - 不宜向用户暴露的实现细节(如 `payload_json`、`selector`)应设 `visible = 0` 或给出可理解中文名。 ## 元数据同步规则 1. 新增用户可见业务表 → 同步 `_jiangchang_tables`(`visible=1` 方可在数据管理展示)。 2. 新增用户可见字段 → 同步 `_jiangchang_columns`。 3. 删除/重命名表或字段 → 物理迁移 + 同步清理元数据;**废弃表**用 `drop_deprecated_user_table`,禁止只删元数据留物理表。 4. 初始化脚本必须幂等。 5. 元数据中的表、字段必须在库中真实存在;可见项不得缺少中文 `display_name`。 6. `init_db()` 后:`iter_user_tables` 中每个物理业务表都应有对应 `_jiangchang_tables.display_name`,或已被 `drop_deprecated_user_table` 物理删除。 ## 开发自检 - `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` 为单一权威来源。 - 模板定义「如何声明能力」;具体技能定义「声明什么业务能力」;宿主负责「如何通用渲染和执行」。