Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 28c3c8d8ab | |||
| 571c135114 | |||
| d840745758 | |||
| cf3f0ae990 |
27
CHANGELOG.md
27
CHANGELOG.md
@@ -9,10 +9,33 @@
|
|||||||
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
|
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
|
||||||
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
|
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
|
||||||
|
|
||||||
|
## 1.0.91
|
||||||
|
|
||||||
|
- 沉淀「表与表之间禁止 SQLite 外键」契约:只存业务 id,关联与级联在 repository/service;界面用 `lookup`
|
||||||
|
- `display_metadata_validator` 扫描业务表 DDL,含 `FOREIGN KEY` / `REFERENCES` 时报错
|
||||||
|
- 交活 Checklist 新增 A6-6;`lookup` 文档用语改为「跨表引用 id」,避免与 SQLite 外键混淆
|
||||||
|
|
||||||
|
## 1.0.90
|
||||||
|
|
||||||
|
- 沉淀数据管理表格说明契约:`_jiangchang_tables.grid_help` / `_jiangchang_columns.grid_help`
|
||||||
|
- 有内容才显示表上方说明和列头「?」;不写则与今日一致
|
||||||
|
- 与新增/编辑表单的 `description` 独立,禁止互拷(见 `references/SCHEMA.md`)
|
||||||
|
|
||||||
|
## 1.0.89
|
||||||
|
|
||||||
|
- 仓库增加 MIT 许可证,版权归属深圳匠厂科技有限公司
|
||||||
|
- 增加 SECURITY.md:未公开漏洞请通过匠厂官网联系,不要发在公开 Issue
|
||||||
|
- 模板 developer_ids 改为占位 12345,复制后请换成自己的匠厂用户 ID
|
||||||
|
|
||||||
|
## 1.0.88
|
||||||
|
|
||||||
|
- `release.ps1` 默认 tag 说明改为 UTF-8 字节写入,不再依赖脚本源码里的中文默认参数(Windows PowerShell 5.1 会把无 BOM 的 UTF-8 按系统代码页解析)
|
||||||
|
- 不改写已发布 tag(v1.0.86 / v1.0.87 说明仍可能乱码)
|
||||||
|
|
||||||
## 1.0.87
|
## 1.0.87
|
||||||
|
|
||||||
- `release.ps1` 打 annotated tag / 自动 commit 改为 UTF-8 消息文件 + `git -F`,避免 Windows PowerShell 按控制台代码页编码 `-m` 参数
|
- `release.ps1` 打 annotated tag / 自动 commit 改为 UTF-8 消息文件 + `git -F`,避免 `-m` 走控制台代码页
|
||||||
- 验收时 Gitea tag 说明应为「正式发布」,而不是「姝e紡鍙戝竷」;不改写已发布的历史 tag(含 v1.0.86)
|
- 默认说明仍可能乱码(见 1.0.88)
|
||||||
|
|
||||||
## 1.0.86
|
## 1.0.86
|
||||||
|
|
||||||
|
|||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 深圳匠厂科技有限公司
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
20
SECURITY.md
Normal file
20
SECURITY.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# 安全漏洞披露
|
||||||
|
|
||||||
|
本仓库是匠厂技能开发模板。请把**未公开的安全漏洞**发给深圳匠厂科技有限公司,不要在公开 Issue、PR 或讨论区里贴完整复现步骤、PoC 或利用细节。
|
||||||
|
|
||||||
|
## 怎么报告
|
||||||
|
|
||||||
|
请通过匠厂官网联系渠道提交:[https://www.jc2009.com/](https://www.jc2009.com/)
|
||||||
|
|
||||||
|
建议在联系时注明:
|
||||||
|
|
||||||
|
- 标题或开头带 `[Security]`
|
||||||
|
- 影响的仓库 / 版本(例如 `skill-template`、tag `v1.0.x`)
|
||||||
|
- 问题类型、影响范围、复现步骤
|
||||||
|
- 若已有修复思路,可一并说明
|
||||||
|
|
||||||
|
我们会确认收悉,并在有结论后告知是否修复、是否公开披露。
|
||||||
|
|
||||||
|
## 不在本页范围
|
||||||
|
|
||||||
|
本页只说明**如何向我们报告漏洞**。技能运行时的权限、RPA 合规、加密发布等产品安全设计,见 `development/` 与 `references/`,不要写进本文件。
|
||||||
5
SKILL.md
5
SKILL.md
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: 技能开发模板(通用业务版)
|
name: 技能开发模板(通用业务版)
|
||||||
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
|
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
|
||||||
version: 1.0.87
|
version: 1.0.91
|
||||||
author: 深圳匠厂科技有限公司
|
author: 深圳匠厂科技有限公司
|
||||||
metadata:
|
metadata:
|
||||||
openclaw:
|
openclaw:
|
||||||
@@ -10,8 +10,7 @@ metadata:
|
|||||||
emoji: "📦"
|
emoji: "📦"
|
||||||
category: "通用"
|
category: "通用"
|
||||||
developer_ids:
|
developer_ids:
|
||||||
- 10032
|
- 12345 # 占位,请换成你的匠厂用户 ID
|
||||||
- 12428
|
|
||||||
allowed-tools:
|
allowed-tools:
|
||||||
- bash
|
- bash
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.0",
|
"version": "1.0.91",
|
||||||
"skill": "your-skill-slug"
|
"skill": "your-skill-slug"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ checklist_version: 1.0
|
|||||||
- [ ] A6-3 **P1** 重复执行同一业务键时,结果符合宣称(更新/去重/追加),无莫名脏数据或静默丢数据
|
- [ ] A6-3 **P1** 重复执行同一业务键时,结果符合宣称(更新/去重/追加),无莫名脏数据或静默丢数据
|
||||||
- [ ] A6-4 **P1** 「零结果成功」与「失败」可区分
|
- [ ] A6-4 **P1** 「零结果成功」与「失败」可区分
|
||||||
- [ ] A6-5 **P1** 若产出文件:落在约定数据位置且可找到,不丢在不明临时路径
|
- [ ] A6-5 **P1** 若产出文件:落在约定数据位置且可找到,不丢在不明临时路径
|
||||||
|
- [ ] A6-6 **P0** 有业务表时:`sqlite_master` 中业务表 DDL **不得**含 SQLite `FOREIGN KEY` / `REFERENCES`;跨表只存业务 id,关联与级联在 repository/service;界面名称用 `lookup`(见 `references/SCHEMA.md`);可跑 `validate_template_database` / `init-db` 后自检
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -527,6 +527,7 @@ assets/actions.json → placements / bind / executionProfile / entry
|
|||||||
|
|
||||||
- `db/connection.py`
|
- `db/connection.py`
|
||||||
只做连接和建表(模板默认只建 `_jiangchang_*` 元数据表;业务表按需 CREATE)
|
只做连接和建表(模板默认只建 `_jiangchang_*` 元数据表;业务表按需 CREATE)
|
||||||
|
**业务表之间禁止 SQLite `FOREIGN KEY` / `REFERENCES`**:只存业务 id,存在性 / 级联 / 删除在 repository/service 处理(见 [`references/SCHEMA.md`](../references/SCHEMA.md)「表与表之间:禁止 SQLite 外键」)
|
||||||
|
|
||||||
- `db/<domain>_repository.py`
|
- `db/<domain>_repository.py`
|
||||||
只做业务表增删查改(有业务表才建;模板不再默认提供任务审计表)
|
只做业务表增删查改(有业务表才建;模板不再默认提供任务审计表)
|
||||||
|
|||||||
@@ -622,7 +622,7 @@ HINT: <next step>
|
|||||||
| 字段 | 必填 | 说明 |
|
| 字段 | 必填 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `ref_table` | 是 | 同库被引用表 |
|
| `ref_table` | 是 | 同库被引用表 |
|
||||||
| `value_column` | 是 | 提交给 CLI 的值(编号 / 外键) |
|
| `value_column` | 是 | 提交给 CLI 的值(业务 id / 编号) |
|
||||||
| `label_column` | 是 | 主文案列 |
|
| `label_column` | 是 | 主文案列 |
|
||||||
| `extra_column` | 否 | 第三展示列(任意列名,不限「备注」) |
|
| `extra_column` | 否 | 第三展示列(任意列名,不限「备注」) |
|
||||||
| `filter_column` / `filter_value` | 否 | 等值过滤 |
|
| `filter_column` / `filter_value` | 否 | 等值过滤 |
|
||||||
@@ -658,6 +658,7 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件
|
|||||||
| URL(薄层) | `display_type` 或 `format`: `url` | `display_type: url` |
|
| URL(薄层) | `display_type` 或 `format`: `url` | `display_type: url` |
|
||||||
| Email(薄层) | `display_type` 或 `format`: `email` | `display_type: email` |
|
| Email(薄层) | `display_type` 或 `format`: `email` | `display_type: email` |
|
||||||
| 说明灰字 | `description` | `_jiangchang_columns.description` |
|
| 说明灰字 | `description` | `_jiangchang_columns.description` |
|
||||||
|
| 表格横幅 / 列头「?」 | (无;Action 不用) | `_jiangchang_tables.grid_help` / `_jiangchang_columns.grid_help` |
|
||||||
| 默认值 | `default` | SQLite 列 `DEFAULT` |
|
| 默认值 | `default` | SQLite 列 `DEFAULT` |
|
||||||
| 只读 | `readOnly: true` | 列 `editable: 0` / 宿主只读规则 |
|
| 只读 | `readOnly: true` | 列 `editable: 0` / 宿主只读规则 |
|
||||||
| **条件显隐** | **`visibleWhen`**(见下节) | **本版 CRUD 无对称**;仅 Action / 定时任务参数表单 |
|
| **条件显隐** | **`visibleWhen`**(见下节) | **本版 CRUD 无对称**;仅 Action / 定时任务参数表单 |
|
||||||
@@ -727,6 +728,8 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件
|
|||||||
| Skill Action 弹窗 | `inputSchema.title` | `inputSchema.description` | `inputSchema.default` | `type` + `display_type`/`format` |
|
| Skill Action 弹窗 | `inputSchema.title` | `inputSchema.description` | `inputSchema.default` | `type` + `display_type`/`format` |
|
||||||
| 数据管理新增/编辑 | `_jiangchang_columns.display_name` | `_jiangchang_columns.description` | 物理列 SQLite `DEFAULT` | `display_type` + 物理类型推断 |
|
| 数据管理新增/编辑 | `_jiangchang_columns.display_name` | `_jiangchang_columns.description` | 物理列 SQLite `DEFAULT` | `display_type` + 物理类型推断 |
|
||||||
|
|
||||||
|
数据管理**表格**上方说明与列头「?」用 `_jiangchang_tables.grid_help` / `_jiangchang_columns.grid_help`(空则不渲染),**不要**用 `description`。见 [`SCHEMA.md`](SCHEMA.md)「表格说明 `grid_help`」。
|
||||||
|
|
||||||
CRUD 侧宿主义务与禁止事项见 [`SCHEMA.md`](SCHEMA.md)「表单与展示:稳定字段类型」「新增/编辑表单:字段说明与默认值」。同一业务含义两边文案与控件应对齐,但分别配置。
|
CRUD 侧宿主义务与禁止事项见 [`SCHEMA.md`](SCHEMA.md)「表单与展示:稳定字段类型」「新增/编辑表单:字段说明与默认值」。同一业务含义两边文案与控件应对齐,但分别配置。
|
||||||
|
|
||||||
示例:
|
示例:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
- 定义真实 SQLite 表结构
|
- 定义真实 SQLite 表结构
|
||||||
- 使用英文 `snake_case` 表名和字段名
|
- 使用英文 `snake_case` 表名和字段名
|
||||||
- 使用 `_jiangchang_tables` 提供表级展示元数据
|
- 使用 `_jiangchang_tables` 提供表级展示元数据
|
||||||
- 使用 `_jiangchang_columns` 提供字段级展示元数据(含可选 `description` 字段说明)
|
- 使用 `_jiangchang_columns` 提供字段级展示元数据(含可选 `description` 表单说明、可选 `grid_help` 表格说明)
|
||||||
- 需要「新增表单预填」时,在物理列上声明 SQLite `DEFAULT`(勿把默认值只写在文案里)
|
- 需要「新增表单预填」时,在物理列上声明 SQLite `DEFAULT`(勿把默认值只写在文案里)
|
||||||
- 定义哪些字段可见、可搜索、可创建、可编辑
|
- 定义哪些字段可见、可搜索、可创建、可编辑
|
||||||
- 定义字段展示类型和枚举选项
|
- 定义字段展示类型和枚举选项
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
- 根据 schema 渲染数据管理页面
|
- 根据 schema 渲染数据管理页面
|
||||||
- 根据 `editable` 推导字段是否可创建或编辑
|
- 根据 `editable` 推导字段是否可创建或编辑
|
||||||
- 在**新增 / 编辑**表单中,若列有 `_jiangchang_columns.description`,在标签下方展示该说明(灰字;空则不显示)
|
- 在**新增 / 编辑**表单中,若列有 `_jiangchang_columns.description`,在标签下方展示该说明(灰字;空则不显示)
|
||||||
|
- 在数据管理**表格**中,若表有 `_jiangchang_tables.grid_help` 则在表上方展示可折叠说明;若列有 `_jiangchang_columns.grid_help` 则在列头显示「?」;空/未写则不渲染。**不**把 `description` 当作表格说明
|
||||||
- 打开**新增**表单时,对可创建字段用 SQLite 列 `DEFAULT`(`PRAGMA table_info.dflt_value`)预填;无 DEFAULT 则留空
|
- 打开**新增**表单时,对可创建字段用 SQLite 列 `DEFAULT`(`PRAGMA table_info.dflt_value`)预填;无 DEFAULT 则留空
|
||||||
- 执行通用查询、新增、编辑、删除、导入、导出
|
- 执行通用查询、新增、编辑、删除、导入、导出
|
||||||
- 对字段名、表名、类型和权限进行最终校验
|
- 对字段名、表名、类型和权限进行最终校验
|
||||||
@@ -88,6 +89,23 @@ metadata:
|
|||||||
|
|
||||||
若创建/更新/删除带有领域副作用(自动建目录、加密、级联清理),不要依赖宿主默认直写:在 `assets/actions.json` 用可选字段 `replaces: create|update|delete` 接管默认按钮,详见 [`ACTIONS.md`](ACTIONS.md)。
|
若创建/更新/删除带有领域副作用(自动建目录、加密、级联清理),不要依赖宿主默认直写:在 `assets/actions.json` 用可选字段 `replaces: create|update|delete` 接管默认按钮,详见 [`ACTIONS.md`](ACTIONS.md)。
|
||||||
|
|
||||||
|
## 表与表之间:禁止 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」类能力。使用通用名称表述,勿把具体平台表名写进模板实现。
|
适用于「同步 / refresh / collect」类能力。使用通用名称表述,勿把具体平台表名写进模板实现。
|
||||||
@@ -138,7 +156,8 @@ metadata:
|
|||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `table_name` | 是 | 真实英文表名 |
|
| `table_name` | 是 | 真实英文表名 |
|
||||||
| `display_name` | 是 | 用户可见的中文业务名称 |
|
| `display_name` | 是 | 用户可见的中文业务名称 |
|
||||||
| `description` | 否 | 表说明 |
|
| `description` | 否 | 表说明(表单/备注用途);**不**用于表格上方横幅 |
|
||||||
|
| `grid_help` | 否 | 数据管理表格上方说明;空/未写则不出横幅。与 `description` 独立,禁止互拷(见下节) |
|
||||||
| `sort_order` | 否 | 兼容保留字段,不用于替代物理字段顺序 |
|
| `sort_order` | 否 | 兼容保留字段,不用于替代物理字段顺序 |
|
||||||
| `visible` | 是 | `1` 在宿主数据管理中展示 / `0` 隐藏 |
|
| `visible` | 是 | `1` 在宿主数据管理中展示 / `0` 隐藏 |
|
||||||
| `readonly` | 是 | `1` 整张表只读;`0` 允许宿主根据字段权限执行写操作 |
|
| `readonly` | 是 | `1` 整张表只读;`0` 允许宿主根据字段权限执行写操作 |
|
||||||
@@ -212,6 +231,43 @@ upsert_table_metadata(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 表格说明 `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,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
禁止:
|
禁止:
|
||||||
|
|
||||||
- 使用中文真实表名
|
- 使用中文真实表名
|
||||||
@@ -228,6 +284,7 @@ upsert_table_metadata(
|
|||||||
| `column_name` | 是 | 真实英文字段名 |
|
| `column_name` | 是 | 真实英文字段名 |
|
||||||
| `display_name` | 是 | 用户可见的中文字段名(表头与表单标签);保持简短 |
|
| `display_name` | 是 | 用户可见的中文字段名(表头与表单标签);保持简短 |
|
||||||
| `description` | 否 | 字段说明(给人看的短文案);**宿主在新增/编辑表单标签下方展示**(见下节);不要塞进 `display_name` |
|
| `description` | 否 | 字段说明(给人看的短文案);**宿主在新增/编辑表单标签下方展示**(见下节);不要塞进 `display_name` |
|
||||||
|
| `grid_help` | 否 | 数据管理表头「?」;空/未写则不出。与表单 `description` 独立,禁止互拷 |
|
||||||
| `display_order` | 否 | 兼容字段,默认保持 `NULL` |
|
| `display_order` | 否 | 兼容字段,默认保持 `NULL` |
|
||||||
| `visible` | 是 | 是否进入数据管理 schema /「列」面板(`0` = **硬隐藏**,用户无法勾回;默认不展示但可勾回请用表级 `default_visible_columns_json`) |
|
| `visible` | 是 | 是否进入数据管理 schema /「列」面板(`0` = **硬隐藏**,用户无法勾回;默认不展示但可勾回请用表级 `default_visible_columns_json`) |
|
||||||
| `searchable` | 是 | 是否参与搜索 |
|
| `searchable` | 是 | 是否参与搜索 |
|
||||||
@@ -251,7 +308,7 @@ upsert_table_metadata(
|
|||||||
| `number` | `INTEGER` / `REAL` / `NUMERIC` | 原文 | **数字输入**(`type=number`) | 数字;无 `display_type` 时,物理类型为 INT/REAL/NUMERIC 也应走数字控件 |
|
| `number` | `INTEGER` / `REAL` / `NUMERIC` | 原文 | **数字输入**(`type=number`) | 数字;无 `display_type` 时,物理类型为 INT/REAL/NUMERIC 也应走数字控件 |
|
||||||
| `boolean` | `INTEGER` 0/1 或 BOOL | 开关语义展示 | 开关 | `0` / `1` |
|
| `boolean` | `INTEGER` 0/1 或 BOOL | 开关语义展示 | 开关 | `0` / `1` |
|
||||||
| (枚举) | 任意 + `options_json` **数组** | 标签映射 | 下拉选择 | 选项 `value` |
|
| (枚举) | 任意 + `options_json` **数组** | 标签映射 | 下拉选择 | 选项 `value` |
|
||||||
| `lookup` | 外键式标量 + `options_json` **对象** | 标签映射 | 下拉选择(查关联表) | 关联键值 |
|
| `lookup` | 跨表引用 id 标量 + `options_json` **对象** | 标签映射 | 下拉选择(查关联表) | 业务 id(非 SQLite 外键) |
|
||||||
| (敏感) | 任意;列 `sensitive=1` | 掩码/隐藏策略由宿主定 | 新建可用密码框;编辑策略与既有宿主一致 | 原文(勿日志明文) |
|
| (敏感) | 任意;列 `sensitive=1` | 掩码/隐藏策略由宿主定 | 新建可用密码框;编辑策略与既有宿主一致 | 原文(勿日志明文) |
|
||||||
| `date` | `TEXT` 推荐 `YYYY-MM-DD`,或可安全归一的值 | 日期 | **日期选择**(日) | `YYYY-MM-DD` |
|
| `date` | `TEXT` 推荐 `YYYY-MM-DD`,或可安全归一的值 | 日期 | **日期选择**(日) | `YYYY-MM-DD` |
|
||||||
| `datetime_unix_seconds` | `INTEGER` | `YYYY-MM-DD HH:mm:ss` | **日期时间选择**;提交写回 **Unix 秒** | Unix **秒**整数;禁止改成 ISO 字符串 |
|
| `datetime_unix_seconds` | `INTEGER` | `YYYY-MM-DD HH:mm:ss` | **日期时间选择**;提交写回 **Unix 秒** | Unix **秒**整数;禁止改成 ISO 字符串 |
|
||||||
@@ -277,7 +334,9 @@ upsert_table_metadata(
|
|||||||
| 字段说明 | `_jiangchang_columns.description`(可选) | 新增/编辑表单中,在标签下方以灰字展示;空则不显示 |
|
| 字段说明 | `_jiangchang_columns.description`(可选) | 新增/编辑表单中,在标签下方以灰字展示;空则不显示 |
|
||||||
| 创建默认值 | 物理列 SQLite `DEFAULT`(如 `limit_n INTEGER NOT NULL DEFAULT 0`) | 打开「新增」时,对可创建字段预填该默认值;无 DEFAULT 则留空 |
|
| 创建默认值 | 物理列 SQLite `DEFAULT`(如 `limit_n INTEGER NOT NULL DEFAULT 0`) | 打开「新增」时,对可创建字段预填该默认值;无 DEFAULT 则留空 |
|
||||||
| 编辑已有行 | — | **不**用 DEFAULT 覆盖已有行数据 |
|
| 编辑已有行 | — | **不**用 DEFAULT 覆盖已有行数据 |
|
||||||
| 表格列头 | 只用 `display_name` | 长说明**不**进入列头 |
|
| 表格列头标题 | `display_name` | 短名;不要把长说明塞进列头文字 |
|
||||||
|
| 表格列头「?」 | `_jiangchang_columns.grid_help`(可选) | 有内容才显示「?」;**不**回退 `description` |
|
||||||
|
| 表格上方说明 | `_jiangchang_tables.grid_help`(可选) | 有内容才显示横幅;**不**回退表级 `description` |
|
||||||
|
|
||||||
规则与禁止:
|
规则与禁止:
|
||||||
|
|
||||||
@@ -512,9 +571,9 @@ CREATE TABLE demo_items (
|
|||||||
|
|
||||||
宿主据此渲染选择框;技能侧负责保证 JSON 合法且与物理字段取值一致。
|
宿主据此渲染选择框;技能侧负责保证 JSON 合法且与物理字段取值一致。
|
||||||
|
|
||||||
### 2)表关联 lookup(对象,推荐用于外键式字段)
|
### 2)跨表引用 lookup(对象,用于关联 id 字段)
|
||||||
|
|
||||||
当字段取值依赖同库另一张业务表时,`display_type` 设为 `lookup`,`options_json` 写对象(**不是**数组):
|
当字段取值依赖同库另一张业务表时,`display_type` 设为 `lookup`,`options_json` 写对象(**不是**数组)。这是**展示与表单下拉**契约,**不是** SQLite 外键:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -530,7 +589,7 @@ CREATE TABLE demo_items (
|
|||||||
| 字段 | 必填 | 说明 |
|
| 字段 | 必填 | 说明 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `ref_table` | 是 | 同库被引用表名(snake_case 标识符) |
|
| `ref_table` | 是 | 同库被引用表名(snake_case 标识符) |
|
||||||
| `value_column` | 是 | 写入当前字段的值列(编号 / 外键) |
|
| `value_column` | 是 | 写入当前字段的值列(业务 id / 编号) |
|
||||||
| `label_column` | 是 | 下拉主文案列(名称) |
|
| `label_column` | 是 | 下拉主文案列(名称) |
|
||||||
| `extra_column` | 否 | 第三展示列(不必叫「备注」);有值时拼进括号 |
|
| `extra_column` | 否 | 第三展示列(不必叫「备注」);有值时拼进括号 |
|
||||||
| `filter_column` / `filter_value` | 否 | 等值过滤(如只列出 `enabled=1`) |
|
| `filter_column` / `filter_value` | 否 | 等值过滤(如只列出 `enabled=1`) |
|
||||||
@@ -552,7 +611,7 @@ Action 弹窗内联 Lookup 与上表 **同构**(字段名一致),见 [`ACT
|
|||||||
|
|
||||||
- `_jiangchang_tables` 和 `_jiangchang_columns` 必须通过 upsert 初始化
|
- `_jiangchang_tables` 和 `_jiangchang_columns` 必须通过 upsert 初始化
|
||||||
- 多次执行初始化不能重复记录
|
- 多次执行初始化不能重复记录
|
||||||
- 重新初始化可以更新展示名、描述、`visible`、`searchable`、`editable`、`display_type`、`options_json`
|
- 重新初始化可以更新展示名、描述、`grid_help`、`visible`、`searchable`、`editable`、`display_type`、`options_json`
|
||||||
- 元数据不能残留旧字段(删除或重命名字段时必须同步清理)
|
- 元数据不能残留旧字段(删除或重命名字段时必须同步清理)
|
||||||
- 元数据不能引用不存在的表或字段
|
- 元数据不能引用不存在的表或字段
|
||||||
- 新增字段或表时必须同步增加元数据
|
- 新增字段或表时必须同步增加元数据
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string]$Prefix = "v",
|
[string]$Prefix = "v",
|
||||||
[string]$Message = "正式发布",
|
[string]$Message,
|
||||||
[switch]$AutoCommit,
|
[switch]$AutoCommit,
|
||||||
[switch]$RequireClean,
|
[switch]$RequireClean,
|
||||||
[string]$CommitMessage,
|
[string]$CommitMessage,
|
||||||
@@ -28,6 +28,13 @@ param(
|
|||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# Default tag message is 「正式发布」 from UTF-8 bytes (not a Chinese literal in
|
||||||
|
# this file). Windows PowerShell 5.1 parses UTF-8 scripts without BOM as the
|
||||||
|
# system ANSI code page, which would turn a source literal into 「姝e紡鍙戝竷」.
|
||||||
|
if (-not $PSBoundParameters.ContainsKey("Message") -or [string]::IsNullOrWhiteSpace($Message)) {
|
||||||
|
$Message = [System.Text.Encoding]::UTF8.GetString([byte[]](0xE6, 0xAD, 0xA3, 0xE5, 0xBC, 0x8F, 0xE5, 0x8F, 0x91, 0xE5, 0xB8, 0x83))
|
||||||
|
}
|
||||||
|
|
||||||
function Remove-GitNoise {
|
function Remove-GitNoise {
|
||||||
param([string[]]$Lines)
|
param([string[]]$Lines)
|
||||||
return @($Lines | Where-Object {
|
return @($Lines | Where-Object {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS _jiangchang_tables (
|
|||||||
table_name TEXT PRIMARY KEY,
|
table_name TEXT PRIMARY KEY,
|
||||||
display_name TEXT NOT NULL,
|
display_name TEXT NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
|
grid_help TEXT,
|
||||||
sort_order INTEGER,
|
sort_order INTEGER,
|
||||||
visible INTEGER NOT NULL DEFAULT 1,
|
visible INTEGER NOT NULL DEFAULT 1,
|
||||||
readonly INTEGER NOT NULL DEFAULT 0,
|
readonly INTEGER NOT NULL DEFAULT 0,
|
||||||
@@ -35,6 +36,7 @@ CREATE TABLE IF NOT EXISTS _jiangchang_columns (
|
|||||||
column_name TEXT NOT NULL,
|
column_name TEXT NOT NULL,
|
||||||
display_name TEXT NOT NULL,
|
display_name TEXT NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
|
grid_help TEXT,
|
||||||
display_order INTEGER,
|
display_order INTEGER,
|
||||||
visible INTEGER NOT NULL DEFAULT 1,
|
visible INTEGER NOT NULL DEFAULT 1,
|
||||||
searchable INTEGER NOT NULL DEFAULT 1,
|
searchable INTEGER NOT NULL DEFAULT 1,
|
||||||
@@ -45,22 +47,35 @@ CREATE TABLE IF NOT EXISTS _jiangchang_columns (
|
|||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def _pragma_column_names(cursor, table_name: str) -> set[str]:
|
||||||
|
cursor.execute(f"PRAGMA table_info({table_name!r})")
|
||||||
|
return {str(row[1]) for row in cursor.fetchall()}
|
||||||
|
|
||||||
|
|
||||||
def _ensure_jiangchang_tables_columns(cursor) -> None:
|
def _ensure_jiangchang_tables_columns(cursor) -> None:
|
||||||
"""旧库补列:CREATE IF NOT EXISTS 不会给已有表加新字段。"""
|
"""旧库补列:CREATE IF NOT EXISTS 不会给已有表加新字段。"""
|
||||||
cursor.execute("PRAGMA table_info('_jiangchang_tables')")
|
cols = _pragma_column_names(cursor, "_jiangchang_tables")
|
||||||
cols = {str(row[1]) for row in cursor.fetchall()}
|
|
||||||
if "default_sort_json" not in cols:
|
if "default_sort_json" not in cols:
|
||||||
cursor.execute("ALTER TABLE _jiangchang_tables ADD COLUMN default_sort_json TEXT")
|
cursor.execute("ALTER TABLE _jiangchang_tables ADD COLUMN default_sort_json TEXT")
|
||||||
if "default_visible_columns_json" not in cols:
|
if "default_visible_columns_json" not in cols:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"ALTER TABLE _jiangchang_tables ADD COLUMN default_visible_columns_json TEXT"
|
"ALTER TABLE _jiangchang_tables ADD COLUMN default_visible_columns_json TEXT"
|
||||||
)
|
)
|
||||||
|
if "grid_help" not in cols:
|
||||||
|
cursor.execute("ALTER TABLE _jiangchang_tables ADD COLUMN grid_help TEXT")
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_jiangchang_columns_columns(cursor) -> None:
|
||||||
|
cols = _pragma_column_names(cursor, "_jiangchang_columns")
|
||||||
|
if "grid_help" not in cols:
|
||||||
|
cursor.execute("ALTER TABLE _jiangchang_columns ADD COLUMN grid_help TEXT")
|
||||||
|
|
||||||
|
|
||||||
def init_display_metadata_tables(cursor) -> None:
|
def init_display_metadata_tables(cursor) -> None:
|
||||||
cursor.execute(_JIANGCHANG_TABLES_DDL)
|
cursor.execute(_JIANGCHANG_TABLES_DDL)
|
||||||
cursor.execute(_JIANGCHANG_COLUMNS_DDL)
|
cursor.execute(_JIANGCHANG_COLUMNS_DDL)
|
||||||
_ensure_jiangchang_tables_columns(cursor)
|
_ensure_jiangchang_tables_columns(cursor)
|
||||||
|
_ensure_jiangchang_columns_columns(cursor)
|
||||||
|
|
||||||
|
|
||||||
def upsert_table_metadata(
|
def upsert_table_metadata(
|
||||||
@@ -69,6 +84,7 @@ def upsert_table_metadata(
|
|||||||
table_name: str,
|
table_name: str,
|
||||||
display_name: str,
|
display_name: str,
|
||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
|
grid_help: str | None = None,
|
||||||
visible: int = 1,
|
visible: int = 1,
|
||||||
readonly: int = 0,
|
readonly: int = 0,
|
||||||
default_sort_json: str | None = None,
|
default_sort_json: str | None = None,
|
||||||
@@ -83,17 +99,20 @@ def upsert_table_metadata(
|
|||||||
default_visible_columns_json: 宿主数据管理默认展示列,JSON 字符串数组,例如
|
default_visible_columns_json: 宿主数据管理默认展示列,JSON 字符串数组,例如
|
||||||
'["stat_type","period","stage"]'
|
'["stat_type","period","stage"]'
|
||||||
为空则全显;用户改过「列」后以本机偏好为准(软默认,区别于 visible=0 硬隐藏)。
|
为空则全显;用户改过「列」后以本机偏好为准(软默认,区别于 visible=0 硬隐藏)。
|
||||||
|
|
||||||
|
grid_help: 数据管理表格上方说明;空则不出横幅。与表单用的 description 独立,禁止互拷。
|
||||||
"""
|
"""
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO _jiangchang_tables (
|
INSERT INTO _jiangchang_tables (
|
||||||
table_name, display_name, description, visible, readonly,
|
table_name, display_name, description, grid_help, visible, readonly,
|
||||||
default_sort_json, default_visible_columns_json
|
default_sort_json, default_visible_columns_json
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(table_name) DO UPDATE SET
|
ON CONFLICT(table_name) DO UPDATE SET
|
||||||
display_name = excluded.display_name,
|
display_name = excluded.display_name,
|
||||||
description = excluded.description,
|
description = excluded.description,
|
||||||
|
grid_help = excluded.grid_help,
|
||||||
visible = excluded.visible,
|
visible = excluded.visible,
|
||||||
readonly = excluded.readonly,
|
readonly = excluded.readonly,
|
||||||
default_sort_json = excluded.default_sort_json,
|
default_sort_json = excluded.default_sort_json,
|
||||||
@@ -103,6 +122,7 @@ def upsert_table_metadata(
|
|||||||
table_name,
|
table_name,
|
||||||
display_name,
|
display_name,
|
||||||
description,
|
description,
|
||||||
|
grid_help,
|
||||||
visible,
|
visible,
|
||||||
readonly,
|
readonly,
|
||||||
default_sort_json,
|
default_sort_json,
|
||||||
@@ -118,6 +138,7 @@ def upsert_column_metadata(
|
|||||||
column_name: str,
|
column_name: str,
|
||||||
display_name: str,
|
display_name: str,
|
||||||
description: str | None = None,
|
description: str | None = None,
|
||||||
|
grid_help: str | None = None,
|
||||||
visible: int = 1,
|
visible: int = 1,
|
||||||
searchable: int = 1,
|
searchable: int = 1,
|
||||||
editable: int = 1,
|
editable: int = 1,
|
||||||
@@ -125,16 +146,18 @@ def upsert_column_metadata(
|
|||||||
options_json: str | None = None,
|
options_json: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# display_order 保留为 NULL:宿主按 PRAGMA table_info cid 展示字段,不读 display_order。
|
# display_order 保留为 NULL:宿主按 PRAGMA table_info cid 展示字段,不读 display_order。
|
||||||
|
# grid_help: 表头「?」;空则不出。与表单用的 description 独立,禁止互拷。
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO _jiangchang_columns (
|
INSERT INTO _jiangchang_columns (
|
||||||
table_name, column_name, display_name, description,
|
table_name, column_name, display_name, description, grid_help,
|
||||||
display_order, visible, searchable, editable, display_type, options_json
|
display_order, visible, searchable, editable, display_type, options_json
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(table_name, column_name) DO UPDATE SET
|
ON CONFLICT(table_name, column_name) DO UPDATE SET
|
||||||
display_name = excluded.display_name,
|
display_name = excluded.display_name,
|
||||||
description = excluded.description,
|
description = excluded.description,
|
||||||
|
grid_help = excluded.grid_help,
|
||||||
display_order = NULL,
|
display_order = NULL,
|
||||||
visible = excluded.visible,
|
visible = excluded.visible,
|
||||||
searchable = excluded.searchable,
|
searchable = excluded.searchable,
|
||||||
@@ -147,6 +170,7 @@ def upsert_column_metadata(
|
|||||||
column_name,
|
column_name,
|
||||||
display_name,
|
display_name,
|
||||||
description,
|
description,
|
||||||
|
grid_help,
|
||||||
visible,
|
visible,
|
||||||
searchable,
|
searchable,
|
||||||
editable,
|
editable,
|
||||||
@@ -171,6 +195,7 @@ def seed_column_metadata_batch(
|
|||||||
column_name=str(column["column_name"]),
|
column_name=str(column["column_name"]),
|
||||||
display_name=str(column["display_name"]),
|
display_name=str(column["display_name"]),
|
||||||
description=str(column["description"]) if column.get("description") else None,
|
description=str(column["description"]) if column.get("description") else None,
|
||||||
|
grid_help=str(column["grid_help"]) if column.get("grid_help") else None,
|
||||||
visible=int(column.get("visible", 1)),
|
visible=int(column.get("visible", 1)),
|
||||||
searchable=int(column.get("searchable", 1)),
|
searchable=int(column.get("searchable", 1)),
|
||||||
editable=int(column.get("editable", 1)),
|
editable=int(column.get("editable", 1)),
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ _ENGLISH_PLACEHOLDER_NAME_PATTERNS = (
|
|||||||
)
|
)
|
||||||
_KEBAB_SLUG = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
_KEBAB_SLUG = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
||||||
_CJK_RE = re.compile(r"[\u4e00-\u9fff]")
|
_CJK_RE = re.compile(r"[\u4e00-\u9fff]")
|
||||||
|
_FK_DDL_PATTERN = re.compile(r"\bFOREIGN\s+KEY\b|\bREFERENCES\b", re.I)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -412,6 +413,27 @@ def _table_exists(cursor, table_name: str) -> bool:
|
|||||||
return cursor.fetchone() is not None
|
return cursor.fetchone() is not None
|
||||||
|
|
||||||
|
|
||||||
|
def validate_no_sqlite_foreign_keys(cursor) -> tuple[list[str], list[str]]:
|
||||||
|
"""业务表 DDL 不得含 FOREIGN KEY / REFERENCES(关联走 repository/service)。"""
|
||||||
|
errors: list[str] = []
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
SELECT name, sql FROM sqlite_master
|
||||||
|
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
for table_name, sql in cursor.fetchall():
|
||||||
|
if table_name in METADATA_TABLES:
|
||||||
|
continue
|
||||||
|
ddl = str(sql or "")
|
||||||
|
if ddl and _FK_DDL_PATTERN.search(ddl):
|
||||||
|
errors.append(
|
||||||
|
f"业务表 {table_name!r} 的 DDL 含 SQLite FOREIGN KEY/REFERENCES;"
|
||||||
|
"请改为业务 id 列 + repository/service 逻辑关联(见 references/SCHEMA.md)"
|
||||||
|
)
|
||||||
|
return errors, []
|
||||||
|
|
||||||
|
|
||||||
def validate_display_metadata(conn: sqlite3.Connection, *, check_column_order: bool = True) -> ValidationReport:
|
def validate_display_metadata(conn: sqlite3.Connection, *, check_column_order: bool = True) -> ValidationReport:
|
||||||
report = ValidationReport()
|
report = ValidationReport()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
@@ -425,6 +447,9 @@ def validate_display_metadata(conn: sqlite3.Connection, *, check_column_order: b
|
|||||||
if not has_tables_meta or not has_columns_meta:
|
if not has_tables_meta or not has_columns_meta:
|
||||||
return report
|
return report
|
||||||
|
|
||||||
|
fk_errors, _ = validate_no_sqlite_foreign_keys(cur)
|
||||||
|
report.errors.extend(fk_errors)
|
||||||
|
|
||||||
for table_name in iter_user_tables(cur):
|
for table_name in iter_user_tables(cur):
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"SELECT display_name FROM _jiangchang_tables WHERE table_name = ?",
|
"SELECT display_name FROM _jiangchang_tables WHERE table_name = ?",
|
||||||
@@ -514,5 +539,5 @@ def validate_display_metadata(conn: sqlite3.Connection, *, check_column_order: b
|
|||||||
|
|
||||||
|
|
||||||
def validate_template_database(conn: sqlite3.Connection) -> ValidationReport:
|
def validate_template_database(conn: sqlite3.Connection) -> ValidationReport:
|
||||||
"""模板自检入口:校验展示元数据完整性。模板默认无业务表。"""
|
"""模板自检入口:校验展示元数据完整性,并扫描业务表 DDL 是否含 SQLite 外键。"""
|
||||||
return validate_display_metadata(conn)
|
return validate_display_metadata(conn)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。"""
|
"""技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。"""
|
||||||
|
|
||||||
SKILL_SLUG = "your-skill-slug"
|
SKILL_SLUG = "your-skill-slug"
|
||||||
SKILL_VERSION = "1.0.87"
|
SKILL_VERSION = "1.0.91"
|
||||||
LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug"
|
LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug"
|
||||||
PLATFORM_KIT_MIN_VERSION = "1.2.11"
|
PLATFORM_KIT_MIN_VERSION = "1.2.11"
|
||||||
|
|||||||
@@ -225,6 +225,37 @@ class TestDisplayMetadata(unittest.TestCase):
|
|||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
def test_validate_rejects_sqlite_foreign_keys_on_business_tables(self) -> None:
|
||||||
|
from db.display_metadata import (
|
||||||
|
_JIANGCHANG_COLUMNS_DDL,
|
||||||
|
_JIANGCHANG_TABLES_DDL,
|
||||||
|
)
|
||||||
|
from db.display_metadata_validator import validate_display_metadata
|
||||||
|
|
||||||
|
conn = sqlite3.connect(":memory:")
|
||||||
|
try:
|
||||||
|
conn.execute(_JIANGCHANG_TABLES_DDL)
|
||||||
|
conn.execute(_JIANGCHANG_COLUMNS_DDL)
|
||||||
|
conn.execute("CREATE TABLE parent_rows (id INTEGER PRIMARY KEY, title TEXT)")
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE child_rows (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
parent_id INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (parent_id) REFERENCES parent_rows(id)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
report = validate_display_metadata(conn)
|
||||||
|
self.assertFalse(report.ok)
|
||||||
|
self.assertTrue(
|
||||||
|
any("FOREIGN KEY" in err or "REFERENCES" in err for err in report.errors),
|
||||||
|
msg=f"expected FK error, got: {report.errors}",
|
||||||
|
)
|
||||||
|
self.assertTrue(any("child_rows" in err for err in report.errors))
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
def test_validate_missing_both_metadata_tables(self) -> None:
|
def test_validate_missing_both_metadata_tables(self) -> None:
|
||||||
conn = sqlite3.connect(":memory:")
|
conn = sqlite3.connect(":memory:")
|
||||||
try:
|
try:
|
||||||
@@ -578,6 +609,100 @@ metadata:
|
|||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
def test_grid_help_upsert_and_migrate_from_legacy_schema(self) -> None:
|
||||||
|
conn = sqlite3.connect(":memory:")
|
||||||
|
try:
|
||||||
|
from db.display_metadata import (
|
||||||
|
init_display_metadata_tables,
|
||||||
|
upsert_column_metadata,
|
||||||
|
upsert_table_metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE _jiangchang_tables (
|
||||||
|
table_name TEXT PRIMARY KEY,
|
||||||
|
display_name TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
sort_order INTEGER,
|
||||||
|
visible INTEGER NOT NULL DEFAULT 1,
|
||||||
|
readonly INTEGER NOT NULL DEFAULT 0,
|
||||||
|
default_sort_json TEXT,
|
||||||
|
default_visible_columns_json TEXT
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE _jiangchang_columns (
|
||||||
|
table_name TEXT NOT NULL,
|
||||||
|
column_name TEXT NOT NULL,
|
||||||
|
display_name TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
display_order INTEGER,
|
||||||
|
visible INTEGER NOT NULL DEFAULT 1,
|
||||||
|
searchable INTEGER NOT NULL DEFAULT 1,
|
||||||
|
editable INTEGER NOT NULL DEFAULT 1,
|
||||||
|
display_type TEXT,
|
||||||
|
options_json TEXT,
|
||||||
|
PRIMARY KEY (table_name, column_name)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
init_display_metadata_tables(cur)
|
||||||
|
cur.execute("PRAGMA table_info('_jiangchang_tables')")
|
||||||
|
self.assertIn("grid_help", {str(row[1]) for row in cur.fetchall()})
|
||||||
|
cur.execute("PRAGMA table_info('_jiangchang_columns')")
|
||||||
|
self.assertIn("grid_help", {str(row[1]) for row in cur.fetchall()})
|
||||||
|
|
||||||
|
upsert_table_metadata(
|
||||||
|
cur,
|
||||||
|
table_name="demo_items",
|
||||||
|
display_name="示例记录",
|
||||||
|
description="表单备注,不应出现在表格横幅",
|
||||||
|
grid_help="表格横幅说明",
|
||||||
|
)
|
||||||
|
upsert_column_metadata(
|
||||||
|
cur,
|
||||||
|
table_name="demo_items",
|
||||||
|
column_name="title",
|
||||||
|
display_name="标题",
|
||||||
|
description="填标题",
|
||||||
|
grid_help="列头问号说明",
|
||||||
|
)
|
||||||
|
upsert_table_metadata(
|
||||||
|
cur,
|
||||||
|
table_name="empty_help",
|
||||||
|
display_name="无表格说明",
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT description, grid_help
|
||||||
|
FROM _jiangchang_tables
|
||||||
|
WHERE table_name = 'demo_items'
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.assertEqual(cur.fetchone(), ("表单备注,不应出现在表格横幅", "表格横幅说明"))
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT description, grid_help
|
||||||
|
FROM _jiangchang_columns
|
||||||
|
WHERE table_name = 'demo_items' AND column_name = 'title'
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.assertEqual(cur.fetchone(), ("填标题", "列头问号说明"))
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT grid_help FROM _jiangchang_tables
|
||||||
|
WHERE table_name = 'empty_help'
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
self.assertTrue(not cur.fetchone()[0])
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -348,6 +348,11 @@ class TestDocsStandards(unittest.TestCase):
|
|||||||
"文件 / 图片上传",
|
"文件 / 图片上传",
|
||||||
"default_visible_columns_json",
|
"default_visible_columns_json",
|
||||||
"默认展示列",
|
"默认展示列",
|
||||||
|
"grid_help",
|
||||||
|
"表格说明 `grid_help`",
|
||||||
|
"表与表之间:禁止 SQLite 外键",
|
||||||
|
"禁止 SQLite 外键",
|
||||||
|
"跨表引用 lookup",
|
||||||
):
|
):
|
||||||
self.assertIn(marker, text, msg=f"SCHEMA.md missing {marker!r}")
|
self.assertIn(marker, text, msg=f"SCHEMA.md missing {marker!r}")
|
||||||
|
|
||||||
@@ -365,6 +370,7 @@ class TestDocsStandards(unittest.TestCase):
|
|||||||
"SCHEMA.md",
|
"SCHEMA.md",
|
||||||
"字段条件显隐",
|
"字段条件显隐",
|
||||||
"visibleWhen",
|
"visibleWhen",
|
||||||
|
"grid_help",
|
||||||
):
|
):
|
||||||
self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}")
|
self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user