From b5d120469b0fb32fd0d72439ebdbd97810ae0860 Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Tue, 25 Aug 2026 13:40:08 +0800 Subject: [PATCH] Release skill-template v1.0.99: table registry contract and drop_deprecated_user_table --- CHANGELOG.md | 6 ++ SKILL.md | 2 +- development/CHECKLIST.md | 2 + development/DATA_PATHS.md | 4 ++ development/LOGGING.md | 10 ++- references/SCHEMA.md | 48 ++++++++++++- scripts/db/display_metadata.py | 26 +++++++ ...test_table_deprecation_migration.py.sample | 66 ++++++++++++++++++ tests/test_display_metadata.py | 67 +++++++++++++++++++ tests/test_docs_standards.py | 3 + 10 files changed, 229 insertions(+), 5 deletions(-) create mode 100644 tests/samples/test_table_deprecation_migration.py.sample diff --git a/CHANGELOG.md b/CHANGELOG.md index df53ebc..2f6f1d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ - 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节 - 打 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 - 沉淀数据管理**列筛选与全局搜索**契约:`searchable` 同时控制列筛选字段列表与顶栏全局搜索;存库 value / 展示 label 分工;筛选栏枚举/boolean/lookup 用下拉 Select(与表单开关/分段单选区分) diff --git a/SKILL.md b/SKILL.md index 94cdc27..25bc30c 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 技能开发模板(通用业务版) description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。" -version: 1.0.98 +version: 1.0.99 author: 深圳匠厂科技有限公司 metadata: openclaw: diff --git a/development/CHECKLIST.md b/development/CHECKLIST.md index 9182fe5..e2fd4d7 100644 --- a/development/CHECKLIST.md +++ b/development/CHECKLIST.md @@ -138,6 +138,8 @@ checklist_version: 1.0 - [ ] 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-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`) --- diff --git a/development/DATA_PATHS.md b/development/DATA_PATHS.md index cf6f2b1..ed5a7db 100644 --- a/development/DATA_PATHS.md +++ b/development/DATA_PATHS.md @@ -176,6 +176,10 @@ DOWNLOAD_OUTPUT_DIR=./outputs/videos - `tests/test_runtime_paths.py` 覆盖 `resolve_data_path` / `resolve_input_path`。 - `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. 相关文档 diff --git a/development/LOGGING.md b/development/LOGGING.md index feff8a4..8c208ff 100644 --- a/development/LOGGING.md +++ b/development/LOGGING.md @@ -122,9 +122,15 @@ RPA 等待用 `interruptible_sleep`,**不要**裸 `asyncio.sleep`(否则暂 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(用户可见动作与存证) diff --git a/references/SCHEMA.md b/references/SCHEMA.md index be6065c..186eb32 100644 --- a/references/SCHEMA.md +++ b/references/SCHEMA.md @@ -25,6 +25,7 @@ - 读取 `_jiangchang_*` 元数据 - 读取 SQLite `PRAGMA table_info` +- **数据管理侧栏/表列表**:当库中存在 `_jiangchang_tables` 时,**仅展示**在该表注册且 `visible=1` 的业务表;孤儿物理表(有表、无注册元数据)**不得**出现在侧栏,直接访问 schema API 应返回「表不存在」。极老库无 `_jiangchang_tables` 时,宿主可保留物理表枚举 fallback(兼容 `_schema_meta` 时代) - 根据 schema 渲染数据管理页面 - 根据 `editable` 推导字段是否可创建或编辑 - 在**新增 / 编辑**表单中,若列有 `_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` → **创建时间**。 @@ -778,11 +821,12 @@ seed_column_metadata_batch( ## 元数据同步规则 -1. 新增用户可见业务表 → 同步 `_jiangchang_tables`。 +1. 新增用户可见业务表 → 同步 `_jiangchang_tables`(`visible=1` 方可在数据管理展示)。 2. 新增用户可见字段 → 同步 `_jiangchang_columns`。 -3. 删除/重命名表或字段 → 同步清理或迁移元数据。 +3. 删除/重命名表或字段 → 物理迁移 + 同步清理元数据;**废弃表**用 `drop_deprecated_user_table`,禁止只删元数据留物理表。 4. 初始化脚本必须幂等。 5. 元数据中的表、字段必须在库中真实存在;可见项不得缺少中文 `display_name`。 +6. `init_db()` 后:`iter_user_tables` 中每个物理业务表都应有对应 `_jiangchang_tables.display_name`,或已被 `drop_deprecated_user_table` 物理删除。 ## 开发自检 diff --git a/scripts/db/display_metadata.py b/scripts/db/display_metadata.py index f815733..6fe3b3a 100644 --- a/scripts/db/display_metadata.py +++ b/scripts/db/display_metadata.py @@ -277,6 +277,32 @@ def init_display_metadata(cursor) -> None: 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]: cursor.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name" diff --git a/tests/samples/test_table_deprecation_migration.py.sample b/tests/samples/test_table_deprecation_migration.py.sample new file mode 100644 index 0000000..9702626 --- /dev/null +++ b/tests/samples/test_table_deprecation_migration.py.sample @@ -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() diff --git a/tests/test_display_metadata.py b/tests/test_display_metadata.py index 470b168..c4d97dd 100644 --- a/tests/test_display_metadata.py +++ b/tests/test_display_metadata.py @@ -738,6 +738,73 @@ metadata: finally: 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__": unittest.main() diff --git a/tests/test_docs_standards.py b/tests/test_docs_standards.py index 01347b3..88d9202 100644 --- a/tests/test_docs_standards.py +++ b/tests/test_docs_standards.py @@ -360,6 +360,9 @@ class TestDocsStandards(unittest.TestCase): "列筛选与全局搜索", "searchable: 1", "筛选栏控件", + "表增删改与退役", + "drop_deprecated_user_table", + "孤儿物理表", ): self.assertIn(marker, text, msg=f"SCHEMA.md missing {marker!r}")