Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8403ce9b4 | |||
| 8c8811e777 | |||
| a70da6a076 |
16
CHANGELOG.md
16
CHANGELOG.md
@@ -9,6 +9,22 @@
|
||||
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
|
||||
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
|
||||
|
||||
## 1.0.113
|
||||
|
||||
- 固化数据管理契约:`visible=0` 只藏界面;`default_sort_json` 仍可引用隐藏的非敏感物理列(含主键)
|
||||
- 元数据校验:排序列必须真实存在;引用隐藏列仅提示、不报错
|
||||
|
||||
## 1.0.112
|
||||
|
||||
- 新技能的名字不必再以平台或 `default` 结尾。没有抖音、小红书、亚马逊这类外部平台时,用动词加名词即可
|
||||
- 不要再把 `default` 当作占位平台。已经发布、名字末尾是 `default` 的技能不用改名
|
||||
|
||||
## 1.0.111
|
||||
|
||||
- 沉淀数据管理呈现加法:同一 `cellPresentation.enabled` 支持单元格、整行、整列、行列交叉区域(闭集 tone)
|
||||
- 新增 `_jiangchang_grid_presentation` 契约 / Schema / 示例;v1 `cells[]` 保持不变;禁止整表 `all`×`all`;整行色只涂表格、不铺到详情字段
|
||||
- 内部隐藏表补上两张呈现旁路表;模板默认仍不启用着色
|
||||
|
||||
## 1.0.110
|
||||
|
||||
- 数据管理侧栏除了生成出来的 HTML 快照,也会直接显示安装包里 `assets/html/pages/` 的页面,装完就有,不用点生成
|
||||
|
||||
6
SKILL.md
6
SKILL.md
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: 技能开发模板(通用业务版)
|
||||
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
|
||||
version: 1.0.110
|
||||
version: 1.0.113
|
||||
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 必须能分开触发;禁止默认绑成不可拆的一步。一键无准备态的技能可跳过本条。
|
||||
@@ -159,7 +159,7 @@ python {baseDir}/scripts/main.py init-db
|
||||
|
||||
## 技能命名与 slug
|
||||
|
||||
- 标准格式:`{verb}-{noun-phrase}-{platform}`(详见 `development/NAMING.md`)。
|
||||
- 标准格式:`{verb}-{noun-phrase}`,绑定外部平台时才加 `-{platform}`(详见 `development/NAMING.md`)。不要用 `default` 占位。
|
||||
- slug / repo / 目录 / `SKILL_SLUG` / DB 文件名:**英文** kebab-case;`name`、`description`、根 `README.md`:**中文**。
|
||||
- 复制为新业务技能前**必须**阅读 `development/NAMING.md`。
|
||||
- `name`(frontmatter):**用户可见的中文业务名称**;`description` 是说明,不是名称。
|
||||
|
||||
@@ -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)。
|
||||
|
||||
32
assets/examples/skill-grid-presentation.example.json
Normal file
32
assets/examples/skill-grid-presentation.example.json
Normal file
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
all
|
||||
batch
|
||||
multi
|
||||
default
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
93
assets/schemas/skill-grid-presentation.schema.json
Normal file
93
assets/schemas/skill-grid-presentation.schema.json
Normal file
@@ -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 = 按本条规则计入进页/同步提醒(不按命中格子数)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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`)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -925,7 +925,7 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
|
||||
每个新 skill 发布前,建议技术人员逐条确认:
|
||||
|
||||
- [ ] 目录结构符合当前模板
|
||||
- [ ] slug 符合 [`NAMING.md`](NAMING.md)(verb-noun-platform)
|
||||
- [ ] slug 符合 [`NAMING.md`](NAMING.md)(动词-名词,平台可选;不要用 `default` 占位)
|
||||
- [ ] 目录名、`SKILL.md` slug、`constants.SKILL_SLUG` 三者一致
|
||||
- [ ] `SKILL.md` 中 slug、名称、描述都已替换
|
||||
- [ ] `SKILL.md` 的 `developer_ids` 已换成**本人**匠厂用户 ID(设置 → 用户信息 → 复制),不是模板示例 ID
|
||||
|
||||
@@ -13,23 +13,24 @@
|
||||
| ❌ **不修改** | 其他仓库(尤其 `account-manager`)不在本文约束范围内 |
|
||||
| ✅ **模板占位** | 模板本体的 `your-skill-slug` 占位必须继续通过全部测试 |
|
||||
|
||||
**新技能勿仿**历史 2 段无平台形态(如 `reconcile-finance`)或不符合 verb-noun-platform 的旧 repo 名;校验器对历史 repo **不追溯**,但规范中明确标注为反模式。
|
||||
**新技能**使用「动词-名词」,外部平台可选。不要再为了凑段数追加 `default`。校验器不追溯已上线仓库;已经发布、末尾是 `default` 的技能不用改名。
|
||||
|
||||
---
|
||||
|
||||
## 2. 命名格式
|
||||
|
||||
标准形态(约 95% 新技能):
|
||||
标准形态:
|
||||
|
||||
```text
|
||||
{verb}-{noun-phrase}-{platform}
|
||||
{verb}-{noun-phrase}
|
||||
{verb}-{noun-phrase}-{platform} # 仅当技能绑定外部平台时
|
||||
```
|
||||
|
||||
| 部分 | 规则 |
|
||||
|------|------|
|
||||
| **verb** | 1 个词,来自 [动词白名单](#5-动词白名单) |
|
||||
| **noun-phrase** | 1~3 个词(按 `-` 分段),描述业务对象 |
|
||||
| **platform** | 1 个词,来自 [平台白名单](#6-平台白名单);可含连字符(如 `single-window`) |
|
||||
| **platform** | **可选**。没有抖音、小红书、亚马逊这类外部系统时省略。有平台时 1 个词,来自 [平台白名单](#6-平台白名单);可含连字符(如 `single-window`) |
|
||||
|
||||
**展示层分工不变:**
|
||||
|
||||
@@ -44,8 +45,8 @@
|
||||
|
||||
| 约束 | 值 |
|
||||
|------|-----|
|
||||
| 格式 | `{verb}-{noun-phrase}-{platform}`(标准型,默认) |
|
||||
| 总段数(按 `-` 分割) | **3~5 段**(标准型);scope 型见 [§4](#4-三类合法形态) |
|
||||
| 格式 | `{verb}-{noun-phrase}`;有外部平台时才加 `-{platform}` |
|
||||
| 总段数(按 `-` 分割) | **2~5 段** |
|
||||
| 总长度 | **≤ 48 字符** |
|
||||
| 字符集 | 小写字母、数字、连字符;kebab-case |
|
||||
| 禁止 | 中文 slug、下划线、**平台放最前**(如 `amazon-download-xxx`) |
|
||||
@@ -56,12 +57,26 @@
|
||||
|
||||
## 4. 三类合法形态
|
||||
|
||||
### 4.1 标准型(95%)
|
||||
### 4.1 无平台(常见)
|
||||
|
||||
`verb-noun-platform` 或 `verb-noun-noun-platform`(3~5 段)
|
||||
`verb-noun` 或 `verb-noun-noun`(2~4 段)
|
||||
|
||||
- 第 1 段 ∈ 动词白名单
|
||||
- 最后 1 段(或 hyphenated 平台的最后 2 段)∈ 平台白名单
|
||||
- 其余为 noun-phrase(1~3 段)
|
||||
- **不要**追加 `default`
|
||||
|
||||
示例:
|
||||
|
||||
- `generate-monthly-statements`
|
||||
- `reconcile-finance`
|
||||
- `process-article`
|
||||
|
||||
### 4.2 标准型(绑定外部平台)
|
||||
|
||||
`verb-noun-platform`(在无平台形态后加平台,总段数仍 ≤ 5)
|
||||
|
||||
- 第 1 段 ∈ 动词白名单
|
||||
- 最后 1 段(或 hyphenated 平台的最后若干段)∈ 平台白名单
|
||||
- 中间为 noun-phrase(1~3 段)
|
||||
|
||||
示例:
|
||||
@@ -71,19 +86,19 @@
|
||||
- `enter-sales-receipt-kingdee`
|
||||
- `fill-export-declaration-single-window`
|
||||
|
||||
### 4.2 scope 型(少见)
|
||||
### 4.3 scope 型(少见)
|
||||
|
||||
`verb-noun-scope`
|
||||
|
||||
- 最后 1 段 ∈ scope 白名单:`all` | `batch` | `multi` | `default`
|
||||
- 最后 1 段 ∈ scope 白名单:`all` | `batch` | `multi`
|
||||
- 第 1 段 ∈ 动词白名单
|
||||
- 总段数 3~5
|
||||
- 至少还有一段名词(因此最少 3 段)
|
||||
|
||||
示例:`generate-article-all`
|
||||
|
||||
> 说明:`geo-generate-article-all` 若作为新技能且不在豁免表、首段 `geo` 不在动词表,应写为 `generate-article-all`。
|
||||
> `default` 已从 scope 词表移除。它不是平台,也不表示范围。新技能不要写 `generate-article-default` 这类占位。已发布且末尾为 `default` 的仓库保持原 slug;同步本校验后只会警告,不会因此失败。
|
||||
|
||||
### 4.3 历史豁免(不校验语义)
|
||||
### 4.4 历史豁免(不校验语义)
|
||||
|
||||
以下 slug 跳过 verb/platform 语义校验(见 `LEGACY_EXEMPT_SLUGS`):
|
||||
|
||||
@@ -121,7 +136,7 @@
|
||||
| lianlian | single-window | export-rebate |
|
||||
| etax | | |
|
||||
|
||||
scope 词表:[`assets/naming/scopes.txt`](../assets/naming/scopes.txt) — `all`、`batch`、`multi`、`default`
|
||||
scope 词表:[`assets/naming/scopes.txt`](../assets/naming/scopes.txt) — `all`、`batch`、`multi`。不含 `default`。
|
||||
|
||||
---
|
||||
|
||||
@@ -164,7 +179,7 @@ slug **末段 platform** 与 `account-manager` 中的 `platform_key` **语义对
|
||||
|
||||
## 9. 复制新技能流程
|
||||
|
||||
1. **先定 slug** — 按本文规范拟定 `{verb}-{noun-phrase}-{platform}`,确认在白名单内且 ≤ 48 字符
|
||||
1. **先定 slug** — `{verb}-{noun-phrase}`,仅在绑定外部平台时加 `-{platform}`。确认动词/平台在白名单内且 ≤ 48 字符。不要用 `default` 占位。
|
||||
2. **复制目录** — **推荐** [`tools/scaffold_skill.ps1`](../tools/scaffold_skill.ps1)(`-Slug` 与目录名一致);勿整包复制带走 `.git`
|
||||
2.5. **清理 Git(若未用 scaffold)** — 删除 `.git`、`.openclaw-skill-template`、`.pytest_cache`;再 `git init` 与 `git remote add origin`
|
||||
3. **全局替换** — `your-skill-slug` → 新 slug;更新 `SKILL.md`、`constants.py`、文档中的占位
|
||||
@@ -179,10 +194,12 @@ slug **末段 platform** 与 `account-manager` 中的 `platform_key` **语义对
|
||||
|
||||
| slug | 说明 |
|
||||
|------|------|
|
||||
| `download-settlement-report-amazon` | 标准型,4 段 |
|
||||
| `add-product-shopee` | 标准型,3 段 |
|
||||
| `enter-sales-receipt-kingdee` | 标准型 |
|
||||
| `fill-export-declaration-single-window` | 标准型,平台含连字符 |
|
||||
| `generate-monthly-statements` | 无平台 |
|
||||
| `reconcile-finance` | 无平台,动词 + 一个名词 |
|
||||
| `download-settlement-report-amazon` | 有平台 |
|
||||
| `add-product-shopee` | 有平台 |
|
||||
| `enter-sales-receipt-kingdee` | 有平台 |
|
||||
| `fill-export-declaration-single-window` | 有平台,平台含连字符 |
|
||||
| `generate-article-all` | scope 型 |
|
||||
|
||||
### ❌ 反例
|
||||
@@ -191,7 +208,7 @@ slug **末段 platform** 与 `account-manager` 中的 `platform_key` **语义对
|
||||
|------|------|
|
||||
| `amazon-download-settlement-report` | 平台在最前 |
|
||||
| `enter-cross-border-sales-receipt-voucher-kingdee` | 过长、noun 臃肿 |
|
||||
| `reconcile-finance` | 新技能禁止 2 段无平台 |
|
||||
| `generate-article-default` | `default` 不是平台;应写成 `generate-article` |
|
||||
| `download_settlement_amazon` | 非 kebab-case(下划线) |
|
||||
|
||||
---
|
||||
@@ -201,14 +218,14 @@ slug **末段 platform** 与 `account-manager` 中的 `platform_key` **语义对
|
||||
- **平台前置**:`amazon-download-...`、`shopee-add-...`
|
||||
- **英文占位 name**:复制后仍保留 `My Skill` 或纯英文 slug 风格名称
|
||||
- **只改目录名**:`SKILL.md` slug 与 `constants.SKILL_SLUG` 未同步
|
||||
- **noun 堆砌**:中间段过多导致超 48 字符或难以阅读
|
||||
- **仿历史 2 段 repo**:`reconcile-finance`、`content-manager` 等
|
||||
- **用 default 占位**:`generate-article-default`。没有外部平台就省略末段
|
||||
- **名词堆砌**:中间段过多导致超 48 字符或难以阅读
|
||||
|
||||
---
|
||||
|
||||
## 12. 演进说明(当前不启用)
|
||||
|
||||
当技能库规模 **> 200** 时,可再评估引入 `{domain}-verb-noun-platform` 前缀形态。**当前不启用**,新技能一律使用本文 verb-noun-platform 标准型或 scope 型。
|
||||
当技能库规模 **> 200** 时,可再评估引入 `{domain}-verb-noun-platform` 前缀形态。**当前不启用**。新技能使用动词-名词;只有绑定外部平台时才使用 verb-noun-platform,或使用 scope 型。
|
||||
|
||||
---
|
||||
|
||||
@@ -218,9 +235,9 @@ slug **末段 platform** 与 `account-manager` 中的 `platform_key` **语义对
|
||||
|
||||
| 历史 slug | 说明 |
|
||||
|-----------|------|
|
||||
| `account-manager` | 跨平台账号管理,非 verb-noun-platform |
|
||||
| `reconcile-finance` | 2 段无 platform |
|
||||
| `scrape-contacts-lianjia` | 平台名不在当前白名单(链家) |
|
||||
| `account-manager` | 跨平台账号管理,非动词-名词 |
|
||||
| `reconcile-finance` | 现已是合法的无平台形态;旧文档曾把它标成反例 |
|
||||
| `scrape-contacts-lianjia` | 平台名不在当前白名单(链家);新技能若要表达链家,先把平台加入白名单 |
|
||||
| `skill-template` | 模板仓库本身 |
|
||||
|
||||
校验器通过 `LEGACY_EXEMPT_SLUGS` 对模板占位与少数豁免 slug 跳过语义检查;**新复制出去的业务 skill 不在豁免表内**,必须满足 §3 硬约束。
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
| 是否满足完整快照语义 / 空结果保护实现 | references/SCHEMA.md | 见 `test_sync_contract.py.sample` |
|
||||
| 是否把系统导出与业务同步混淆 | references/ACTIONS.md;SCHEMA.md | 文档 + sample;勿做源码关键词误报扫描 |
|
||||
| `db/` 层不得调浏览器或拼业务流程 | development/DEVELOPMENT.md §11 | 需跨文件调用图 |
|
||||
| slug 命名语义(verb-noun-platform) | development/NAMING.md | 部分覆盖于 `tests/test_slug_naming.py`,非本矩阵 hard policy |
|
||||
| slug 命名语义(动词-名词,平台可选) | development/NAMING.md | 部分覆盖于 `tests/test_slug_naming.py`,非本矩阵 hard policy |
|
||||
| 复制模板不得保留 `.git` / template marker | development/DEVELOPMENT.md §4 | 由 `tests/test_scaffold_guard.py` 守护,属脚手架场景 |
|
||||
| 共享 runtime 导入来源非技能目录 | development/RUNTIME.md | 运行时依赖已安装包,见 `tests/test_platform_import.py` |
|
||||
| RPA 真实轨迹、选择器纪律、状态等待、HITL 超时与有限重试是否正确实现 | development/AI_RPA.md;development/RPA.md §0–§1 | 文档和默认节奏已守护,真实行为与 DOM 质量仍需人工验收 |
|
||||
|
||||
@@ -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 流**。
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
- **adapter profile**:[`tests/test_adapter_profile_policy.py`](../tests/test_adapter_profile_policy.py) + [`adapter_test_utils`](../tests/adapter_test_utils.py) ——验证在未授权情况下绝不误判开启真实网络/RPA。
|
||||
- **发布打包守护**:[`tests/test_release_packaging_constraints.py`](../tests/test_release_packaging_constraints.py) ——`scripts/**/*.py` 单文件 < 1000 行、文本文件 UTF-8 without BOM。
|
||||
- **开发规范守护(hard policy)**:[`tests/test_development_policy_guard.py`](../tests/test_development_policy_guard.py) ——规则索引见 [`development/POLICY_MATRIX.md`](POLICY_MATRIX.md),只执行可机器检测的 hard 规则。
|
||||
- **slug 语义**:[`tests/test_slug_naming.py`](../tests/test_slug_naming.py) ——verb-noun-platform 命名规范校验。
|
||||
- **slug 语义**:[`tests/test_slug_naming.py`](../tests/test_slug_naming.py) ——动词-名词命名;平台可选,`default` 不再作为占位。
|
||||
- **HTML 报告壳**:[`tests/test_html_page_service.py`](../tests/test_html_page_service.py) 守护 `assets/html/example-report.html` 的数据管理 iframe 预览布局基线,并守护 ACTIONS.md 里的壳子页目录 `assets/html/pages` 与 `jiangchang.call`。业务技能若有自定义 `assets/html/*.html` 或 `pages/*.html`,建议在自有仓库加同类静态断言(见 `references/ACTIONS.md`「宿主预览布局」)。不要把 `example-report.html` 放进 `pages/`。
|
||||
|
||||
**原则:`tests/test_*.py` 不允许隐形访问外网、不允许拉起真实浏览器、不允许读写开发者机器的真实数据根**。如果需要仿真服务器,也应仅在 integration(并由档位变量放行)。
|
||||
|
||||
@@ -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` **独立**。省略则不显示悬停,不回退。短名已够懂时不要写。需宿主已支持该字段;当前实现为顶栏技能按钮,不含宿主内置删除/刷新/新增/导入/导出 |
|
||||
@@ -776,6 +776,7 @@ HINT: <next step>
|
||||
- 依赖或宣传表顶栏「更多操作」溢出菜单(宿主已取消该能力)。
|
||||
- 把「表格行数据默认排序」与按钮顺序混淆——行排序见 [`SCHEMA.md`](SCHEMA.md) 的 `default_sort_json`。
|
||||
- 把「默认展示列」与 `visible=0` 硬隐藏混淆——默认展示列见 [`SCHEMA.md`](SCHEMA.md) 的 `default_visible_columns_json`。
|
||||
- 误以为 `visible=0` 的列不能出现在 `default_sort_json`——隐藏只影响展示/筛选/搜索;ORDER BY 仍可用非敏感物理列(含隐藏主键)。
|
||||
- 新增 `row-context-menu` 等 placement,或要求技能再维护一份与 `row` 平行的菜单清单。
|
||||
|
||||
### Action 类型 × placements(常见示例,不是限制)
|
||||
|
||||
@@ -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`) | 格子 / 列 / 区域 |
|
||||
|------|-------------------|------------------|
|
||||
| 数据管理表格 | 给整行(`<tr>`)上色 | 给对应单元格上色 |
|
||||
| 行详情 | **不**把行色铺到每个字段 | 对应字段可保留 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)。
|
||||
|
||||
@@ -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`。
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -179,11 +180,36 @@ JSON 数组(可多字段,按顺序):
|
||||
|
||||
规则:
|
||||
|
||||
- `column` 必须是该表真实列名;敏感列会被宿主忽略
|
||||
- `column` 必须是该表**真实物理列名**(存在于 SQLite 表);不存在则宿主忽略该段或技能校验报错
|
||||
- **可以**引用 `visible=0` 的列(含隐藏的主键 `id`)。`visible=0` 只表示界面不展示 / 不进默认 SELECT·筛选·搜索;**不表示不能 ORDER BY**
|
||||
- 敏感列(宿主标为 sensitive)不得用于排序;技能侧勿把密钥类字段写进 `default_sort_json`
|
||||
- `direction` 仅 `asc` / `desc`(缺省按 `desc`)
|
||||
- 用户在 UI 上手动改排序后,以用户选择为准;声明只影响未选择时的默认
|
||||
- 通过 `upsert_table_metadata(..., default_sort_json=...)` 写入;旧库由 `init_display_metadata_tables` 幂等 `ALTER` 补列
|
||||
|
||||
与 `visible` 的分工(勿再踩坑):
|
||||
|
||||
| 能力 | `visible=0` 是否仍可用 |
|
||||
|------|------------------------|
|
||||
| 表格列展示 / 「列」面板 | 否(硬隐藏) |
|
||||
| 默认 SELECT、列筛选、顶栏搜索 | 否 |
|
||||
| **`default_sort_json` / ORDER BY** | **是**(非敏感列) |
|
||||
| 行主键定位(`$row.$pk` 等) | 是(主键即使隐藏仍应可排序、可定位;宿主查询层保留) |
|
||||
|
||||
示例(编号硬隐藏,但仍按编号排序——合法):
|
||||
|
||||
```python
|
||||
upsert_table_metadata(
|
||||
cursor,
|
||||
table_name="period_rule_overrides",
|
||||
display_name="本月默认金额",
|
||||
readonly=1,
|
||||
default_sort_json='[{"column":"period","direction":"desc"},{"column":"id","direction":"asc"}]',
|
||||
)
|
||||
# id 列元数据可见性:
|
||||
# upsert_column_metadata(..., column_name="id", visible=0, ...)
|
||||
```
|
||||
|
||||
示例(消息时间线):
|
||||
|
||||
```python
|
||||
@@ -276,6 +302,8 @@ upsert_column_metadata(
|
||||
- 只写英文物理名而不写中文 `display_name`
|
||||
- 让元数据引用不存在的表
|
||||
- 用 `visible=0` 假装「默认不展示但用户还能勾出来」(那是硬隐藏,勾不回来)
|
||||
- 误以为「列 `visible=0` 就不能写进 `default_sort_json`」——排序与展示已拆开;隐藏编号/主键仍可排序(须列真实存在且非敏感)
|
||||
- 把不存在的列名写进 `default_sort_json`(技能校验应报错;宿主也会忽略无效段)
|
||||
|
||||
### `_jiangchang_columns` 字段级标准
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -95,6 +103,7 @@ def upsert_table_metadata(
|
||||
default_sort_json: 宿主数据管理默认排序,JSON 数组,例如
|
||||
'[{"column":"sent_at","direction":"desc"},{"column":"id","direction":"desc"}]'
|
||||
为空则宿主回退到 updated_at → created_at → 主键。
|
||||
排序列须为真实物理列;**允许**引用 visible=0 的非敏感列(隐藏≠不能 ORDER BY)。
|
||||
|
||||
default_visible_columns_json: 宿主数据管理默认展示列,JSON 字符串数组,例如
|
||||
'["stat_type","period","stage"]'
|
||||
|
||||
@@ -279,6 +279,95 @@ def validate_filter_search_metadata(
|
||||
return [], warnings
|
||||
|
||||
|
||||
def parse_default_sort_json(raw: object) -> tuple[list[dict[str, str]], list[str]]:
|
||||
"""解析 default_sort_json;返回 (specs, errors)。specs 项含 column / direction。"""
|
||||
errors: list[str] = []
|
||||
if raw is None or str(raw).strip() == "":
|
||||
return [], []
|
||||
text = str(raw).strip()
|
||||
try:
|
||||
parsed = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
return [], ["default_sort_json 不是合法 JSON"]
|
||||
if not isinstance(parsed, list):
|
||||
return [], ["default_sort_json 必须是 JSON 数组"]
|
||||
out: list[dict[str, str]] = []
|
||||
for index, item in enumerate(parsed):
|
||||
if not isinstance(item, dict):
|
||||
errors.append(f"default_sort_json[{index}] 必须是对象")
|
||||
continue
|
||||
column = str(
|
||||
item.get("column")
|
||||
or item.get("columnName")
|
||||
or item.get("column_name")
|
||||
or ""
|
||||
).strip()
|
||||
direction_raw = str(item.get("direction") or "desc").strip().lower()
|
||||
if not column:
|
||||
errors.append(f"default_sort_json[{index}] 缺少 column")
|
||||
continue
|
||||
if direction_raw not in {"asc", "desc"}:
|
||||
errors.append(f"default_sort_json[{index}].direction 只能是 asc 或 desc")
|
||||
continue
|
||||
out.append({"column": column, "direction": direction_raw})
|
||||
return out, errors
|
||||
|
||||
|
||||
def validate_default_sort_metadata(
|
||||
cursor,
|
||||
table_name: str,
|
||||
*,
|
||||
physical_columns: list[str] | None = None,
|
||||
) -> tuple[list[str], list[str]]:
|
||||
"""校验表级 default_sort_json:列须真实存在;允许 visible=0;不存在则报错。
|
||||
|
||||
契约:visible=0 只藏界面,ORDER BY 仍可用非敏感物理列(含隐藏主键)。
|
||||
"""
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
table_cols = {
|
||||
str(row[1]) for row in cursor.execute("PRAGMA table_info(_jiangchang_tables)").fetchall()
|
||||
}
|
||||
if "default_sort_json" not in table_cols:
|
||||
return [], []
|
||||
cursor.execute(
|
||||
"SELECT default_sort_json FROM _jiangchang_tables WHERE table_name = ?",
|
||||
(table_name,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return [], []
|
||||
specs, parse_errors = parse_default_sort_json(row[0])
|
||||
for msg in parse_errors:
|
||||
errors.append(f"{table_name}: {msg}")
|
||||
if parse_errors:
|
||||
return errors, warnings
|
||||
|
||||
physical = set(physical_columns or list_physical_column_names(cursor, table_name))
|
||||
for spec in specs:
|
||||
column = spec["column"]
|
||||
if column not in physical:
|
||||
errors.append(
|
||||
f"{table_name}: default_sort_json 引用了不存在的列 {column!r}"
|
||||
)
|
||||
continue
|
||||
# visible=0 合法:不报错。仅提示契约,避免作者误删排序列。
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT visible FROM _jiangchang_columns
|
||||
WHERE table_name = ? AND column_name = ?
|
||||
""",
|
||||
(table_name, column),
|
||||
)
|
||||
meta = cursor.fetchone()
|
||||
if meta is not None and meta[0] is not None and int(meta[0]) == 0:
|
||||
warnings.append(
|
||||
f"{table_name}.{column}: default_sort_json 引用了 visible=0 列"
|
||||
"(合法:隐藏列仍可 ORDER BY;见 references/SCHEMA.md)"
|
||||
)
|
||||
return errors, warnings
|
||||
|
||||
|
||||
def validate_column_permissions(
|
||||
cursor,
|
||||
table_name: str,
|
||||
@@ -558,6 +647,14 @@ def validate_display_metadata(conn: sqlite3.Connection, *, check_column_order: b
|
||||
_, filter_warnings = validate_filter_search_metadata(cur, table_name)
|
||||
report.warnings.extend(filter_warnings)
|
||||
|
||||
sort_errors, sort_warnings = validate_default_sort_metadata(
|
||||
cur,
|
||||
table_name,
|
||||
physical_columns=physical_columns,
|
||||
)
|
||||
report.errors.extend(sort_errors)
|
||||
report.warnings.extend(sort_warnings)
|
||||
|
||||
cur.execute("SELECT table_name, column_name FROM _jiangchang_columns")
|
||||
for table_name, column_name in cur.fetchall():
|
||||
if table_name in METADATA_TABLES:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。"""
|
||||
|
||||
SKILL_SLUG = "your-skill-slug"
|
||||
SKILL_VERSION = "1.0.110"
|
||||
SKILL_VERSION = "1.0.113"
|
||||
LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug"
|
||||
PLATFORM_KIT_MIN_VERSION = "1.2.11"
|
||||
|
||||
@@ -7,7 +7,7 @@ import re
|
||||
|
||||
_KEBAB_SLUG = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
||||
_MAX_SLUG_LENGTH = 48
|
||||
_MIN_SEGMENTS = 3
|
||||
_MIN_SEGMENTS = 2
|
||||
_MAX_SEGMENTS = 5
|
||||
_MAX_NOUN_SEGMENTS = 3
|
||||
|
||||
@@ -60,7 +60,9 @@ _BUILTIN_PLATFORMS = frozenset(
|
||||
"etax",
|
||||
}
|
||||
)
|
||||
_BUILTIN_SCOPES = frozenset({"all", "batch", "multi", "default"})
|
||||
_BUILTIN_SCOPES = frozenset({"all", "batch", "multi"})
|
||||
# 旧技能用 default 占位末段。不再当成合法 scope;校验只警告,避免同步后挡住已发布仓库。
|
||||
_DEPRECATED_PLATFORM_PLACEHOLDER = "default"
|
||||
|
||||
LEGACY_EXEMPT_SLUGS: frozenset[str] = frozenset(
|
||||
{
|
||||
@@ -117,11 +119,12 @@ def _match_platform_suffix(
|
||||
|
||||
def classify_slug_form(segments: list[str], *, slug: str = "") -> str:
|
||||
"""
|
||||
返回: 'legacy_exempt' | 'standard' | 'scope' | 'invalid'
|
||||
返回: 'legacy_exempt' | 'standard' | 'scope' | 'plain' | 'invalid'
|
||||
|
||||
- legacy_exempt: slug in LEGACY_EXEMPT_SLUGS
|
||||
- standard: 末段匹配 platforms, 首段 in verbs, 3<=len<=5
|
||||
- scope: 末段 in scopes, 首段 in verbs, 3<=len<=5
|
||||
- standard: 末段匹配 platforms,首段 in verbs,2<=len<=5,且动词后至少还有一段名词
|
||||
- scope: 末段 in scopes(all/batch/multi),首段 in verbs,至少 verb-noun-scope
|
||||
- plain: 动词 + 名词,无平台、无 scope
|
||||
- invalid: 其他
|
||||
"""
|
||||
if slug in LEGACY_EXEMPT_SLUGS:
|
||||
@@ -138,12 +141,14 @@ def classify_slug_form(segments: list[str], *, slug: str = "") -> str:
|
||||
return "invalid"
|
||||
|
||||
if segments[-1] in scopes:
|
||||
if len(segments) < 3:
|
||||
return "invalid"
|
||||
return "scope"
|
||||
|
||||
if _match_platform_suffix(segments, platforms) is not None:
|
||||
return "standard"
|
||||
|
||||
return "invalid"
|
||||
return "plain"
|
||||
|
||||
|
||||
def validate_slug_semantics(
|
||||
@@ -173,8 +178,8 @@ def validate_slug_semantics(
|
||||
|
||||
if segment_count < _MIN_SEGMENTS:
|
||||
msg = (
|
||||
f"slug 段数应为 {_MIN_SEGMENTS}~{_MAX_SEGMENTS}(verb-noun-platform),"
|
||||
f"当前为 {segment_count} 段:{slug!r};新技能禁止 2 段无平台形态(如 reconcile-finance)"
|
||||
f"slug 段数应为 {_MIN_SEGMENTS}~{_MAX_SEGMENTS}(动词-名词,平台可选),"
|
||||
f"当前为 {segment_count} 段:{slug!r}"
|
||||
)
|
||||
(errors if strict else warnings).append(msg)
|
||||
return errors, warnings
|
||||
@@ -197,9 +202,6 @@ def validate_slug_semantics(
|
||||
|
||||
form = classify_slug_form(segments, slug=slug)
|
||||
if form == "scope":
|
||||
if segments[-1] not in scopes:
|
||||
msg = f"slug 末段应为 scope 白名单中的词:{segments[-1]!r}(slug={slug!r})"
|
||||
(errors if strict else warnings).append(msg)
|
||||
noun_count = segment_count - 2
|
||||
elif form == "standard":
|
||||
platform_match = _match_platform_suffix(segments, platforms)
|
||||
@@ -210,13 +212,17 @@ def validate_slug_semantics(
|
||||
else:
|
||||
_platform_name, platform_part_count = platform_match
|
||||
noun_count = segment_count - 1 - platform_part_count
|
||||
elif form == "plain":
|
||||
noun_count = segment_count - 1
|
||||
if segments[-1] == _DEPRECATED_PLATFORM_PLACEHOLDER:
|
||||
warnings.append(
|
||||
"slug 末段 default 已不再作为平台占位;没有外部平台时直接省略。"
|
||||
f"已发布的技能不用改名:{slug!r}"
|
||||
)
|
||||
else:
|
||||
if segment_count >= _MIN_SEGMENTS:
|
||||
if segments[-1] not in scopes:
|
||||
platform_match = _match_platform_suffix(segments, platforms)
|
||||
if platform_match is None:
|
||||
msg = f"slug 末段应匹配平台或 scope 白名单:{slug!r}"
|
||||
(errors if strict else warnings).append(msg)
|
||||
if segments and segments[-1] in scopes and segment_count < 3:
|
||||
msg = f"scope 型 slug 至少为动词-名词-scope:{slug!r}"
|
||||
(errors if strict else warnings).append(msg)
|
||||
noun_count = 0
|
||||
|
||||
if noun_count > _MAX_NOUN_SEGMENTS:
|
||||
|
||||
@@ -736,6 +736,7 @@ class TestActionsManifest(unittest.TestCase):
|
||||
"afterFailure",
|
||||
"cellPresentation",
|
||||
"CELL_PRESENTATION.md",
|
||||
"整行",
|
||||
"labelColumn",
|
||||
"selectionMode",
|
||||
"勾选驱动",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -595,6 +595,53 @@ metadata:
|
||||
self.assertIn("readonly = 1", text)
|
||||
self.assertIn("seed_column_metadata_batch", text)
|
||||
self.assertIn("valid_column_names", text)
|
||||
self.assertIn("可以**引用 `visible=0` 的列", text)
|
||||
self.assertIn("default_sort_json` / ORDER BY", text)
|
||||
|
||||
def test_default_sort_json_allows_hidden_column_and_rejects_missing(self) -> None:
|
||||
conn = sqlite3.connect(":memory:")
|
||||
try:
|
||||
from db.display_metadata import (
|
||||
init_display_metadata_tables,
|
||||
seed_column_metadata_batch,
|
||||
upsert_table_metadata,
|
||||
)
|
||||
from db.display_metadata_validator import validate_default_sort_metadata
|
||||
|
||||
cur = conn.cursor()
|
||||
init_display_metadata_tables(cur)
|
||||
cur.execute(
|
||||
"CREATE TABLE demo_sort (id INTEGER PRIMARY KEY, title TEXT NOT NULL)"
|
||||
)
|
||||
upsert_table_metadata(
|
||||
cur,
|
||||
table_name="demo_sort",
|
||||
display_name="排序示例",
|
||||
default_sort_json='[{"column":"title","direction":"asc"},{"column":"id","direction":"desc"}]',
|
||||
)
|
||||
seed_column_metadata_batch(
|
||||
cur,
|
||||
table_name="demo_sort",
|
||||
columns=(
|
||||
{"column_name": "id", "display_name": "编号", "visible": 0, "editable": 0},
|
||||
{"column_name": "title", "display_name": "标题", "visible": 1, "editable": 1},
|
||||
),
|
||||
valid_column_names=["id", "title"],
|
||||
)
|
||||
errors, warnings = validate_default_sort_metadata(cur, "demo_sort")
|
||||
self.assertEqual(errors, [])
|
||||
self.assertTrue(any("visible=0" in w and "id" in w for w in warnings))
|
||||
|
||||
upsert_table_metadata(
|
||||
cur,
|
||||
table_name="demo_sort",
|
||||
display_name="排序示例",
|
||||
default_sort_json='[{"column":"missing_col","direction":"asc"}]',
|
||||
)
|
||||
errors, _ = validate_default_sort_metadata(cur, "demo_sort")
|
||||
self.assertTrue(any("missing_col" in e for e in errors))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_default_visible_columns_json_upsert_and_migrate(self) -> None:
|
||||
conn = sqlite3.connect(":memory:")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -44,6 +44,8 @@ class TestSlugNaming(unittest.TestCase):
|
||||
for slug in (
|
||||
"download-settlement-report-amazon",
|
||||
"add-product-shopee",
|
||||
"generate-monthly-statements",
|
||||
"reconcile-finance",
|
||||
"generate-article-all",
|
||||
"your-skill-slug",
|
||||
"account-manager",
|
||||
@@ -56,8 +58,7 @@ class TestSlugNaming(unittest.TestCase):
|
||||
cases = (
|
||||
"amazon-download-settlement-report",
|
||||
"enter-cross-border-sales-receipt-voucher-kingdee",
|
||||
"reconcile-finance",
|
||||
"download-settlement-report-unknownplat",
|
||||
"generate",
|
||||
"foobar-settlement-report-amazon",
|
||||
)
|
||||
for slug in cases:
|
||||
@@ -78,6 +79,19 @@ class TestSlugNaming(unittest.TestCase):
|
||||
self.assertTrue(errors)
|
||||
self.assertTrue(any("48" in err for err in errors))
|
||||
|
||||
def test_plain_slug_without_platform(self) -> None:
|
||||
slug = "generate-monthly-statements"
|
||||
self.assertEqual(classify_slug_form(parse_slug_segments(slug), slug=slug), "plain")
|
||||
errors, warnings = validate_slug_semantics(slug)
|
||||
self.assertEqual(errors, [], msg=f"errors={errors!r} warnings={warnings!r}")
|
||||
self.assertEqual(warnings, [])
|
||||
|
||||
def test_default_suffix_warns_but_does_not_fail(self) -> None:
|
||||
slug = "generate-article-default"
|
||||
errors, warnings = validate_slug_semantics(slug)
|
||||
self.assertEqual(errors, [], msg=f"errors={errors!r}")
|
||||
self.assertTrue(any("default" in item for item in warnings), msg=f"warnings={warnings!r}")
|
||||
|
||||
def test_legacy_exempt_set(self) -> None:
|
||||
self.assertIn("your-skill-slug", LEGACY_EXEMPT_SLUGS)
|
||||
self.assertIn("account-manager", LEGACY_EXEMPT_SLUGS)
|
||||
|
||||
Reference in New Issue
Block a user