diff --git a/CHANGELOG.md b/CHANGELOG.md index edd8866..76fd8e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ - 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节 - 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段 +## 1.0.75 + +- 正式沉淀数据管理「单元格呈现」通用契约(`references/CELL_PRESENTATION.md`):技能声明某表某行某列的语义 tone,宿主映射主题样式 +- 用途不限(超时未填、库存预警、状态异常、审核驳回等);推荐旁路表 `_jiangchang_cell_presentation`;填完后重算即取消强调 +- 新增 Schema / 示例;`actions.json` 可选顶层 `cellPresentation.enabled` + ## 1.0.74 - 沉淀宿主 Skill Action 编排契约:`hostEffects`(`onSuccess` / `onFailure`)与 `steps`(`before` / `afterSuccess` / `afterFailure`) diff --git a/SKILL.md b/SKILL.md index b85631e..b15e441 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 技能开发模板(通用业务版) description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。" -version: 1.0.74 +version: 1.0.75 author: 深圳匠厂科技有限公司 metadata: openclaw: @@ -36,6 +36,7 @@ allowed-tools: - **`bind.tables`**:只决定数据管理出现在哪些表;含 `toolbar` 时**必须**显式非空绑定。 - **`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`。 - **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;特殊业务报告才另做 `export-*`。 - **准备 vs 执行(适用则拆)**:若技能存在「先写入/准备本地数据」与「再对外产生副作用」两段,Agent 必须能分开触发;禁止默认绑成不可拆的一步。一键无准备态的技能可跳过本条。 diff --git a/assets/examples/README.md b/assets/examples/README.md index d037552..60242da 100644 --- a/assets/examples/README.md +++ b/assets/examples/README.md @@ -2,3 +2,5 @@ - `version-response.json`:`python main.py version` 成功时 stdout 单行对象的形状参考。 - `log-get-response.json`:若技能有日志表,`log-get` 成功时单条对象的形状参考。 +- `skill-policy-snapshot.*.example.json`:Data Policy Snapshot 形状参考(见 `references/POLICY.md`)。 +- `skill-cell-presentation.example.json`:数据管理单元格呈现文档形状参考(见 `references/CELL_PRESENTATION.md`)。 diff --git a/assets/examples/skill-cell-presentation.example.json b/assets/examples/skill-cell-presentation.example.json new file mode 100644 index 0000000..6ce7040 --- /dev/null +++ b/assets/examples/skill-cell-presentation.example.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 1, + "skill": "your-skill-slug", + "table": "business_records", + "rowKey": "id", + "generatedAt": "2026-08-10T06:00:00Z", + "cells": [ + { + "rowId": "42", + "column": "qty", + "tone": "danger", + "message": "库存低于安全线", + "alert": true + }, + { + "rowId": "42", + "column": "status", + "tone": "warning", + "message": "待审核" + }, + { + "rowId": "99", + "column": "print_done_at", + "tone": "danger", + "message": "环节到期未填", + "alert": true + } + ] +} diff --git a/assets/schemas/skill-actions.schema.json b/assets/schemas/skill-actions.schema.json index 48032aa..570fa01 100644 --- a/assets/schemas/skill-actions.schema.json +++ b/assets/schemas/skill-actions.schema.json @@ -25,9 +25,24 @@ "policy": { "$ref": "#/$defs/policyDeclaration", "description": "可选。启用 Data Policy(L1/L2)。缺省或不 enabled = L0,宿主不拉 Snapshot。详见 references/POLICY.md。" + }, + "cellPresentation": { + "$ref": "#/$defs/cellPresentationDeclaration", + "description": "可选。启用数据管理单元格呈现(P1)。缺省或不 enabled = P0,宿主不读标色。详见 references/CELL_PRESENTATION.md。" } }, "$defs": { + "cellPresentationDeclaration": { + "type": "object", + "required": ["enabled"], + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "description": "仅 true 启用;false 等同 P0。" + } + } + }, "policyDeclaration": { "type": "object", "required": ["enabled"], diff --git a/assets/schemas/skill-cell-presentation.schema.json b/assets/schemas/skill-cell-presentation.schema.json new file mode 100644 index 0000000..983c48f --- /dev/null +++ b/assets/schemas/skill-cell-presentation.schema.json @@ -0,0 +1,78 @@ +{ + "$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。", + "type": "object", + "required": ["schemaVersion", "skill", "table", "rowKey", "cells"], + "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、feishu_record_id);cells[].rowId 的值取自该列" + }, + "generatedAt": { + "type": "string", + "minLength": 1, + "description": "可选。ISO-8601 或技能约定的生成时间,便于排障" + }, + "cells": { + "type": "array", + "description": "当前应对该表生效的呈现项;空数组 = 该表无特殊呈现(清除旧样式)。同 (rowId, column) 重复时宿主可后者覆盖前者。", + "items": { "$ref": "#/$defs/cellItem" } + } + }, + "$defs": { + "tone": { + "type": "string", + "enum": ["danger", "warning", "info", "success", "muted"], + "description": "语义色板(闭集)。宿主映射到底色/字色/左边线等;禁止技能下发原始 CSS 或任意 hex。" + }, + "cellItem": { + "type": "object", + "required": ["rowId", "column", "tone"], + "additionalProperties": false, + "properties": { + "rowId": { + "type": "string", + "minLength": 1, + "description": "rowKey 列的字符串形式主键/业务键" + }, + "column": { + "type": "string", + "minLength": 1, + "pattern": "^[a-z][a-z0-9_]*$", + "description": "要强调的业务列名(snake_case),必须是真实列" + }, + "tone": { "$ref": "#/$defs/tone" }, + "message": { + "type": "string", + "maxLength": 500, + "description": "可选。悬停/无障碍说明(短文案);不要塞长报告" + }, + "alert": { + "type": "boolean", + "description": "可选,默认 false。true = 可计入进页/同步后的提醒汇总(如 Toast);填完数据并重算后该项消失则提醒自然关闭" + } + } + } + } +} diff --git a/development/SKILL_ACTION_RUNTIME.md b/development/SKILL_ACTION_RUNTIME.md index 3863c62..6c88aa4 100644 --- a/development/SKILL_ACTION_RUNTIME.md +++ b/development/SKILL_ACTION_RUNTIME.md @@ -19,8 +19,9 @@ 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)。 -4. **长耗时建议 async** — 开浏览器、RPA、HTTP 大文件下载等建议 `executionProfile: "async"`,进**任务中心**;这是对**方法**的选择,**不**限制可以出现在哪些 placement / 入口。5. **Agent:短 CLI / 长 Action** — 短查询可用共享 Python bash CLI;长任务 / RPA **禁止** `exec` + `process poll`,必须 `run_skill_action`。 +3. **五职责正交** — `placements` / `bind.tables` / `executionProfile` / `entrypoint` / domain service 各管一事(见 ACTIONS.md)。可选第六轴 **policy**(见 `references/POLICY.md`):未启用 = L0 与今日一致。可选 **`hostEffects` / `steps`**(见 ACTIONS.md):数据管理交互前后的 UI/级联编排;省略 = 不额外改 UI(匠厂宿主 ≥2.8.5)。可选 **`cellPresentation`**(见 `references/CELL_PRESENTATION.md`):单元格语义标色;省略 = P0。 +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 流**。 7. **显式声明 `executionProfile`** — 心智默认 sync、例外才 async;每个 action **必须**写明 `sync` 或 `async`;宿主省略时按 sync,新技能仍禁止赌缺省;禁止第三种模糊模式(`auto` / `background` / `deferred` 等)。 diff --git a/references/ACTIONS.md b/references/ACTIONS.md index 27e393d..d163cd3 100644 --- a/references/ACTIONS.md +++ b/references/ACTIONS.md @@ -74,7 +74,7 @@ Action 是**可选能力**: | `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)。 +模板最小示例见 [`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)。 ## 六职责正交(必读) @@ -86,6 +86,7 @@ Action 是**可选能力**: | **`executionProfile`** | 宿主是**同步等待**还是**异步 Job** | 不限制可展示入口,不决定是否弹窗 | | **`interaction`** | 可点击入口(数据管理 toolbar/row/batch、默认 CRUD 接管、未来 skill-detail)**点了怎么交互** | 不决定 sync/async;**cron 触发与 agent 忽略** | | **`hostEffects` / `steps`** | **可选**:数据管理交互跑完前后,宿主要不要刷新表 / 清选区 / 级联删等 | 不决定业务写什么;**省略 = 宿主不额外改 UI**(≥2.8.5 起不再硬编码 mutation 后刷新);cron/agent 忽略 | +| **`cellPresentation`** | **可选**:某表某行某列的语义标色/强调(tone + 说明) | 不替代业务列;不下发 CSS;用途不限(超时、预警、驳回等);见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md) | | **`policy` / Snapshot** | **可选**:当前身份在数据管理下允许哪些 Action / 宿主内置能力 / 行列范围 | 不替代 placements;不替代 domain 写路径鉴权;多数技能不配 = L0 全开 | | **`entrypoint`** | 实际执行哪个 CLI 命令与参数 | 不写业务逻辑 | | **domain service** | **真正完成什么业务** | 不感知 toolbar / cron / agent 来源 | @@ -96,13 +97,14 @@ Action 是**可选能力**: | 层级 | 稳定字段 / 能力 | |------|----------------| -| 顶层 | `schemaVersion`、`skill`、`actions`;可选 **`policy`**(Data Policy 启用块) | +| 顶层 | `schemaVersion`、`skill`、`actions`;可选 **`policy`**(Data Policy 启用块);可选 **`cellPresentation`**(单元格呈现启用块) | | action | `id`、`label`、`description`、`placements`、`entrypoint`、**`executionProfile`(必填)**;可选 `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`) | | **bind** | `{ "tables": ["business_table"] }`;可选 `inputMapping`(行内/批量预填,见下节) | | **policy** | 可选;`enabled` + `entrypoint`(推荐 `policy` CLI);见 POLICY.md。未启用 = L0 | +| **cellPresentation** | 可选;`{ "enabled": true }` 时宿主应加载单元格呈现;见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)。未启用 = P0 | | inputSchema | 根 `type: object`;properties 使用 `string` / `number` / `boolean`;支持 `enum`、`default`、`required`、`sensitive`、`readOnly` | | confirmation | `{ "message": "..." }` — **风险文案**(confirm/form 弹窗黄条;agent 元数据);**不是**「要不要弹窗」的开关 | | **executionProfile** | `"sync"` \| `"async"`(**必须显式写出**,禁止依赖缺省) | diff --git a/references/CELL_PRESENTATION.md b/references/CELL_PRESENTATION.md new file mode 100644 index 0000000..a5c9fca --- /dev/null +++ b/references/CELL_PRESENTATION.md @@ -0,0 +1,210 @@ +# 数据管理单元格呈现(Cell Presentation)契约(v1.0) + +> **状态:正式契约 v1.0(`schemaVersion` 1)。** +> **目标:** 冻结「技能声明某表某行某列的语义呈现 → 宿主映射主题样式」的通用能力,供任意业务场景复用。 +> **非目标:** 超时/SLA 专用字段;技能下发任意 CSS / 任意色值;宿主实现业务规则引擎。 +> **配套 Schema:** [`../assets/schemas/skill-cell-presentation.schema.json`](../assets/schemas/skill-cell-presentation.schema.json)。 + +相关正交轴见 [`ACTIONS.md`](ACTIONS.md)、[`POLICY.md`](POLICY.md)、[`SCHEMA.md`](SCHEMA.md)。 + +--- + +## 0. 一句话 + +技能可以声明:**哪张业务表、哪一行、哪一列**需要特殊标色/强调(语义 `tone` + 可选说明)。 +用途不限:时效超时未填、库存预警、状态异常、必填缺失、审核驳回等。 +**不是**「再加一列超时文字」;强调落在**真正要关注的业务格子**上。 + +--- + +## 0.1 能力档位 + +| 档位 | 技能行为 | 宿主行为 | +|------|----------|----------| +| **P0(默认)** | 不写呈现数据 / 未启用 | 数据管理表格与今日一致,无特殊标色 | +| **P1** | 按规则写入呈现文档或推荐旁路表 | 渲染对应单元格的语义样式;可选汇总 `alert: true` 做进页提醒 | + +多数技能保持 **P0**。需要视觉强调时再启用。 + +--- + +## 1. 职责边界 + +| 角色 | 负责 | 不负责 | +|------|------|--------| +| **技能** | 按**当前业务数据**算出哪些格子要强调;在数据变更后**重算**并覆盖旧结果 | 不写宿主 CSS;不把强调文案污染进业务真值列冒充「变红」 | +| **宿主** | 读取呈现结果,按闭集 `tone` 映射主题样式;可选 Toast/提醒汇总 | 不理解「提前 3 天交单」等业务规则;不为单个技能写死样式 | + +**派生状态(重要):** 呈现是计算结果,不是粘性开关。 +用户补填数据 / 同步刷新后技能重算 → 该项消失 → **样式与提醒自然关闭**。不要单独维护「已关闭提醒」状态机(除非产品另做「忽略此条」能力,本契约不冻结)。 + +--- + +## 2. 文档形状(冻) + +技能产出一份 **Cell Presentation Document**(可落库、可 CLI 打印,形状相同),须符合 Schema。 + +```json +{ + "schemaVersion": 1, + "skill": "your-skill-slug", + "table": "business_records", + "rowKey": "id", + "generatedAt": "2026-08-10T06:00:00Z", + "cells": [ + { + "rowId": "42", + "column": "qty", + "tone": "danger", + "message": "库存低于安全线", + "alert": true + }, + { + "rowId": "42", + "column": "status", + "tone": "warning", + "message": "待审核" + } + ] +} +``` + +| 字段 | 必填 | 说明 | +|------|------|------| +| `schemaVersion` | 是 | 固定 `1` | +| `skill` | 是 | 与 slug 一致 | +| `table` | 是 | 当前文档作用的业务表 | +| `rowKey` | 是 | 行匹配列(`id` / `feishu_record_id` 等) | +| `generatedAt` | 否 | 排障用 | +| `cells` | 是 | 数组;**空数组 = 该表清除全部特殊呈现** | + +### 2.1 `cells[]` 项 + +| 字段 | 必填 | 说明 | +|------|------|------| +| `rowId` | 是 | `rowKey` 列值的字符串形式 | +| `column` | 是 | 要强调的**业务列**(snake_case),须真实存在 | +| `tone` | 是 | 见 §3 | +| `message` | 否 | 短说明(≤500);悬停 / 无障碍 | +| `alert` | 否 | 默认 `false`;`true` 可计入进页/同步提醒汇总 | + +同一 `(rowId, column)` 多条:宿主可**后者覆盖前者**。 +`column` 对用户不可见(policy 打码 / 列隐藏)时:宿主可仍应用样式,或跳过——**推荐跳过不可见列的强调**,避免泄露「有问题」的存在感(技能仍可写,宿主按可见性过滤)。 + +--- + +## 3. `tone` 闭集(冻) + +| tone | 语义(技能怎么选) | 宿主怎么呈现(示例,主题可调) | +|------|-------------------|-------------------------------| +| `danger` | 必须立刻处理 / 已违规 / 严重超时 | 强强调(如浅红底) | +| `warning` | 即将到期 / 需注意 | 中等强调(如浅黄) | +| `info` | 提示性标记 | 轻强调(如浅蓝) | +| `success` | 正向完成态(少用在「待填」场景) | 浅绿等 | +| `muted` | 弱化 / 次要 | 灰底或灰字 | + +**禁止:** 技能下发原始 `#RRGGBB`、任意 CSS class、内联 style。 +以后若扩展色板,只能**追加新 tone token** 并升契约,不能开放任意色值。 + +「技能配置什么就显示什么」在本契约中的含义是:技能选择 **哪一格 + 哪个 tone + 什么说明**;**像素级颜色由宿主主题决定**,以保证多技能 UI 一致与安全。 + +--- + +## 4. 推荐持久化(给宿主实现) + +v1 **推荐**技能将「当前表」的呈现写入用户库旁路表(与业务表分离,**不是**多余业务超时列): + +```sql +CREATE TABLE IF NOT EXISTS _jiangchang_cell_presentation ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + table_name TEXT NOT NULL, + row_key TEXT NOT NULL, + row_id TEXT NOT NULL, + column_name 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, row_id, column_name) +); +``` + +约定: + +- 重算某表时:**先删该 `table_name` 全部行,再插入新结果**(或等价事务替换),保证填完后旧 `danger` 不会残留。 +- 宿主打开数据管理某表时:若该技能启用了呈现(见 §5)且存在此表,则按当前页行的 `rowKey` 值匹配并着色。 +- 也允许技能用 CLI 打印整份 Document 供调试;**运行时以库内旁路表为准**(除非后续宿主另定拉取入口)。 + +未创建该旁路表 = P0,宿主忽略。 + +--- + +## 5. 在 `actions.json` 中启用(可选,推荐) + +与 `policy` 类似,顶层可选: + +```json +{ + "schemaVersion": 1, + "skill": "your-skill-slug", + "cellPresentation": { + "enabled": true + }, + "actions": [] +} +``` + +| 字段 | 说明 | +|------|------| +| `cellPresentation` | 省略 = P0 | +| `cellPresentation.enabled` | 仅 `true` 表示宿主应尝试加载呈现;`false`/缺省等同 P0 | + +v1 **不**要求单独 CLI entrypoint(与 policy 不同);技能在 `sync` / 写路径 / `init-db` 后自行维护旁路表即可。若未来需要「仅刷新呈现」Action,用普通 Skill Action 即可,不必扩展本启用块。 + +--- + +## 6. 何时重算(技能必须遵守) + +至少在以下时机用**同一套 domain 规则**重算并覆盖该表呈现: + +1. 全量/增量把外部数据写入本地投影之后(如同步成功) +2. 会改变「是否仍需强调」的写路径成功之后(新建、更新、登记等) +3. (可选)用户显式「刷新呈现」类 Action + +进页 Toast:宿主可在加载表后统计当前页或全表 `alert=1` 的条数;**0 则不打扰**。 +提醒关闭 = 重算后没有对应 cell,而不是用户点「我知道了」写死关闭(除非另做忽略列表,本契约不冻)。 + +--- + +## 7. 与其它契约的关系 + +| 契约 | 关系 | +|------|------| +| **SCHEMA / `_jiangchang_columns`** | 管列是否可见、类型、中文名;**不管**条件标色 | +| **POLICY Snapshot** | 管谁能看哪些 Action/行列;呈现仍建议按可见列过滤 | +| **hostEffects / steps** | 管 Action 跑完后是否 `reloadTable`;重载后宿主应重新读呈现 | +| **业务列真值** | 保持干净;不要把 `【超时】` 写进业务字段冒充变色 | + +--- + +## 8. 反模式 + +- 为标色新增「超时环节」「时效状态」等**业务展示冗余列**当作终态方案 +- 技能下发 CSS / 内联颜色 +- 只在首次超时时打标、填完不重算导致红字残留 +- 宿主写死某个技能 slug 的特殊染色逻辑 +- 把 Cell Presentation 做成仅服务 SLA 的专用协议 + +--- + +## 9. 示例场景(说明用途不限) + +| 场景 | column | tone | message 例 | +|------|--------|------|------------| +| 打单超时未填 | `print_done_at` | `danger` | 打单已超 1 天未填 | +| 库存低于安全线 | `qty` | `warning` | 低于安全库存 | +| 审核驳回 | `status` | `danger` | 已驳回,请修改 | +| 选填备注缺失但不紧急 | `notes` | `muted` | 建议补充备注 | + +完整 JSON 例见 [`../assets/examples/skill-cell-presentation.example.json`](../assets/examples/skill-cell-presentation.example.json)。 diff --git a/references/README.md b/references/README.md index b722cd5..f6cc076 100644 --- a/references/README.md +++ b/references/README.md @@ -20,6 +20,7 @@ | [`ACTIONS.md`](ACTIONS.md) | Skill Action、sync/async、任务中心、多入口(可选能力) | | [`SCHEMA.md`](SCHEMA.md) | 本地 SQLite 与数据管理展示契约 | | [`POLICY.md`](POLICY.md) | Data Policy:可选身份授权、Snapshot、行 scope(多数技能 L0 不配) | +| [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md) | 数据管理单元格呈现:语义 tone 标色/强调(用途不限;多数技能 P0 不配) | | [`../development/`](../development/) | 开发、测试、RPA 深度规范(给人 / 编码助手) | -按需阅读:短操作优先 `CLI.md`;Action / 任务中心读 `ACTIONS.md`;结构化字段读 `SCHEMA.md`;权限 opt-in 读 `POLICY.md`。 +按需阅读:短操作优先 `CLI.md`;Action / 任务中心读 `ACTIONS.md`;结构化字段读 `SCHEMA.md`;权限 opt-in 读 `POLICY.md`;格子标色读 `CELL_PRESENTATION.md`。 diff --git a/references/SCHEMA.md b/references/SCHEMA.md index 083a877..ae504e1 100644 --- a/references/SCHEMA.md +++ b/references/SCHEMA.md @@ -13,6 +13,7 @@ - 使用 `_jiangchang_columns` 提供字段级展示元数据 - 定义哪些字段可见、可搜索、可创建、可编辑 - 定义字段展示类型和枚举选项 +- (可选)按业务规则维护单元格呈现旁路数据(语义 tone;见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)) - 保证元数据与真实数据库结构一致 - 保证元数据初始化幂等 @@ -27,6 +28,7 @@ - 不猜测技能业务含义 - 不根据具体技能名称写特殊逻辑 - 可在技能安装/更新成功后静默调用 `python scripts/main.py init-db` 做幂等 ensure(内部即 `init_db()`);失败不影响安装结果 +- 若技能启用了单元格呈现(见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)),读取旁路呈现数据并为单元格映射语义样式(`tone`);不猜测业务规则 ## 初始化入口 diff --git a/tests/test_actions_manifest.py b/tests/test_actions_manifest.py index a7da68d..a2df1a3 100644 --- a/tests/test_actions_manifest.py +++ b/tests/test_actions_manifest.py @@ -547,6 +547,8 @@ class TestActionsManifest(unittest.TestCase): "reloadTable", "afterSuccess", "afterFailure", + "cellPresentation", + "CELL_PRESENTATION.md", ): self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}") self.assertNotIn( diff --git a/tests/test_cell_presentation_contract.py b/tests/test_cell_presentation_contract.py new file mode 100644 index 0000000..e600d81 --- /dev/null +++ b/tests/test_cell_presentation_contract.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +"""Cell Presentation 契约:schema + 文档 + 示例。""" +from __future__ import annotations + +import json +import os +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: + 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) + + +class TestCellPresentationContract(unittest.TestCase): + def test_schema_file_exists_and_parses(self) -> None: + schema = _load_schema() + self.assertEqual(schema.get("title"), "SkillCellPresentationDocument") + 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"}) + + def test_example_validates_against_schema(self) -> None: + try: + import jsonschema + except ImportError: + self.skipTest("jsonschema not installed") + jsonschema.validate(instance=_load_example(), schema=_load_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) + 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( + os.path.join(root, "assets", "schemas", "skill-cell-presentation.schema.json") + ) + ) + self.assertTrue( + os.path.isfile( + os.path.join(root, "assets", "examples", "skill-cell-presentation.example.json") + ) + ) + + def test_cell_presentation_md_covers_contract(self) -> None: + path = os.path.join(get_skill_root(), "references", "CELL_PRESENTATION.md") + with open(path, encoding="utf-8") as f: + text = f.read() + for marker in ( + "tone", + "danger", + "warning", + "alert", + "_jiangchang_cell_presentation", + "cellPresentation", + "派生", + "任意 CSS", + "用途不限", + "schemaVersion", + ): + self.assertIn(marker, text, msg=f"CELL_PRESENTATION.md missing {marker!r}") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_docs_standards.py b/tests/test_docs_standards.py index 2773c97..d5ffd7a 100644 --- a/tests/test_docs_standards.py +++ b/tests/test_docs_standards.py @@ -300,6 +300,7 @@ class TestDocsStandards(unittest.TestCase): self.assertTrue(os.path.isfile(os.path.join(root, "assets", "actions.json"))) 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, "assets", "schemas", "skill-actions.schema.json")) ) @@ -308,6 +309,11 @@ class TestDocsStandards(unittest.TestCase): os.path.join(root, "assets", "schemas", "skill-policy-snapshot.schema.json") ) ) + self.assertTrue( + os.path.isfile( + os.path.join(root, "assets", "schemas", "skill-cell-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")))