diff --git a/CHANGELOG.md b/CHANGELOG.md index d4e9a7f..02987f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ - 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节 - 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段 +## 1.0.111 + +- 沉淀数据管理呈现加法:同一 `cellPresentation.enabled` 支持单元格、整行、整列、行列交叉区域(闭集 tone) +- 新增 `_jiangchang_grid_presentation` 契约 / Schema / 示例;v1 `cells[]` 保持不变;禁止整表 `all`×`all`;整行色只涂表格、不铺到详情字段 +- 内部隐藏表补上两张呈现旁路表;模板默认仍不启用着色 + ## 1.0.110 - 数据管理侧栏除了生成出来的 HTML 快照,也会直接显示安装包里 `assets/html/pages/` 的页面,装完就有,不用点生成 diff --git a/SKILL.md b/SKILL.md index 0a9b5c6..211919b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 技能开发模板(通用业务版) description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。" -version: 1.0.110 +version: 1.0.111 author: 深圳匠厂科技有限公司 metadata: openclaw: @@ -42,7 +42,7 @@ allowed-tools: - **用户可见文案**:数据管理弹窗 / 任务中心文案须全中文产品用语;勿把 `pickCount`、`async`、`pending`、CLI 子命令写进弹窗(见 `references/ACTIONS.md`「用户可见文案」)。 - **`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`。 +- **`cellPresentation`(可选)**:需要数据管理「某格 / 整行 / 整列 / 区域」特殊标色时启用;技能按当前数据写入呈现(语义 tone),宿主映射主题样式;**未启用 = P0 无标色**。用途不限(超时、预警、驳回等)。详见 `references/CELL_PRESENTATION.md`。 - **业务行生命周期(有处理队列时)**:对某一行做生成/触达/私信等处理时,默认只操作「可操作」角色状态;开始抢占「进行中」;结束回写成功或失败(失败带原因);重做靠人工改回可操作。状态名由本技能自定。详见 `references/WORK_ITEM_STATUS.md`。 - **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;文件型特殊报告才另做 `export-*`(落 `output/`);**HTML**:壳子页放 `assets/html/pages/`,快照写 `html/` 并用固定 Action id `render-html`;页面用 `jiangchang.call` 调已声明命令(见 `references/ACTIONS.md`)。 - **准备 vs 执行(适用则拆)**:若技能存在「先写入/准备本地数据」与「再对外产生副作用」两段,Agent 必须能分开触发;禁止默认绑成不可拆的一步。一键无准备态的技能可跳过本条。 diff --git a/assets/examples/README.md b/assets/examples/README.md index 4b68735..3313371 100644 --- a/assets/examples/README.md +++ b/assets/examples/README.md @@ -3,6 +3,7 @@ - `version-response.json`:`python main.py version` 成功时 stdout 单行对象的形状参考。 - `skill-policy-snapshot.*.example.json`:Data Policy Snapshot 形状参考(见 `references/POLICY.md`)。 - `skill-cell-presentation.example.json`:数据管理单元格呈现文档形状参考(见 `references/CELL_PRESENTATION.md`)。 +- `skill-grid-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 形状参考(单选 + `multiple: true` 多选并列;跨表引用勿填编号;见 `references/ACTIONS.md`「跨表引用:Lookup」)。 - `visible-when-action.example.json`:Action 参数字段条件显隐 `visibleWhen` 形状参考(互斥模式联动;见 `references/ACTIONS.md`「字段条件显隐」;≠ Policy L3)。 diff --git a/assets/examples/skill-grid-presentation.example.json b/assets/examples/skill-grid-presentation.example.json new file mode 100644 index 0000000..0c3670c --- /dev/null +++ b/assets/examples/skill-grid-presentation.example.json @@ -0,0 +1,32 @@ +{ + "schemaVersion": 1, + "skill": "your-skill-slug", + "table": "business_records", + "rowKey": "id", + "generatedAt": "2026-08-10T06:00:00Z", + "rules": [ + { + "ruleKey": "qty-low-rows", + "rows": ["42", "99"], + "columns": "all", + "tone": "danger", + "message": "库存低于安全线", + "alert": true + }, + { + "ruleKey": "status-column", + "rows": "all", + "columns": ["status"], + "tone": "warning", + "message": "待审核" + }, + { + "ruleKey": "overdue-print-region", + "rows": ["7", "8"], + "columns": ["print_done_at", "ship_done_at"], + "tone": "danger", + "message": "环节到期未填", + "alert": true + } + ] +} diff --git a/assets/schemas/skill-actions.schema.json b/assets/schemas/skill-actions.schema.json index 0438321..c3c37f6 100644 --- a/assets/schemas/skill-actions.schema.json +++ b/assets/schemas/skill-actions.schema.json @@ -28,7 +28,7 @@ }, "cellPresentation": { "$ref": "#/$defs/cellPresentationDeclaration", - "description": "可选。启用数据管理单元格呈现(P1)。缺省或不 enabled = P0,宿主不读标色。详见 references/CELL_PRESENTATION.md。" + "description": "可选。启用数据管理呈现(P1:格子/行/列/区域)。缺省或不 enabled = P0,宿主不读标色。详见 references/CELL_PRESENTATION.md。" } }, "$defs": { diff --git a/assets/schemas/skill-cell-presentation.schema.json b/assets/schemas/skill-cell-presentation.schema.json index 983c48f..9ebbcf3 100644 --- a/assets/schemas/skill-cell-presentation.schema.json +++ b/assets/schemas/skill-cell-presentation.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://openclaw.local/skill-template/skill-cell-presentation.schema.json", "title": "SkillCellPresentationDocument", - "description": "匠厂数据管理「单元格呈现」文档(schemaVersion 1)。技能按业务规则为「某表某行某列」声明语义 tone(非任意 CSS);宿主映射主题样式。用途不限(时效超时、库存预警、状态异常、必填缺失等)。详见 references/CELL_PRESENTATION.md。", + "description": "匠厂数据管理「单元格呈现」文档(schemaVersion 1)。技能按业务规则为「某表某行某列」声明语义 tone(非任意 CSS);宿主映射主题样式。整行/整列/区域见 skill-grid-presentation.schema.json,同一 cellPresentation.enabled。用途不限(时效超时、库存预警、状态异常、必填缺失等)。详见 references/CELL_PRESENTATION.md。", "type": "object", "required": ["schemaVersion", "skill", "table", "rowKey", "cells"], "additionalProperties": false, diff --git a/assets/schemas/skill-grid-presentation.schema.json b/assets/schemas/skill-grid-presentation.schema.json new file mode 100644 index 0000000..1d70f85 --- /dev/null +++ b/assets/schemas/skill-grid-presentation.schema.json @@ -0,0 +1,93 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://openclaw.local/skill-template/skill-grid-presentation.schema.json", + "title": "SkillGridPresentationDocument", + "description": "匠厂数据管理「Grid Presentation」文档(schemaVersion 1)。技能按业务规则为 rows×columns 声明语义 tone(整行、整列、区域或单格);禁止 all×all。非任意 CSS。与 skill-cell-presentation.schema.json(v1 cells[])并存,同一 cellPresentation.enabled。详见 references/CELL_PRESENTATION.md。", + "type": "object", + "required": ["schemaVersion", "skill", "table", "rowKey", "rules"], + "additionalProperties": false, + "properties": { + "schemaVersion": { + "type": "integer", + "const": 1 + }, + "skill": { + "type": "string", + "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$", + "minLength": 1, + "description": "与 SKILL.md metadata.openclaw.slug / actions.json skill 一致" + }, + "table": { + "type": "string", + "minLength": 1, + "pattern": "^[a-z][a-z0-9_]*$", + "description": "业务表物理名(snake_case)" + }, + "rowKey": { + "type": "string", + "minLength": 1, + "pattern": "^[a-z][a-z0-9_]*$", + "description": "用于匹配行的业务表列名(如 id);rules[].rows 的 id 取自该列" + }, + "generatedAt": { + "type": "string", + "minLength": 1, + "description": "可选。ISO-8601 或技能约定的生成时间,便于排障" + }, + "rules": { + "type": "array", + "description": "当前应对该表生效的 grid 规则;空数组 = 该表无 grid 呈现(清除旧行/列/区域样式)。同 ruleKey 由 UNIQUE 约束覆盖。", + "items": { "$ref": "#/$defs/ruleItem" } + } + }, + "$defs": { + "tone": { + "type": "string", + "enum": ["danger", "warning", "info", "success", "muted"], + "description": "语义色板(闭集)。宿主映射到底色/字色/左边线等;禁止技能下发原始 CSS 或任意 hex。" + }, + "axisTarget": { + "description": "\"all\" = 该轴全部;非空字符串数组 = 有限集合(行 id 或列名)", + "oneOf": [ + { "type": "string", "const": "all" }, + { + "type": "array", + "minItems": 1, + "items": { "type": "string", "minLength": 1 } + } + ] + }, + "ruleItem": { + "type": "object", + "required": ["ruleKey", "rows", "columns", "tone"], + "additionalProperties": false, + "not": { + "properties": { + "rows": { "const": "all" }, + "columns": { "const": "all" } + }, + "required": ["rows", "columns"] + }, + "properties": { + "ruleKey": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "本表内唯一。建议稳定业务键(重算时覆盖同一条)" + }, + "rows": { "$ref": "#/$defs/axisTarget" }, + "columns": { "$ref": "#/$defs/axisTarget" }, + "tone": { "$ref": "#/$defs/tone" }, + "message": { + "type": "string", + "maxLength": 500, + "description": "可选。悬停/无障碍说明(短文案);不要塞长报告" + }, + "alert": { + "type": "boolean", + "description": "可选,默认 false。true = 按本条规则计入进页/同步提醒(不按命中格子数)" + } + } + } + } +} diff --git a/development/CHECKLIST.md b/development/CHECKLIST.md index 57d452e..7b8d4d9 100644 --- a/development/CHECKLIST.md +++ b/development/CHECKLIST.md @@ -149,6 +149,7 @@ checklist_version: 1.0 - [ ] A6-12 **P2** `display_type: boolean` 的展示列推荐同时配置 `options_json` 中文 label(如 停用/启用),避免仅靠宿主默认「是/否」;无 boolean 列 → **N/A** - [ ] A6-13 **P0** 新技能**不得**创建 `task_logs`;过程/进度/业务结果走运行日志、任务中心、业务表(见 `LOGGING.md`、`SCHEMA.md`) - [ ] A6-14 **P0** 废弃用户可见表时:`drop_deprecated_user_table` 物理删表 + 清元数据;**禁止**只删元数据留物理表;有迁移时须提供旧库回归测试(见 `SCHEMA.md`、`tests/samples/test_table_deprecation_migration.py.sample`) +- [ ] A6-15 **P1** 若启用 `cellPresentation`:旁路表不得出现在数据管理侧栏;整行着色不得铺到详情每个字段;禁止 `all`×`all`;tone 闭集。未启用 → **N/A**(见 `references/CELL_PRESENTATION.md`) --- diff --git a/development/SKILL_ACTION_RUNTIME.md b/development/SKILL_ACTION_RUNTIME.md index 54a5d17..4b5383b 100644 --- a/development/SKILL_ACTION_RUNTIME.md +++ b/development/SKILL_ACTION_RUNTIME.md @@ -21,7 +21,7 @@ 1. **宿主管调度,技能管业务** — 不自行实现任务中心 / Job 轮询 / 后台守护进程。 2. **单业务内核、多入口** — 数据管理、定时任务、Agent、技能详情与直接 CLI 复用同一 domain service;禁止按入口复制业务逻辑。 -3. **五职责正交** — `placements` / `bind.tables` / `executionProfile` / `entrypoint` / domain service 各管一事(见 ACTIONS.md)。可选第六轴 **policy**(见 `references/POLICY.md`):未启用 = L0 与今日一致。可选 **`hostEffects` / `steps`**(见 ACTIONS.md):数据管理交互前后的 UI/级联编排;省略 = 不额外改 UI(匠厂宿主 ≥2.8.5)。可选 **`hoverHint`**(见 ACTIONS.md):数据管理工具栏悬停;省略 = 不显示,不回退说明或确认黄条。可选 **`visibleWhen`**(见 ACTIONS.md):Action 参数表单字段条件显隐;省略 = 字段始终平铺;**勿与** Policy L3 行态 `visibleWhen` 混淆;**勿**用其藏映射主键。可选 **`pathRoot`**(见 ACTIONS.md):`file`/`directory` 原生选路对话框起始目录(相对技能数据根;省略默认 `{skill_data}/input`)。可选 **`bind.labelColumn`**(见 ACTIONS.md):选择摘要可读标签列;省略 = 宿主常见列名启发。可选 **`bind.selectionMode`**(见 ACTIONS.md):表勾选如何驱动 Action(`required` / `none` / `optional`);省略 = 宿主按 `$selection.ids` / `batch` 推断。可选 **`revealInForm`**(见 ACTIONS.md):有「已选择」时仍强制显示某 `readOnly` 字段;省略 = 映射只读默认不占 form 参数区(仍提交)。可选 **`cellPresentation`**(见 `references/CELL_PRESENTATION.md`):单元格语义标色;省略 = P0。**用户可见文案**(见 ACTIONS.md「用户可见文案」):弹窗 `description` / 确认黄条 / 参数说明 / 任务失败 `message` 须全中文产品用语;Agent 路由勿塞进弹窗。 +3. **五职责正交** — `placements` / `bind.tables` / `executionProfile` / `entrypoint` / domain service 各管一事(见 ACTIONS.md)。可选第六轴 **policy**(见 `references/POLICY.md`):未启用 = L0 与今日一致。可选 **`hostEffects` / `steps`**(见 ACTIONS.md):数据管理交互前后的 UI/级联编排;省略 = 不额外改 UI(匠厂宿主 ≥2.8.5)。可选 **`hoverHint`**(见 ACTIONS.md):数据管理工具栏悬停;省略 = 不显示,不回退说明或确认黄条。可选 **`visibleWhen`**(见 ACTIONS.md):Action 参数表单字段条件显隐;省略 = 字段始终平铺;**勿与** Policy L3 行态 `visibleWhen` 混淆;**勿**用其藏映射主键。可选 **`pathRoot`**(见 ACTIONS.md):`file`/`directory` 原生选路对话框起始目录(相对技能数据根;省略默认 `{skill_data}/input`)。可选 **`bind.labelColumn`**(见 ACTIONS.md):选择摘要可读标签列;省略 = 宿主常见列名启发。可选 **`bind.selectionMode`**(见 ACTIONS.md):表勾选如何驱动 Action(`required` / `none` / `optional`);省略 = 宿主按 `$selection.ids` / `batch` 推断。可选 **`revealInForm`**(见 ACTIONS.md):有「已选择」时仍强制显示某 `readOnly` 字段;省略 = 映射只读默认不占 form 参数区(仍提交)。可选 **`cellPresentation`**(见 `references/CELL_PRESENTATION.md`):单元格/整行/整列/区域语义标色;省略 = P0。**用户可见文案**(见 ACTIONS.md「用户可见文案」):弹窗 `description` / 确认黄条 / 参数说明 / 任务失败 `message` 须全中文产品用语;Agent 路由勿塞进弹窗。 4. **长耗时建议 async** — 开浏览器、RPA、HTTP 大文件下载等建议 `executionProfile: "async"`,进**任务中心**;这是对**方法**的选择,**不**限制可以出现在哪些 placement / 入口。 5. **Agent:短 CLI / 长 Action** — 短查询可用共享 Python bash CLI;长任务 / RPA **禁止** `exec` + `process poll`,必须 `run_skill_action`。 6. **进度走 Journal** — `emit` / `video.add_step` → Run Journal;UI / Skill Run Card / 任务中心展示;**不靠 stdout 流**。 diff --git a/references/ACTIONS.md b/references/ACTIONS.md index d7eac6c..480db06 100644 --- a/references/ACTIONS.md +++ b/references/ACTIONS.md @@ -77,7 +77,7 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表** | `skill` | 是 | 必须与 `SKILL.md` 的 `metadata.openclaw.slug` 一致 | | `actions` | 是 | Action 数组,每项 `id` 唯一;**数组相对顺序**决定数据管理 toolbar / row / batch 技能按钮的展示顺序(见下节) | -模板最小示例见 [`assets/actions.json`](../assets/actions.json);JSON Schema 见 [`assets/schemas/skill-actions.schema.json`](../assets/schemas/skill-actions.schema.json)。可选 Data Policy 见 [`POLICY.md`](POLICY.md) 与 [`assets/schemas/skill-policy-snapshot.schema.json`](../assets/schemas/skill-policy-snapshot.schema.json)。可选单元格呈现见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md) 与 [`assets/schemas/skill-cell-presentation.schema.json`](../assets/schemas/skill-cell-presentation.schema.json)。 +模板最小示例见 [`assets/actions.json`](../assets/actions.json);JSON Schema 见 [`assets/schemas/skill-actions.schema.json`](../assets/schemas/skill-actions.schema.json)。可选 Data Policy 见 [`POLICY.md`](POLICY.md) 与 [`assets/schemas/skill-policy-snapshot.schema.json`](../assets/schemas/skill-policy-snapshot.schema.json)。可选单元格/整行/整列呈现见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)、[`assets/schemas/skill-cell-presentation.schema.json`](../assets/schemas/skill-cell-presentation.schema.json) 与 [`assets/schemas/skill-grid-presentation.schema.json`](../assets/schemas/skill-grid-presentation.schema.json)。 ## 六职责正交(必读) @@ -91,7 +91,7 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表** | **`hoverHint`** | **可选**:数据管理**工具栏悬停**文案 | 不决定短按钮名;不决定点下去弹不弹窗;**不回退** `description` / `confirmation.message`;省略 = 不显示悬停。当前宿主实现为顶栏技能按钮 | | **`inputSchema.visibleWhen`** | **可选**:Action **参数表单**字段条件显隐(先选模式再出对应参数) | **不是** Data Policy L3 行态 `visibleWhen`;不决定 placements;旧宿主忽略则字段平铺全显 | | **`hostEffects` / `steps`** | **可选**:数据管理交互跑完前后,宿主要不要刷新表 / 清选区 / 级联删等 | 不决定业务写什么;**省略 = 宿主不额外改 UI**(≥2.8.5 起不再硬编码 mutation 后刷新);cron/agent 忽略 | -| **`cellPresentation`** | **可选**:某表某行某列的语义标色/强调(tone + 说明) | 不替代业务列;不下发 CSS;用途不限(超时、预警、驳回等);见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md) | +| **`cellPresentation`** | **可选**:某表某格 / 整行 / 整列 / 区域的语义标色/强调(tone + 说明) | 不替代业务列;不下发 CSS;用途不限(超时、预警、驳回等);见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md) | | **`policy` / Snapshot** | **可选**:当前身份在数据管理下允许哪些 Action / 宿主内置能力 / 行列范围 | 不替代 placements;不替代 domain 写路径鉴权;多数技能不配 = L0 全开 | | **`entrypoint`** | 实际执行哪个 CLI 命令与参数 | 不写业务逻辑 | | **domain service** | **真正完成什么业务** | 不感知 toolbar / cron / agent 来源 | @@ -102,14 +102,14 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表** | 层级 | 稳定字段 / 能力 | |------|----------------| -| 顶层 | `schemaVersion`、`skill`、`actions`;可选 **`policy`**(Data Policy 启用块);可选 **`cellPresentation`**(单元格呈现启用块) | +| 顶层 | `schemaVersion`、`skill`、`actions`;可选 **`policy`**(Data Policy 启用块);可选 **`cellPresentation`**(格子/行/列/区域呈现启用块) | | 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"` \| `"import"` — 接管默认新增/编辑/删除/导入(须 `bind.tables`) | | **bind** | `{ "tables": ["business_table"] }`;可选 `inputMapping`、`columnMapping`、**`labelColumn`**、**`selectionMode`**(行内/批量预填、选择摘要标签、勾选驱动模式,见下节) | | **policy** | 可选;`enabled` + `entrypoint`(推荐 `policy` CLI);见 POLICY.md。未启用 = L0 | -| **cellPresentation** | 可选;`{ "enabled": true }` 时宿主应加载单元格呈现;见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)。未启用 = P0 | +| **cellPresentation** | 可选;`{ "enabled": true }` 时宿主应加载格子与 grid 呈现;见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)。未启用 = P0 | | inputSchema | 根 `type: object`;properties 使用 `string` / `number` / `boolean`;支持 `enum`、`default`、`required`、`sensitive`、`readOnly`、可选 **`revealInForm`**;可选 `display_type` / `format`(含 `file` / `directory` 等);可选 **`pathRoot`**(file/directory 起始目录,相对技能数据根);可选 **`visibleWhen`**(字段条件显隐,见专节) | | confirmation | `{ "message": "..." }` — **风险文案**(confirm/form 弹窗黄条;agent 元数据);**不是**「要不要弹窗」的开关 | | **hoverHint** | 可选字符串。数据管理**工具栏悬停**文案;与 `description` / `confirmation.message` **独立**。省略则不显示悬停,不回退。短名已够懂时不要写。需宿主已支持该字段;当前实现为顶栏技能按钮,不含宿主内置删除/刷新/新增/导入/导出 | diff --git a/references/CELL_PRESENTATION.md b/references/CELL_PRESENTATION.md index a5c9fca..077504e 100644 --- a/references/CELL_PRESENTATION.md +++ b/references/CELL_PRESENTATION.md @@ -1,9 +1,9 @@ # 数据管理单元格呈现(Cell Presentation)契约(v1.0) -> **状态:正式契约 v1.0(`schemaVersion` 1)。** -> **目标:** 冻结「技能声明某表某行某列的语义呈现 → 宿主映射主题样式」的通用能力,供任意业务场景复用。 +> **状态:正式契约 v1.0(`cells[]` 文档 `schemaVersion` 1)+ 加法 Grid Presentation。** +> **目标:** 冻结「技能声明语义呈现 → 宿主映射主题样式」的通用能力:某一格、整行、整列、若干行×若干列,供任意业务场景复用。 > **非目标:** 超时/SLA 专用字段;技能下发任意 CSS / 任意色值;宿主实现业务规则引擎。 -> **配套 Schema:** [`../assets/schemas/skill-cell-presentation.schema.json`](../assets/schemas/skill-cell-presentation.schema.json)。 +> **配套 Schema:** [`../assets/schemas/skill-cell-presentation.schema.json`](../assets/schemas/skill-cell-presentation.schema.json)(格子)、[`../assets/schemas/skill-grid-presentation.schema.json`](../assets/schemas/skill-grid-presentation.schema.json)(行/列/区域)。 相关正交轴见 [`ACTIONS.md`](ACTIONS.md)、[`POLICY.md`](POLICY.md)、[`SCHEMA.md`](SCHEMA.md)。 @@ -11,9 +11,17 @@ ## 0. 一句话 -技能可以声明:**哪张业务表、哪一行、哪一列**需要特殊标色/强调(语义 `tone` + 可选说明)。 +技能可以声明:**哪张业务表**上,**哪些行 × 哪些列**需要特殊标色/强调(语义 `tone` + 可选说明)。 + +| 想涂什么 | 怎么声明 | +|----------|----------| +| 某一格 | 一行 × 一列(v1 `cells[]` 或一条格子规则) | +| 整行 | 若干行 × 该轴全部列 | +| 整列 | 该轴全部行 × 若干列 | +| 区域 | 若干行 × 若干列 | + 用途不限:时效超时未填、库存预警、状态异常、必填缺失、审核驳回等。 -**不是**「再加一列超时文字」;强调落在**真正要关注的业务格子**上。 +**不是**「再加一列超时文字」;强调落在**真正要关注的业务格子或行**上。 --- @@ -22,9 +30,20 @@ | 档位 | 技能行为 | 宿主行为 | |------|----------|----------| | **P0(默认)** | 不写呈现数据 / 未启用 | 数据管理表格与今日一致,无特殊标色 | -| **P1** | 按规则写入呈现文档或推荐旁路表 | 渲染对应单元格的语义样式;可选汇总 `alert: true` 做进页提醒 | +| **P1** | 按规则写入呈现文档或推荐旁路表 | 渲染对应格子 / 行 / 列 / 区域的语义样式;可选汇总 `alert: true` 做进页提醒 | -多数技能保持 **P0**。需要视觉强调时再启用。 +多数技能保持 **P0**。需要视觉强调时再启用。模板 `actions.json` **默认不启用**。 + +--- + +## 0.2 宿主能力 + +| 能力 | 旁路表 | 宿主 | +|------|--------|------| +| 格子着色 | `_jiangchang_cell_presentation` | 既有宿主即可 | +| 整行 / 整列 / 区域 | `_jiangchang_grid_presentation` | 需已读取该表(匠厂数据管理已支持)。**旧宿主忽略该表**,格子着色仍有效 | + +同一开关 `cellPresentation.enabled` 同时打开两张旁路表。未建表 = 该通道为空,宿主忽略。 --- @@ -32,17 +51,17 @@ | 角色 | 负责 | 不负责 | |------|------|--------| -| **技能** | 按**当前业务数据**算出哪些格子要强调;在数据变更后**重算**并覆盖旧结果 | 不写宿主 CSS;不把强调文案污染进业务真值列冒充「变红」 | -| **宿主** | 读取呈现结果,按闭集 `tone` 映射主题样式;可选 Toast/提醒汇总 | 不理解「提前 3 天交单」等业务规则;不为单个技能写死样式 | +| **技能** | 按**当前业务数据**算出哪些格子/行/列要强调;在数据变更后**重算**并覆盖旧结果 | 不写宿主 CSS;不把强调文案污染进业务真值列冒充「变红」 | +| **宿主** | 读取呈现结果,按闭集 `tone` 映射主题样式;按几何涂表格;可选 Toast/提醒汇总 | 不理解「提前 3 天交单」等业务规则;不为单个技能写死样式 | **派生状态(重要):** 呈现是计算结果,不是粘性开关。 用户补填数据 / 同步刷新后技能重算 → 该项消失 → **样式与提醒自然关闭**。不要单独维护「已关闭提醒」状态机(除非产品另做「忽略此条」能力,本契约不冻结)。 --- -## 2. 文档形状(冻) +## 2. 格子文档形状(冻,`schemaVersion` 1) -技能产出一份 **Cell Presentation Document**(可落库、可 CLI 打印,形状相同),须符合 Schema。 +技能产出一份 **Cell Presentation Document**(可落库、可 CLI 打印,形状相同),须符合格子 Schema。v1 **只描述某一格**;整行/整列用 §2.1。 ```json { @@ -71,14 +90,14 @@ | 字段 | 必填 | 说明 | |------|------|------| -| `schemaVersion` | 是 | 固定 `1` | +| `schemaVersion` | 是 | 固定 `1`(本形状不升版本) | | `skill` | 是 | 与 slug 一致 | | `table` | 是 | 当前文档作用的业务表 | | `rowKey` | 是 | 行匹配列(`id` / `feishu_record_id` 等) | | `generatedAt` | 否 | 排障用 | -| `cells` | 是 | 数组;**空数组 = 该表清除全部特殊呈现** | +| `cells` | 是 | 数组;**空数组 = 该表清除全部格子呈现** | -### 2.1 `cells[]` 项 +### 2.0 `cells[]` 项 | 字段 | 必填 | 说明 | |------|------|------| @@ -91,6 +110,74 @@ 同一 `(rowId, column)` 多条:宿主可**后者覆盖前者**。 `column` 对用户不可见(policy 打码 / 列隐藏)时:宿主可仍应用样式,或跳过——**推荐跳过不可见列的强调**,避免泄露「有问题」的存在感(技能仍可写,宿主按可见性过滤)。 +### 2.1 Grid 文档形状(加法,独立 Schema) + +整行 / 整列 / 区域用 **Grid Presentation Document**(`rules[]`)。与 `cells[]` **并存**,不替代、不升 `cells` 的 `schemaVersion`。 + +统一几何:**`rows` × `columns`**。某一侧为 `"all"`(或省略,落库时写成 JSON `"all"`)= 该轴全部。禁止两侧都是全部(见 §4.2)。 + +```json +{ + "schemaVersion": 1, + "skill": "your-skill-slug", + "table": "business_records", + "rowKey": "id", + "generatedAt": "2026-08-10T06:00:00Z", + "rules": [ + { + "ruleKey": "qty-low-rows", + "rows": ["42", "99"], + "columns": "all", + "tone": "danger", + "message": "库存低于安全线", + "alert": true + }, + { + "ruleKey": "status-column", + "rows": "all", + "columns": ["status"], + "tone": "warning", + "message": "待审核" + }, + { + "ruleKey": "overdue-print-region", + "rows": ["7", "8"], + "columns": ["print_done_at", "ship_done_at"], + "tone": "danger", + "message": "环节到期未填", + "alert": true + } + ] +} +``` + +| 字段 | 必填 | 说明 | +|------|------|------| +| `schemaVersion` | 是 | 固定 `1` | +| `skill` / `table` / `rowKey` | 是 | 同格子文档;`rowKey` 用于匹配 `rows` 里的 id | +| `rules` | 是 | 数组;**空数组 = 该表清除全部 grid 规则** | + +#### `rules[]` 项 + +| 字段 | 必填 | 说明 | +|------|------|------| +| `ruleKey` | 是 | 本表内唯一(建议稳定业务键,便于重算覆盖) | +| `rows` | 是 | `"all"` 或行 id 字符串数组(对应 `rowKey` 列值) | +| `columns` | 是 | `"all"` 或业务列名数组 | +| `tone` | 是 | 见 §3 | +| `message` | 否 | ≤500 | +| `alert` | 否 | 默认 `false`;按**规则条数**计提醒,不按命中格子数 | + +| `rows` × `columns` | 宿主 scope | +|--------------------|-----------| +| 1 行 × 1 列 | `cell` | +| 若干行 × 若干列(非单格) | `region` | +| 若干行 × `"all"` | `row`(整行) | +| `"all"` × 若干列 | `column`(整列) | +| `"all"` × `"all"` | **禁止**,宿主丢弃该规则 | + +完整 JSON 例见 [`../assets/examples/skill-grid-presentation.example.json`](../assets/examples/skill-grid-presentation.example.json)。 + --- ## 3. `tone` 闭集(冻) @@ -106,13 +193,13 @@ **禁止:** 技能下发原始 `#RRGGBB`、任意 CSS class、内联 style。 以后若扩展色板,只能**追加新 tone token** 并升契约,不能开放任意色值。 -「技能配置什么就显示什么」在本契约中的含义是:技能选择 **哪一格 + 哪个 tone + 什么说明**;**像素级颜色由宿主主题决定**,以保证多技能 UI 一致与安全。 +「技能配置什么就显示什么」在本契约中的含义是:技能选择 **哪一格/行/列 + 哪个 tone + 什么说明**;**像素级颜色由宿主主题决定**,以保证多技能 UI 一致与安全。 --- ## 4. 推荐持久化(给宿主实现) -v1 **推荐**技能将「当前表」的呈现写入用户库旁路表(与业务表分离,**不是**多余业务超时列): +v1 **推荐**技能将「当前表」的**格子**呈现写入用户库旁路表(与业务表分离,**不是**多余业务超时列): ```sql CREATE TABLE IF NOT EXISTS _jiangchang_cell_presentation ( @@ -130,19 +217,60 @@ CREATE TABLE IF NOT EXISTS _jiangchang_cell_presentation ( ); ``` +### 4.1 整行 / 整列 / 区域(`_jiangchang_grid_presentation`) + +```sql +CREATE TABLE IF NOT EXISTS _jiangchang_grid_presentation ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + table_name TEXT NOT NULL, + rule_key TEXT NOT NULL, + row_key TEXT NOT NULL DEFAULT '', + rows_json TEXT NOT NULL, + columns_json TEXT NOT NULL, + tone TEXT NOT NULL, + message TEXT, + alert INTEGER NOT NULL DEFAULT 0, + generated_at TEXT, + updated_at INTEGER NOT NULL DEFAULT (unixepoch()), + UNIQUE (table_name, rule_key) +); +``` + +`rows_json` / `columns_json` 存 JSON:字符串 `"all"`,或 id / 列名数组(如 `["42","99"]`)。 +`row_key`:有限行集合时必填(与文档 `rowKey` 相同,如 `id`);整列(`rows_json = "all"`)可空字符串。 + +两张表都列入技能 `INTERNAL_METADATA_TABLES`(见 `scripts/db/display_metadata.py`),**不得**作为业务表出现在数据管理侧栏。模板 **不**在 `init_db()` 里默认建这两张表;技能用到时再 `CREATE`。 + +### 4.2 冲突与特异性 + +同一格被多条规则命中时,宿主按 **格子 > 区域 > 行 > 列** 取胜。同级后者覆盖前者。 + +宿主先加载 grid 规则,再加载 v1 `cells[]`(同级时 **v1 格子仍赢**),因此旧技能只写 `cells[]` 的行为不变。 + +**禁止 `all` × `all`**(会把整张表涂满)。技能不要写;宿主遇到则丢弃该规则。 + +### 4.3 表格 vs 详情 + +| 表面 | 整行(scope=`row`) | 格子 / 列 / 区域 | +|------|-------------------|------------------| +| 数据管理表格 | 给整行(``)上色 | 给对应单元格上色 | +| 行详情 | **不**把行色铺到每个字段 | 对应字段可保留 tone | + +整行强调的是「这一行在表里扎眼」,不是「打开详情后每个值都变色」。 + 约定: -- 重算某表时:**先删该 `table_name` 全部行,再插入新结果**(或等价事务替换),保证填完后旧 `danger` 不会残留。 -- 宿主打开数据管理某表时:若该技能启用了呈现(见 §5)且存在此表,则按当前页行的 `rowKey` 值匹配并着色。 +- 重算某表时:**先删该 `table_name` 在两张旁路表中的全部行,再插入新结果**(或等价事务替换),保证填完后旧 `danger` 不会残留。只维护其中一张时,至少清掉自己负责的那张,并删掉已迁走的旧行(例如从格子改成整行后,删掉对应 `_jiangchang_cell_presentation`)。 +- 宿主打开数据管理某表时:若该技能启用了呈现(见 §5)且存在旁路表,则按当前页行的 `rowKey` 值匹配并着色。 - 也允许技能用 CLI 打印整份 Document 供调试;**运行时以库内旁路表为准**(除非后续宿主另定拉取入口)。 -未创建该旁路表 = P0,宿主忽略。 +未创建该旁路表 = 该通道 P0,宿主忽略。 --- ## 5. 在 `actions.json` 中启用(可选,推荐) -与 `policy` 类似,顶层可选: +与 `policy` 类似,顶层可选。**一份开关同时启用格子与 grid:** ```json { @@ -158,7 +286,7 @@ CREATE TABLE IF NOT EXISTS _jiangchang_cell_presentation ( | 字段 | 说明 | |------|------| | `cellPresentation` | 省略 = P0 | -| `cellPresentation.enabled` | 仅 `true` 表示宿主应尝试加载呈现;`false`/缺省等同 P0 | +| `cellPresentation.enabled` | 仅 `true` 表示宿主应尝试加载呈现(两张旁路表);`false`/缺省等同 P0 | v1 **不**要求单独 CLI entrypoint(与 policy 不同);技能在 `sync` / 写路径 / `init-db` 后自行维护旁路表即可。若未来需要「仅刷新呈现」Action,用普通 Skill Action 即可,不必扩展本启用块。 @@ -172,8 +300,8 @@ v1 **不**要求单独 CLI entrypoint(与 policy 不同);技能在 `sync` 2. 会改变「是否仍需强调」的写路径成功之后(新建、更新、登记等) 3. (可选)用户显式「刷新呈现」类 Action -进页 Toast:宿主可在加载表后统计当前页或全表 `alert=1` 的条数;**0 则不打扰**。 -提醒关闭 = 重算后没有对应 cell,而不是用户点「我知道了」写死关闭(除非另做忽略列表,本契约不冻)。 +进页 Toast:宿主可在加载表后统计当前页或全表 `alert=1` 的**规则条数**(grid 按规则计,不按命中格子数);**0 则不打扰**。 +提醒关闭 = 重算后没有对应 cell/rule,而不是用户点「我知道了」写死关闭(除非另做忽略列表,本契约不冻)。 --- @@ -181,7 +309,7 @@ v1 **不**要求单独 CLI entrypoint(与 policy 不同);技能在 `sync` | 契约 | 关系 | |------|------| -| **SCHEMA / `_jiangchang_columns`** | 管列是否可见、类型、中文名;**不管**条件标色 | +| **SCHEMA / `_jiangchang_columns`** | 管列是否可见、类型、中文名;**不管**条件标色。呈现旁路表须进内部隐藏表集合 | | **POLICY Snapshot** | 管谁能看哪些 Action/行列;呈现仍建议按可见列过滤 | | **hostEffects / steps** | 管 Action 跑完后是否 `reloadTable`;重载后宿主应重新读呈现 | | **业务列真值** | 保持干净;不要把 `【超时】` 写进业务字段冒充变色 | @@ -195,16 +323,21 @@ v1 **不**要求单独 CLI entrypoint(与 policy 不同);技能在 `sync` - 只在首次超时时打标、填完不重算导致红字残留 - 宿主写死某个技能 slug 的特殊染色逻辑 - 把 Cell Presentation 做成仅服务 SLA 的专用协议 +- 写 `rows: "all"` 且 `columns: "all"` 企图整表变色 +- 假定打开行详情后,整行色会铺到每一个字段 +- 把某一业务技能的检查逻辑(例如「检查列 → 整行」)复制进模板当通用运行时 --- ## 9. 示例场景(说明用途不限) -| 场景 | column | tone | message 例 | -|------|--------|------|------------| -| 打单超时未填 | `print_done_at` | `danger` | 打单已超 1 天未填 | -| 库存低于安全线 | `qty` | `warning` | 低于安全库存 | -| 审核驳回 | `status` | `danger` | 已驳回,请修改 | -| 选填备注缺失但不紧急 | `notes` | `muted` | 建议补充备注 | +| 场景 | 几何 | tone | message 例 | +|------|------|------|------------| +| 打单超时未填 | 格子 `print_done_at` | `danger` | 打单已超 1 天未填 | +| 库存低于安全线 | **整行**(该行 × `all`) | `warning` | 低于安全库存 | +| 审核驳回 | 格子或整列 `status` | `danger` | 已驳回,请修改 | +| 选填备注缺失但不紧急 | 格子 `notes` | `muted` | 建议补充备注 | +| 两个日期列同时逾期 | 区域:若干行 × `print_done_at`+`ship_done_at` | `danger` | 环节到期未填 | -完整 JSON 例见 [`../assets/examples/skill-cell-presentation.example.json`](../assets/examples/skill-cell-presentation.example.json)。 +格子 JSON 例见 [`../assets/examples/skill-cell-presentation.example.json`](../assets/examples/skill-cell-presentation.example.json)。 +Grid JSON 例见 [`../assets/examples/skill-grid-presentation.example.json`](../assets/examples/skill-grid-presentation.example.json)。 diff --git a/references/README.md b/references/README.md index 8619000..eeb0ed6 100644 --- a/references/README.md +++ b/references/README.md @@ -20,8 +20,8 @@ | [`ACTIONS.md`](ACTIONS.md) | Skill Action、sync/async、任务中心、多入口、按钮中文名 `label` 参考(按入口定位表挂载;先读 `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 不配) | +| [`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`;队列处理状态机读 `WORK_ITEM_STATUS.md`。 +按需阅读:短操作优先 `CLI.md`;Action / 任务中心读 `ACTIONS.md`;结构化字段读 `SCHEMA.md`;权限 opt-in 读 `POLICY.md`;格子/行/列标色读 `CELL_PRESENTATION.md`;队列处理状态机读 `WORK_ITEM_STATUS.md`。 diff --git a/references/SCHEMA.md b/references/SCHEMA.md index e000a44..17ba7ca 100644 --- a/references/SCHEMA.md +++ b/references/SCHEMA.md @@ -17,7 +17,7 @@ - 需要「新增表单预填」时,在物理列上声明 SQLite `DEFAULT`(勿把默认值只写在文案里) - 定义哪些字段可见、可搜索、可创建、可编辑 - 定义字段展示类型和枚举选项 -- (可选)按业务规则维护单元格呈现旁路数据(语义 tone;见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)) +- (可选)按业务规则维护单元格 / 整行 / 整列 / 区域呈现旁路数据(语义 tone;见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)) - 保证元数据与真实数据库结构一致 - 保证元数据初始化幂等 @@ -36,7 +36,7 @@ - 不猜测技能业务含义 - 不根据具体技能名称写特殊逻辑 - 可在技能安装/更新成功后静默调用 `python scripts/main.py init-db` 做幂等 ensure(内部即 `init_db()`);失败不影响安装结果 -- 若技能启用了单元格呈现(见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)),读取旁路呈现数据并为单元格映射语义样式(`tone`);不猜测业务规则 +- 若技能启用了单元格呈现(见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)),读取旁路呈现数据并为格子 / 行 / 列 / 区域映射语义样式(`tone`);不猜测业务规则;整行色只涂表格行,不铺到详情字段 ## 初始化入口 @@ -45,7 +45,8 @@ SQLite **没有**可靠的 `COMMENT ON TABLE/COLUMN`;SQL 文件里的 `-- 注释` 也不会成为可查询结构。 因此中文展示名称必须写入元数据表;字段顺序必须写在 `CREATE TABLE` 的列定义顺序中(`PRAGMA table_info` 的 `cid`)。 -> **禁止**新技能使用宿主的旧版 `_schema_meta` 表。仅 `_jiangchang_tables` / `_jiangchang_columns` 为正式契约。 +> **禁止**新技能使用宿主的旧版 `_schema_meta` 表。仅 `_jiangchang_tables` / `_jiangchang_columns` 为正式契约。 +> 可选呈现旁路表 `_jiangchang_cell_presentation` / `_jiangchang_grid_presentation` **不是**展示元数据,但须列入 `INTERNAL_METADATA_TABLES`(侧栏不可见)。见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)。 ## 技能名称(SKILL.md) diff --git a/scripts/db/display_metadata.py b/scripts/db/display_metadata.py index 6fe3b3a..3a91327 100644 --- a/scripts/db/display_metadata.py +++ b/scripts/db/display_metadata.py @@ -9,7 +9,15 @@ from __future__ import annotations from typing import Iterable # 宿主会隐藏这些表,不作为业务表展示(见 jiangchang electron/data-management/types.ts) -INTERNAL_METADATA_TABLES = frozenset({"_jiangchang_tables", "_jiangchang_columns"}) +# 呈现旁路表由技能按需 CREATE,见 references/CELL_PRESENTATION.md;未建表不影响 P0。 +INTERNAL_METADATA_TABLES = frozenset( + { + "_jiangchang_tables", + "_jiangchang_columns", + "_jiangchang_cell_presentation", + "_jiangchang_grid_presentation", + } +) LEGACY_METADATA_TABLES = frozenset({"_schema_meta"}) METADATA_TABLES = INTERNAL_METADATA_TABLES | LEGACY_METADATA_TABLES diff --git a/scripts/util/constants.py b/scripts/util/constants.py index fcc45ba..5e64c79 100644 --- a/scripts/util/constants.py +++ b/scripts/util/constants.py @@ -1,6 +1,6 @@ """技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。""" SKILL_SLUG = "your-skill-slug" -SKILL_VERSION = "1.0.110" +SKILL_VERSION = "1.0.111" LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug" PLATFORM_KIT_MIN_VERSION = "1.2.11" diff --git a/tests/test_actions_manifest.py b/tests/test_actions_manifest.py index 84de4f2..df724fd 100644 --- a/tests/test_actions_manifest.py +++ b/tests/test_actions_manifest.py @@ -736,6 +736,7 @@ class TestActionsManifest(unittest.TestCase): "afterFailure", "cellPresentation", "CELL_PRESENTATION.md", + "整行", "labelColumn", "selectionMode", "勾选驱动", diff --git a/tests/test_cell_presentation_contract.py b/tests/test_cell_presentation_contract.py index e600d81..57a7739 100644 --- a/tests/test_cell_presentation_contract.py +++ b/tests/test_cell_presentation_contract.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Cell Presentation 契约:schema + 文档 + 示例。""" +"""Cell / Grid Presentation 契约:schema + 文档 + 示例。""" from __future__ import annotations import json @@ -9,31 +9,34 @@ import unittest from util.runtime_paths import get_skill_root -def _load_schema() -> dict: - path = os.path.join( - get_skill_root(), - "assets", - "schemas", - "skill-cell-presentation.schema.json", - ) - with open(path, encoding="utf-8") as f: +def _skill_path(*parts: str) -> str: + return os.path.join(get_skill_root(), *parts) + + +def _load_json(*parts: str) -> dict: + with open(_skill_path(*parts), encoding="utf-8") as f: return json.load(f) -def _load_example() -> dict: - path = os.path.join( - get_skill_root(), - "assets", - "examples", - "skill-cell-presentation.example.json", - ) - with open(path, encoding="utf-8") as f: - return json.load(f) +def _load_cell_schema() -> dict: + return _load_json("assets", "schemas", "skill-cell-presentation.schema.json") + + +def _load_cell_example() -> dict: + return _load_json("assets", "examples", "skill-cell-presentation.example.json") + + +def _load_grid_schema() -> dict: + return _load_json("assets", "schemas", "skill-grid-presentation.schema.json") + + +def _load_grid_example() -> dict: + return _load_json("assets", "examples", "skill-grid-presentation.example.json") class TestCellPresentationContract(unittest.TestCase): def test_schema_file_exists_and_parses(self) -> None: - schema = _load_schema() + schema = _load_cell_schema() self.assertEqual(schema.get("title"), "SkillCellPresentationDocument") self.assertIs(schema.get("additionalProperties"), False) self.assertEqual(schema["properties"]["schemaVersion"].get("const"), 1) @@ -45,33 +48,38 @@ class TestCellPresentationContract(unittest.TestCase): import jsonschema except ImportError: self.skipTest("jsonschema not installed") - jsonschema.validate(instance=_load_example(), schema=_load_schema()) + jsonschema.validate(instance=_load_cell_example(), schema=_load_cell_schema()) def test_actions_schema_defines_cell_presentation_enable_block(self) -> None: - path = os.path.join( - get_skill_root(), "assets", "schemas", "skill-actions.schema.json" - ) - with open(path, encoding="utf-8") as f: - actions_schema = json.load(f) + actions_schema = _load_json("assets", "schemas", "skill-actions.schema.json") self.assertIn("cellPresentation", actions_schema["properties"]) self.assertIn("cellPresentationDeclaration", actions_schema["$defs"]) def test_docs_and_example_exist(self) -> None: - root = get_skill_root() - self.assertTrue(os.path.isfile(os.path.join(root, "references", "CELL_PRESENTATION.md"))) + self.assertTrue(os.path.isfile(_skill_path("references", "CELL_PRESENTATION.md"))) self.assertTrue( os.path.isfile( - os.path.join(root, "assets", "schemas", "skill-cell-presentation.schema.json") + _skill_path("assets", "schemas", "skill-cell-presentation.schema.json") ) ) self.assertTrue( os.path.isfile( - os.path.join(root, "assets", "examples", "skill-cell-presentation.example.json") + _skill_path("assets", "examples", "skill-cell-presentation.example.json") + ) + ) + self.assertTrue( + os.path.isfile( + _skill_path("assets", "schemas", "skill-grid-presentation.schema.json") + ) + ) + self.assertTrue( + os.path.isfile( + _skill_path("assets", "examples", "skill-grid-presentation.example.json") ) ) def test_cell_presentation_md_covers_contract(self) -> None: - path = os.path.join(get_skill_root(), "references", "CELL_PRESENTATION.md") + path = _skill_path("references", "CELL_PRESENTATION.md") with open(path, encoding="utf-8") as f: text = f.read() for marker in ( @@ -80,14 +88,77 @@ class TestCellPresentationContract(unittest.TestCase): "warning", "alert", "_jiangchang_cell_presentation", + "_jiangchang_grid_presentation", "cellPresentation", "派生", "任意 CSS", "用途不限", "schemaVersion", + "整行", + "整列", + "all", + "格子 > 区域 > 行 > 列", + "详情", ): self.assertIn(marker, text, msg=f"CELL_PRESENTATION.md missing {marker!r}") + def test_grid_schema_file_exists_and_parses(self) -> None: + schema = _load_grid_schema() + self.assertEqual(schema.get("title"), "SkillGridPresentationDocument") + self.assertIs(schema.get("additionalProperties"), False) + self.assertEqual(schema["properties"]["schemaVersion"].get("const"), 1) + tones = set(schema["$defs"]["tone"].get("enum") or []) + self.assertEqual(tones, {"danger", "warning", "info", "success", "muted"}) + self.assertIn("not", schema["$defs"]["ruleItem"]) + + def test_grid_example_validates_against_schema(self) -> None: + try: + import jsonschema + except ImportError: + self.skipTest("jsonschema not installed") + jsonschema.validate(instance=_load_grid_example(), schema=_load_grid_schema()) + + def test_grid_schema_rejects_all_by_all(self) -> None: + try: + import jsonschema + except ImportError: + self.skipTest("jsonschema not installed") + payload = { + "schemaVersion": 1, + "skill": "your-skill-slug", + "table": "business_records", + "rowKey": "id", + "rules": [ + { + "ruleKey": "paint-everything", + "rows": "all", + "columns": "all", + "tone": "danger", + } + ], + } + with self.assertRaises(jsonschema.ValidationError): + jsonschema.validate(instance=payload, schema=_load_grid_schema()) + + def test_internal_metadata_hides_presentation_tables(self) -> None: + from db.display_metadata import INTERNAL_METADATA_TABLES, METADATA_TABLES + + self.assertIn("_jiangchang_cell_presentation", INTERNAL_METADATA_TABLES) + self.assertIn("_jiangchang_grid_presentation", INTERNAL_METADATA_TABLES) + self.assertTrue(INTERNAL_METADATA_TABLES <= METADATA_TABLES) + + def test_template_actions_do_not_enable_presentation_by_default(self) -> None: + manifest = _load_json("assets", "actions.json") + self.assertNotEqual(manifest.get("cellPresentation", {}).get("enabled"), True) + + def test_checklist_covers_grid_presentation(self) -> None: + path = _skill_path("development", "CHECKLIST.md") + with open(path, encoding="utf-8") as f: + text = f.read() + self.assertIn("A6-15", text) + self.assertIn("cellPresentation", text) + self.assertIn("CELL_PRESENTATION.md", text) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_docs_standards.py b/tests/test_docs_standards.py index 648f2b5..695250a 100644 --- a/tests/test_docs_standards.py +++ b/tests/test_docs_standards.py @@ -327,6 +327,11 @@ class TestDocsStandards(unittest.TestCase): os.path.join(root, "assets", "schemas", "skill-cell-presentation.schema.json") ) ) + self.assertTrue( + os.path.isfile( + os.path.join(root, "assets", "schemas", "skill-grid-presentation.schema.json") + ) + ) self.assertTrue(os.path.isfile(os.path.join(root, "tests", "test_actions_manifest.py"))) self.assertTrue(os.path.isfile(os.path.join(root, "tests", "test_data_management_contract.py"))) @@ -375,6 +380,7 @@ class TestDocsStandards(unittest.TestCase): "表增删改与退役", "drop_deprecated_user_table", "孤儿物理表", + "_jiangchang_grid_presentation", ): self.assertIn(marker, text, msg=f"SCHEMA.md missing {marker!r}") @@ -416,6 +422,8 @@ class TestDocsStandards(unittest.TestCase): text = self._read("assets/examples/README.md") self.assertIn("visible-when-action.example.json", text) self.assertIn("selection-mode-optional.action.example.json", text) + self.assertIn("skill-cell-presentation.example.json", text) + self.assertIn("skill-grid-presentation.example.json", text) checklist = self._read("development/CHECKLIST.md") self.assertIn("A4-7", checklist) self.assertIn("visibleWhen", checklist)