diff --git a/SKILL.md b/SKILL.md index 56b9c2b..059edb7 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 技能开发模板(通用业务版) description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。" -version: 1.0.40 +version: 1.0.42 author: 深圳匠厂科技有限公司 metadata: openclaw: @@ -38,7 +38,7 @@ allowed-tools: - 只读查询(`health` / `stats` 等)可用 `"sync"`,立即返回结果。 - 批量处理用参数化 `pickCount` / `--pick N` 顺序消费队列,**不要并发**同一 Profile。 - 详细契约:`development/SKILL_ACTION_RUNTIME.md`、`references/ACTIONS.md`、`references/SCHEMA.md`。 -- 模板源仓库本身仅示范 `health` / `version` / `config-path`(sync);定制后按业务增加 Action,**一个能力一个 Action + 多 placements**。 +- 模板源仓库本身仅示范 `health` / `version` / `config-path` / `init-db`(运维/诊断 CLI);定制后按业务增加 Action,**一个能力一个 Action + 多 placements**。 ## 面向用户问答(LLM 规则) @@ -83,6 +83,7 @@ allowed-tools: python {baseDir}/scripts/main.py health python {baseDir}/scripts/main.py config-path python {baseDir}/scripts/main.py version +python {baseDir}/scripts/main.py init-db ``` 配置:仓库 `.env.example` 为模板;用户 `.env` 在 `{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/{slug}/.env`,启动时自动 bootstrap。优先级:进程环境变量 > 用户 `.env` > `.env.example`。 diff --git a/development/DEVELOPMENT.md b/development/DEVELOPMENT.md index 15e616b..2dd5ce7 100644 --- a/development/DEVELOPMENT.md +++ b/development/DEVELOPMENT.md @@ -454,7 +454,7 @@ metadata: 也就是说,`cli/app.py` 的职责是: 1. 打印帮助 -2. 定义 `run / logs / log-get / health / version`(及业务子命令) +2. 定义 `run / logs / log-get / health / version / init-db`(及业务子命令) 3. 把参数转交给 `service.task_service`(或薄适配后再进 domain service) 不要在 `cli/app.py` 里直接写: @@ -538,7 +538,7 @@ assets/actions.json → placements / bind / executionProfile / entry - **运行时金标准**见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) - 字段契约见 [`references/ACTIONS.md`](../references/ACTIONS.md)(`placements` / `bind.tables` / `executionProfile` 正交) - JSON Schema 见 [`assets/schemas/skill-actions.schema.json`](../assets/schemas/skill-actions.schema.json) -- 模板最小示例仅暴露 `health` / `version` / `config-path`(均显式 `executionProfile: "sync"`;**不**自动加 toolbar) +- 模板最小示例仅暴露 `health` / `version` / `config-path`(均显式 `executionProfile: "sync"`;**不**自动加 toolbar);CLI 另含 `init-db` 供宿主静默建库(不必做成 Action) - 含 `toolbar` 的 Action:**必须**声明合法 `bind.tables`(`POLICY-SKILL-ACTION-003`) - 含浏览器 RPA / 长耗时的业务技能:**必须**增加至少一条 `"executionProfile": "async"` 且 `placements` 含 `agent` 的 action(`POLICY-SKILL-ACTION-001`) - 没有宿主直接操作需求时,**可以删除** `actions.json`;删除后勿保留失效引用 diff --git a/development/REQUIREMENTS.md b/development/REQUIREMENTS.md index 5898945..ee1ea13 100644 --- a/development/REQUIREMENTS.md +++ b/development/REQUIREMENTS.md @@ -60,7 +60,7 @@ - 输出结构化结果(stdout JSON 或固定文本 + 机读字段) - 写入 `task_logs`(及必要的业务表,见 `references/SCHEMA.md`) - 支持失败可诊断(稳定错误码、`ERROR:` 前缀、必要截图/日志) -- 完成 CLI 入口与 `health` / `version` 最小可运行验证 +- 完成 CLI 入口与 `health` / `version` / `init-db` 最小可运行验证 - 完成正式环境发布与安装验证(如适用) ## 4. 功能范围 @@ -81,6 +81,7 @@ - 支持 `python scripts/main.py health` - 支持 `python scripts/main.py version` +- 支持 `python scripts/main.py init-db`(幂等建库;宿主安装/更新后可静默调用) - 支持 `python scripts/main.py ` - 支持核心业务编排(HTTP / 批处理 / 可选 RPA,按四档 adapter 选型) - 支持写入任务日志(`task_logs`) @@ -154,7 +155,7 @@ 什么情况下才算开发完成: - 代码结构符合模板规范;`SKILL.md` slug 与 `constants.SKILL_SLUG` 一致 -- `health`、`version` 命令执行正常 +- `health`、`version`、`init-db` 命令执行正常 - 主命令(如 `run`)在 mock / simulator 档位可重复验证 - `python tests/run_tests.py -v` 必跑测试全部通过 - `task_logs` 写入和查询符合 `references/SCHEMA.md`(含 `created_at` / `updated_at` Unix 秒级规范) @@ -211,7 +212,7 @@ ## 4. 功能范围 -- 支持 health / version / run +- 支持 health / version / init-db / run - 支持 logs / log-get ## 5. 非功能要求 diff --git a/development/TESTING.md b/development/TESTING.md index b2854f8..66461f5 100644 --- a/development/TESTING.md +++ b/development/TESTING.md @@ -22,7 +22,7 @@ 必跑套件要像一个紧张的守门员:**快、确定、离线**。典型覆盖: -- CLI:导入 [`cli.app`](../scripts/cli/app.py) 走解析链路、`health`(runtime diagnostics)/ `version` / `logs` / `log-get` 冒烟; +- CLI:导入 [`cli.app`](../scripts/cli/app.py) 走解析链路、`health`(runtime diagnostics)/ `version` / `init-db` / `logs` / `log-get` 冒烟; - 架构守护:无 `scripts/jiangchang_skill_core/`、`platform-kit>=1.2.0` 导入来源、文档/runtime 标准(见 `test_platform_import.py` 等); - **真实 subprocess**:[`tests/test_entrypoint_subprocess.py`](../tests/test_entrypoint_subprocess.py) 再调用一遍 `python scripts/main.py`,防路径漂移; - 运行时:`runtime_paths` 与 **`JIANGCHANG_*` 隔离**; @@ -153,6 +153,7 @@ Golden fixture 流程同理([`tests/samples/test_golden_cases.py.sample`](../t - [ ] `python tests/run_tests.py -v` 能通过。 - [ ] `python scripts/main.py health` 能通过。 - [ ] `python scripts/main.py version` 输出 JSON,且 `skill` 与目录名 / `SKILL.md` / `constants.SKILL_SLUG` 一致。 +- [ ] `python scripts/main.py init-db` 幂等创建本地库并输出 JSON(`ok` / `skill` / `db_path`)。 - [ ] 所有 DB / 文件写入都在 `IsolatedDataRoot`(或等价隔离)下测试,不写真实数据目录。 - [ ] 外部系统默认使用 mock / `FakeAdapter`,不访问真实 API,不打开真实 RPA。 - [ ] 至少有 1 个成功路径测试。 diff --git a/references/CLI.md b/references/CLI.md index b35925d..9d4c125 100644 --- a/references/CLI.md +++ b/references/CLI.md @@ -8,6 +8,7 @@ python {baseDir}/scripts/main.py health python {baseDir}/scripts/main.py config-path python {baseDir}/scripts/main.py version +python {baseDir}/scripts/main.py init-db ``` ## 标准行为 @@ -16,6 +17,7 @@ python {baseDir}/scripts/main.py version - **`health`**:只读 runtime 诊断,**不下载、不修复 media-assets,不执行业务动作**;不输出敏感值。 - **`config-path`**:输出 JSON,包含 `skill`、`env_path`、`example_path`。 - **`version`**:输出 JSON(`version`、`skill`)。 +- **`init-db`**:幂等创建/迁移本地 SQLite(含 `_jiangchang_*` 展示元数据)。成功输出一行 JSON(`ok` / `skill` / `db_path`),退出码 `0`。宿主可在技能安装或更新成功后静默调用;不要并入 `health`。 - **`run`**:长时间无 stdout **不代表卡死**;RPA / 外部调用期间应通过 Activity 或 Run Journal 看进度。 - **排查顺序**(由快到慢): 1. `python {baseDir}/scripts/main.py logs` diff --git a/references/SCHEMA.md b/references/SCHEMA.md index 13679f5..f7d291b 100644 --- a/references/SCHEMA.md +++ b/references/SCHEMA.md @@ -26,6 +26,11 @@ - 对字段名、表名、类型和权限进行最终校验 - 不猜测技能业务含义 - 不根据具体技能名称写特殊逻辑 +- 可在技能安装/更新成功后静默调用 `python scripts/main.py init-db` 做幂等 ensure(内部即 `init_db()`);失败不影响安装结果 + +## 初始化入口 + +幂等建库/迁移:`python scripts/main.py init-db`。业务仓储也可懒触发 `init_db()`,与 CLI 结果一致。 SQLite **没有**可靠的 `COMMENT ON TABLE/COLUMN`;SQL 文件里的 `-- 注释` 也不会成为可查询结构。 因此中文展示名称必须写入元数据表;字段顺序必须写在 `CREATE TABLE` 的列定义顺序中(`PRAGMA table_info` 的 `cid`)。 diff --git a/scripts/cli/app.py b/scripts/cli/app.py index cd68eaf..b005585 100644 --- a/scripts/cli/app.py +++ b/scripts/cli/app.py @@ -12,6 +12,7 @@ from typing import List, Optional from service.task_service import ( cmd_config_path, cmd_health, + cmd_init_db, cmd_log_get, cmd_logs, cmd_run, @@ -62,6 +63,7 @@ def _print_full_usage() -> None: print(" python main.py health") print(" python main.py config-path") print(" python main.py version") + print(" python main.py init-db") def build_parser() -> ZhArgumentParser: @@ -99,6 +101,9 @@ def build_parser() -> ZhArgumentParser: sp = sub.add_parser("version", help="版本信息(JSON)") sp.set_defaults(handler=lambda _a: cmd_version()) + + sp = sub.add_parser("init-db", help="幂等初始化本地数据库与数据管理元数据") + sp.set_defaults(handler=lambda _a: cmd_init_db()) return p @@ -111,7 +116,8 @@ def main(argv: Optional[List[str]] = None) -> int: _print_full_usage() return 1 if len(argv) == 2 and argv[0] not in { - "run", "logs", "log-get", "health", "config-path", "version", "-h", "--help" + "run", "logs", "log-get", "health", "config-path", "version", "init-db", + "-h", "--help", }: return cmd_run(target=argv[0], input_id=argv[1]) parser = build_parser() diff --git a/scripts/service/task_service.py b/scripts/service/task_service.py index e25d821..dbacb9e 100644 --- a/scripts/service/task_service.py +++ b/scripts/service/task_service.py @@ -26,8 +26,9 @@ from service.task_run_support import ( ) from util.constants import LOG_LOGGER_NAME, PLATFORM_KIT_MIN_VERSION, SKILL_SLUG, SKILL_VERSION from util.logging_config import get_skill_logger, setup_skill_logging -from util.runtime_paths import get_skill_data_dir, get_skill_root +from util.runtime_paths import get_db_path, get_skill_data_dir, get_skill_root from util.timeutil import unix_to_iso +from db.connection import init_db def _get_task_logger(): @@ -292,3 +293,28 @@ def cmd_health() -> int: def cmd_version() -> int: print(json.dumps({"version": SKILL_VERSION, "skill": SKILL_SLUG}, ensure_ascii=False)) return 0 + + +def cmd_init_db() -> int: + """幂等创建/迁移本地 SQLite 与数据管理元数据(宿主 install/upgrade 可调用)。""" + try: + init_db() + except Exception as exc: # noqa: BLE001 — CLI 边界,统一成可机读失败 + print( + json.dumps( + {"ok": False, "skill": SKILL_SLUG, "error": str(exc)}, + ensure_ascii=False, + ) + ) + return 1 + print( + json.dumps( + { + "ok": True, + "skill": SKILL_SLUG, + "db_path": get_db_path(), + }, + ensure_ascii=False, + ) + ) + return 0 diff --git a/scripts/util/constants.py b/scripts/util/constants.py index e7853e8..01a4620 100644 --- a/scripts/util/constants.py +++ b/scripts/util/constants.py @@ -1,6 +1,6 @@ """技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。""" SKILL_SLUG = "your-skill-slug" -SKILL_VERSION = "1.0.40" +SKILL_VERSION = "1.0.42" LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug" PLATFORM_KIT_MIN_VERSION = "1.2.0" diff --git a/tests/README.md b/tests/README.md index 4e6d613..e652b40 100644 --- a/tests/README.md +++ b/tests/README.md @@ -12,7 +12,7 @@ | 场景 | 放在哪里 | 默认是否运行 | 说明 | |------|----------|--------------|------| -| CLI / health / version / metadata / runtime 路径 | `tests/test_*.py` | 是 | 默认必跑,保持轻量、无外联 | +| CLI / health / version / init-db / metadata / runtime 路径 | `tests/test_*.py` | 是 | 默认必跑,保持轻量、无外联 | | 纯函数 / 业务规则 | `tests/test_*.py` | 是 | 不依赖真实外部系统 | | service 层契约 | 从 `tests/samples/test_service_contract.py.sample` 复制到 `tests/test_service_contract.py` | 是,复制后 | 用 `FakeAdapter` / stub 注入外部依赖 | | 外源同步契约 | 从 `tests/samples/test_sync_contract.py.sample` 复制 | 是,复制后 | 幂等 / 事务 / 空结果保护 / 不导出文件 | @@ -35,6 +35,7 @@ - [ ] `python tests/run_tests.py -v` 能通过(含 platform-kit 导入与无 vendored core 守护)。 - [ ] `python scripts/main.py health` 能通过(输出 runtime diagnostics;warning 不导致非零退出)。 - [ ] `python scripts/main.py version` 输出 JSON,且 `skill` 与目录名 / `SKILL.md` / `constants.SKILL_SLUG` 一致。 +- [ ] `python scripts/main.py init-db` 幂等创建本地库并输出 JSON(`ok` / `skill` / `db_path`)。 - [ ] 所有 DB / 文件写入都在 `IsolatedDataRoot`(或等价隔离)下测试,不写真实数据目录。 - [ ] 外部系统默认使用 mock / `FakeAdapter`,不访问真实 API,不打开真实 RPA。 - [ ] 至少有 1 个成功路径测试。 diff --git a/tests/test_cli_smoke.py b/tests/test_cli_smoke.py index 165c61d..d250b18 100644 --- a/tests/test_cli_smoke.py +++ b/tests/test_cli_smoke.py @@ -27,6 +27,7 @@ class TestCliSmoke(unittest.TestCase): out = buf.getvalue() self.assertIn("通用业务技能模板", out) self.assertIn("health", out) + self.assertIn("init-db", out) def test_health_zero(self) -> None: old_record = os.environ.get("OPENCLAW_RECORD_VIDEO") @@ -57,6 +58,26 @@ class TestCliSmoke(unittest.TestCase): self.assertIn("skill", payload) self.assertEqual(payload["skill"], SKILL_SLUG) + def test_init_db_creates_database_and_is_idempotent(self) -> None: + with IsolatedDataRoot(user_id="_cli_init_db"): + config.reset_cache() + buf = io.StringIO() + with redirect_stdout(buf), redirect_stderr(io.StringIO()): + rc = main(["init-db"]) + self.assertEqual(rc, 0) + payload = json.loads(buf.getvalue().strip()) + self.assertTrue(payload["ok"]) + self.assertEqual(payload["skill"], SKILL_SLUG) + self.assertTrue(os.path.isfile(payload["db_path"])) + + buf2 = io.StringIO() + with redirect_stdout(buf2), redirect_stderr(io.StringIO()): + rc2 = main(["init-db"]) + self.assertEqual(rc2, 0) + payload2 = json.loads(buf2.getvalue().strip()) + self.assertTrue(payload2["ok"]) + self.assertEqual(payload2["db_path"], payload["db_path"]) + def test_logs_empty_returns_zero(self) -> None: with IsolatedDataRoot(): buf = io.StringIO()