Release skill-template v1.0.99: table registry contract and drop_deprecated_user_table
All checks were successful
技能自动化发布 / release (push) Successful in 7s
All checks were successful
技能自动化发布 / release (push) Successful in 7s
This commit is contained in:
@@ -9,6 +9,12 @@
|
|||||||
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
|
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
|
||||||
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
|
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
|
||||||
|
|
||||||
|
## 1.0.99
|
||||||
|
|
||||||
|
- 沉淀数据管理**表注册门禁**契约:存在 `_jiangchang_tables` 时,宿主仅展示注册且 `visible=1` 的表;孤儿物理表不展示(匠厂宿主 ≥6.8.25)
|
||||||
|
- 新增「表增删改与退役」:`drop_deprecated_user_table()` 标准迁移;禁止只删元数据留物理表;新技能禁止 `task_logs`
|
||||||
|
- `LOGGING.md` 可观测性三分法;`DATA_PATHS.md` 卸载不删用户数据;Checklist A6-13 / A6-14;`tests/samples/test_table_deprecation_migration.py.sample`
|
||||||
|
|
||||||
## 1.0.98
|
## 1.0.98
|
||||||
|
|
||||||
- 沉淀数据管理**列筛选与全局搜索**契约:`searchable` 同时控制列筛选字段列表与顶栏全局搜索;存库 value / 展示 label 分工;筛选栏枚举/boolean/lookup 用下拉 Select(与表单开关/分段单选区分)
|
- 沉淀数据管理**列筛选与全局搜索**契约:`searchable` 同时控制列筛选字段列表与顶栏全局搜索;存库 value / 展示 label 分工;筛选栏枚举/boolean/lookup 用下拉 Select(与表单开关/分段单选区分)
|
||||||
|
|||||||
2
SKILL.md
2
SKILL.md
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: 技能开发模板(通用业务版)
|
name: 技能开发模板(通用业务版)
|
||||||
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
|
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
|
||||||
version: 1.0.98
|
version: 1.0.99
|
||||||
author: 深圳匠厂科技有限公司
|
author: 深圳匠厂科技有限公司
|
||||||
metadata:
|
metadata:
|
||||||
openclaw:
|
openclaw:
|
||||||
|
|||||||
@@ -138,6 +138,8 @@ checklist_version: 1.0
|
|||||||
- [ ] A6-10 **P1** 有 HTML 侧栏预览时(S0-12):预览区横向铺满、无横向滚动条;重新生成后内容与页头时间更新;布局符合 `references/ACTIONS.md`「宿主预览布局」。无 HTML 报告 → **N/A**
|
- [ ] A6-10 **P1** 有 HTML 侧栏预览时(S0-12):预览区横向铺满、无横向滚动条;重新生成后内容与页头时间更新;布局符合 `references/ACTIONS.md`「宿主预览布局」。无 HTML 报告 → **N/A**
|
||||||
- [ ] A6-11 **P1** 业务**状态 / 启用类**列若用户需要筛表或顶栏搜中文:`searchable: 1` 且 `options_json`(或 `boolean` + `options_json`)的 **value 与存库一致**;仅 `searchable: 0` 则宿主不提供列筛与全局搜索。见 `references/SCHEMA.md`「列筛选与全局搜索」
|
- [ ] A6-11 **P1** 业务**状态 / 启用类**列若用户需要筛表或顶栏搜中文:`searchable: 1` 且 `options_json`(或 `boolean` + `options_json`)的 **value 与存库一致**;仅 `searchable: 0` 则宿主不提供列筛与全局搜索。见 `references/SCHEMA.md`「列筛选与全局搜索」
|
||||||
- [ ] A6-12 **P2** `display_type: boolean` 的展示列推荐同时配置 `options_json` 中文 label(如 停用/启用),避免仅靠宿主默认「是/否」;无 boolean 列 → **N/A**
|
- [ ] A6-12 **P2** `display_type: boolean` 的展示列推荐同时配置 `options_json` 中文 label(如 停用/启用),避免仅靠宿主默认「是/否」;无 boolean 列 → **N/A**
|
||||||
|
- [ ] A6-13 **P0** 新技能**不得**创建 `task_logs`;过程/进度/业务结果走运行日志、任务中心、业务表(见 `LOGGING.md`、`SCHEMA.md`)
|
||||||
|
- [ ] A6-14 **P0** 废弃用户可见表时:`drop_deprecated_user_table` 物理删表 + 清元数据;**禁止**只删元数据留物理表;有迁移时须提供旧库回归测试(见 `SCHEMA.md`、`tests/samples/test_table_deprecation_migration.py.sample`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ DOWNLOAD_OUTPUT_DIR=./outputs/videos
|
|||||||
- `tests/test_runtime_paths.py` 覆盖 `resolve_data_path` / `resolve_input_path`。
|
- `tests/test_runtime_paths.py` 覆盖 `resolve_data_path` / `resolve_input_path`。
|
||||||
- `POLICY-DATA-PATH-001`:禁止 `.env.example` 中 `./` 路径;禁止业务代码 `abspath(config.get(...))`。
|
- `POLICY-DATA-PATH-001`:禁止 `.env.example` 中 `./` 路径;禁止业务代码 `abspath(config.get(...))`。
|
||||||
|
|
||||||
|
### 卸载技能与数据保留
|
||||||
|
|
||||||
|
宿主**卸载技能只删除安装目录**(如 `.openclaw/skills/{slug}`),**不会**删除 `{JIANGCHANG_DATA_ROOT}/{user_id}/{skill_slug}/` 下的 SQLite、输入输出与日志。重装会复用同一用户库;schema 迁移须在 `init_db()` 中幂等完成(见 [`../references/SCHEMA.md`](../references/SCHEMA.md)「表增删改与退役」)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. 相关文档
|
## 9. 相关文档
|
||||||
|
|||||||
@@ -122,9 +122,15 @@ RPA 等待用 `interruptible_sleep`,**不要**裸 `asyncio.sleep`(否则暂
|
|||||||
|
|
||||||
Journal 会写入 `type: lifecycle` 事件(`paused` / `resumed` / `stopping`),供宿主任务中心显示状态。
|
Journal 会写入 `type: lifecycle` 事件(`paused` / `resumed` / `stopping`),供宿主任务中心显示状态。
|
||||||
|
|
||||||
### 2.3 业务表(可选,不是默认排障通道)
|
### 2.3 可观测性三分法(禁止 task_logs)
|
||||||
|
|
||||||
模板**默认不建**任务审计表。业务结果写入各技能自己的业务表;过程看 `jiangchang.log`,进度/终态看任务中心。
|
| 信息类型 | 去哪里 | 技能怎么做 |
|
||||||
|
|----------|--------|------------|
|
||||||
|
| **过程**(步骤、异常栈、排查) | `jiangchang.log` / 宿主「运行日志」 | `log.info` / `log.exception`,关键节点见 §3 |
|
||||||
|
| **进度 / 终态**(任务中心) | Run Journal:`emit` / `finish` | `job_context` + `finish(status=...)` |
|
||||||
|
| **业务结果** | 业务表字段(status、产物路径、统计等) | repository 回写;见 [`WORK_ITEM_STATUS.md`](../references/WORK_ITEM_STATUS.md) |
|
||||||
|
|
||||||
|
模板**默认不建** `task_logs` 等通用任务审计表。**不要**用业务表冒充任务进度,也不要用任务表代替业务状态机。
|
||||||
|
|
||||||
### 2.4 RPA video step / artifacts(用户可见动作与存证)
|
### 2.4 RPA video step / artifacts(用户可见动作与存证)
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
- 读取 `_jiangchang_*` 元数据
|
- 读取 `_jiangchang_*` 元数据
|
||||||
- 读取 SQLite `PRAGMA table_info`
|
- 读取 SQLite `PRAGMA table_info`
|
||||||
|
- **数据管理侧栏/表列表**:当库中存在 `_jiangchang_tables` 时,**仅展示**在该表注册且 `visible=1` 的业务表;孤儿物理表(有表、无注册元数据)**不得**出现在侧栏,直接访问 schema API 应返回「表不存在」。极老库无 `_jiangchang_tables` 时,宿主可保留物理表枚举 fallback(兼容 `_schema_meta` 时代)
|
||||||
- 根据 schema 渲染数据管理页面
|
- 根据 schema 渲染数据管理页面
|
||||||
- 根据 `editable` 推导字段是否可创建或编辑
|
- 根据 `editable` 推导字段是否可创建或编辑
|
||||||
- 在**新增 / 编辑**表单中,若列有 `_jiangchang_columns.description`,在标签下方展示该说明(灰字;空则不显示)
|
- 在**新增 / 编辑**表单中,若列有 `_jiangchang_columns.description`,在标签下方展示该说明(灰字;空则不显示)
|
||||||
@@ -770,6 +771,48 @@ seed_column_metadata_batch(
|
|||||||
|
|
||||||
不要直接修改用户真实数据库作为迁移方式;元数据初始化必须使用真实物理字段列表。
|
不要直接修改用户真实数据库作为迁移方式;元数据初始化必须使用真实物理字段列表。
|
||||||
|
|
||||||
|
## 表增删改与退役(技能 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` → **创建时间**。
|
- 使用普通用户能理解的业务用语:`error_msg` → **错误信息**,`created_at` → **创建时间**。
|
||||||
@@ -778,11 +821,12 @@ seed_column_metadata_batch(
|
|||||||
|
|
||||||
## 元数据同步规则
|
## 元数据同步规则
|
||||||
|
|
||||||
1. 新增用户可见业务表 → 同步 `_jiangchang_tables`。
|
1. 新增用户可见业务表 → 同步 `_jiangchang_tables`(`visible=1` 方可在数据管理展示)。
|
||||||
2. 新增用户可见字段 → 同步 `_jiangchang_columns`。
|
2. 新增用户可见字段 → 同步 `_jiangchang_columns`。
|
||||||
3. 删除/重命名表或字段 → 同步清理或迁移元数据。
|
3. 删除/重命名表或字段 → 物理迁移 + 同步清理元数据;**废弃表**用 `drop_deprecated_user_table`,禁止只删元数据留物理表。
|
||||||
4. 初始化脚本必须幂等。
|
4. 初始化脚本必须幂等。
|
||||||
5. 元数据中的表、字段必须在库中真实存在;可见项不得缺少中文 `display_name`。
|
5. 元数据中的表、字段必须在库中真实存在;可见项不得缺少中文 `display_name`。
|
||||||
|
6. `init_db()` 后:`iter_user_tables` 中每个物理业务表都应有对应 `_jiangchang_tables.display_name`,或已被 `drop_deprecated_user_table` 物理删除。
|
||||||
|
|
||||||
## 开发自检
|
## 开发自检
|
||||||
|
|
||||||
|
|||||||
@@ -277,6 +277,32 @@ def init_display_metadata(cursor) -> None:
|
|||||||
init_display_metadata_tables(cursor)
|
init_display_metadata_tables(cursor)
|
||||||
|
|
||||||
|
|
||||||
|
def drop_deprecated_user_table(
|
||||||
|
cursor,
|
||||||
|
table_name: str,
|
||||||
|
*,
|
||||||
|
triggers: Iterable[str] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""永久退役用户可见表:删 trigger、DROP 物理表、清理展示元数据(幂等)。
|
||||||
|
|
||||||
|
用于废弃 task_logs 等旧表。禁止只删元数据而保留物理表。
|
||||||
|
见 references/SCHEMA.md「表增删改与退役」。
|
||||||
|
"""
|
||||||
|
for trigger_name in triggers or ():
|
||||||
|
cursor.execute(f"DROP TRIGGER IF EXISTS {trigger_name}")
|
||||||
|
default_updated_at = f"{table_name}_set_updated_at"
|
||||||
|
cursor.execute(f"DROP TRIGGER IF EXISTS {default_updated_at}")
|
||||||
|
cursor.execute(f"DROP TABLE IF EXISTS {_quote_identifier(table_name)}")
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM _jiangchang_columns WHERE table_name = ?",
|
||||||
|
(table_name,),
|
||||||
|
)
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM _jiangchang_tables WHERE table_name = ?",
|
||||||
|
(table_name,),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def iter_user_tables(cursor) -> Iterable[str]:
|
def iter_user_tables(cursor) -> Iterable[str]:
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
|
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
|
||||||
|
|||||||
66
tests/samples/test_table_deprecation_migration.py.sample
Normal file
66
tests/samples/test_table_deprecation_migration.py.sample
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""旧库废弃表迁移样例(复制到 tests/ 并按技能改写)。
|
||||||
|
|
||||||
|
验收:给定旧库含废弃物理表 + 元数据 + 历史行 → init_db() 后表与元数据均清除。
|
||||||
|
见 references/SCHEMA.md「表增删改与退役」。
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from _support import IsolatedDataRoot
|
||||||
|
|
||||||
|
|
||||||
|
class TestTableDeprecationMigration(unittest.TestCase):
|
||||||
|
def test_init_db_drops_legacy_task_logs(self) -> None:
|
||||||
|
with IsolatedDataRoot():
|
||||||
|
from db.connection import get_conn, init_db
|
||||||
|
from db.display_metadata import iter_user_tables
|
||||||
|
|
||||||
|
# 1) 模拟升级前的旧库
|
||||||
|
init_db()
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE task_logs (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
task_type TEXT,
|
||||||
|
status TEXT
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO _jiangchang_tables
|
||||||
|
(table_name, display_name, visible, readonly)
|
||||||
|
VALUES ('task_logs', '任务日志', 1, 0)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
cur.execute("INSERT INTO task_logs (task_type, status) VALUES ('run', 'ok')")
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# 2) 新代码 init_db() 应幂等清除(在 connection.init_db 中调用 drop_deprecated_user_table)
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='task_logs'"
|
||||||
|
)
|
||||||
|
self.assertIsNone(cur.fetchone())
|
||||||
|
cur.execute(
|
||||||
|
"SELECT COUNT(*) FROM _jiangchang_tables WHERE table_name='task_logs'"
|
||||||
|
)
|
||||||
|
self.assertEqual(int(cur.fetchone()[0]), 0)
|
||||||
|
self.assertNotIn("task_logs", list(iter_user_tables(cur)))
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -738,6 +738,73 @@ metadata:
|
|||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
def test_drop_deprecated_user_table_removes_physical_and_metadata(self) -> None:
|
||||||
|
conn = sqlite3.connect(":memory:")
|
||||||
|
try:
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE _jiangchang_tables (
|
||||||
|
table_name TEXT PRIMARY KEY,
|
||||||
|
display_name TEXT,
|
||||||
|
description TEXT,
|
||||||
|
sort_order INTEGER,
|
||||||
|
visible INTEGER,
|
||||||
|
readonly INTEGER
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE _jiangchang_columns (
|
||||||
|
table_name TEXT,
|
||||||
|
column_name TEXT,
|
||||||
|
display_name TEXT,
|
||||||
|
description TEXT,
|
||||||
|
display_order INTEGER,
|
||||||
|
visible INTEGER,
|
||||||
|
searchable INTEGER,
|
||||||
|
editable INTEGER,
|
||||||
|
display_type TEXT,
|
||||||
|
options_json TEXT,
|
||||||
|
PRIMARY KEY (table_name, column_name)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE task_logs (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
status TEXT,
|
||||||
|
created_at INTEGER,
|
||||||
|
updated_at INTEGER
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO _jiangchang_tables VALUES ('task_logs', '任务日志', NULL, 1, 1, 0)"
|
||||||
|
)
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO _jiangchang_columns VALUES "
|
||||||
|
"('task_logs', 'id', '编号', NULL, 1, 1, 1, 0, 'text', NULL)"
|
||||||
|
)
|
||||||
|
cur.execute("INSERT INTO task_logs (status) VALUES ('success')")
|
||||||
|
|
||||||
|
from db.display_metadata import drop_deprecated_user_table, iter_user_tables
|
||||||
|
|
||||||
|
drop_deprecated_user_table(cur, "task_logs")
|
||||||
|
cur.execute(
|
||||||
|
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='task_logs'"
|
||||||
|
)
|
||||||
|
self.assertIsNone(cur.fetchone())
|
||||||
|
cur.execute(
|
||||||
|
"SELECT COUNT(*) FROM _jiangchang_tables WHERE table_name='task_logs'"
|
||||||
|
)
|
||||||
|
self.assertEqual(int(cur.fetchone()[0]), 0)
|
||||||
|
self.assertNotIn("task_logs", list(iter_user_tables(cur)))
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -360,6 +360,9 @@ class TestDocsStandards(unittest.TestCase):
|
|||||||
"列筛选与全局搜索",
|
"列筛选与全局搜索",
|
||||||
"searchable: 1",
|
"searchable: 1",
|
||||||
"筛选栏控件",
|
"筛选栏控件",
|
||||||
|
"表增删改与退役",
|
||||||
|
"drop_deprecated_user_table",
|
||||||
|
"孤儿物理表",
|
||||||
):
|
):
|
||||||
self.assertIn(marker, text, msg=f"SCHEMA.md missing {marker!r}")
|
self.assertIn(marker, text, msg=f"SCHEMA.md missing {marker!r}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user