Compare commits

...

6 Commits

Author SHA1 Message Date
0a6b0171d8 docs: settle work item lifecycle contract (v1.0.95)
All checks were successful
技能自动化发布 / release (push) Successful in 6s
2026-08-21 16:30:34 +08:00
43486afec4 docs: dedupe changelog after lookup.multiple contract (v1.0.94)
All checks were successful
技能自动化发布 / release (push) Successful in 5s
2026-08-21 16:20:53 +08:00
225d5c0777 docs: add lookup.multiple multi-select contract (v1.0.93)
All checks were successful
技能自动化发布 / release (push) Successful in 5s
2026-08-21 16:19:13 +08:00
fbf326d525 Document replaces:import and add CRUD/import action examples
All checks were successful
技能自动化发布 / release (push) Successful in 7s
2026-08-21 11:57:38 +08:00
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
26 changed files with 913 additions and 71 deletions

View File

@@ -9,6 +9,40 @@
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
## 1.0.95
- 正式沉淀「业务行生命周期」通用契约(`references/WORK_ITEM_STATUS.md`):用可操作/进行中/成功·失败·跳过类终态等**角色**约束处理队列,不绑死某一技能的状态名
- 默认只处理可操作态;开始抢占进行中;结束回写成功或失败(失败带原因);重做靠人工改回可操作
- REQUIREMENTS §0.3 状态→角色映射;CHECKLIST S0-11 / A6-7~A6-9;SKILL / ACTIONS / SCHEMA / references 索引交叉引用
## 1.0.94
- 修正 1.0.93 更新日志重复小节
## 1.0.93
- 沉淀 Lookup **多选**契约:同一 `lookup` / `options_json` 对象上加 `multiple: true`(平行扩展单选,不另造 display_type)
- 多选存 `TEXT` 逗号分隔 id;列表展示映射为多个 label;控件由宿主按选项数量选择(短列表勾选 / 长列表 Chip+下拉)
- 更新 SCHEMA / ACTIONS / skill-actions.schema / lookup 示例与 options_json 校验
## 1.0.92
- 沉淀数据管理 `replaces: import`:导入 UI 统一宿主面板,确认后由技能写库
- ACTIONS / schema / 守护测试对齐 create·update·delete·import;补充可抄示例 `replaces-*.action.example.json`
- 修正旧文档「宿主导入仍直写」的过时说明
## 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 许可证,版权归属深圳匠厂科技有限公司

View File

@@ -1,7 +1,7 @@
---
name: 技能开发模板(通用业务版)
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
version: 1.0.89
version: 1.0.95
author: 深圳匠厂科技有限公司
metadata:
openclaw:
@@ -38,6 +38,7 @@ allowed-tools:
- **`executionProfile`**:只决定执行方式——`sync` 当场返回结果,`async` 进**任务中心**。与 placements / 入口**无关**。心智上多数为 sync,仅长任务 / RPA 标 async;宿主省略该字段时按 sync。每个 Action **仍必须**显式写 `sync` 或 `async`。
- **`policy`(可选)**:需要按用户/角色控制数据管理按钮或行列时,在 `actions.json` 顶层启用 Data Policy,由技能 `policy` CLI 输出 Snapshot;**未启用 = 与今日一致(L0)**。详见 `references/POLICY.md`。用户/角色数据源(飞书/库/API)属技能内部,宿主不托管。
- **`cellPresentation`(可选)**:需要数据管理「某格特殊标色/强调」时启用;技能按当前数据写入呈现(语义 tone),宿主映射主题样式;**未启用 = P0 无标色**。用途不限(超时、预警、驳回等)。详见 `references/CELL_PRESENTATION.md`。
- **业务行生命周期(有处理队列时)**:对某一行做生成/触达/私信等处理时,默认只操作「可操作」角色状态;开始抢占「进行中」;结束回写成功或失败(失败带原因);重做靠人工改回可操作。状态名由本技能自定。详见 `references/WORK_ITEM_STATUS.md`。
- **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;文件型特殊报告才另做 `export-*`(落 `output/`);**HTML 整页报告**写 `html/` 并用固定 Action id `render-html`(见 `references/ACTIONS.md`)。
- **准备 vs 执行(适用则拆)**:若技能存在「先写入/准备本地数据」与「再对外产生副作用」两段,Agent 必须能分开触发;禁止默认绑成不可拆的一步。一键无准备态的技能可跳过本条。
@@ -70,8 +71,9 @@ allowed-tools:
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 入口无关。
7. **处理业务行**时遵守状态角色契约(可操作 / 进行中 / 终态);禁止对已成功/失败/跳过默认重复处理。详见 `references/WORK_ITEM_STATUS.md`。
Agent 执行契约细节:`references/CLI.md`、`references/ACTIONS.md`、`references/SCHEMA.md`、按需 `references/POLICY.md`(不要把 `development/` 当 Agent 主读路径)。
Agent 执行契约细节:`references/CLI.md`、`references/ACTIONS.md`、`references/SCHEMA.md`、按需 `references/POLICY.md` / `references/WORK_ITEM_STATUS.md`(不要把 `development/` 当 Agent 主读路径)。
## 共享 Python(必读)

View File

@@ -4,5 +4,6 @@
- `skill-policy-snapshot.*.example.json`:Data Policy Snapshot 形状参考(见 `references/POLICY.md`)。
- `skill-cell-presentation.example.json`:数据管理单元格呈现文档形状参考(见 `references/CELL_PRESENTATION.md`)。
- `render-html.action.example.json`:HTML 报告「重新生成」Action 形状参考(固定 id `render-html`;见 `references/ACTIONS.md`)。
- `lookup-action.example.json`:Action 内联 Lookup 下拉形状参考(跨表引用勿填编号;见 `references/ACTIONS.md`「跨表引用:Lookup」)。
- `lookup-action.example.json`:Action 内联 Lookup 形状参考(单选 + `multiple: true` 多选并列;跨表引用勿填编号;见 `references/ACTIONS.md`「跨表引用:Lookup」)。
- `visible-when-action.example.json`:Action 参数字段条件显隐 `visibleWhen` 形状参考(互斥模式联动;见 `references/ACTIONS.md`「字段条件显隐」;≠ Policy L3)。
- `replaces-create.action.example.json` / `replaces-update.action.example.json` / `replaces-delete.action.example.json` / `replaces-import.action.example.json`:数据管理默认按钮接管(`replaces`)形状参考;**import 时 UI 仍用宿主「导入数据」面板**,确认后调技能写库(见 `references/ACTIONS.md`「`replaces`」)。

View File

@@ -1,12 +1,12 @@
{
"_comment": "形状参考:Action 参数引用同库另一张表时用 Lookup 下拉,勿让用户填内部编号。完整契约见 references/ACTIONS.md「跨表引用:Lookup」与 references/SCHEMA.md。",
"_comment": "形状参考:Action 参数引用同库另一张表时用 Lookup。单选与多选同一 lookup 对象,多选只加 multiple:true。完整契约见 references/ACTIONS.md「跨表引用:Lookup」与 references/SCHEMA.md。",
"schemaVersion": 1,
"skill": "your-skill-slug",
"actions": [
{
"id": "send-with-template",
"label": "按模板发送(示例)",
"description": "演示内联 lookup:从 message_templates 选模板;-- 表示自动挑选。",
"description": "演示内联 lookup 单选:从 message_templates 选模板;-- 表示自动挑选。",
"placements": ["toolbar"],
"executionProfile": "async",
"interaction": "form",
@@ -43,6 +43,41 @@
}
}
}
},
{
"id": "probe-with-engines",
"label": "按引擎抽检(多选示例)",
"description": "演示 lookup.multiple:从 engines 多选,提交为逗号分隔 id。",
"placements": ["toolbar"],
"executionProfile": "async",
"interaction": "form",
"bind": {
"tables": ["queries"]
},
"entrypoint": {
"type": "cli",
"command": "query",
"args": ["batch-probe", "--engine-ids", "{{engineIds}}"]
},
"inputSchema": {
"type": "object",
"properties": {
"engineIds": {
"type": "string",
"title": "将使用的引擎",
"description": "可多选;提交值为逗号分隔编号,如 1,2。",
"display_type": "lookup",
"lookup": {
"ref_table": "engines",
"value_column": "id",
"label_column": "display_name",
"filter_column": "enabled",
"filter_value": 1,
"multiple": true
}
}
}
}
}
]
}

View File

@@ -0,0 +1,28 @@
{
"id": "item-create",
"label": "新增记录",
"description": "新增业务记录并执行领域副作用(走技能 CLI,非宿主直写库)。",
"replaces": "create",
"placements": [],
"executionProfile": "sync",
"interaction": "form",
"bind": {
"tables": ["items"]
},
"hostEffects": {
"onSuccess": ["reloadTable"]
},
"inputSchema": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string", "title": "名称" },
"note": { "type": "string", "title": "备注" }
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["create", "--name", "{{name}}", "--note", "{{note}}"]
}
}

View File

@@ -0,0 +1,33 @@
{
"id": "item-delete",
"label": "删除记录",
"description": "删除业务记录并清理关联副作用。",
"replaces": "delete",
"placements": [],
"executionProfile": "sync",
"interaction": "confirm",
"confirmation": {
"message": "确定删除该记录?关联数据将一并清理。"
},
"bind": {
"tables": ["items"],
"inputMapping": {
"id": "$row.$pk"
}
},
"hostEffects": {
"onSuccess": ["reloadTable", "clearSelection"]
},
"inputSchema": {
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "number", "title": "编号", "readOnly": true }
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["delete", "--id", "{{id}}"]
}
}

View File

@@ -0,0 +1,38 @@
{
"id": "item-import",
"label": "导入记录",
"description": "由宿主「导入数据」面板调用:解析 Excel/CSV 并写入业务表(含领域校验与派生字段)。",
"replaces": "import",
"placements": [],
"executionProfile": "sync",
"interaction": "form",
"bind": {
"tables": ["items"]
},
"hostEffects": {
"onSuccess": ["reloadTable"]
},
"inputSchema": {
"type": "object",
"required": ["filePath"],
"properties": {
"filePath": {
"type": "string",
"title": "导入文件",
"description": "由宿主导入面板传入的 Excel/CSV 路径。",
"format": "file"
},
"onError": {
"type": "string",
"title": "导入规则(出错时)",
"enum": ["跳过该行,继续导入", "立即停止,不再导入后续行"],
"default": "跳过该行,继续导入"
}
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["import", "--file", "{{filePath}}", "--on-error", "{{onError}}"]
}
}

View File

@@ -0,0 +1,34 @@
{
"id": "item-update",
"label": "编辑记录",
"description": "编辑业务记录并重算派生字段。",
"replaces": "update",
"placements": [],
"executionProfile": "sync",
"interaction": "form",
"bind": {
"tables": ["items"],
"inputMapping": {
"id": "$row.$pk",
"name": "$row.name",
"note": "$row.note"
}
},
"hostEffects": {
"onSuccess": ["reloadTable"]
},
"inputSchema": {
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "number", "title": "编号", "readOnly": true },
"name": { "type": "string", "title": "名称" },
"note": { "type": "string", "title": "备注" }
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["update", "--id", "{{id}}", "--name", "{{name}}", "--note", "{{note}}"]
}
}

View File

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

View File

@@ -84,8 +84,8 @@
},
"mutationKind": {
"type": "string",
"enum": ["create", "update", "delete"],
"description": "可选。接管数据管理默认按钮:create=新增、update=编辑、delete=删除。须配合非空 bind.tables;无配置时宿主仍直写 SQLite。"
"enum": ["create", "update", "delete", "import"],
"description": "可选。接管数据管理默认按钮:create=新增、update=编辑、delete=删除、import=导入。须配合非空 bind.tables;无配置时宿主仍直写 SQLite。import 时 UI 仍用宿主「导入数据」面板,确认后调用本 Action 写库。"
},
"scalarArg": {
"type": ["string", "number", "boolean"]
@@ -120,12 +120,12 @@
},
"display_type": {
"type": "string",
"enum": ["textarea", "date", "datetime_unix_seconds", "url", "email", "lookup"],
"description": "可选。表单控件提示;与 format 二选一即可。lookup=关联下拉(须配 lookup 对象或绑到已有 lookup 列)。须与 CRUD display_type 语义对齐(见 ACTIONS.md / SCHEMA.md)。"
"enum": ["textarea", "date", "datetime_unix_seconds", "url", "email", "lookup", "directory", "file"],
"description": "可选。表单控件提示;与 format 二选一即可。lookup=关联下拉(须配 lookup 对象或绑到已有 lookup 列)。file/directory=本地路径选择。须与 CRUD display_type 语义对齐(见 ACTIONS.md / SCHEMA.md)。"
},
"format": {
"type": "string",
"enum": ["textarea", "date", "datetime_unix_seconds", "url", "email", "lookup"],
"enum": ["textarea", "date", "datetime_unix_seconds", "url", "email", "lookup", "directory", "file"],
"description": "可选。display_type 别名,语义相同。"
},
"lookup": {
@@ -214,9 +214,13 @@
"filter_value": {
"type": ["string", "number", "boolean", "null"],
"description": "可选。与 filter_column 配对的等值"
},
"multiple": {
"type": "boolean",
"description": "可选。true=多选 Lookup(存/提交 TEXT 逗号分隔 id);省略或 false=单选(默认)。与 CRUD options_json lookup 同构。"
}
},
"description": "同库 FK 式 Lookup。展示:value · label;有 extra 时 value · label(extra)。宿主亦接受 camelCase 键名。"
"description": "同库 FK 式 Lookup。展示:value · label;有 extra 时 value · label(extra)。multiple 时多选。宿主亦接受 camelCase 键名。"
},
"inputSchema": {
"type": "object",
@@ -420,7 +424,8 @@
"skill-detail": { "type": "string", "enum": ["direct", "confirm", "form"] },
"create": { "type": "string", "enum": ["direct", "confirm", "form"] },
"update": { "type": "string", "enum": ["direct", "confirm", "form"] },
"delete": { "type": "string", "enum": ["direct", "confirm", "form"] }
"delete": { "type": "string", "enum": ["direct", "confirm", "form"] },
"import": { "type": "string", "enum": ["direct", "confirm", "form"] }
},
"description": "按 placement / replaces 覆盖 interaction(只能写 concrete 模式,不能写 auto)。"
},

View File

@@ -59,6 +59,7 @@ checklist_version: 1.0
- [ ] S0-8 是否包含提交、付款、删除、审批、发布、发送等不可逆副作用:是 / 否
- [ ] S0-9 **入口定位表**(见 `PRODUCT_SURFACES.md` / `REQUIREMENTS` §0.1):已填写 / 未填写
- [ ] S0-10 本技能宣称的主用户路径涉及:对话 / 数据管理(执行或仅展示) / 定时(可多选;与定位表一致)
- [ ] S0-11 **业务行生命周期**(见 `references/WORK_ITEM_STATUS.md` / `REQUIREMENTS` §0.3):有处理队列且已声明状态→角色映射 / 不适用
---
@@ -129,6 +130,10 @@ 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` 后自检
- [ ] A6-7 **P0** 有「处理业务行」类 Action 时(见 `WORK_ITEM_STATUS.md`):默认只处理**可操作**角色状态;成功/失败/跳过/进行中再点处理应拒绝或跳过且不产生重复副作用;重做须人工改回可操作。无处理队列 → **N/A**
- [ ] A6-8 **P0** 同上适用时:开始处理后业务表进入**进行中**;成功回写成功类终态;失败回写失败类终态且**失败原因**可读(任务成功但业务行仍停在可操作/进行中 → **FAIL**)
- [ ] A6-9 **P1** 同上适用时:`REQUIREMENTS` §0.3(或等价)已填写本技能状态→角色映射;处理类 Action 确认文案写清仅可操作态
---

View File

@@ -67,7 +67,8 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
宿主约定(与本模板对齐):
- 工具栏「导入」默认从 `input/` 取文件;「导出」默认写到 `output/`
- 工具栏「导入」**UI 一律**打开宿主「导入数据」面板(默认从 `input/` 选文件);「导出」默认写到 `output/`
- 若表声明了 `replaces: import`,确认导入后由技能 CLI 写库(见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「`replaces`」);仅有 `replaces: create` 时宿主可按行调 create(兼容)
- 数据管理扫描 `{skill_data}/html/*.html`(一层、非递归);`<title>` → 侧栏显示名;「重新生成」调用 Action id **`render-html`**(见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「HTML 页面报告」)
---

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`
只做业务表增删查改(有业务表才建;模板不再默认提供任务审计表)

View File

@@ -56,7 +56,7 @@
数据管理有两层,**同等重要**:
1. **执行面**:工具栏 / 行级 / 批量 / `replaces` 接管 CRUD —— 与对话一样触发同一 CLI,走完整业务闭环(传统用户可不聊天)。
1. **执行面**:工具栏 / 行级 / 批量 / `replaces` 接管新增·编辑·删除·**导入** —— 与对话一样触发同一 CLI,走完整业务闭环(传统用户可不聊天)。导入按钮的 UX 始终是宿主「导入数据」面板;有 `replaces:import` 时确认后由技能写库。
2. **运营面**:业务表、状态流向、配置、input/output、日志、HTML —— 看清数据从哪来、到哪去、卡在哪。
**适合:**

View File

@@ -85,6 +85,20 @@
- 需要保存哪些日志、截图、视频和业务结果:
- 定位失败时如何保存 DOM/UIA/XML、候选数量和环境版本:
## 0.3 业务行生命周期(有「处理某一行」类能力时必填)
若本技能存在对业务表某一行做生成、触达、私信、发布等**有副作用**的处理,须遵守 [`../references/WORK_ITEM_STATUS.md`](../references/WORK_ITEM_STATUS.md),并在此声明**本技能状态 → 角色**映射(状态名自定,角色固定):
| 角色 | 本技能状态值(可多个) | 说明 |
|------|------------------------|------|
| 可操作(operable) | (例)新建 / 待生成 / … | 默认唯一可被处理 Action 捞起 |
| 进行中(in_progress) | (例)生成中 / 触达中 / … | 抢占后、结束前 |
| 成功类终态 | (例)已生成 / 已私信 / … | 默认不可再处理 |
| 失败类终态 | (例)失败 / 触达失败 / … | 默认不可再处理;失败原因字段: |
| 跳过类终态 | (例)已跳过 / … | 默认不可再处理 |
无此类处理队列时整节写「不适用」即可。
## 1. 文档目标
说明这份需求文档的作用,以及它解决什么问题。

View File

@@ -29,6 +29,7 @@
3. 有可感知副作用时:提供 `confirmation.message`
4. 需要用户/Agent 入参时:简单 `inputSchema` + entrypoint 中的 `{{fieldName}}`
5. `SKILL.md`「用户意图路由表」与 `CLI.md`「Action 强制黑名单」**出现同一 action id / 命令**
6. 若处理的是**业务队列表中的一行**:遵守 [`WORK_ITEM_STATUS.md`](WORK_ITEM_STATUS.md)——默认只捞**可操作**角色;开始抢占为**进行中**;结束回写成功/失败(失败带原因);确认文案写清仅可操作态可处理
默认模板 `assets/actions.json` 仅含诊断类 sync Action,**不要**为通过本 checklist 而塞假业务 Action;业务技能按需添加。完整字段见下文 Phase 1。
@@ -83,7 +84,7 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
| 字段 / 层 | 只决定什么 | 不决定什么 |
|-----------|------------|------------|
| **`placements`** | Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail;`row`=行级入口,壳由宿主呈现) | 不决定 sync/async,不决定点了是否弹窗,不决定行级用操作列还是右键,不决定谁能点 |
| **`replaces`** | **可选**:接管数据管理默认「新增 / 编辑 / 删除」 | 不强制;无配置时宿主仍直写 SQLite |
| **`replaces`** | **可选**:接管数据管理默认「新增 / 编辑 / 删除 / **导入**」 | 不强制;无配置时宿主仍直写 SQLite;**import 时 UI 仍用宿主「导入数据」面板** |
| **`bind.tables`** | 数据管理里出现在**哪些业务表** | 不决定业务实现 |
| **`executionProfile`** | 宿主是**同步等待**还是**异步 Job** | 不限制可展示入口,不决定是否弹窗 |
| **`interaction`** | 可点击入口(数据管理 toolbar/row/batch、默认 CRUD 接管、未来 skill-detail)**点了怎么交互** | 不决定 sync/async;**cron 触发与 agent 忽略** |
@@ -105,7 +106,7 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
| action | `id`、`label`、`description`、`placements`、`entrypoint`、**`executionProfile`(必填)**;可选 `hoverHint`、`inputSchema`、`confirmation`、**`interaction`**、**`interactionByPlacement`**、**`bind`**、**`replaces`**、**`hostEffects`**、**`steps`**、**`authz`**(仅文档提示) |
| entrypoint | `type = cli`、`command`、`args`(**必填** JSON 数组,项为 `string` / `number` / `boolean`) |
| placements | `toolbar`、`row`、`batch`、`skill-detail`、`agent`、`cron`;仅 `replaces` 时可为空数组 |
| **replaces** | 可选 `"create"` \| `"update"` \| `"delete"` — 接管默认新增/编辑/删除(须 `bind.tables`) |
| **replaces** | 可选 `"create"` \| `"update"` \| `"delete"` \| `"import"` — 接管默认新增/编辑/删除/导入(须 `bind.tables`) |
| **bind** | `{ "tables": ["business_table"] }`;可选 `inputMapping`(行内/批量预填,见下节) |
| **policy** | 可选;`enabled` + `entrypoint`(推荐 `policy` CLI);见 POLICY.md。未启用 = L0 |
| **cellPresentation** | 可选;`{ "enabled": true }` 时宿主应加载单元格呈现;见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)。未启用 = P0 |
@@ -140,32 +141,166 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
- **`inputMapping`(可选)**:把行/勾选映射到 `inputSchema` 字段,例如 `"leadId": "$row.$pk"`、`"ids": "$selection.ids"`。适合 `row` / `batch`。宿主仍会提交这些字段;可用 `inputSchema.properties.*.readOnly: true` 在表单中只读展示。
- `concurrency` / `locks` 仍未稳定,**不要**写入新技能 manifest。
## `replaces`(可选:接管默认新增 / 编辑 / 删除)
## `replaces`(可选:接管默认新增 / 编辑 / 删除 / 导入)
有领域副作用的表(建目录、加密、级联清理)不要依赖宿主直写 SQLite。此时可为该表声明:
有领域副作用的表(归一化字段、建目录、加密、级联清理)不要依赖宿主直写 SQLite。此时可为该表声明 `replaces`,**用户仍点宿主默认按钮**,由技能 CLI 完成写入。
### 怎么选
| 场景 | 建议 |
|------|------|
| 纯表字段、无副作用 | 不配 `replaces`,宿主直写即可 |
| 单条新增/编辑/删除有副作用 | `replaces: create` / `update` / `delete` |
| 批量导入也有副作用(推荐) | 另配 `replaces: import`(与 create 可同表并存) |
| 只有 create、暂无 import | 宿主「导入」面板确认后会**逐行调 create**(兼容);正式产品应补 `import` |
### 导入分流(必读)
点工具栏「导入」时:
1. **UI 一律**打开宿主「导入数据」面板(下载模板 → 选文件 → 映射 → 出错规则 → 预览)。
2. **写库**按表能力分流:
- 有 `replaces: import` → 把选中文件路径 + 出错规则交给该 Action(技能解析并写入)
- 仅有 `replaces: create` → 宿主按行调用 create(兼容)
- 都没有 → 宿主直写 SQLite
**不要**再做一张技能自己的「开始执行」导入表单来冒充导入体验。`placements` 对 import 保持 `[]`;`description` 写明「由宿主导入面板调用」。
可抄文件见 [`assets/examples/`](../assets/examples/):`replaces-*.action.example.json`。
### 示例:create
```json
{
"id": "account-add-web",
"label": "新增账号",
"description": "登记网页账号并初始化 profile 目录。",
"id": "item-create",
"label": "新增记录",
"description": "新增业务记录并执行领域副作用(走技能 CLI,非宿主直写库)。",
"replaces": "create",
"placements": [],
"executionProfile": "sync",
"interaction": "form",
"bind": { "tables": ["accounts"] },
"bind": { "tables": ["items"] },
"hostEffects": { "onSuccess": ["reloadTable"] },
"inputSchema": {
"type": "object",
"required": ["platform_key", "account_label"],
"required": ["name"],
"properties": {
"platform_key": { "type": "string", "title": "平台" },
"account_label": { "type": "string", "title": "账号名称" }
"name": { "type": "string", "title": "名称" },
"note": { "type": "string", "title": "备注" }
}
},
"entrypoint": {
"type": "cli",
"command": "account",
"args": ["add-web", "--platform", "{{platform_key}}", "--label", "{{account_label}}"]
"command": "item",
"args": ["create", "--name", "{{name}}", "--note", "{{note}}"]
}
}
```
### 示例:update
```json
{
"id": "item-update",
"label": "编辑记录",
"description": "编辑业务记录并重算派生字段。",
"replaces": "update",
"placements": [],
"executionProfile": "sync",
"interaction": "form",
"bind": {
"tables": ["items"],
"inputMapping": {
"id": "$row.$pk",
"name": "$row.name",
"note": "$row.note"
}
},
"hostEffects": { "onSuccess": ["reloadTable"] },
"inputSchema": {
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "number", "title": "编号", "readOnly": true },
"name": { "type": "string", "title": "名称" },
"note": { "type": "string", "title": "备注" }
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["update", "--id", "{{id}}", "--name", "{{name}}", "--note", "{{note}}"]
}
}
```
### 示例:delete
```json
{
"id": "item-delete",
"label": "删除记录",
"description": "删除业务记录并清理关联副作用。",
"replaces": "delete",
"placements": [],
"executionProfile": "sync",
"interaction": "confirm",
"confirmation": { "message": "确定删除该记录?关联数据将一并清理。" },
"bind": {
"tables": ["items"],
"inputMapping": { "id": "$row.$pk" }
},
"hostEffects": { "onSuccess": ["reloadTable", "clearSelection"] },
"inputSchema": {
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "number", "title": "编号", "readOnly": true }
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["delete", "--id", "{{id}}"]
}
}
```
### 示例:import
```json
{
"id": "item-import",
"label": "导入记录",
"description": "由宿主「导入数据」面板调用:解析 Excel/CSV 并写入业务表(含领域校验与派生字段)。",
"replaces": "import",
"placements": [],
"executionProfile": "sync",
"interaction": "form",
"bind": { "tables": ["items"] },
"hostEffects": { "onSuccess": ["reloadTable"] },
"inputSchema": {
"type": "object",
"required": ["filePath"],
"properties": {
"filePath": {
"type": "string",
"title": "导入文件",
"description": "由宿主导入面板传入的 Excel/CSV 路径。",
"format": "file"
},
"onError": {
"type": "string",
"title": "导入规则(出错时)",
"enum": ["跳过该行,继续导入", "立即停止,不再导入后续行"],
"default": "跳过该行,继续导入"
}
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["import", "--file", "{{filePath}}", "--on-error", "{{onError}}"]
}
}
```
@@ -173,12 +308,13 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
规则:
- **全可选**:无 `replaces` 时,宿主默认按钮仍直写库,行为与以前一致。
- 取值:`create`(新增)/ `update`(编辑)/ `delete`(删除)。
- 取值:`create`(新增)/ `update`(编辑)/ `delete`(删除)/ `import`(导入写库)。
- **必须**非空 `bind.tables`;同一技能同一表同一 `replaces` 至多一个 Action。
- 与 `placements` 正交:可只接管默认按钮(`placements: []`),不必再挂 toolbar「第二个新增」。
- 点默认按钮时走该 Action 的 `interaction`(create/update 常用 `form`;delete 常用 `confirm`)。
- 与 `placements` 正交:可只接管默认按钮(`placements: []`),不必再挂 toolbar「第二个新增/导入」。
- 点默认「新增/编辑/删除」时走该 Action 的 `interaction`(create/update 常用 `form`;delete 常用 `confirm`)。
- 点默认「导入」时**固定**打开宿主导入面板;确认后若有 `replaces:import` 则调用该 Action(`interaction` 不决定是否再弹技能表单)。
- `update` / `delete` 可用 `inputMapping`(`$row.$pk`、`$selection.ids`)预填主键。
- 宿主导入仍直写:有 `replaces:create` 的表慎用宿主导入,或另做 import Action。
- `import` 约定:`filePath`(或 `file`)+ 可选 `onError`(`skip`/`abort` 或上例中文枚举);技能自行解析文件与表头。
- 整表 `_jiangchang_tables.readonly=1` 时仍隐藏写按钮(优先于 `replaces`)。
- **列元数据复用(lookup)**:Action 表单(含 toolbar/batch 弹窗与 create/update 抽屉)可将 `inputSchema` 字段对齐业务列,复用中文名与 lookup 下拉:
- 优先让属性名 = 物理列名(如 `platform_key`);
@@ -598,7 +734,7 @@ HINT: <next step>
| 当前表已有同含义列,且列上已是 CRUD lookup | `column` / `bind.columnMapping`,或属性名 = 列名 | toolbar/batch 弹窗与 create/update 抽屉都会复用该列已解析的选项 |
| 当前表没有该列(如在「潜客」表上选「话术模板」) | `display_type: "lookup"` + 字段内联 `lookup` 对象 | 打开表单时宿主按当前技能库解析选项 |
内联 `lookup` 与列元数据 `options_json` **同构**(见 [`SCHEMA.md`](SCHEMA.md)「表关联 lookup」):
内联 `lookup` 与列元数据 `options_json` **同构**(见 [`SCHEMA.md`](SCHEMA.md)「跨表引用 lookup」):
```json
{
@@ -615,6 +751,20 @@ HINT: <next step>
"filter_column": "is_active",
"filter_value": 1
}
},
"engineIds": {
"type": "string",
"title": "监测引擎",
"description": "可多选;提交为逗号分隔编号。",
"display_type": "lookup",
"lookup": {
"ref_table": "engines",
"value_column": "id",
"label_column": "display_name",
"filter_column": "enabled",
"filter_value": 1,
"multiple": true
}
}
}
```
@@ -622,20 +772,26 @@ HINT: <next step>
| 字段 | 必填 | 说明 |
|------|------|------|
| `ref_table` | 是 | 同库被引用表 |
| `value_column` | 是 | 提交给 CLI 的值(编号 / 外键) |
| `value_column` | 是 | 提交给 CLI 的值(业务 id / 编号) |
| `label_column` | 是 | 主文案列 |
| `extra_column` | 否 | 第三展示列(任意列名,不限「备注」) |
| `filter_column` / `filter_value` | 否 | 等值过滤 |
| `multiple` | 否 | `true` = 多选;省略/`false` = 单选(默认) |
**下拉展示(宿主统一,技能只配列):** `{value} · {label}`;有非空 `extra_column` 时为 `{value} · {label}({extra})`。例:`2 · 通用私信-留资`、`2 · 通用私信-留资(dm)`。
**下拉/选项展示(宿主统一,技能只配列):** 单选为 `{value} · {label}`;有非空 `extra_column` 时为 `{value} · {label}({extra})`。例:`2 · 通用私信-留资`、`2 · 通用私信-留资(dm)`。
**可选「自动」:** 字段不要放进 `required`;不要用 `minimum: 1` 卡死留空。宿主会省略空的可选数字参数,避免「提交的数据无效」。
**多选存值与模板:** Action / CRUD 存 **`TEXT` 逗号分隔 id**(如 `1,2,5`);`{{engineIds}}` 原样替换进 CLI。多选字段在 `inputSchema` 里用 `type: "string"`(逗号串),不要用尚未稳定的 JSON array 当业务控件。
**多选控件(宿主策略):** 选项少(建议 ≤6)用勾选列表;选项多用字段内 Chip + 可搜索下拉(不是再开业务弹窗)。
**可选「自动」(单选常见):** 字段不要放进 `required`;不要用 `minimum: 1` 卡死留空。宿主会省略空的可选数字参数,避免「提交的数据无效」。
**反模式(勿做):**
- 标题写成「某某编号」,说明里教用户去左侧表抄 id
- 用静态 `enum` 罗列会增长/变更的业务行(会过期)
- 在技能里自己拼假 label 列代替宿主统一格式(难维护)
- 另造 `lookup_multi` / 第二套 display_type(应只加 `multiple: true`)
形状参考:[`assets/examples/lookup-action.example.json`](../assets/examples/lookup-action.example.json)。
@@ -651,18 +807,20 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件
| 数字 | `type: number` | `display_type: number` 或 INT/REAL/NUMERIC |
| 开关 | `type: boolean` | `display_type: boolean` |
| 枚举下拉 | `enum: [...]` | `options_json` 数组 |
| 关联下拉(Lookup) | 内联 `display_type: lookup` + `lookup`,或绑列 `column` / `columnMapping` | `display_type: lookup` + `options_json` 对象 |
| 关联下拉(Lookup) | 内联 `display_type: lookup` + `lookup`(可选 `multiple`),或绑列 | 同左;多选存 `TEXT` 逗号分隔 id |
| 密码 | `sensitive: true` | 列 `sensitive` |
| 日期(日) | `display_type` 或 `format`: `date` | `display_type: date` → 存 `YYYY-MM-DD` |
| 日期时间 | `display_type` 或 `format`: `datetime_unix_seconds` | 同左;**存/传 Unix 秒** |
| 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 / 定时任务参数表单 |
**本版不做(勿宣传):** 文件上传、多选/标签数组、复杂 `oneOf` / nested object 专用表单;`visibleWhen` 多条件 AND/OR 树、跨 Action 依赖、CRUD 新增/编辑对称显隐。
**本版不做(勿宣传):** 文件上传、复杂 `oneOf` / nested object 专用表单;`visibleWhen` 多条件 AND/OR 树、跨 Action 依赖、CRUD 新增/编辑对称显隐。
(**Lookup 多选**已支持:同一 `lookup` + `multiple: true`,见上文;不是任意标签数组。)
日期时间约定:
@@ -675,7 +833,7 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件
- 根:`object`;属性:`string` / `number` / `boolean`(`object`/`array` 仅作 JSON 多行编辑兼容,新技能慎用)
- `enum` / `default` / `required` / `sensitive` / `readOnly`
- `display_type` / `format`:`textarea` | `date` | `datetime_unix_seconds` | `url` | `email` | **`lookup`**
- 字段内联 `lookup`(与 CRUD `options_json` lookup 对象同构;可选 `extra_column`)
- 字段内联 `lookup`(与 CRUD `options_json` lookup 对象同构;可选 `extra_column`、`multiple`)
- 属性级 `description`(表单标签下方灰字说明)
- 简单模板替换(`{{fieldName}}`)
- 可选空数字字段提交时由宿主省略,避免 `""` 触发 number 校验失败
@@ -727,6 +885,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)「表单与展示:稳定字段类型」「新增/编辑表单:字段说明与默认值」。同一业务含义两边文案与控件应对齐,但分别配置。
示例:
@@ -777,13 +937,15 @@ CRUD 侧宿主义务与禁止事项见 [`SCHEMA.md`](SCHEMA.md)「表单与展
"pickCount": {
"type": "number",
"title": "处理数量",
"description": "从队列选取 pending 记录条数",
"default": 1,
"minimum": 1
"description": "从队列选取「可操作」状态记录的条数(具体状态名由本技能定义,见 WORK_ITEM_STATUS)"
}
}
```
默认**不要**把失败/成功/跳过混进该 pick;重做靠人工改回可操作态。详见 [`WORK_ITEM_STATUS.md`](WORK_ITEM_STATUS.md)。
若还需要默认篇数,可在同一 `properties` 里为 `pickCount` 增加 `"default": 1`、`"minimum": 1`。
约定:
- `boolean` 在宿主中渲染为开关
@@ -799,7 +961,7 @@ CRUD 侧宿主义务与禁止事项见 [`SCHEMA.md`](SCHEMA.md)「表单与展
- 短名看不懂或会覆盖数据时,用独立的 `hoverHint`;不要把确认黄条复用成悬停
- 模板参数使用 `{{fieldName}}`,且必须在 `inputSchema.properties` 中声明
- `entrypoint.args` 项当前只允许 `string`、`number`、`boolean`;不允许对象或 `null`
- **不要**在当前模板中宣传文件上传、多选数组、`oneOf`、复杂 nested object 等尚未稳定支持的能力
- **不要**在当前模板中宣传文件上传、任意 JSON 多选数组、`oneOf`、复杂 nested object 等尚未稳定支持的能力;Lookup 多选请用 `lookup.multiple`
## 风险与入口配置
- 查询、诊断、版本检查可以放到 `skill-detail` 或 `agent`

View File

@@ -150,7 +150,7 @@
4. `mode=open`:等价「L1 不限制按钮」,可与 L2 行列限制并存。
5. **`denylist` 不是 v1 合法值**;出现则 §6 fail-closed。
`replaces` 接管的默认新增/编辑/删除:视为对应 Action 的 id 是否在 allowlist 中;不在则宿主隐藏该接管按钮。`mode=open` 时不按 id 限制 replaces。
`replaces` 接管的默认新增/编辑/删除/导入:视为对应 Action 的 id 是否在 allowlist 中;不在则宿主隐藏该接管按钮(导入按钮是否可用还受 `hostCapabilities` 含 `import` 影响)。`mode=open` 时不按 id 限制 replaces。
### 2.4 `hostCapabilities`(L1 可选冻)

View File

@@ -21,6 +21,7 @@
| [`SCHEMA.md`](SCHEMA.md) | 本地 SQLite 与数据管理展示契约 |
| [`POLICY.md`](POLICY.md) | Data Policy:可选身份授权、Snapshot、行 scope(多数技能 L0 不配) |
| [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md) | 数据管理单元格呈现:语义 tone 标色/强调(用途不限;多数技能 P0 不配) |
| [`WORK_ITEM_STATUS.md`](WORK_ITEM_STATUS.md) | 业务行生命周期:处理队列的可操作/进行中/终态角色与重做规则(有处理行 Action 时适用) |
| [`../development/`](../development/) | 开发、测试、RPA 深度规范(给人 / 编码助手) |
按需阅读:短操作优先 `CLI.md`;Action / 任务中心读 `ACTIONS.md`;结构化字段读 `SCHEMA.md`;权限 opt-in 读 `POLICY.md`;格子标色读 `CELL_PRESENTATION.md`。
按需阅读:短操作优先 `CLI.md`;Action / 任务中心读 `ACTIONS.md`;结构化字段读 `SCHEMA.md`;权限 opt-in 读 `POLICY.md`;格子标色读 `CELL_PRESENTATION.md`;队列处理状态机读 `WORK_ITEM_STATUS.md`。

View File

@@ -13,7 +13,7 @@
- 定义真实 SQLite 表结构
- 使用英文 `snake_case` 表名和字段名
- 使用 `_jiangchang_tables` 提供表级展示元数据
- 使用 `_jiangchang_columns` 提供字段级展示元数据(含可选 `description` 字段说明)
- 使用 `_jiangchang_columns` 提供字段级展示元数据(含可选 `description` 表单说明、可选 `grid_help` 表格说明)
- 需要「新增表单预填」时,在物理列上声明 SQLite `DEFAULT`(勿把默认值只写在文案里)
- 定义哪些字段可见、可搜索、可创建、可编辑
- 定义字段展示类型和枚举选项
@@ -28,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 则留空
- 执行通用查询、新增、编辑、删除、导入、导出
- 对字段名、表名、类型和权限进行最终校验
@@ -86,7 +87,24 @@ metadata:
不要把「外部来源只读」错误理解成「技能本地数据库一定只读」。技能通过 `_jiangchang_tables.readonly` 与 `_jiangchang_columns.editable` 声明本地表的实际写权限。
若创建/更新/删除带有领域副作用(自动建目录、加密、级联清理),不要依赖宿主默认直写:在 `assets/actions.json` 用可选字段 `replaces: create|update|delete` 接管默认按钮,详见 [`ACTIONS.md`](ACTIONS.md)。
若创建/更新/删除/导入带有领域副作用(自动建目录、加密、级联清理、归一化字段),不要依赖宿主默认直写:在 `assets/actions.json` 用可选字段 `replaces: create|update|delete|import` 接管默认按钮。其中 **import** 时用户仍使用宿主「导入数据」面板,确认后由技能写库。详见 [`ACTIONS.md`](ACTIONS.md) 与 `assets/examples/replaces-*.action.example.json`。
## 表与表之间:禁止 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)。
## 标准数据同步语义(外源 → 本地库)
@@ -138,7 +156,8 @@ metadata:
|------|------|------|
| `table_name` | 是 | 真实英文表名 |
| `display_name` | 是 | 用户可见的中文业务名称 |
| `description` | 否 | 表说明 |
| `description` | 否 | 表说明(表单/备注用途);**不**用于表格上方横幅 |
| `grid_help` | 否 | 数据管理表格上方说明;空/未写则不出横幅。与 `description` 独立,禁止互拷(见下节) |
| `sort_order` | 否 | 兼容保留字段,不用于替代物理字段顺序 |
| `visible` | 是 | `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` | 是 | 真实英文字段名 |
| `display_name` | 是 | 用户可见的中文字段名(表头与表单标签);保持简短 |
| `description` | 否 | 字段说明(给人看的短文案);**宿主在新增/编辑表单标签下方展示**(见下节);不要塞进 `display_name` |
| `grid_help` | 否 | 数据管理表头「?」;空/未写则不出。与表单 `description` 独立,禁止互拷 |
| `display_order` | 否 | 兼容字段,默认保持 `NULL` |
| `visible` | 是 | 是否进入数据管理 schema /「列」面板(`0` = **硬隐藏**,用户无法勾回;默认不展示但可勾回请用表级 `default_visible_columns_json`) |
| `searchable` | 是 | 是否参与搜索 |
@@ -251,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` **对象**;可选 `multiple` | 单选:标签映射;多选:多标签(如「豆包、元宝」) | 单选下拉;多选见下文 | 单选:业务 id 标量;多选:`TEXT` 逗号分隔 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 字符串 |
@@ -263,9 +320,11 @@ upsert_table_metadata(
**明确不在本版稳定范围(勿在新技能宣传为已支持):**
- 文件 / 图片上传
- 多选、标签数组、复杂 `oneOf` / nested object 表单
- 复杂 `oneOf` / nested object 表单(任意 JSON 数组当业务多选控件)
- 把业务可编辑时间存成毫秒、ISO 文本或 SQLite `datetime` 文本(审计字段规范见「标准时间字段」)
> **Lookup 多选**已纳入本版契约(见下文 `multiple`),与「任意多选/标签数组」不是同一能力。
`datetime_unix_milliseconds`:仅兼容**列表展示**旧数据;**新技能不要用**;可编辑时间一律 `datetime_unix_seconds`。
#### 新增/编辑表单:字段说明与默认值(宿主支持)
@@ -277,7 +336,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` |
规则与禁止:
@@ -464,6 +525,19 @@ readonly = 0
模板**默认不建**此类审计表。过程看 `jiangchang.log`,进度/终态看任务中心,业务结果看业务表。
### 业务行生命周期(处理队列)
若技能对某一业务行做「生成 / 触达 / 私信 / 发布」等有副作用处理,状态字段须遵守 **角色模型**(可操作 / 进行中 / 成功·失败·跳过类终态),**不要**把某一技能的状态名当成全平台唯一枚举。默认只处理可操作态;重做靠人工改回可操作。完整规则见 [`WORK_ITEM_STATUS.md`](WORK_ITEM_STATUS.md)。
建议:
| 项 | 说明 |
|----|------|
| 状态列 | `options_json` 列出本技能全部业务状态(含进行中与各类终态) |
| 失败原因 | 失败类终态应有用户可读的原因字段(勿写密钥) |
| 可编辑 | 终态/进行中建议可改回可操作,或提供等价 Action |
| `grid_help` | 可一句话说明「仅某某状态可处理」 |
表级 `readonly = 1` 时,宿主数据管理只读展示;技能 repository 仍可写入。
### 通用示例:`demo_items`(测试 fixture 参考)
@@ -512,9 +586,11 @@ CREATE TABLE demo_items (
宿主据此渲染选择框;技能侧负责保证 JSON 合法且与物理字段取值一致。
### 2)表关联 lookup(对象,推荐用于外键式字段)
### 2)跨表引用 lookup(对象,用于关联 id 字段)
当字段取值依赖同库另一张业务表时,`display_type` 设为 `lookup`,`options_json` 写对象(**不是**数组):
当字段取值依赖同库另一张业务表时,`display_type` 设为 `lookup`,`options_json` 写对象(**不是**数组)。这是**展示与表单选择**契约,**不是** SQLite 外键。
**单选(默认,与既有行为一致):**
```json
{
@@ -527,32 +603,60 @@ CREATE TABLE demo_items (
}
```
**多选(平行扩展,仅多一个开关):**
```json
{
"ref_table": "engines",
"value_column": "id",
"label_column": "display_name",
"filter_column": "enabled",
"filter_value": 1,
"multiple": true
}
```
| 字段 | 必填 | 说明 |
|---|---|---|
| `ref_table` | 是 | 同库被引用表名(snake_case 标识符) |
| `value_column` | 是 | 写入当前字段的值列(编号 / 外键) |
| `label_column` | 是 | 下拉主文案列(名称) |
| `value_column` | 是 | 写入当前字段的值列(业务 id / 编号) |
| `label_column` | 是 | 选项主文案列(名称) |
| `extra_column` | 否 | 第三展示列(不必叫「备注」);有值时拼进括号 |
| `filter_column` / `filter_value` | 否 | 等值过滤(如只列出 `enabled=1`) |
| `multiple` | 否 | `true` = 多选 Lookup;省略或 `false` = **单选**(默认) |
宿主在加载表 schema 时查询 `ref_table`,把结果展开成 `{value,label}` 下拉选项;**物理列仍存标量**(如 `platform_key` 文本、`account_id` 整数),不做嵌套子表。
宿主在加载表 schema 时查询 `ref_table`,把结果展开成 `{value,label}` 选项列表。
**存库约定:**
| 模式 | 物理列建议 | 存什么 |
|------|------------|--------|
| 单选 | 整数或文本标量 | 一个业务 id(如 `2`、`douyin`) |
| 多选 | **`TEXT`** | 逗号分隔 id,如 `1,2,5`(与 `$selection.ids` 风格一致);**不要**建联接子表当 UI 多选 |
**统一展示文案(宿主固定一种,技能只配列):**
- 默认:`{value} · {label}`,例如 `2 · 通用私信-留资`、`douyin · 抖音`
- 单选默认:`{value} · {label}`,例如 `2 · 通用私信-留资`、`douyin · 抖音`
- 配置了 `extra_column` 且该行有非空值:`{value} · {label}({extra})`
- 未配置 `extra_column`、或额外列为空:不加 `()`
- `label` 为空或与 `value` 相同时:只显示 `value`(可再带额外括号)
- **多选列表格**:按 id 拆开后逐个映射为 label,再用顿号/逗号拼接(如 `豆包、元宝`);过长可截断并提示「+N」
Action 弹窗内联 Lookup 与上表 **同构**(字段名一致),见 [`ACTIONS.md`](ACTIONS.md)「跨表引用:Lookup」。
**控件呈现(宿主策略,非第二套契约):**
示例:账号表的平台字段 → 平台信息表;凭据表的账号编号 → 账号信息表;私信 Action 选话术 → `message_templates`。
- 单选:下拉 Select(现状)
- 多选且选项较少(建议 ≤6):勾选列表
- 多选且选项较多:字段内 Chip + 可搜索下拉(**不是**再开一层业务弹窗)
Action 弹窗内联 Lookup 与上表 **同构**(字段名一致,含 `multiple`),见 [`ACTIONS.md`](ACTIONS.md)「跨表引用:Lookup」。
示例:账号表的平台字段 → 平台信息表;凭据表的账号编号 → 账号信息表;私信 Action 选话术 → `message_templates`;抽检 Action 多选引擎 → `engines` + `multiple: true`。
## 幂等初始化标准
- `_jiangchang_tables` 和 `_jiangchang_columns` 必须通过 upsert 初始化
- 多次执行初始化不能重复记录
- 重新初始化可以更新展示名、描述、`visible`、`searchable`、`editable`、`display_type`、`options_json`
- 重新初始化可以更新展示名、描述、`grid_help`、`visible`、`searchable`、`editable`、`display_type`、`options_json`
- 元数据不能残留旧字段(删除或重命名字段时必须同步清理)
- 元数据不能引用不存在的表或字段
- 新增字段或表时必须同步增加元数据

View File

@@ -0,0 +1,157 @@
# 业务行生命周期契约(Work Item Lifecycle)
> **状态:正式契约 v1.0。**
> **目标:** 冻结「对某一行做有副作用处理」时的**状态角色与操作权**规则,使队列型技能(生成、触达、私信、发布等)行为一致、可验收。
> **非目标:** 规定某一技能的具体状态码或状态个数;强制所有表都有 status;替代任务中心 / 运行日志;规定 Data Policy 授权。
> **适用范围:** 有「处理业务行」类 Action 的技能。纯配置表、目录表、只读展示表标 N/A。
相关正交轴:[`ACTIONS.md`](ACTIONS.md)(入口与 sync/async)、[`SCHEMA.md`](SCHEMA.md)(表与展示)、[`POLICY.md`](POLICY.md)(谁能点)、任务中心(宿主 Job)。
---
## 0. 为什么需要本契约
若「处理这一行」的按钮对**任意状态**都可点,会出现:
- 已成功的行被重复执行(重复发信、重复生成、覆盖产物)
- 失败行被默认批量静默重试,运营无法区分「未做」与「做过但失败」
- 任务中心显示成功,业务表仍停在初始态(结果未回写)
本契约要求:**默认可自动操作的只有「可操作」角色;其它角色须人工改回可操作后再处理。**
---
## 1. 状态角色(通用,不绑死枚举)
技能自己定义业务状态字符串与中文标签;契约只认**角色**。一个角色可对应**一个或多个**业务状态。
| 角色 | 含义 | 默认可被处理 Action 捞起? |
|------|------|---------------------------|
| **operable(可操作)** | 尚未成功处理、允许自动或行操作启动处理 | **是(默认唯一)** |
| **in_progress(进行中)** | 已抢占,正在处理 | **否**(防双跑) |
| **terminal_success(成功类终态)** | 已按业务定义完成 | **否** |
| **terminal_failure(失败类终态)** | 已尝试且失败;通常带失败原因 | **否** |
| **terminal_skipped(跳过类终态)** | 人工放弃或不处理 | **否** |
### 1.1 举例(仅对照,不是规范枚举)
| 角色 | 技能 A(内容队列)可能叫 | 技能 B(线索触达)可能叫 |
|------|--------------------------|--------------------------|
| operable | 待生成、新建 | 新建、私信未发 |
| in_progress | 生成中 | 触达中 |
| terminal_success | 已生成 | 已私信 |
| terminal_failure | 失败 | 触达失败 |
| terminal_skipped | 已跳过 | 已忽略 |
技能可以有更多业务状态(审核中、待确认、部分成功等):须在需求里写明各自映射到哪一角色,或声明「不属于处理队列、本契约 N/A」。
### 1.2 技能必须声明映射
复制模板后,在 `development/REQUIREMENTS.md`(或等价需求)中写清:
```text
处理队列表:<table>
状态字段:<column>
可操作:<status1>, <status2>, …
进行中:<status…>
成功类终态:…
失败类终态:…(失败原因字段:…)
跳过类终态:…
```
未声明映射的「处理队列」技能,交活 Checklist 不得标 PASS。
---
## 2. 硬规则(技能实现必须遵守)
1. **默认只认 operable**
行操作 / 批量 / 定时 / Agent 触发的「处理」类 Action,pick 与 claim **默认**只包含可操作状态。
不得把成功、失败、跳过、进行中混进默认捞取集合(除非另做**显式命名**的「重试失败」类 Action,且确认文案写清范围——仍推荐不如人工改回可操作简单)。
2. **开始即原子抢占 → in_progress**
在真正产生副作用前,用「仅当当前为 operable 时 UPDATE 为进行中」的方式抢占。抢占失败 → 跳过或返回明确错误(如「当前不是可操作状态」),**禁止**继续副作用。
3. **结束必须回写业务表**
- 成功 → 对应成功类终态,并写入产物路径/摘要等业务字段(若有)
- 失败 → 对应失败类终态,并写入**失败原因**(用户可读;勿塞密钥)
- 禁止「任务中心成功、业务行仍停在可操作/进行中」而不回写
4. **重做靠人工改回 operable**
成功 / 失败 / 跳过 / 卡住的进行中,默认不能再点处理。运营在数据管理把状态改回可操作后,方可再次处理。
5. **任务中心 ≠ 业务状态**
Job「进行中 / 成功 / 失败」是宿主任务态。业务「进行中」必须写在业务表。进度 `emit` 不替代状态回写。
6. **过程与结果分工**(延续既有约定)
过程 → 运行日志 / 任务中心 Activity;业务结果 → 业务表。**不要**靠新建 `task_logs` 一类审计表充当状态机。
---
## 3. Action 与文案约定
- 处理类 Action 的 `description` / `confirmation.message` 应写明:**仅处理「可操作」状态**(用本技能用户能懂的词,如「仅待生成」「仅新建」)。
- 对非可操作行触发时:返回明确失败码与中文说明,提示「请先改回 xxx」。
- 长任务(`async`):除任务中心进度外,业务行应尽快进入进行中,避免界面长时间仍显示可操作导致连点。
正交提醒:
| 轴 | 管什么 | 不管什么 |
|----|--------|----------|
| 本契约 | 什么**状态**能处理 | 谁有权点按钮 |
| Data Policy | 谁有权 | 状态机 |
| executionProfile | sync/async 通道 | 业务终态 |
---
## 4. 数据管理展示建议
- 状态列:`options_json` 枚举完整业务状态(含进行中与各类终态)。
- 失败原因列:失败类终态可见;成功后宜清空或保留末次原因(技能自定,须一致)。
- 终态与进行中的状态字段建议 **可编辑**(或提供「改回可操作」Action),以便人工重做;**不要**只能靠删行重建。
- `grid_help` 可一句话写清:「仅某某状态可处理;其它状态请改回后再操作。」
详见 [`SCHEMA.md`](SCHEMA.md)。
---
## 5. 可观测性(与 ACTIONS / 日志约定对齐)
推荐阶段(名称可自定,须能对应到开始 / 关键节点 / 结束):
1. 已抢占(业务 → 进行中)
2. 处理中关键步骤(可 `emit`)
3. 回写成功或失败
故意失败一条时:业务表应为失败类终态 + 原因;任务中心为失败;日志含 `task_failed` / `error_code=`。
---
## 6. 非目标与常见误区
| 误区 | 正确做法 |
|------|----------|
| 把「新建/成功/失败/跳过」写成全平台唯一枚举 | 只用角色;各技能自定状态名 |
| 默认批量包含失败「方便自动重试」 | 默认不含;重试改回可操作或单独显式 Action |
| 只有任务成功、不改业务状态 | 必须回写业务表 |
| 用进行中替代任务中心 | 两者都要:业务防双跑,任务轨看进度 |
| 所有表都套本契约 | 无「处理行」副作用的表标 N/A |
---
## 7. 验收摘要(完整条目见 CHECKLIST)
有处理队列时至少验证:
1. 成功类终态再点处理 → 拒绝或跳过,不产生重复副作用
2. 可操作行处理中 → 先见进行中,结束后见成功或失败+原因
3. 失败行改回可操作后 → 可以再次处理
4. 任务中心有过程(长任务)且业务表状态与结果一致
---
## 8. 版本
| 版本 | 说明 |
|------|------|
| v1.0 | 首版:角色模型 + 硬规则 + 需求映射 + 与 Action/SCHEMA/任务中心边界 |

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
@@ -218,6 +219,8 @@ def validate_options_json(options_json: str | None) -> tuple[list[str], list[str
errors.append(f"options_json.{key} 必须是安全标识符,当前为 {raw!r}")
if parsed.get("filter_column") not in (None, "") and "filter_value" not in parsed:
warnings.append("options_json lookup 声明了 filter_column 但未提供 filter_value")
if "multiple" in parsed and not isinstance(parsed.get("multiple"), bool):
errors.append("options_json lookup.multiple 必须是布尔值")
return errors, warnings
if not isinstance(parsed, list):
@@ -412,6 +415,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 +449,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 +541,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.89"
SKILL_VERSION = "1.0.95"
LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug"
PLATFORM_KIT_MIN_VERSION = "1.2.11"

View File

@@ -188,7 +188,7 @@ class TestActionsManifest(unittest.TestCase):
if replaces is not None:
self.assertIn(
replaces,
{"create", "update", "delete"},
{"create", "update", "delete", "import"},
msg=f"{action_id} invalid replaces {replaces!r}",
)
bind = action.get("bind") or {}
@@ -236,7 +236,7 @@ class TestActionsManifest(unittest.TestCase):
self.assertIn("interactionByPlacement", props)
by_placement = props["interactionByPlacement"]
self.assertIs(by_placement.get("additionalProperties"), False)
for key in ("toolbar", "row", "batch", "cron", "agent", "skill-detail", "create", "update", "delete"):
for key in ("toolbar", "row", "batch", "cron", "agent", "skill-detail", "create", "update", "delete", "import"):
self.assertIn(key, by_placement.get("properties") or {})
self.assertEqual(
set((by_placement["properties"][key].get("enum") or [])),
@@ -246,7 +246,7 @@ class TestActionsManifest(unittest.TestCase):
self.assertEqual(props["replaces"].get("$ref"), "#/$defs/mutationKind")
self.assertEqual(
set(schema["$defs"]["mutationKind"].get("enum") or []),
{"create", "update", "delete"},
{"create", "update", "delete", "import"},
)
def test_schema_defines_hover_hint(self) -> None:

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

@@ -301,6 +301,7 @@ class TestDocsStandards(unittest.TestCase):
self.assertTrue(os.path.isfile(os.path.join(root, "references", "ACTIONS.md")))
self.assertTrue(os.path.isfile(os.path.join(root, "references", "POLICY.md")))
self.assertTrue(os.path.isfile(os.path.join(root, "references", "CELL_PRESENTATION.md")))
self.assertTrue(os.path.isfile(os.path.join(root, "references", "WORK_ITEM_STATUS.md")))
self.assertTrue(
os.path.isfile(os.path.join(root, "assets", "schemas", "skill-actions.schema.json"))
)
@@ -348,6 +349,13 @@ class TestDocsStandards(unittest.TestCase):
"文件 / 图片上传",
"default_visible_columns_json",
"默认展示列",
"grid_help",
"表格说明 `grid_help`",
"表与表之间:禁止 SQLite 外键",
"禁止 SQLite 外键",
"跨表引用 lookup",
"\"multiple\": true",
"Lookup 多选",
):
self.assertIn(marker, text, msg=f"SCHEMA.md missing {marker!r}")
@@ -359,12 +367,14 @@ class TestDocsStandards(unittest.TestCase):
"跨表引用:Lookup",
"display_type: \"lookup\"",
"extra_column",
"multiple",
"数据管理新增/编辑",
"_jiangchang_columns.description",
"datetime_unix_seconds",
"SCHEMA.md",
"字段条件显隐",
"visibleWhen",
"grid_help",
):
self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}")