Compare commits

...

7 Commits

Author SHA1 Message Date
28c3c8d8ab Release v1.0.91: document no SQLite FK between business tables
All checks were successful
技能自动化发布 / release (push) Successful in 4s
2026-08-19 16:03:42 +08:00
571c135114 Release v1.0.90: document grid_help data-management contract
All checks were successful
技能自动化发布 / release (push) Successful in 7s
2026-08-18 11:00:20 +08:00
d840745758 chore: add MIT license, SECURITY.md, and placeholder developer_ids
All checks were successful
技能自动化发布 / release (push) Successful in 5s
2026-08-18 10:44:56 +08:00
cf3f0ae990 fix: default git tag message from UTF-8 bytes for Windows PowerShell 5.1
All checks were successful
技能自动化发布 / release (push) Successful in 5s
2026-08-18 10:19:47 +08:00
a11cdf4e7e fix: write git tag/commit messages via UTF-8 -F file
All checks were successful
技能自动化发布 / release (push) Successful in 5s
2026-08-18 10:16:33 +08:00
81096051dd fix: invoke git without cmd.exe so annotated tags keep UTF-8
All checks were successful
技能自动化发布 / release (push) Successful in 6s
2026-08-18 10:13:28 +08:00
f221f81d4e docs: add PRODUCT_SURFACES peer entry contract for skill authors
All checks were successful
技能自动化发布 / release (push) Successful in 5s
2026-08-17 18:49:05 +08:00
21 changed files with 681 additions and 94 deletions

View File

@@ -9,6 +9,44 @@
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
- 打 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
- `release.ps1` 打 annotated tag / 自动 commit 改为 UTF-8 消息文件 + `git -F`,避免 `-m` 走控制台代码页
- 默认说明仍可能乱码(见 1.0.88)
## 1.0.86
- 去掉 `release.ps1` 里通过 `cmd /c` 调 git 的写法;中文 tag 说明在部分 Windows 控制台代码页下仍可能乱码,见 1.0.87
## 1.0.85
- 新增开发主文 `development/PRODUCT_SURFACES.md`:说明对话 / 数据管理 / 定时任务平级关系、何时走哪个入口、如何做入口定位
- 需求与交活清单增加入口定位表;纠正「只做对话即可」与「Action 一概可选」的误导语气
- 数据管理明确为 UI 执行面 + 过程可见,与对话同内核
## 1.0.84
- 沉淀 Action 参数表单字段条件显隐契约 `visibleWhen`(先选模式再出对应参数)

21
LICENSE Normal file
View 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
View 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/`,不要写进本文件。

View File

@@ -1,7 +1,7 @@
---
name: 技能开发模板(通用业务版)
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
version: 1.0.84
version: 1.0.91
author: 深圳匠厂科技有限公司
metadata:
openclaw:
@@ -10,8 +10,7 @@ metadata:
emoji: "📦"
category: "通用"
developer_ids:
- 10032
- 12428
- 12345 # 占位,请换成你的匠厂用户 ID
allowed-tools:
- bash
---
@@ -31,7 +30,9 @@ allowed-tools:
### 架构铁律(先读)
- **宿主产品入口平级**:对话任务 / 数据管理 / 定时任务是同一技能能力的不同触发场景,**同等重要**;挂哪些由本技能 UX 定义(入口定位表),详见 `development/PRODUCT_SURFACES.md`。以后宿主若增加新入口,仍是「新触发场景 + 同一内核」。
- **单业务内核、多入口**:数据管理 / 定时任务 / Agent / 技能详情 / 直接 CLI 必须复用同一 domain service;禁止按入口复制业务逻辑,禁止按 `source` 分叉业务行为。
- **数据管理**:既是 **UI 执行面**(点按钮走与对话相同的闭环),也是数据与过程可见;不是「只读库」。
- **`placements`**:只决定 Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail)。其中 `row` 表示**行级入口**(宿主可呈现为操作列、行右键、行内「⋯」等;技能配什么就显示什么,无独立菜单 DSL)。数据管理技能按钮顺序见 `actions` **数组相对顺序**(详见 `references/ACTIONS.md`),无独立 sortOrder。
- **`bind.tables`**:只决定数据管理出现在哪些表;含 `toolbar` 时**必须**显式非空绑定。
- **`executionProfile`**:只决定执行方式——`sync` 当场返回结果,`async` 进**任务中心**。与 placements / 入口**无关**。心智上多数为 sync,仅长任务 / RPA 标 async;宿主省略该字段时按 sync。每个 Action **仍必须**显式写 `sync` 或 `async`。
@@ -66,7 +67,7 @@ allowed-tools:
1. **禁止**对长任务使用 `exec`/`bash` 跑 `…/scripts/main.py run`(或等价业务命令)再 `process poll` 干等。
2. **禁止**用 `uv run python …` 跑技能脚本(会建临时环境,缺 `jiangchang_skill_core`)。
3. **禁止** Agent 自行 `pip install`。
4. Action **可选**:无数据管理 / 定时 / Agent 结构化调用需求时可不提供 `actions.json`;有长任务 / RPA 时**必须**提供至少一条 `executionProfile: "async"` 且 `placements` 含 `agent` 的 Action。
4. **入口按定位表挂载**:复制业务技能后须在 `development/REQUIREMENTS.md` 填写入口定位表(见 `development/PRODUCT_SURFACES.md`)。定位需要数据管理 / 定时 / Agent 结构化调用时,必须提供 `actions.json` 并声明对应 `placements`;定位确认无需任何宿主结构化入口时可不提供。有长任务 / RPA 时**必须**提供至少一条 `executionProfile: "async"` 且 `placements` 含 `agent` 的 Action。
5. **一个业务能力一个 Action**,用多个 `placements` 挂入口;底层仍是同一 `scripts/main.py` CLI。
6. 是否进任务中心**只**看该 Action 的 `executionProfile`,与数据管理 / 定时 / Agent 入口无关。
@@ -128,7 +129,7 @@ python {baseDir}/scripts/main.py init-db
| 根目录 `CHANGELOG.md` | 普通用户 | 市场 **更新日志** Tab(按版本小节 → `changelog`) |
| `SKILL.md`(本文) | LLM / OpenClaw 平台 | 技能入口、触发与运行契约摘要 |
| `references/` | Agent 编排 / 调用 | CLI、Action、schema 契约(Agent 主读) |
| `development/` | 开发者 / AI 编程代理 | 需求、开发教程、测试、深度技术规范 |
| `development/` | 开发者 / AI 编程代理 | 需求、开发教程、测试、深度技术规范;**产品入口**见 `development/PRODUCT_SURFACES.md` |
## 目录约定
@@ -143,7 +144,7 @@ python {baseDir}/scripts/main.py init-db
1. 从本模板创建新业务技能时,**不得保留模板 `.git`**;优先使用 [`tools/scaffold_skill.ps1`](tools/scaffold_skill.ps1)(见 [`tools/README.md`](tools/README.md))。
2. 业务技能仓库中**不得**存在 `.openclaw-skill-template`(仅 template 源仓库保留)。
3. 复制目录为你的新 skill 仓库,全局替换 `your-skill-slug` 等占位词。
4. 按 `development/DEVELOPMENT.md` 完成开发与文档定制。
4. 按 `development/DEVELOPMENT.md` 完成开发与文档定制;开发前先读 `development/PRODUCT_SURFACES.md` 并填写入口定位表。
5. 用户市场四 Tab 材料写入根 `README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md`;Agent 调用契约见 `references/CLI.md`、`references/SCHEMA.md`、`references/ACTIONS.md`。
6. 同步修改 `scripts/util/constants.py` 中的 `SKILL_SLUG` / `SKILL_VERSION`。
7. 本地 SQLite 的中文表名/字段名、标准时间字段与字段顺序规范见 `references/SCHEMA.md`。

View File

@@ -1,6 +1,6 @@
# assets
- `actions.json`:Skill Action manifest 最小示例(**可选**;不需要宿主入口时可删除)。每个 action **必须**显式声明 `executionProfile`(`sync` \| `async`)。默认仅 `health` / `version` / `config-path`,不放 toolbar,故无 `bind` 示例。
- `actions.json`:Skill Action manifest 最小示例(按入口定位表决定是否保留/扩展;见 `development/PRODUCT_SURFACES.md`)。每个 action **必须**显式声明 `executionProfile`(`sync` \| `async`)。默认仅 `health` / `version` / `config-path`,不放 toolbar,故无 `bind` 示例。
- `html/example-report.html`:HTML 报告页最小壳(`{{TITLE}}` / `{{GENERATED_AT}}` / `{{BODY}}`);渲染后须写入数据目录 `html/`,见 `references/ACTIONS.md`「HTML 页面报告」。
- `examples/`:CLI 成功输出形状示例、以及 `render-html.action.example.json`(虚构路径与数据)。
- `schemas/`:轻量 JSON Schema(`skill-actions.schema.json` 为**新技能严格规范**)。

View File

@@ -1,4 +1,4 @@
{
"version": "1.0.0",
"version": "1.0.91",
"skill": "your-skill-slug"
}

View File

@@ -57,6 +57,8 @@ checklist_version: 1.0
- [ ] S0-6 是否需要在任务中心查看进度或结果:是 / 否
- [ ] S0-7 外部系统账号、数据、设备与操作权限的授权来源:
- [ ] S0-8 是否包含提交、付款、删除、审批、发布、发送等不可逆副作用:是 / 否
- [ ] S0-9 **入口定位表**(见 `PRODUCT_SURFACES.md` / `REQUIREMENTS` §0.1):已填写 / 未填写
- [ ] S0-10 本技能宣称的主用户路径涉及:对话 / 数据管理(执行或仅展示) / 定时(可多选;与定位表一致)
---
@@ -86,6 +88,8 @@ checklist_version: 1.0
- [ ] A3-2 **P0** 缺参或非法输入时稳定失败(可读错误,不假成功)
- [ ] A3-3 **P1** 失败可诊断:能从任务记录/提示判断失败阶段,而非只有“失败了”
- [ ] A3-4 **P0** 同一能力从已声明的不同入口触发时,业务结果一致(不出现入口分叉成两套逻辑)
- [ ] A3-5 **P0** `REQUIREMENTS` 入口定位表已填写;定位为「挂」的入口在 `actions.json`(及必要表元数据)中有对应声明
- [ ] A3-6 **P1** 定位为「不挂」的入口有书面理由(不是空白遗漏)
---
@@ -125,6 +129,7 @@ checklist_version: 1.0
- [ ] A6-3 **P1** 重复执行同一业务键时,结果符合宣称(更新/去重/追加),无莫名脏数据或静默丢数据
- [ ] A6-4 **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` 后自检
---
@@ -164,7 +169,7 @@ checklist_version: 1.0
- [ ] A9-1 **P0** 仓库默认测试套件通过(执行技能约定的一键测试命令,如 `python tests/run_tests.py -v`)
- [ ] A9-2 **P0** 不以「仅模拟档通过」作为交活依据(有真实档要求时必须真实跑通或书面阶段性豁免)
- [ ] A9-3 **P0** **NEED_MANUAL** 已声明的宿主入口(详情/Agent/工具栏/定时等)按声明抽测可通过
- [ ] A9-3 **P0** **NEED_MANUAL** 入口定位表中定为「挂」的宿主入口(详情/Agent/工具栏/定时等)按声明抽测可通过;未在定位表中规划且未声明的入口可标 N/A(须引用定位表理由),**禁止**用「未声明」掩盖「从未做入口定位」
- [ ] A9-4 **P1** 对外版本一致:详情、version、更新日志、发布标签能对上
- [ ] A9-5 **P1** 开发者按平台规则能安装并测到该技能(若平台有开发者可见性要求)

View File

@@ -527,6 +527,7 @@ assets/actions.json → placements / bind / executionProfile / entry
- `db/connection.py`
只做连接和建表(模板默认只建 `_jiangchang_*` 元数据表;业务表按需 CREATE)
**业务表之间禁止 SQLite `FOREIGN KEY` / `REFERENCES`**:只存业务 id,存在性 / 级联 / 删除在 repository/service 处理(见 [`references/SCHEMA.md`](../references/SCHEMA.md)「表与表之间:禁止 SQLite 外键」)
- `db/<domain>_repository.py`
只做业务表增删查改(有业务表才建;模板不再默认提供任务审计表)
@@ -543,17 +544,20 @@ assets/actions.json → placements / bind / executionProfile / entry
- 打印用户提示
- 拼接业务流程
## 11.5 Skill Action manifest(可选)
## 11.5 Skill Action manifest(按入口定位挂载)
若技能需要出现在宿主**数据管理按钮**、**技能详情**、**定时任务**或 **Agent 直接调用**中,提供 `assets/actions.json`。
先读 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md):对话 / 数据管理 / 定时任务**平级**;哪个主能力出现在哪个入口,由本技能 UX 在 `REQUIREMENTS` 入口定位表中定义。
若定位表要求出现在宿主**数据管理按钮**、**技能详情**、**定时任务**或 **Agent 直接调用**中,提供 `assets/actions.json` 并写入对应 `placements`。
- **产品入口与何时走哪个**见 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md)
- **运行时金标准**见 [`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);CLI 另含 `init-db` 供宿主静默建库(不必做成 Action)
- 含 `toolbar` 的 Action:**必须**声明合法 `bind.tables`(`POLICY-SKILL-ACTION-003`)
- 含浏览器 RPA / 长耗时的业务技能:**必须**增加至少一条 `"executionProfile": "async"` 且 `placements` 含 `agent` 的 action(`POLICY-SKILL-ACTION-001`)
- 没有宿主直接操作需求时,**可以删除** `actions.json`;删除后勿保留失效引用
- 入口定位表确认无任何宿主结构化入口需求时,**可以删除** `actions.json`;删除后勿保留失效引用
- scaffold **不会**移除 Schema 中的 `bind` 支持,**不会**生成平台业务 Action 或业务表名;仅替换占位 slug
宿主执行方式:
@@ -818,9 +822,14 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
- 安装后状态正常
- 不会出现缺文件、缺入口或安装失败的问题
### 第七步:按声明做宿主多入口验收(已声明的入口都要测;须看用户体验)
### 第七步:按入口定位与声明做宿主多入口验收(须看用户体验)
安装完成后,不要只停留在“已安装”状态,也**不要**只在本地 mock 通就认为完成。宿主侧栏有多条与技能相关的入口;**按本技能 `assets/actions.json` 的 `placements` / `executionProfile` 声明逐项自测**(契约见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md)、[`../references/ACTIONS.md`](../references/ACTIONS.md))。未声明的入口可以跳过;**已声明却测不到,视为验收失败**。不要只测对话、忽略其它入口。
安装完成后,不要只停留在“已安装”状态,也**不要**只在本地 mock 通就认为完成。对话任务 / 数据管理 / 定时任务在宿主上**平级**(见 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md))。先核对本技能 `REQUIREMENTS` **入口定位表**,再按 `assets/actions.json` 的 `placements` / `executionProfile` 对「定位为挂」的入口逐项自测(字段契约见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md)、[`../references/ACTIONS.md`](../references/ACTIONS.md))。
- 定位为「挂」却未声明或测不到 → **验收失败**
- 定位为「不挂」且写了理由 → 可跳过该入口
- **禁止**从未做入口定位、却用「未声明所以跳过」交差
- 不要只测对话、忽略定位表中已挂的其它入口
**用户体验(与功能入口并列,后台通了不算完):**

View File

@@ -0,0 +1,172 @@
# 宿主产品入口(对话 / 数据管理 / 定时任务)
本文给**技能开发者**与 **AI 编程助手**读:说明匠厂宿主上几个**平级**业务入口各自干什么、**什么时候走哪个**、以及如何为本技能做入口定位。
字段怎么写见 [`../references/ACTIONS.md`](../references/ACTIONS.md)、[`../references/SCHEMA.md`](../references/SCHEMA.md);任务中心 / sync·async 见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md)。
**与其它分类正交:**
| 文档维度 | 回答的问题 |
|----------|------------|
| 本文(产品入口) | 用户从宿主哪条路径触发同一能力 |
| Type A–D / 目录分层 | 技能有没有库、会不会调兄弟技能 |
| RPA 四象限 | 外部系统怎么接 |
不要把三者合并成一个大枚举。
---
## 1. 入口是什么(先立规矩)
技能跑在**匠厂宿主**上,不是独立软件。宿主侧栏里的入口,是同一技能能力的不同**触发场景**:
| 今日常见入口 | 触发方式 | 底层 |
|--------------|----------|------|
| **对话任务**(新建任务) | 自然语言 → Agent | 短 CLI,或 `run_skill_action`(`placements` 含 `agent`) |
| **数据管理** | 点工具栏 / 行 / 批量 / 表单(UI) | `run_skill_action`(`source=data-management`)→ **同一 CLI** |
| **定时任务** | 到点「技能直调」或定时 Agent | `run_skill_action`(`cron` / `cron-agent`)→ **同一 CLI** |
**铁律:**
1. **入口平级**:没有「对话主、数据管理/定时辅」。只是触发场景与交互方式不同,各自有优势。
2. **同一业务能力、同一 domain service**:禁止按入口复制逻辑,禁止按 `source` 分叉业务结果。
3. **同能力可挂多个入口**:不是三选一;按本技能 UX 决定挂哪些。
4. **可扩展**:以后若宿主增加新入口,仍是「新触发场景 + 同一内核」,定位方法不变(定位表多一列即可)。
5. **任务中心不是业务入口**:只表示 `executionProfile: async` 的执行壳;与从哪个入口点进来无关。
---
## 2. 什么时候走哪个入口
### 2.1 对话任务——「用嘴发起」
**适合:**
- 用户用自然语言描述意图,条件一次说清或还要追问
- 探索式:「能不能帮我……」「先看看再决定」
- 对象临时指定(贴链接、说一个范围)
- 需要 Agent 解释结果、失败原因、下一步
- 用户不想打开数据管理,只想在聊天里办完
**技术自问:** 用户会不会用一句话说清这件事?需要模型帮理解和确认吗?——会,就为该能力挂 `agent`(长任务 / RPA 必须 Action + 通常 `async`;短查询可用共享 Python CLI)。
**注意:** 若技能还有「对着表运营 / 定时啃队列」等场景,**不要把对话当成唯一路径**,否则传统用户与省 token 路径会丢掉。
### 2.2 数据管理——「用界面发起 + 看着数据做」
数据管理有两层,**同等重要**:
1. **执行面**:工具栏 / 行级 / 批量 / `replaces` 接管 CRUD —— 与对话一样触发同一 CLI,走完整业务闭环(传统用户可不聊天)。
2. **运营面**:业务表、状态流向、配置、input/output、日志、HTML —— 看清数据从哪来、到哪去、卡在哪。
**适合:**
- 在业务数据上操作:看队列、选一行重试、勾选批量、顶栏「处理 N 条 / 同步」
- 已知流程、参数结构化(表单 / 确认 / 一点即跑)——**省 token**,不必每次过模型
- 准备与执行拆开:表里先准备,再在表上点执行
- 服务不愿用 AI 对话的用户
**技术自问:** 用户会不会打开这张表,靠点选就把主流程跑完?——要,就挂 `toolbar` / `row` / `batch` / `replaces`,并做好 `_jiangchang_*` 中文元数据。
**不要:** 把数据管理理解成「只给开发者看 SQLite」;也不要在完全没有结构化操作对象时硬造空表按钮。
### 2.3 定时任务——「到点自动发起」
**适合:**
- 同一能力需按节奏重复:消费队列、定期同步、巡检
- 无人值守:默认参数即可跑,人不在场也正确、安全
- 与对话 / 数据管理挂**同一个** Action,只改触发方式
**技术自问:** 这件事明天能不能在人不盯着时自动再跑一遍?——能,挂 `cron`;不能,定位表写明不挂及理由。
**慎挂:** 每次对象都不同须人指定;强不可逆且须当场确认;主路径依赖验证码 / 人工介入。
### 2.4 场景对照(贴需求用)
| 若用户场景是… | 优先考虑 | 说明 |
|---------------|----------|------|
| 「帮我处理这个……」(临时、口语) | 对话 | 灵活、可追问 |
| 「对着表点一下就跑」 | 数据管理 | 结构化、省 token、传统用户 |
| 「每天自动跑,不用我管」 | 定时 | 无人值守 |
| 「偶尔对话下发,平时表上运营,夜里自动啃队列」 | **多个入口都挂同一能力** | 平级互补,不是重复开发 |
| 「只看结果,从不在表上触发」 | 表可仅展示;执行挂对话/定时 | 由本技能 UX 定 |
---
## 3. 如何为本技能做入口定位(必做)
模板**不**规定「某类技能必须三入口都挂满」。由技术按**本技能干什么 + 用户体验**填写定位表(写入 [`REQUIREMENTS.md`](REQUIREMENTS.md))。
对每个**主能力**(一条业务 CLI / 一个 Action)填一行:
| 主能力(Action id / CLI) | 对话 | 数据管理 | 定时 | 不挂理由(若不挂) |
|---------------------------|------|----------|------|--------------------|
| (例)`download-run-pick` | 挂 | toolbar | 挂 | — |
| (例)`download-video` | 挂 | 不挂 | 不挂 | 对象每次不同,须人指定链接 |
**数据管理列怎么写:** `不挂` / `toolbar` / `row` / `batch` / `replaces` / `仅展示表(无业务按钮)`(可组合)。
**规则只有三条:**
1. 定位为「挂」的入口:必须在 `actions.json`(及必要元数据)中声明,且宿主可测通,并走同一内核。
2. 定位为「不挂」的入口:必须有一句理由;禁止空白「忘了」。
3. 未来新入口:表上多一列,方法不变。
完整 ≠ 每个命令塞满所有入口;完整 = **想清楚用户路径后,该出现的入口都挂上了,且同内核**。
---
## 4. 怎么挂(最小路径)
| 我想让用户… | 主要改什么 |
|-------------|------------|
| 能在对话里触发 | `SKILL.md` 意图路由 + `actions.json` 的 `placements` 含 `agent`(长任务 `async`) |
| 能在数据管理看见结果 / 过程 | 业务表 + `init-db` + `_jiangchang_*`(见 SCHEMA) |
| 能在表上点按钮走闭环 | `placements` 含 `toolbar`/`row`/`batch`,`toolbar` 须 `bind.tables` |
| 能设定时自动跑 | 同一 Action 的 `placements` 含 `cron`;参数适合无人值守 |
细节与 Schema:[`../references/ACTIONS.md`](../references/ACTIONS.md)。运行时:[`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md)。
---
## 5. 反例(不要这样做)
- 只做对话 Action,从未填写入口定位表,也未考虑数据管理 / 定时用户路径
- 为 toolbar / cron / agent 复制三套业务实现,或按 `source` 分叉结果
- 把数据管理当成只读调试窗,主闭环只能聊天完成(若 UX 上本应支持 UI)
- 不可逆、须确认的主路径默认挂 `cron` 且无产品确认
- 用「Action 可选 / 定时以后再说」当作「可以不思考入口」
---
## 6. 附录:定位表示例(参考,非强制)
### 示例 A:队列消费类(对话 + 表上处理 + 定时)
| 主能力 | 对话 | 数据管理 | 定时 | 理由摘要 |
|--------|------|----------|------|----------|
| 入队 / 准备 | 挂 | 可 replaces 或表新增 | 不挂 | 准备态由人或对话写入 |
| 处理 N 条 pending | 挂 | toolbar | 挂 | 运营点选与无人值守同一内核 |
| 单条立即执行 | 挂 | 可选 row | 不挂 | 对象临时指定 |
### 示例 B:单次确认发布类
| 主能力 | 对话 | 数据管理 | 定时 | 理由摘要 |
|--------|------|----------|------|----------|
| 发布一条 | 挂 | 按是否落草稿表决定 | 不挂 | 不可逆,需人确认;不做无人值守 |
### 示例 C:无库短查询
| 主能力 | 对话 | 数据管理 | 定时 | 理由摘要 |
|--------|------|----------|------|----------|
| 查询 | 挂(或短 CLI) | 不挂 | 不挂 | 无本地业务表与重复批跑需求 |
---
## 7. 相关文档
- 需求里填定位表:[`REQUIREMENTS.md`](REQUIREMENTS.md)
- 交活怎么验:[`CHECKLIST.md`](CHECKLIST.md)
- 开发步骤中的 Action 挂载:[`DEVELOPMENT.md`](DEVELOPMENT.md) §11.5、§15
- Agent 侧字段契约:[`../references/ACTIONS.md`](../references/ACTIONS.md)

View File

@@ -9,13 +9,14 @@
| 步骤 | 做什么 | 详见 |
|------|--------|------|
| 1 | **拿到仓库**:① 项目经理在 [自建 Gitea](https://git.jc2009.com/)(`git.jc2009.com`,非 Gitea 官网)开仓并授权后 `git clone`;或 ② 本地已有 `skill-template` 时用 scaffold / 复制出新目录(遵守 Git 红线) | [`DEVELOPMENT.md`](DEVELOPMENT.md) §1、§4 |
| 2 | **先读 AI + RPA 总纲**;界面技能先完成元素拾取/element map,再按浏览器、桌面或手机读分端手册 | [`AI_RPA.md`](AI_RPA.md)、[`RPA_ELEMENT_LOCATING.md`](RPA_ELEMENT_LOCATING.md) |
| 3 | 按 [`NAMING.md`](NAMING.md) 确认 slug;**先填** [`REQUIREMENTS.md`](REQUIREMENTS.md),再写业务代码 | [`REQUIREMENTS.md`](REQUIREMENTS.md)、[`NAMING.md`](NAMING.md) |
| 4 | 替换标识与占位,按四象限 / examples 实现 `scripts/service/` | [`DEVELOPMENT.md`](DEVELOPMENT.md) §4~§14 |
| 5 | **配置 `developer_ids`(开发自测必做)**:开发期技能在匠厂默认不公开;不加则开发者自己也装测不了。从宿主设置复制用户 ID 写入 `SKILL.md` | [`DEVELOPMENT.md`](DEVELOPMENT.md) §6「关于 developer_ids」 |
| 6 | 本地单测(mock)通过后,按业务默认 **`real_rpa`** 真实跑通;再 `release.ps1`;看 Gitea CI。**mock 通 ≠ 完成** | [`ADAPTER.md`](ADAPTER.md);[`DEVELOPMENT.md`](DEVELOPMENT.md) §15 |
| 7 | 匠厂安装后,按已声明入口自测,并看**用户体验**(进度、失败提示、有头 RPA 等) | [`DEVELOPMENT.md`](DEVELOPMENT.md) §15;[`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) |
| 8 | **交活验收(第三方)**:对照 [`CHECKLIST.md`](CHECKLIST.md) 验收;报告写入仓库根目录 [`acceptance-reports/`](../acceptance-reports/) | [`CHECKLIST.md`](CHECKLIST.md) |
| 2 | **先读宿主产品入口**:对话 / 数据管理 / 定时任务平级;何时走哪个;为本技能填入口定位表 | [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md) |
| 3 | **先读 AI + RPA 总纲**;界面技能先完成元素拾取/element map,再按浏览器、桌面或手机读分端手册 | [`AI_RPA.md`](AI_RPA.md)、[`RPA_ELEMENT_LOCATING.md`](RPA_ELEMENT_LOCATING.md) |
| 4 | 按 [`NAMING.md`](NAMING.md) 确认 slug;**先填** [`REQUIREMENTS.md`](REQUIREMENTS.md)(含入口定位表),再写业务代码 | [`REQUIREMENTS.md`](REQUIREMENTS.md)、[`NAMING.md`](NAMING.md) |
| 5 | 替换标识与占位,按四象限 / examples 实现 `scripts/service/` | [`DEVELOPMENT.md`](DEVELOPMENT.md) §4~§14 |
| 6 | **配置 `developer_ids`(开发自测必做)**:开发期技能在匠厂默认不公开;不加则开发者自己也装测不了。从宿主设置复制用户 ID 写入 `SKILL.md` | [`DEVELOPMENT.md`](DEVELOPMENT.md) §6「关于 developer_ids」 |
| 7 | 本地单测(mock)通过后,按业务默认 **`real_rpa`** 真实跑通;再 `release.ps1`;看 Gitea CI。**mock 通 ≠ 完成** | [`ADAPTER.md`](ADAPTER.md);[`DEVELOPMENT.md`](DEVELOPMENT.md) §15 |
| 8 | 匠厂安装后,按**入口定位表已挂**的入口自测,并看**用户体验**(进度、失败提示、有头 RPA 等) | [`DEVELOPMENT.md`](DEVELOPMENT.md) §15;[`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md);[`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) |
| 9 | **交活验收(第三方)**:对照 [`CHECKLIST.md`](CHECKLIST.md) 验收;报告写入仓库根目录 [`acceptance-reports/`](../acceptance-reports/) | [`CHECKLIST.md`](CHECKLIST.md) |
脚手架与 Git 防串库:[`../tools/README.md`](../tools/README.md)(`scaffold_skill.ps1`)。业务仓也可直接从自建 Gitea clone,见 [`DEVELOPMENT.md`](DEVELOPMENT.md) §4 来源 A。
@@ -25,23 +26,24 @@
开始定制 skill 前,建议按以下顺序阅读:
1. [`AI_RPA.md`](AI_RPA.md) — **AI Agent + RPA 总纲**:职责边界、合规红线、风控、操作节奏与完整闭环
2. [`ACCOUNT_MANAGER.md`](ACCOUNT_MANAGER.md) — **浏览器账号标准**:一账号一 Profile、租约、多账号、数据查看、验收与排障
3. [`SHARED_REPOS.md`](SHARED_REPOS.md) — **公共仓库目录**(模板 / 账号管理 / platform-kit 等,可扩展)
4. [`REQUIREMENTS.md`](REQUIREMENTS.md) — 需求文档模板与验收标准
5. [`NAMING.md`](NAMING.md) — slug / 仓库名命名规范(复制模板前必读)
6. [`DEVELOPMENT.md`](DEVELOPMENT.md) — 完整开发步骤与目录规范
7. [`TESTING.md`](TESTING.md) — 测试分层、隔离数据根与档位开关
8. [`LOGGING.md`](LOGGING.md) — 日志分层、必打节点、敏感信息红线(**涉及长任务、RPA、外部系统时必读**)
9. [`ADAPTER.md`](ADAPTER.md) — 涉及外部系统对接时
10. [`RPA.md`](RPA.md) — 涉及浏览器 / 桌面 / 手机自动化时的会话、风控和工程总纲
11. [`RPA_ELEMENT_LOCATING.md`](RPA_ELEMENT_LOCATING.md) — **所有界面技能必读**:真实拾取、element map、唯一性、降级和 AI 编程约束
12. 按执行端选读:[`RPA_BROWSER.md`](RPA_BROWSER.md) / [`RPA_DESKTOP.md`](RPA_DESKTOP.md) / [`RPA_MOBILE.md`](RPA_MOBILE.md)
13. [`CONFIG.md`](CONFIG.md) — `.env` 规范与 bootstrap 机制
14. [`DATA_PATHS.md`](DATA_PATHS.md) — 下载/导入/导出等本地文件路径标准(涉及文件读写时必读)
15. [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) — Skill Action sync/async、任务中心、Agent 禁令、队列 pick(涉及 RPA / 长任务 / 数据管理按钮 / Cron / Agent 时必读)
16. [`RUNTIME.md`](RUNTIME.md) — 共享 runtime、数据路径、发布打包与编码约定
17. [`CHECKLIST.md`](CHECKLIST.md) — **交活验收清单(第三方视角)**;报告输出到 [`../acceptance-reports/`](../acceptance-reports/)
1. [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md) — **宿主产品入口**:对话 / 数据管理 / 定时任务平级;何时走哪个;入口定位表
2. [`AI_RPA.md`](AI_RPA.md) — **AI Agent + RPA 总纲**:职责边界、合规红线、风控、操作节奏与完整闭环
3. [`ACCOUNT_MANAGER.md`](ACCOUNT_MANAGER.md) — **浏览器账号标准**:一账号一 Profile、租约、多账号、数据查看、验收与排障
4. [`SHARED_REPOS.md`](SHARED_REPOS.md) — **公共仓库目录**(模板 / 账号管理 / platform-kit 等,可扩展)
5. [`REQUIREMENTS.md`](REQUIREMENTS.md) — 需求文档模板与验收标准(含入口定位表)
6. [`NAMING.md`](NAMING.md) — slug / 仓库名命名规范(复制模板前必读)
7. [`DEVELOPMENT.md`](DEVELOPMENT.md) — 完整开发步骤与目录规范
8. [`TESTING.md`](TESTING.md) — 测试分层、隔离数据根与档位开关
9. [`LOGGING.md`](LOGGING.md) — 日志分层、必打节点、敏感信息红线(**涉及长任务、RPA、外部系统时必读**)
10. [`ADAPTER.md`](ADAPTER.md) — 涉及外部系统对接时
11. [`RPA.md`](RPA.md) — 涉及浏览器 / 桌面 / 手机自动化时的会话、风控和工程总纲
12. [`RPA_ELEMENT_LOCATING.md`](RPA_ELEMENT_LOCATING.md) — **所有界面技能必读**:真实拾取、element map、唯一性、降级和 AI 编程约束
13. 按执行端选读:[`RPA_BROWSER.md`](RPA_BROWSER.md) / [`RPA_DESKTOP.md`](RPA_DESKTOP.md) / [`RPA_MOBILE.md`](RPA_MOBILE.md)
14. [`CONFIG.md`](CONFIG.md) — `.env` 规范与 bootstrap 机制
15. [`DATA_PATHS.md`](DATA_PATHS.md) — 下载/导入/导出等本地文件路径标准(涉及文件读写时必读)
16. [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) — Skill Action sync/async、任务中心、Agent 禁令、队列 pick(涉及 RPA / 长任务 / 数据管理按钮 / Cron / Agent 时必读)
17. [`RUNTIME.md`](RUNTIME.md) — 共享 runtime、数据路径、发布打包与编码约定
18. [`CHECKLIST.md`](CHECKLIST.md) — **交活验收清单(第三方视角)**;报告输出到 [`../acceptance-reports/`](../acceptance-reports/)
Agent 调用契约见 [`../references/CLI.md`](../references/CLI.md)、[`../references/SCHEMA.md`](../references/SCHEMA.md)、[`../references/ACTIONS.md`](../references/ACTIONS.md)。
用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.md),不要写进本目录。

View File

@@ -31,7 +31,23 @@
- **登记 ≠ 登录**:ensure/add-web 只建账号与 Profile;站点登录由本技能在有头浏览器中完成
- **反模式**:把「需要登录态才能采到数据」的需求写成 `optional`,再省略 `ensure_logged_in`
## 0.1 AI + RPA 协作与授权边界(涉及外部系统时必填)
## 0.1 宿主入口定位(必填)
先读 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md)。对话任务 / 数据管理 / 定时任务在宿主上**平级**;挂哪些由本技能业务与 UX 定,模板不规定「必须三入口都挂满」。
对每个主能力填一行(可增行;「不挂」必须写理由):
| 主能力(Action id / CLI) | 对话 | 数据管理 | 定时 | 不挂理由(若不挂) |
|---------------------------|------|----------|------|--------------------|
| (例)`<action-or-cli>` | 挂 / 不挂 | 不挂 / toolbar / row / batch / replaces / 仅展示表 | 挂 / 不挂 | |
说明:
- 数据管理列可写组合(如 `toolbar + row`)
- 「仅展示表」= 用户要看过程/结果,但不在表上触发该能力
- 定位为「挂」的入口须在 `assets/actions.json`(及必要表元数据)中落实,并与对话路径共用同一 domain service
## 0.2 AI + RPA 协作与授权边界(涉及外部系统时必填)
### Agent 设计
@@ -131,12 +147,13 @@
- 支持 `python scripts/main.py <your-main-command>`
- 支持核心业务编排(HTTP / 批处理 / 可选 RPA,按四档 adapter 选型)
- 支持失败可诊断(统一文件日志 + 任务中心 `finish`)
- 完成 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md) 入口定位表,并按表落实已挂入口(对话 / 数据管理 / 定时按本技能 UX)
### 可后续扩展
- 支持定时批跑
- 支持失败自动重试
- 支持多 `<目标系统>` 轮询
- 入口定位表中已标明「本期不挂」且写了理由的能力(例如暂不挂定时)
## 5. 非功能要求
@@ -214,7 +231,7 @@
- 真实联调样例仍放 `tests/integration/`,默认套件不包含真实外联(与业务 `.env` 默认 `real_rpa` 不冲突)
- `SKILL.md` 的 `developer_ids` 已配置为开发者本人匠厂用户 ID(开发期不公开时,否则本人无法在市场安装自测;取 ID 步骤见 `development/DEVELOPMENT.md` §6)
- 发布后 Gitea 工作流成功;匠厂技能市场对开发者账号可见最新版本并可安装
- 安装后按本技能 `actions.json` 声明完成宿主多入口验收(见 `development/DEVELOPMENT.md` §15);**须看用户体验**(进度可见、失败可读、RPA 有头、文案与真实行为一致等),后台通了不算完
- 安装后按本技能入口定位表与 `actions.json` 声明完成宿主多入口验收(见 `development/DEVELOPMENT.md` §15、`PRODUCT_SURFACES.md`);**须看用户体验**(进度可见、失败可读、RPA 有头、文案与真实行为一致等),后台通了不算完
- 已人工核验授权与技术路线:无破解、逆向、私有协议、验证码绕过、Hook / 注入、自动换号或其他规避平台限制的实现
- RPA 真实主路径已验证状态等待、`1~3` 秒正常操作节奏、有限重试、账号 / 设备串行、人工介入、结果核验和结束存证
- RPA 关键元素已在真实界面拾取并登记 element map;关键动作唯一定位,重启会话后复测通过
@@ -304,7 +321,7 @@
- 命令可运行;tests/run_tests.py -v 通过
- 业务表(若有)符合 SCHEMA;过程看运行日志,进度看任务中心
- `developer_ids` 已配置为「你的用户ID」(匠厂设置中复制的正整数)
- 正式环境安装验证通过;已声明的宿主入口(新建任务 / 数据管理 / 定时任务 / 任务中心 / 技能详情等)按 DEVELOPMENT §15 自测
- 正式环境安装验证通过;已声明的宿主入口(新建任务 / 数据管理 / 定时任务 / 任务中心 / 技能详情等)按 DEVELOPMENT §15 与入口定位表自测
## 10. 开发注意事项
- 不修改无关项目;不引入旧模板结构

View File

@@ -2,6 +2,8 @@
本文是匠厂宿主 **Skill Action / 任务中心** 的技能侧金标准。涉及浏览器 RPA、长耗时任务、数据管理按钮、定时任务或 Agent 直接调用时**必读**。
**产品入口(平级关系、何时挂哪个)**先读 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md);本文管 sync/async、任务中心与编排。
**边界:**
| 文档 | 职责 |

View File

@@ -2,6 +2,8 @@
本文件说明 `assets/actions.json` 与 `scripts/main.py` CLI 的对应关系,供 **Agent 编排**与宿主 **Skill Action Runtime** 使用。这是 Agent 侧关于 Action / 任务中心的**主契约**;实现细节与开发规范见 [`../development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md)。
**先读产品入口:** 对话 / 数据管理 / 定时任务在宿主上**平级**,何时挂哪个入口见 [`../development/PRODUCT_SURFACES.md`](../development/PRODUCT_SURFACES.md)(本文只讲字段与执行模型)。
**边界:** 用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.md);CLI 参数见 [`CLI.md`](CLI.md)。
## Agent 何时用 Action(与 CLI 的分工)
@@ -48,10 +50,10 @@ JSON Schema 使用 `additionalProperties: false` 表达模板严格边界;这
## Action 是否必需
Action 是**可选能力**:
Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**决定(见 [`../development/PRODUCT_SURFACES.md`](../development/PRODUCT_SURFACES.md)),不是「可有可无的附属能力」:
- 没有宿主直接操作需求的技能,**可以不提供** `actions.json`。
- 需要出现在**数据管理按钮**、**技能详情**、**定时任务**或 **Agent 直接调用**中的技能,**必须**提供 `assets/actions.json`。
- 定位需要出现在**数据管理按钮**、**技能详情**、**定时任务**或 **Agent 直接调用**中时,**必须**提供 `assets/actions.json` 并声明对应入口。
- 定位确认无任何宿主结构化入口时,**可以不提供** `actions.json`。
- **含浏览器 RPA / 长耗时任务**的技能:**必须**提供至少一条 `executionProfile: "async"` 且 `placements` 含 `agent` 的 action(见 `POLICY-SKILL-ACTION-001`)。
## 文件位置
@@ -620,7 +622,7 @@ HINT: <next step>
| 字段 | 必填 | 说明 |
|------|------|------|
| `ref_table` | 是 | 同库被引用表 |
| `value_column` | 是 | 提交给 CLI 的值(编号 / 外键) |
| `value_column` | 是 | 提交给 CLI 的值(业务 id / 编号) |
| `label_column` | 是 | 主文案列 |
| `extra_column` | 否 | 第三展示列(任意列名,不限「备注」) |
| `filter_column` / `filter_value` | 否 | 等值过滤 |
@@ -656,6 +658,7 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件
| URL(薄层) | `display_type` 或 `format`: `url` | `display_type: url` |
| Email(薄层) | `display_type` 或 `format`: `email` | `display_type: email` |
| 说明灰字 | `description` | `_jiangchang_columns.description` |
| 表格横幅 / 列头「?」 | (无;Action 不用) | `_jiangchang_tables.grid_help` / `_jiangchang_columns.grid_help` |
| 默认值 | `default` | SQLite 列 `DEFAULT` |
| 只读 | `readOnly: true` | 列 `editable: 0` / 宿主只读规则 |
| **条件显隐** | **`visibleWhen`**(见下节) | **本版 CRUD 无对称**;仅 Action / 定时任务参数表单 |
@@ -725,6 +728,8 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件
| Skill Action 弹窗 | `inputSchema.title` | `inputSchema.description` | `inputSchema.default` | `type` + `display_type`/`format` |
| 数据管理新增/编辑 | `_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)「表单与展示:稳定字段类型」「新增/编辑表单:字段说明与默认值」。同一业务含义两边文案与控件应对齐,但分别配置。
示例:

View File

@@ -17,7 +17,7 @@
| 根目录 [`CHANGELOG.md`](../CHANGELOG.md) | 市场更新日志 Tab(版本小节 → `changelog`) |
| [`SKILL.md`](../SKILL.md) | LLM / OpenClaw 平台技能入口与触发摘要 |
| [`CLI.md`](CLI.md) | 共享 Python、命令行入口、参数与短操作契约 |
| [`ACTIONS.md`](ACTIONS.md) | Skill Action、sync/async、任务中心、多入口(可选能力) |
| [`ACTIONS.md`](ACTIONS.md) | Skill Action、sync/async、任务中心、多入口(按入口定位表挂载;先读 `development/PRODUCT_SURFACES.md`) |
| [`SCHEMA.md`](SCHEMA.md) | 本地 SQLite 与数据管理展示契约 |
| [`POLICY.md`](POLICY.md) | Data Policy:可选身份授权、Snapshot、行 scope(多数技能 L0 不配) |
| [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md) | 数据管理单元格呈现:语义 tone 标色/强调(用途不限;多数技能 P0 不配) |

View File

@@ -1,6 +1,9 @@
# 数据存储与数据管理展示规范
本文是 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()` 会一并初始化。
## 职责边界
@@ -10,7 +13,7 @@
- 定义真实 SQLite 表结构
- 使用英文 `snake_case` 表名和字段名
- 使用 `_jiangchang_tables` 提供表级展示元数据
- 使用 `_jiangchang_columns` 提供字段级展示元数据(含可选 `description` 字段说明)
- 使用 `_jiangchang_columns` 提供字段级展示元数据(含可选 `description` 表单说明、可选 `grid_help` 表格说明)
- 需要「新增表单预填」时,在物理列上声明 SQLite `DEFAULT`(勿把默认值只写在文案里)
- 定义哪些字段可见、可搜索、可创建、可编辑
- 定义字段展示类型和枚举选项
@@ -25,6 +28,7 @@
- 根据 schema 渲染数据管理页面
- 根据 `editable` 推导字段是否可创建或编辑
- 在**新增 / 编辑**表单中,若列有 `_jiangchang_columns.description`,在标签下方展示该说明(灰字;空则不显示)
- 在数据管理**表格**中,若表有 `_jiangchang_tables.grid_help` 则在表上方展示可折叠说明;若列有 `_jiangchang_columns.grid_help` 则在列头显示「?」;空/未写则不渲染。**不**把 `description` 当作表格说明
- 打开**新增**表单时,对可创建字段用 SQLite 列 `DEFAULT`(`PRAGMA table_info.dflt_value`)预填;无 DEFAULT 则留空
- 执行通用查询、新增、编辑、删除、导入、导出
- 对字段名、表名、类型和权限进行最终校验
@@ -85,6 +89,23 @@ metadata:
若创建/更新/删除带有领域副作用(自动建目录、加密、级联清理),不要依赖宿主默认直写:在 `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」类能力。使用通用名称表述,勿把具体平台表名写进模板实现。
@@ -135,7 +156,8 @@ metadata:
|------|------|------|
| `table_name` | 是 | 真实英文表名 |
| `display_name` | 是 | 用户可见的中文业务名称 |
| `description` | 否 | 表说明 |
| `description` | 否 | 表说明(表单/备注用途);**不**用于表格上方横幅 |
| `grid_help` | 否 | 数据管理表格上方说明;空/未写则不出横幅。与 `description` 独立,禁止互拷(见下节) |
| `sort_order` | 否 | 兼容保留字段,不用于替代物理字段顺序 |
| `visible` | 是 | `1` 在宿主数据管理中展示 / `0` 隐藏 |
| `readonly` | 是 | `1` 整张表只读;`0` 允许宿主根据字段权限执行写操作 |
@@ -209,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,
)
```
禁止:
- 使用中文真实表名
@@ -225,6 +284,7 @@ upsert_table_metadata(
| `column_name` | 是 | 真实英文字段名 |
| `display_name` | 是 | 用户可见的中文字段名(表头与表单标签);保持简短 |
| `description` | 否 | 字段说明(给人看的短文案);**宿主在新增/编辑表单标签下方展示**(见下节);不要塞进 `display_name` |
| `grid_help` | 否 | 数据管理表头「?」;空/未写则不出。与表单 `description` 独立,禁止互拷 |
| `display_order` | 否 | 兼容字段,默认保持 `NULL` |
| `visible` | 是 | 是否进入数据管理 schema /「列」面板(`0` = **硬隐藏**,用户无法勾回;默认不展示但可勾回请用表级 `default_visible_columns_json`) |
| `searchable` | 是 | 是否参与搜索 |
@@ -248,7 +308,7 @@ upsert_table_metadata(
| `number` | `INTEGER` / `REAL` / `NUMERIC` | 原文 | **数字输入**(`type=number`) | 数字;无 `display_type` 时,物理类型为 INT/REAL/NUMERIC 也应走数字控件 |
| `boolean` | `INTEGER` 0/1 或 BOOL | 开关语义展示 | 开关 | `0` / `1` |
| (枚举) | 任意 + `options_json` **数组** | 标签映射 | 下拉选择 | 选项 `value` |
| `lookup` | 外键式标量 + `options_json` **对象** | 标签映射 | 下拉选择(查关联表) | 关联键值 |
| `lookup` | 跨表引用 id 标量 + `options_json` **对象** | 标签映射 | 下拉选择(查关联表) | 业务 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 字符串 |
@@ -274,7 +334,9 @@ upsert_table_metadata(
| 字段说明 | `_jiangchang_columns.description`(可选) | 新增/编辑表单中,在标签下方以灰字展示;空则不显示 |
| 创建默认值 | 物理列 SQLite `DEFAULT`(如 `limit_n INTEGER NOT NULL DEFAULT 0`) | 打开「新增」时,对可创建字段预填该默认值;无 DEFAULT 则留空 |
| 编辑已有行 | — | **不**用 DEFAULT 覆盖已有行数据 |
| 表格列头 | 只用 `display_name` | 长说明**不**进入列头 |
| 表格列头标题 | `display_name` | 短名;不要把长说明塞进列头文字 |
| 表格列头「?」 | `_jiangchang_columns.grid_help`(可选) | 有内容才显示「?」;**不**回退 `description` |
| 表格上方说明 | `_jiangchang_tables.grid_help`(可选) | 有内容才显示横幅;**不**回退表级 `description` |
规则与禁止:
@@ -509,9 +571,9 @@ CREATE TABLE demo_items (
宿主据此渲染选择框;技能侧负责保证 JSON 合法且与物理字段取值一致。
### 2)表关联 lookup(对象,推荐用于外键式字段)
### 2)跨表引用 lookup(对象,用于关联 id 字段)
当字段取值依赖同库另一张业务表时,`display_type` 设为 `lookup`,`options_json` 写对象(**不是**数组):
当字段取值依赖同库另一张业务表时,`display_type` 设为 `lookup`,`options_json` 写对象(**不是**数组)。这是**展示与表单下拉**契约,**不是** SQLite 外键:
```json
{
@@ -527,7 +589,7 @@ CREATE TABLE demo_items (
| 字段 | 必填 | 说明 |
|---|---|---|
| `ref_table` | 是 | 同库被引用表名(snake_case 标识符) |
| `value_column` | 是 | 写入当前字段的值列(编号 / 外键) |
| `value_column` | 是 | 写入当前字段的值列(业务 id / 编号) |
| `label_column` | 是 | 下拉主文案列(名称) |
| `extra_column` | 否 | 第三展示列(不必叫「备注」);有值时拼进括号 |
| `filter_column` / `filter_value` | 否 | 等值过滤(如只列出 `enabled=1`) |
@@ -549,7 +611,7 @@ Action 弹窗内联 Lookup 与上表 **同构**(字段名一致),见 [`ACT
- `_jiangchang_tables` 和 `_jiangchang_columns` 必须通过 upsert 初始化
- 多次执行初始化不能重复记录
- 重新初始化可以更新展示名、描述、`visible`、`searchable`、`editable`、`display_type`、`options_json`
- 重新初始化可以更新展示名、描述、`grid_help`、`visible`、`searchable`、`editable`、`display_type`、`options_json`
- 元数据不能残留旧字段(删除或重命名字段时必须同步清理)
- 元数据不能引用不存在的表或字段
- 新增字段或表时必须同步增加元数据

View File

@@ -17,7 +17,7 @@
[CmdletBinding()]
param(
[string]$Prefix = "v",
[string]$Message = "正式发布",
[string]$Message,
[switch]$AutoCommit,
[switch]$RequireClean,
[string]$CommitMessage,
@@ -28,6 +28,13 @@ param(
Set-StrictMode -Version Latest
$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 {
param([string[]]$Lines)
return @($Lines | Where-Object {
@@ -35,40 +42,85 @@ function Remove-GitNoise {
})
}
function Convert-GitOutputLines {
param($Output)
return @(
@($Output) | ForEach-Object {
if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.ToString() }
else { "$_" }
}
)
}
# Call git.exe directly. Never wrap with `cmd /c`: on Chinese Windows, cmd
# re-encodes the command line through the OEM code page.
# Also never pass Chinese via `git -m`: Windows PowerShell 5.1 encodes native
# argv using the console code page, so 「正式发布」 becomes 「姝e紡鍙戝竷」
# in the annotated tag. Commit/tag messages go through a UTF-8 file + `-F`.
function Invoke-GitRaw {
param([Parameter(Mandatory = $true)][string[]]$GitArgs)
$prev = $ErrorActionPreference
$ErrorActionPreference = "Continue"
try {
$output = & git @GitArgs 2>&1
$code = $LASTEXITCODE
} finally {
$ErrorActionPreference = $prev
}
return [pscustomobject]@{
ExitCode = $code
Lines = @(Remove-GitNoise -Lines @(Convert-GitOutputLines -Output $output))
}
}
function Invoke-Git {
param([Parameter(Mandatory = $true)][string]$Args)
Write-Host ">> git $Args" -ForegroundColor DarkGray
& cmd /c "git $Args 2>&1" | Out-Null
if ($LASTEXITCODE -ne 0) {
throw "git command failed: git $Args"
param([Parameter(Mandatory = $true)][string[]]$GitArgs)
Write-Host (">> git " + ($GitArgs -join " ")) -ForegroundColor DarkGray
$r = Invoke-GitRaw -GitArgs $GitArgs
if ($r.ExitCode -ne 0) {
throw "git command failed: git $($GitArgs -join ' ')"
}
}
function Invoke-GitWithMessageFile {
param(
[Parameter(Mandatory = $true)][string[]]$GitArgsPrefix,
[Parameter(Mandatory = $true)][string]$Message
)
$msgFile = Join-Path ([System.IO.Path]::GetTempPath()) ("jiangchang-git-msg-" + [guid]::NewGuid().ToString("N") + ".txt")
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText($msgFile, ($Message.TrimEnd() + "`n"), $utf8NoBom)
try {
Invoke-Git -GitArgs (@("-c", "i18n.commitEncoding=utf-8") + $GitArgsPrefix + @("-F", $msgFile))
} finally {
Remove-Item -LiteralPath $msgFile -Force -ErrorAction SilentlyContinue
}
}
function Get-GitOutput {
param([Parameter(Mandatory = $true)][string]$Args)
$output = & cmd /c "git $Args 2>&1"
if ($LASTEXITCODE -ne 0) {
throw "git command failed: git $Args"
param([Parameter(Mandatory = $true)][string[]]$GitArgs)
$r = Invoke-GitRaw -GitArgs $GitArgs
if ($r.ExitCode -ne 0) {
throw "git command failed: git $($GitArgs -join ' ')"
}
return @(Remove-GitNoise -Lines @($output))
return @($r.Lines)
}
function Test-Repo {
$output = & cmd /c "git rev-parse --is-inside-work-tree 2>&1"
if ($LASTEXITCODE -ne 0) {
$r = Invoke-GitRaw -GitArgs @("rev-parse", "--is-inside-work-tree")
if ($r.ExitCode -ne 0) {
return $false
}
$clean = Remove-GitNoise -Lines @($output)
return (($clean | Select-Object -First 1) -eq "true")
return (($r.Lines | Select-Object -First 1) -eq "true")
}
function Get-CurrentBranch {
$b = (Get-GitOutput "branch --show-current" | Select-Object -First 1).Trim()
$b = (Get-GitOutput -GitArgs @("branch", "--show-current") | Select-Object -First 1).Trim()
return $b
}
function Get-StatusPorcelain {
$lines = @(Get-GitOutput "status --porcelain")
$lines = @(Get-GitOutput -GitArgs @("status", "--porcelain"))
return $lines
}
@@ -92,7 +144,7 @@ function Parse-SemVerTag {
function Get-NextTag {
param([string]$TagPrefix)
$tags = Get-GitOutput "tag --list"
$tags = Get-GitOutput -GitArgs @("tag", "--list")
$parsed = @()
foreach ($t in $tags) {
@@ -181,7 +233,7 @@ function Ensure-CleanOrAutoCommit {
if ($NeedClean) {
Write-Host "Working tree is not clean and -RequireClean is enabled." -ForegroundColor Yellow
Remove-GitNoise -Lines @(& cmd /c "git status --short 2>&1") | ForEach-Object { Write-Host $_ }
(Invoke-GitRaw -GitArgs @("status", "--short")).Lines | ForEach-Object { Write-Host $_ }
throw "Abort: dirty working tree."
}
@@ -200,8 +252,8 @@ function Ensure-CleanOrAutoCommit {
return
}
Invoke-Git "add -A"
Invoke-Git "commit -m `"$commitMsg`""
Invoke-Git -GitArgs @("add", "-A")
Invoke-GitWithMessageFile -GitArgsPrefix @("commit") -Message $commitMsg
}
try {
@@ -223,10 +275,10 @@ try {
if ($DryRun) {
Write-Host "[DryRun] Would run: git fetch --tags --prune origin" -ForegroundColor Yellow
} else {
Invoke-Git "fetch --tags --prune origin"
Invoke-Git -GitArgs @("fetch", "--tags", "--prune", "origin")
}
$skillRoot = (Get-GitOutput "rev-parse --show-toplevel" | Select-Object -First 1).Trim()
$skillRoot = (Get-GitOutput -GitArgs @("rev-parse", "--show-toplevel") | Select-Object -First 1).Trim()
if (Test-Path -LiteralPath (Join-Path $skillRoot "SKILL.md")) {
Assert-SkillReleasePackagingSources -SkillRoot $skillRoot
}
@@ -235,8 +287,8 @@ try {
Ensure-CleanOrAutoCommit -DoAutoCommit:$AutoCommit -NeedClean:$RequireClean -IsDryRun:$DryRun -Msg $CommitMessage
$null = Remove-GitNoise -Lines @(& cmd /c 'git rev-parse --abbrev-ref --symbolic-full-name "@{u}" 2>&1')
$hasUpstream = ($LASTEXITCODE -eq 0)
$upstream = Invoke-GitRaw -GitArgs @("rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}")
$hasUpstream = ($upstream.ExitCode -eq 0)
if ($DryRun) {
if ($hasUpstream) {
@@ -246,29 +298,29 @@ try {
}
} else {
if ($hasUpstream) {
Invoke-Git "push"
Invoke-Git -GitArgs @("push")
} else {
Invoke-Git "push -u origin $branch"
Invoke-Git -GitArgs @("push", "-u", "origin", $branch)
}
}
$nextTag = Get-NextTag -TagPrefix $Prefix
Write-Host "Next tag: $nextTag" -ForegroundColor Green
$existing = @(Get-GitOutput "tag --list `"$nextTag`"")
$existing = @(Get-GitOutput -GitArgs @("tag", "--list", $nextTag))
if ($existing.Length -gt 0) {
throw "Tag already exists: $nextTag"
}
if ($DryRun) {
Write-Host "[DryRun] Would run: git tag -a $nextTag -m `"$Message`"" -ForegroundColor Yellow
Write-Host "[DryRun] Would run: git tag -a $nextTag -F <utf8-msg-file> ($Message)" -ForegroundColor Yellow
Write-Host "[DryRun] Would run: git push origin $nextTag" -ForegroundColor Yellow
Write-Host "=== DryRun Complete ===" -ForegroundColor Cyan
exit 0
}
Invoke-Git "tag -a $nextTag -m `"$Message`""
Invoke-Git "push origin $nextTag"
Invoke-GitWithMessageFile -GitArgsPrefix @("tag", "-a", $nextTag) -Message $Message
Invoke-Git -GitArgs @("push", "origin", $nextTag)
Write-Host "Release success: $nextTag" -ForegroundColor Green
Write-Host "=== Release Script Done ===" -ForegroundColor Cyan

View File

@@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS _jiangchang_tables (
table_name TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
description TEXT,
grid_help TEXT,
sort_order INTEGER,
visible INTEGER NOT NULL DEFAULT 1,
readonly INTEGER NOT NULL DEFAULT 0,
@@ -35,6 +36,7 @@ CREATE TABLE IF NOT EXISTS _jiangchang_columns (
column_name TEXT NOT NULL,
display_name TEXT NOT NULL,
description TEXT,
grid_help TEXT,
display_order INTEGER,
visible 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:
"""旧库补列:CREATE IF NOT EXISTS 不会给已有表加新字段。"""
cursor.execute("PRAGMA table_info('_jiangchang_tables')")
cols = {str(row[1]) for row in cursor.fetchall()}
cols = _pragma_column_names(cursor, "_jiangchang_tables")
if "default_sort_json" not in cols:
cursor.execute("ALTER TABLE _jiangchang_tables ADD COLUMN default_sort_json TEXT")
if "default_visible_columns_json" not in cols:
cursor.execute(
"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:
cursor.execute(_JIANGCHANG_TABLES_DDL)
cursor.execute(_JIANGCHANG_COLUMNS_DDL)
_ensure_jiangchang_tables_columns(cursor)
_ensure_jiangchang_columns_columns(cursor)
def upsert_table_metadata(
@@ -69,6 +84,7 @@ def upsert_table_metadata(
table_name: str,
display_name: str,
description: str | None = None,
grid_help: str | None = None,
visible: int = 1,
readonly: int = 0,
default_sort_json: str | None = None,
@@ -83,17 +99,20 @@ def upsert_table_metadata(
default_visible_columns_json: 宿主数据管理默认展示列,JSON 字符串数组,例如
'["stat_type","period","stage"]'
为空则全显;用户改过「列」后以本机偏好为准(软默认,区别于 visible=0 硬隐藏)。
grid_help: 数据管理表格上方说明;空则不出横幅。与表单用的 description 独立,禁止互拷。
"""
cursor.execute(
"""
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
)
VALUES (?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(table_name) DO UPDATE SET
display_name = excluded.display_name,
description = excluded.description,
grid_help = excluded.grid_help,
visible = excluded.visible,
readonly = excluded.readonly,
default_sort_json = excluded.default_sort_json,
@@ -103,6 +122,7 @@ def upsert_table_metadata(
table_name,
display_name,
description,
grid_help,
visible,
readonly,
default_sort_json,
@@ -118,6 +138,7 @@ def upsert_column_metadata(
column_name: str,
display_name: str,
description: str | None = None,
grid_help: str | None = None,
visible: int = 1,
searchable: int = 1,
editable: int = 1,
@@ -125,16 +146,18 @@ def upsert_column_metadata(
options_json: str | None = None,
) -> None:
# display_order 保留为 NULL:宿主按 PRAGMA table_info cid 展示字段,不读 display_order。
# grid_help: 表头「?」;空则不出。与表单用的 description 独立,禁止互拷。
cursor.execute(
"""
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
)
VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?)
ON CONFLICT(table_name, column_name) DO UPDATE SET
display_name = excluded.display_name,
description = excluded.description,
grid_help = excluded.grid_help,
display_order = NULL,
visible = excluded.visible,
searchable = excluded.searchable,
@@ -147,6 +170,7 @@ def upsert_column_metadata(
column_name,
display_name,
description,
grid_help,
visible,
searchable,
editable,
@@ -171,6 +195,7 @@ def seed_column_metadata_batch(
column_name=str(column["column_name"]),
display_name=str(column["display_name"]),
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)),
searchable=int(column.get("searchable", 1)),
editable=int(column.get("editable", 1)),

View File

@@ -30,6 +30,7 @@ _ENGLISH_PLACEHOLDER_NAME_PATTERNS = (
)
_KEBAB_SLUG = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
_CJK_RE = re.compile(r"[\u4e00-\u9fff]")
_FK_DDL_PATTERN = re.compile(r"\bFOREIGN\s+KEY\b|\bREFERENCES\b", re.I)
@dataclass
@@ -412,6 +413,27 @@ def _table_exists(cursor, table_name: str) -> bool:
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:
report = ValidationReport()
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:
return report
fk_errors, _ = validate_no_sqlite_foreign_keys(cur)
report.errors.extend(fk_errors)
for table_name in iter_user_tables(cur):
cur.execute(
"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:
"""模板自检入口:校验展示元数据完整性。模板默认无业务表。"""
"""模板自检入口:校验展示元数据完整性,并扫描业务表 DDL 是否含 SQLite 外键。"""
return validate_display_metadata(conn)

View File

@@ -1,6 +1,6 @@
"""技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。"""
SKILL_SLUG = "your-skill-slug"
SKILL_VERSION = "1.0.84"
SKILL_VERSION = "1.0.91"
LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug"
PLATFORM_KIT_MIN_VERSION = "1.2.11"

View File

@@ -225,6 +225,37 @@ class TestDisplayMetadata(unittest.TestCase):
finally:
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:
conn = sqlite3.connect(":memory:")
try:
@@ -578,6 +609,100 @@ metadata:
finally:
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__":
unittest.main()

View File

@@ -348,6 +348,11 @@ class TestDocsStandards(unittest.TestCase):
"文件 / 图片上传",
"default_visible_columns_json",
"默认展示列",
"grid_help",
"表格说明 `grid_help`",
"表与表之间:禁止 SQLite 外键",
"禁止 SQLite 外键",
"跨表引用 lookup",
):
self.assertIn(marker, text, msg=f"SCHEMA.md missing {marker!r}")
@@ -365,6 +370,7 @@ class TestDocsStandards(unittest.TestCase):
"SCHEMA.md",
"字段条件显隐",
"visibleWhen",
"grid_help",
):
self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}")