Compare commits

...

6 Commits

Author SHA1 Message Date
9fa5953713 docs: form field type contract Action/CRUD symmetry (v1.0.77)
All checks were successful
技能自动化发布 / release (push) Successful in 7s
2026-08-11 15:52:27 +08:00
db2325dbe4 docs: contract for CRUD form description and SQLite DEFAULT prefill (v1.0.76)
All checks were successful
技能自动化发布 / release (push) Successful in 6s
2026-08-11 14:38:32 +08:00
16ee121ab3 Release v1.0.75: cell presentation contract for data management
All checks were successful
技能自动化发布 / release (push) Successful in 6s
2026-08-10 14:31:04 +08:00
139d176841 Release v1.0.74: hostEffects/steps Action contract
All checks were successful
技能自动化发布 / release (push) Successful in 12s
2026-08-10 12:05:58 +08:00
0b6bf0b2fa Release v1.0.73: Data Policy contract and schemas
All checks were successful
技能自动化发布 / release (push) Successful in 7s
2026-08-09 10:53:31 +08:00
51be1517c0 docs: clarify row placement as row-level shell including context menu
All checks were successful
技能自动化发布 / release (push) Successful in 7s
2026-08-09 10:20:20 +08:00
22 changed files with 1593 additions and 36 deletions

View File

@@ -9,6 +9,42 @@
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
## 1.0.77
- 固化 Action / CRUD **对称**表单字段类型契约:text、textarea/json、number、boolean、enum/lookup、sensitive、date、datetime_unix_seconds、url/email(薄层)
- 明确可编辑业务时间用 Unix **秒**读写;审计 `created_at`/`updated_at` 仍只读展示
- 明确本版**不做**文件上传、多选/标签数组、复杂 nested 表单,避免宿主反复扩面
- 更新 `SCHEMA.md`、`ACTIONS.md`、`skill-actions.schema.json`(`display_type`/`format`)
## 1.0.76
- 固化数据管理「新增/编辑」表单契约:列 `description` 须在标签下展示;创建默认值取自 SQLite 列 `DEFAULT`(见 `references/SCHEMA.md`)
- 明确禁止把说明塞进 `display_name`;与 Skill Action 的 `inputSchema.description` / `default` 分工写清(见 `ACTIONS.md`)
- 宿主须按该契约实现下发与预填;技能侧用元数据说明 + DDL DEFAULT 即可,第一期不新增表单默认值元数据列
## 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`)
- 支持成功后 `reloadTable` 等 UI 副作用与级联删除步骤;需匠厂宿主 ≥2.8.5;省略则不自动刷表
- 更新 `skill-actions.schema.json`、ACTIONS.md、SKILL_ACTION_RUNTIME
## 1.0.73
- 正式发布 Data Policy 契约(`references/POLICY.md` v1.0):可选身份授权,默认 L0 零配置
- 新增 Snapshot Schema 与示例;`actions.json` 支持顶层 `policy` 与 Action 可选 `authz` 提示
- ACTIONS 扩展为六职责正交(含 policy)
## 1.0.72
- 澄清 `placements: "row"` 契约:表示数据管理**行级入口**;技能配置决定菜单项,宿主可呈现为操作列、行右键和/或行内「⋯」(同一批 Action,无独立菜单 DSL / `row-context-menu` placement)
- 更新 ACTIONS.md、SKILL_ACTION_RUNTIME、DEVELOPMENT 验收表与 skill-actions.schema 描述
## 1.0.71
- 文档对齐:Skill Action `sync` 默认超时约 **60s**(`SKILL_ACTION_SYNC_TIMEOUT_MS`);接近或超过约 60s 的任务应标 `async`

View File

@@ -1,7 +1,7 @@
---
name: 技能开发模板(通用业务版)
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
version: 1.0.71
version: 1.0.77
author: 深圳匠厂科技有限公司
metadata:
openclaw:
@@ -32,9 +32,11 @@ allowed-tools:
### 架构铁律(先读)
- **单业务内核、多入口**:数据管理 / 定时任务 / Agent / 技能详情 / 直接 CLI 必须复用同一 domain service;禁止按入口复制业务逻辑,禁止按 `source` 分叉业务行为。
- **`placements`**:只决定 Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail)。数据管理技能按钮顺序见 `actions` **数组相对顺序**(详见 `references/ACTIONS.md`),无独立 sortOrder。
- **`placements`**:只决定 Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail)。其中 `row` 表示**行级入口**(宿主可呈现为操作列、行右键、行内「⋯」等;技能配什么就显示什么,无独立菜单 DSL)。数据管理技能按钮顺序见 `actions` **数组相对顺序**(详见 `references/ACTIONS.md`),无独立 sortOrder。
- **`bind.tables`**:只决定数据管理出现在哪些表;含 `toolbar` 时**必须**显式非空绑定。
- **`executionProfile`**:只决定执行方式——`sync` 当场返回结果,`async` 进**任务中心**。与 placements / 入口**无关**。心智上多数为 sync,仅长任务 / RPA 标 async;宿主省略该字段时按 sync。每个 Action **仍必须**显式写 `sync` 或 `async`。
- **`policy`(可选)**:需要按用户/角色控制数据管理按钮或行列时,在 `actions.json` 顶层启用 Data Policy,由技能 `policy` CLI 输出 Snapshot;**未启用 = 与今日一致(L0)**。详见 `references/POLICY.md`。用户/角色数据源(飞书/库/API)属技能内部,宿主不托管。
- **`cellPresentation`(可选)**:需要数据管理「某格特殊标色/强调」时启用;技能按当前数据写入呈现(语义 tone),宿主映射主题样式;**未启用 = P0 无标色**。用途不限(超时、预警、驳回等)。详见 `references/CELL_PRESENTATION.md`。
- **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;特殊业务报告才另做 `export-*`。
- **准备 vs 执行(适用则拆)**:若技能存在「先写入/准备本地数据」与「再对外产生副作用」两段,Agent 必须能分开触发;禁止默认绑成不可拆的一步。一键无准备态的技能可跳过本条。
@@ -68,7 +70,7 @@ allowed-tools:
5. **一个业务能力一个 Action**,用多个 `placements` 挂入口;底层仍是同一 `scripts/main.py` CLI。
6. 是否进任务中心**只**看该 Action 的 `executionProfile`,与数据管理 / 定时 / Agent 入口无关。
Agent 执行契约细节:`references/CLI.md`、`references/ACTIONS.md`、`references/SCHEMA.md`(不要把 `development/` 当 Agent 主读路径)。
Agent 执行契约细节:`references/CLI.md`、`references/ACTIONS.md`、`references/SCHEMA.md`、按需 `references/POLICY.md`(不要把 `development/` 当 Agent 主读路径)。
## 共享 Python(必读)

View File

@@ -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`)。

View File

@@ -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
}
]
}

View File

@@ -0,0 +1,13 @@
{
"schemaVersion": 1,
"skill": "your-skill-slug",
"actor": {
"userId": "u1",
"displayName": "张三",
"roles": ["clerk"]
},
"actions": {
"mode": "allowlist",
"ids": ["whoami", "create-record", "sync-records"]
}
}

View File

@@ -0,0 +1,28 @@
{
"schemaVersion": 1,
"skill": "your-skill-slug",
"actor": {
"userId": "u1"
},
"actions": {
"mode": "open"
},
"data": {
"tables": {
"orders": {
"rows": {
"mode": "scope",
"scope": {
"all": [
{
"field": "owner_user_id",
"op": "eq",
"valueFrom": "actor.userId"
}
]
}
}
}
}
}
}

View File

@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openclaw.local/skill-template/skill-actions.schema.json",
"title": "SkillActionManifest",
"description": "匠厂 skill-template 对新技能的严格 Action manifest 规范(schemaVersion 1)。additionalProperties=false 表示模板推荐契约;宿主运行时可能为兼容历史 manifest 接受部分缺省字段,但新技能不应依赖缺省行为。",
"description": "匠厂 skill-template 对新技能的严格 Action manifest 规范(schemaVersion 1)。additionalProperties=false 表示模板推荐契约;宿主运行时可能为兼容历史 manifest 接受部分缺省字段,但新技能不应依赖缺省行为。含可选 hostEffects/steps(匠厂宿主 ≥2.8.5)。",
"type": "object",
"required": ["schemaVersion", "skill", "actions"],
"additionalProperties": false,
@@ -20,14 +20,67 @@
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/action" },
"description": "Action 列表。同一 placement(toolbar/row/batch)下,宿主按本数组中匹配项的相对顺序全部直接渲染技能按钮(无「更多操作」溢出);调整顺序即调整数组先后。无独立 sortOrder 字段。宿主内置刷新/导出/查看等不参与本顺序。表顶栏宽时左右横排,窄时左右分栏且栏内竖排。"
"description": "Action 列表。同一 placement(toolbar/row/batch)下,宿主按本数组中匹配项的相对顺序展示。toolbar/batch:表顶栏全部直接展示(无「更多操作」溢出)。row:行级入口,宿主可呈现为操作列、行右键和/或行内「⋯」(同一批 Action、同一顺序);不在本字段配置排序、截断或菜单树。调整顺序即调整数组先后。无独立 sortOrder 字段。宿主内置刷新/导出/查看等不参与本顺序。表顶栏宽时左右横排,窄时左右分栏且栏内竖排。"
},
"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"],
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"description": "仅 true 启用;false 等同 L0。"
},
"entrypoint": {
"$ref": "#/$defs/entrypoint",
"description": "enabled=true 时必填。推荐 command=policy,args 可含 {{table}}/{{skill}}/{{userId}}。"
},
"appliesTo": {
"type": "array",
"items": {
"type": "string",
"enum": ["data-management"]
},
"description": "缺省 [\"data-management\"]。本期宿主只实现 data-management。"
}
},
"allOf": [
{
"if": {
"properties": { "enabled": { "const": true } },
"required": ["enabled"]
},
"then": {
"required": ["entrypoint"]
}
}
]
},
"placement": {
"type": "string",
"enum": ["toolbar", "row", "batch", "cron", "agent", "skill-detail"],
"description": "稳定支持 toolbar/row/batch/cron/agent/skill-detail(对齐匠厂宿主)。placements 与 executionProfile 正交,不做条件限制。"
"description": "稳定支持 toolbar/row/batch/cron/agent/skill-detail(对齐匠厂宿主)。row=行级入口(操作列/行右键/⋯ 等由宿主呈现,无独立 row-context-menu placement)。placements 与 executionProfile 正交,不做条件限制。"
},
"mutationKind": {
"type": "string",
@@ -64,6 +117,16 @@
"minLength": 1,
"pattern": "^[a-z][a-z0-9_]*$",
"description": "可选。绑定业务表物理列名;replaces create/update 时宿主可复用该列的中文名与 lookup 下拉"
},
"display_type": {
"type": "string",
"enum": ["textarea", "date", "datetime_unix_seconds", "url", "email"],
"description": "可选。表单控件提示;与 format 二选一即可。须与 CRUD display_type 语义对齐(见 ACTIONS.md / SCHEMA.md)。"
},
"format": {
"type": "string",
"enum": ["textarea", "date", "datetime_unix_seconds", "url", "email"],
"description": "可选。display_type 别名,语义相同。"
}
}
},
@@ -145,6 +208,75 @@
},
"description": "表级绑定;toolbar 必填。可选 inputMapping / columnMapping。concurrency/locks 勿写入。"
},
"hostEffectId": {
"type": "string",
"enum": ["reloadTable", "refreshCatalog", "clearSelection"],
"description": "数据管理 UI 副作用(匠厂宿主 ≥2.8.5)。reloadTable=刷新当前表(含 Data Policy);refreshCatalog=刷新技能目录;clearSelection=清空多选。"
},
"hostEffects": {
"type": "object",
"additionalProperties": false,
"properties": {
"onSuccess": {
"type": "array",
"items": { "$ref": "#/$defs/hostEffectId" },
"uniqueItems": true,
"description": "sync 退出 0,或 async 任务已被任务中心接收时触发(不是 Job 跑完)。"
},
"onFailure": {
"type": "array",
"items": { "$ref": "#/$defs/hostEffectId" },
"uniqueItems": true,
"description": "sync 失败,或 async 启动被拒时触发。"
}
},
"description": "可选。UI 副作用简写;省略则宿主不额外改 UI。与 placements/executionProfile/interaction 正交;cron/agent 忽略。新技能有编排需求时优先用 steps。"
},
"actionStep": {
"type": "object",
"required": ["when", "do"],
"additionalProperties": false,
"properties": {
"when": {
"type": "string",
"enum": ["before", "afterSuccess", "afterFailure"],
"description": "before=CLI 前;afterSuccess/afterFailure 时机同 hostEffects.onSuccess/onFailure。"
},
"do": {
"type": "string",
"enum": ["deleteByFk", "deleteRows", "reloadTable", "refreshCatalog", "clearSelection"],
"description": "闭环操作或 UI 效果。deleteByFk/deleteRows 须配合 table(及 fk)。"
},
"table": {
"type": "string",
"minLength": 1,
"pattern": "^[a-z][a-z0-9_]*$",
"description": "deleteByFk / deleteRows 目标表(snake_case)。"
},
"fk": {
"type": "string",
"minLength": 1,
"pattern": "^[a-z][a-z0-9_]*$",
"description": "deleteByFk 时子表外键列名。"
}
},
"allOf": [
{
"if": {
"properties": { "do": { "const": "deleteByFk" } },
"required": ["do"]
},
"then": { "required": ["table", "fk"] }
},
{
"if": {
"properties": { "do": { "const": "deleteRows" } },
"required": ["do"]
},
"then": { "required": ["table"] }
}
]
},
"action": {
"type": "object",
"required": ["id", "label", "description", "placements", "entrypoint", "executionProfile"],
@@ -167,7 +299,7 @@
"type": "array",
"minItems": 0,
"items": { "$ref": "#/$defs/placement" },
"description": "决定 Action 出现在哪里;与 executionProfile 正交。仅 replaces 接管默认 CRUD 时可为空数组。数据管理 toolbar/row/batch 的按钮顺序由顶层 actions 数组相对顺序决定;表顶栏全部直接展示(无溢出菜单),不在本字段配置排序或截断。"
"description": "决定 Action 出现在哪里;与 executionProfile 正交。仅 replaces 接管默认 CRUD 时可为空数组。数据管理 toolbar/row/batch 的顺序由顶层 actions 数组相对顺序决定。表顶栏 toolbar/batch 全部直接展示(无溢出菜单);row 为行级入口(操作列/行右键/⋯),不在本字段配置排序、截断或菜单树。"
},
"replaces": { "$ref": "#/$defs/mutationKind" },
"entrypoint": { "$ref": "#/$defs/entrypoint" },
@@ -199,7 +331,25 @@
},
"description": "按 placement / replaces 覆盖 interaction(只能写 concrete 模式,不能写 auto)。"
},
"bind": { "$ref": "#/$defs/bind" }
"bind": { "$ref": "#/$defs/bind" },
"hostEffects": { "$ref": "#/$defs/hostEffects" },
"steps": {
"type": "array",
"maxItems": 8,
"items": { "$ref": "#/$defs/actionStep" },
"description": "可选。有序宿主编排(before / afterSuccess / afterFailure);单次最多 8 步。与 hostEffects 可并存(同效果可能各跑一次)。新技能优先用 steps。省略则无额外步骤。需匠厂宿主 ≥2.8.5。"
},
"authz": {
"type": "object",
"additionalProperties": false,
"properties": {
"capability": {
"type": "string",
"minLength": 1,
"description": "可选文档/静态提示用能力名;宿主不得仅凭此显隐按钮,运行时以 policy Snapshot 为准。"
}
}
}
},
"if": {
"properties": {

View File

@@ -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);填完数据并重算后该项消失则提醒自然关闭"
}
}
}
}
}

View File

@@ -0,0 +1,196 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openclaw.local/skill-template/skill-policy-snapshot.schema.json",
"title": "SkillDataPolicySnapshot",
"description": "匠厂 Skill Data Policy Snapshot(schemaVersion 1)。由技能 policy CLI 输出到 stdout;宿主用于数据管理按钮/行列过滤。详见 references/POLICY.md。",
"type": "object",
"required": ["schemaVersion", "skill", "actor", "actions"],
"additionalProperties": true,
"properties": {
"schemaVersion": {
"type": "integer",
"const": 1
},
"skill": {
"type": "string",
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
"minLength": 1
},
"actor": {
"type": "object",
"required": ["userId"],
"additionalProperties": true,
"properties": {
"userId": { "type": "string", "minLength": 1 },
"displayName": { "type": "string" },
"roles": {
"type": "array",
"items": { "type": "string", "minLength": 1 }
},
"attributes": {
"type": "object",
"additionalProperties": {
"type": ["string", "number", "boolean", "null"]
}
}
}
},
"actions": {
"type": "object",
"required": ["mode"],
"additionalProperties": false,
"properties": {
"mode": {
"type": "string",
"enum": ["allowlist", "open"],
"description": "v1 仅允许 allowlist/open;denylist 非法。"
},
"ids": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
},
"description": "mode=allowlist 时必填;mode=open 时应省略或 []。"
}
},
"allOf": [
{
"if": {
"properties": { "mode": { "const": "allowlist" } },
"required": ["mode"]
},
"then": {
"required": ["ids"]
}
}
]
},
"hostCapabilities": {
"type": "array",
"items": {
"type": "string",
"enum": ["refresh", "export", "view", "edit", "delete", "import"]
},
"description": "省略=不限制宿主内置能力;写出则按 allowlist(含 []=全禁)。"
},
"data": {
"type": "object",
"additionalProperties": false,
"properties": {
"tables": {
"type": "object",
"additionalProperties": { "$ref": "#/$defs/tablePolicy" }
}
}
},
"extensions": {
"type": "object",
"description": "技能私有扩展;宿主必须忽略未知键。"
}
},
"$defs": {
"predicate": {
"type": "object",
"required": ["field", "op"],
"additionalProperties": false,
"properties": {
"field": {
"type": "string",
"minLength": 1,
"pattern": "^[a-z][a-z0-9_]*$"
},
"op": {
"type": "string",
"enum": [
"eq",
"ne",
"in",
"not_in",
"gt",
"gte",
"lt",
"lte",
"is_null",
"is_not_null"
]
},
"value": {
"description": "与 valueFrom 互斥;in/not_in 时为非空数组。"
},
"valueFrom": {
"type": "string",
"const": "actor.userId",
"description": "v1 仅允许 actor.userId。"
}
}
},
"scope": {
"oneOf": [
{ "$ref": "#/$defs/predicate" },
{
"type": "object",
"required": ["all"],
"additionalProperties": false,
"properties": {
"all": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/predicate" }
}
}
},
{
"type": "object",
"required": ["any"],
"additionalProperties": false,
"properties": {
"any": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/predicate" }
}
}
}
]
},
"tablePolicy": {
"type": "object",
"additionalProperties": false,
"properties": {
"accessible": { "type": "boolean" },
"columns": {
"type": "object",
"additionalProperties": false,
"properties": {
"visible": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[a-z][a-z0-9_]*$"
}
},
"writable": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[a-z][a-z0-9_]*$"
}
}
}
},
"rows": {
"type": "object",
"additionalProperties": false,
"properties": {
"mode": {
"type": "string",
"enum": ["scope", "open", "delegate"]
},
"scope": { "$ref": "#/$defs/scope" }
}
}
}
}
}
}

View File

@@ -841,7 +841,7 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
|-------------------------|----------------|--------------|
| **新建任务**(对话 Agent) | `placements` 含 `agent`;或短查询走共享 Python CLI | 自然语言能触发主流程;长任务 / RPA 须走 `run_skill_action`,禁止 bash 干等 |
| **数据管理 · 表顶栏** | `placements` 含 `toolbar`(须合法非空 `bind.tables`) | 左侧能看到本技能库表;表顶栏出现按钮;点按行为符合预期 |
| **数据管理 · 行内** | `placements` 含 `row`(建议 `bind.tables` + `bind.inputMapping` 预填主键) | 行内操作链接可见(需有主键列);点按后入参/行为正确 |
| **数据管理 · 行级** | `placements` 含 `row`(建议 `bind.tables` + `bind.inputMapping` 预填主键) | 操作列和/或行右键(及可选「⋯」)出现本技能行级 Action(需有主键列);点按后入参/行为与壳无关、均正确 |
| **数据管理 · 批量** | `placements` 含 `batch` | 勾选行后批量按钮可用;无勾选时禁用;批量行为正确 |
| **定时任务** | `placements` 含 `cron`(创建时选「技能直调」) | 能选到本技能 Action、保存并触发;到点或「立即运行」行为正确 |
| **任务中心** | 不是 placement;由 `executionProfile: "async"` 决定是否进 Job | 长任务出现进度 / 可取消;来源标签与触发入口一致 |

View File

@@ -67,3 +67,6 @@
| `SKILL.md` / `constants.SKILL_SLUG` 一致性 | development/DEVELOPMENT.md §16 | 已有 `tests/test_skill_metadata.py` |
| platform_kit_min_version >= 1.2.2 | development/RUNTIME.md | 已有 `tests/test_platform_import.py` |
| 宿主按 `bind.tables` 过滤 toolbar 按钮 | references/ACTIONS.md | 待宿主升级后完整生效;模板侧已强制显式绑定 |
| Data Policy 未启用(L0)时宿主不得拉 Snapshot / 不得改按钮与行列行为 | references/POLICY.md | soft;宿主实现契约 |
| Data Policy 启用后 fail-closed(禁止回退 L0 全开);写路径 domain 必须再鉴权 | references/POLICY.md §6–§7 | soft;`POLICY-SKILL-AUTHZ-*` 硬门禁可后续加 |
| `actions.mode` 仅 allowlist/open;`hostCapabilities` 省略=不限制;scope `valueFrom` 仅 `actor.userId` | references/POLICY.md §0.1 | soft;Schema 已约束 |

View File

@@ -19,7 +19,7 @@
1. **宿主管调度,技能管业务** — 不自行实现任务中心 / Job 轮询 / 后台守护进程。
2. **单业务内核、多入口** — 数据管理、定时任务、Agent、技能详情与直接 CLI 复用同一 domain service;禁止按入口复制业务逻辑。
3. **五职责正交** — `placements` / `bind.tables` / `executionProfile` / `entrypoint` / domain service 各管一事(见 ACTIONS.md)。
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 流**。
@@ -66,7 +66,7 @@
|-------------------------|-------------------|--------|--------------|
| **新建任务**(对话 Agent) | `agent` | `placements` 含 `agent`;`run_skill_action` | 侧栏「新建任务」→ 对话触发 |
| **数据管理 · 顶栏** | `data-management` | `toolbar` + 必填 `bind.tables` | 侧栏「数据管理」→ 表顶栏按钮 |
| **数据管理 · 行内** | `data-management` | `row`(建议 `bind.tables` + `inputMapping`) | 同行操作链接(需主键) |
| **数据管理 · 行级** | `data-management` | `row`(建议 `bind.tables` + `inputMapping`) | 操作列和/或**行右键**(及可选「⋯」)可见并触发(需主键);技能配什么就显示什么 |
| **数据管理 · 批量** | `data-management` | `batch` | 勾选行后点批量按钮 |
| **定时任务** | `cron` | `placements` 含 `cron` | 侧栏「定时任务」→「技能直调」 |
| **任务中心** | (各入口触发的 Job) | `executionProfile: "async"` | 侧栏「任务中心」查看进度 / 取消;**不是** placement |

View File

@@ -74,37 +74,44 @@ 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)。
模板最小示例见 [`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)。
## 五职责正交(必读)
## 六职责正交(必读)
| 字段 / 层 | 只决定什么 | 不决定什么 |
|-----------|------------|------------|
| **`placements`** | Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail) | 不决定 sync/async,不决定点了是否弹窗 |
| **`placements`** | Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail;`row`=行级入口,壳由宿主呈现) | 不决定 sync/async,不决定点了是否弹窗,不决定行级用操作列还是右键,不决定谁能点 |
| **`replaces`** | **可选**:接管数据管理默认「新增 / 编辑 / 删除」 | 不强制;无配置时宿主仍直写 SQLite |
| **`bind.tables`** | 数据管理里出现在**哪些业务表** | 不决定业务实现 |
| **`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 来源 |
一个业务能力通常只定义**一个** Action,用 `placements` 同时挂到多个入口。不要拆成 `sync-records-toolbar` / `sync-records-cron` 等副本,除非语义确实不同;即便拆分,也必须调用同一个领域业务内核。
一个业务能力通常只定义**一个** Action,用 `placements` 同时挂到多个入口。不要拆成 `sync-records-toolbar` / `sync-records-cron` 等副本,除非语义确实不同;即便拆分,也必须调用同一个领域业务内核。需要按用户/角色藏按钮或滤行时,**不要**按角色复制 Action,应启用顶层 `policy` 并由技能算出 Snapshot(见 [`POLICY.md`](POLICY.md))。
## Phase 1 当前稳定支持(新技能应只使用这些)
| 层级 | 稳定字段 / 能力 |
|------|----------------|
| 顶层 | `schemaVersion`、`skill`、`actions` |
| action | `id`、`label`、`description`、`placements`、`entrypoint`、**`executionProfile`(必填)**;可选 `inputSchema`、`confirmation`、**`interaction`**、**`interactionByPlacement`**、**`bind`**、**`replaces`** |
| 顶层 | `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`(行内/批量预填,见下节) |
| inputSchema | 根 `type: object`;properties 使用 `string` / `number` / `boolean`;支持 `enum`、`default`、`required`、`sensitive`、`readOnly` |
| **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`;可选 `display_type` / `format`(`textarea` / `date` / `datetime_unix_seconds` / `url` / `email`) |
| confirmation | `{ "message": "..." }` — **风险文案**(confirm/form 弹窗黄条;agent 元数据);**不是**「要不要弹窗」的开关 |
| **executionProfile** | `"sync"` \| `"async"`(**必须显式写出**,禁止依赖缺省) |
| **interaction** | `"auto"` \| `"direct"` \| `"confirm"` \| `"form"`(可选;省略≈`auto`) |
| **interactionByPlacement** | 可选;按 placement 覆盖为 `direct`/`confirm`/`form`(不可写 `auto`) |
| **hostEffects** | 可选;`onSuccess` / `onFailure` → `reloadTable` \| `refreshCatalog` \| `clearSelection`(匠厂宿主 ≥2.8.5) |
| **steps** | 可选;有序 `{ when, do, table?, fk? }`,`when`=`before`\|`afterSuccess`\|`afterFailure`;新技能优先(≥2.8.5) |
## `bind.tables`(表级绑定)
@@ -246,6 +253,84 @@ Action 是**可选能力**:
- **cron 触发 / agent**:忽略 `interaction`(无人值守 / 模型填参);`confirmation` 对 agent 仍作 `requiresConfirmation` 元数据。
- 不要用 `interaction: "direct"` 掩盖需要用户填写的可编辑字段——宿主在 `auto` 下会因可编辑参数回落到 `form`;显式 `direct` + 可编辑字段时参数取 default/预填,作者须自觉。
## `hostEffects` / `steps`(执行前后宿主编排,≥2.8.5)
与 `placements` / `executionProfile` / `interaction` **正交**。决定的是:数据管理里点完 Action 之后(或之前),宿主要不要**刷新当前表、刷新目录、清空多选、级联删子表**等。
- **省略 / 空** ⇒ 宿主**不**额外改 UI(≥2.8.5 起不再对 mutation 硬编码刷新)。本地库若已由技能写新,界面仍可能旧,直到用户点内置「刷新」或本 Action 声明了 `reloadTable`。
- **仅数据管理交互路径生效**(toolbar / row / batch / skill-detail / `replaces`);**cron 与 agent 忽略**(与 `interaction` 相同)。
- 需匠厂宿主 **≥ 2.8.5**;更旧宿主会忽略未知字段(若未做严格 Schema 校验)。
- **新技能优先写 `steps`**;`hostEffects` 仍可作为「只刷 UI」的简写。两者可并存;同一 UI 效果若两边都声明,宿主可能各跑一次。
### `hostEffects`(UI 副作用简写)
```json
{
"hostEffects": {
"onSuccess": ["reloadTable"],
"onFailure": ["clearSelection"]
}
}
```
| 字段 | 触发时机 |
|------|----------|
| `onSuccess` | **sync** 退出码 0;或 **async** 任务已被任务中心**接收**(不是 Job 跑完) |
| `onFailure` | **sync** 失败;或 **async** **启动被拒** |
闭集 effect id:`reloadTable`(重载当前表 + 常伴随 Data Policy 再拉)、`refreshCatalog`(刷新技能目录)、`clearSelection`(清空多选)。未知 id 丢弃。
**async 注意**:`sync-data` 一类长同步若写 `onSuccess: ["reloadTable"]`,会在**入队成功时**就刷表,可能仍是旧数据;更适合 sync 的新建/登记,或等 Job 真正完成后再刷(若宿主后续扩展完成回调)。
### `steps`(有序编排,推荐)
```json
{
"steps": [
{ "when": "before", "do": "deleteByFk", "table": "child_rows", "fk": "parent_id" },
{ "when": "afterSuccess", "do": "reloadTable" },
{ "when": "afterFailure", "do": "clearSelection" }
]
}
```
| `when` | 含义 |
|--------|------|
| `before` | 调用 CLI **之前** |
| `afterSuccess` | 同 `hostEffects.onSuccess` |
| `afterFailure` | 同 `hostEffects.onFailure` |
| `do` | 说明 |
|------|------|
| `reloadTable` / `refreshCatalog` / `clearSelection` | 同 hostEffects |
| `deleteByFk` | 按外键批量删子表;须 `table` + `fk`;行按钮会把当前行主键当父键 |
| `deleteRows` | 删指定表行;须 `table` |
规则:单次最多 **8** 步;未知步骤丢弃;`deleteByFk` / `deleteRows` 仅走 Skill Action steps 路径时可删 `metadata_readonly` 投影表(人工 CRUD 仍守只读)。整库只读 / 视图仍禁止写。
典型:业务表「新建 / 登记 / 更新」成功后加 `{ "when": "afterSuccess", "do": "reloadTable" }`,避免用户傻等旧表格。
```json
{
"id": "register-one",
"label": "登记本行",
"description": "更新当前行业务字段。",
"placements": ["row"],
"executionProfile": "sync",
"interaction": "form",
"bind": {
"tables": ["business_records"],
"inputMapping": { "recordId": "$row.$pk" }
},
"steps": [{ "when": "afterSuccess", "do": "reloadTable" }],
"entrypoint": {
"type": "cli",
"command": "register",
"args": ["--id", "{{recordId}}"]
}
}
```
默认模板 `actions.json` 仅含 `health` / `version` / `config-path`,不放在 toolbar/row/batch,故无需 `bind` 示例污染默认 manifest。业务示例:
```json
@@ -352,12 +437,20 @@ HINT: <next step>
| 值 | 含义 |
|----|------|
| `toolbar` | 数据管理表顶栏技能按钮(须配合 `bind.tables`) |
| `row` | 数据管理行内操作(建议 `bind.tables` + `inputMapping`;表需有主键) |
| `row` | 数据管理**行级入口**(建议 `bind.tables` + `inputMapping`;表需有主键)。技能声明后,由宿主呈现为操作列链接、**行右键菜单**和/或行内「⋯」等壳;**不**新增 `row-context-menu` 等 placement |
| `batch` | 数据管理批量操作(需勾选行;无勾选时宿主禁用按钮) |
| `skill-detail` | 技能详情页直调(与安装/四 Tab 同属技能市场详情;已声明则须自测) |
| `agent` | Agent 可直接调用(`run_skill_action`) |
| `cron` | 定时任务「技能直调」 |
### 行级入口 `row`(宿主呈现契约)
- **技能决定有什么**:`placements` 含 `"row"`(且通过 `bind.tables` / policy 等过滤)的 Action,构成该表行的技能菜单项集合;配什么就出现什么。
- **宿主决定怎么摆**:可用操作列、行右键、行内「⋯」等呈现**同一批** Action;点击均走同一 `launch`(`placement: "row"`),`interaction` / `executionProfile` 与壳无关。
- **顺序**:各壳内技能项顺序 = 下方「数据管理技能按钮顺序」契约(`actions` 数组相对顺序)。
- **宿主内置项**:查看 / 编辑 / 删除由宿主提供,**不是**技能 `actions.json` 配置出来的;若出现在行右键,须与技能项分组(例如分隔线后),且不得要求技能再配一份菜单 DSL。
- **禁止**:为右键单独发明 placement、菜单注册表或第二套 Action 列表。
### 数据管理技能按钮顺序(宿主正式契约)
宿主**没有**单独的 `sortOrder` / `uiOrder` 字段。同一 `placement`(`toolbar` / `row` / `batch`)下,技能按钮的展示顺序 = `actions` 数组中**匹配该项 placement(且通过 `bind.tables` 过滤)的相对先后**。
@@ -369,8 +462,8 @@ HINT: <next step>
3. 同一 Action 可同时挂多个 placement;它在各入口的位置,分别是该入口匹配子集里的相对位置。
4. **宿主内置按钮不参与本契约、位置固定**:
- 表顶栏右侧:删除(有勾选时)/ 刷新 / 新增 / 导入 / 导出 —— 技能 toolbar/batch 按钮在左侧,不能与右侧内置按钮混排。
- 行内「操作」列:技能 `row` 按钮在前;其后固定为宿主「查看」→(非只读)「编辑」→「删除」。
5. **表顶栏全部直接展示,无「更多操作」溢出菜单**:匹配到的 toolbar/batch 技能按钮一律渲染(顺序仍按本契约)。
- 行级:技能 `row` 项按本顺序出现在操作列 / 行右键(及可选「⋯」)中;宿主「查看」→(非只读)「编辑」→「删除」为内置项,位置由宿主固定(操作列常在技能项之后;右键中可与技能项分组)。
5. **表顶栏全部直接展示,无「更多操作」溢出菜单**:匹配到的 toolbar/batch 技能按钮一律渲染(顺序仍按本契约)。**行级**不以「顶栏溢出菜单」模型截断;行右键是 `row` 的正式呈现壳之一,不是 toolbar 式「更多操作」截断。
6. **表顶栏宽窄布局(宿主行为,技能无需配置)**:
- 够宽:左右各横排一行(左技能按钮 + 右宿主工具)。
- 不够宽:保持左右分栏,栏内按钮上下竖排(左栏技能、右栏刷新/导出等)。
@@ -378,8 +471,9 @@ HINT: <next step>
禁止:
- 臆造 `sortOrder` / `order` / `priority` 等字段指望宿主排序(模板 Schema `additionalProperties: false` 会拒收)。
- 依赖或宣传「更多操作」溢出菜单(宿主已取消该能力)。
- 依赖或宣传表顶栏「更多操作」溢出菜单(宿主已取消该能力)。
- 把「表格行数据默认排序」与按钮顺序混淆——行排序见 [`SCHEMA.md`](SCHEMA.md) 的 `default_sort_json`。
- 新增 `row-context-menu` 等 placement,或要求技能再维护一份与 `row` 平行的菜单清单。
### Action 类型 × placements(常见示例,不是限制)
@@ -409,12 +503,53 @@ HINT: <next step>
## inputSchema 与参数表单
当前宿主稳定支持:
### 表单字段类型对照(须与 CRUD 对称)
- `object` / `string` / `number` / `boolean`
Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件语义**。CRUD 侧配置见 [`SCHEMA.md`](SCHEMA.md)「表单与展示:稳定字段类型」;本表只写 Action 怎么配。
| 控件语义 | Action `inputSchema` 怎么写 | CRUD 怎么写(对照) |
|----------|----------------------------|---------------------|
| 单行文本 | `type: string` | `display_type: text` 或缺省 |
| 多行文本 | `display_type` 或 `format`: `textarea` | `display_type: textarea` |
| JSON 长文本 | `type: object`/`array`(以 JSON 字符串编辑)或 `textarea` | `display_type: json` / `textarea` |
| 数字 | `type: number` | `display_type: number` 或 INT/REAL/NUMERIC |
| 开关 | `type: boolean` | `display_type: boolean` |
| 枚举下拉 | `enum: [...]` | `options_json` 数组 |
| 关联下拉 | 绑列 lookup(`column` / `columnMapping`) | `display_type: lookup` |
| 密码 | `sensitive: true` | 列 `sensitive` |
| 日期(日) | `display_type` 或 `format`: `date` | `display_type: date` → 存 `YYYY-MM-DD` |
| 日期时间 | `display_type` 或 `format`: `datetime_unix_seconds` | 同左;**存/传 Unix 秒** |
| URL(薄层) | `display_type` 或 `format`: `url` | `display_type: url` |
| Email(薄层) | `display_type` 或 `format`: `email` | `display_type: email` |
| 说明灰字 | `description` | `_jiangchang_columns.description` |
| 默认值 | `default` | SQLite 列 `DEFAULT` |
| 只读 | `readOnly: true` | 列 `editable: 0` / 宿主只读规则 |
**本版不做(勿宣传):** 文件上传、多选/标签数组、复杂 `oneOf` / nested object 专用表单。
日期时间约定:
- Action 提交与 CRUD 写库统一用 Unix **秒**整数(与 SCHEMA 标准时间字段一致)
- `date` 只表示日历日,值为 `YYYY-MM-DD`
- 脏/无法归一的旧预设:日期控件可回退为普通文本,避免卡死表单
### 当前宿主稳定支持(摘要)
- 根:`object`;属性:`string` / `number` / `boolean`(`object`/`array` 仅作 JSON 多行编辑兼容,新技能慎用)
- `enum` / `default` / `required` / `sensitive` / `readOnly`
- `display_type` / `format`:`textarea` | `date` | `datetime_unix_seconds` | `url` | `email`
- 属性级 `description`(表单标签下方灰字说明)
- 简单模板替换(`{{fieldName}}`)
**与数据管理「新增 / 编辑」表单的分工(勿混用配置入口):**
| 表单 | 短标题 | 说明灰字 | 默认值 | 控件类型 |
|------|--------|----------|--------|----------|
| Skill Action 弹窗 | `inputSchema.title` | `inputSchema.description` | `inputSchema.default` | `type` + `display_type`/`format` |
| 数据管理新增/编辑 | `_jiangchang_columns.display_name` | `_jiangchang_columns.description` | 物理列 SQLite `DEFAULT` | `display_type` + 物理类型推断 |
CRUD 侧宿主义务与禁止事项见 [`SCHEMA.md`](SCHEMA.md)「表单与展示:稳定字段类型」「新增/编辑表单:字段说明与默认值」。同一业务含义两边文案与控件应对齐,但分别配置。
示例:
```json
@@ -425,12 +560,31 @@ HINT: <next step>
"properties": {
"query": {
"type": "string",
"title": "查询条件"
"title": "查询条件",
"description": "支持关键词或完整业务编号"
},
"limit": {
"type": "number",
"title": "返回数量",
"description": "最多返回多少条;填 0 表示不限制(若业务支持)",
"default": 50
},
"dueDate": {
"type": "string",
"title": "截止日期",
"display_type": "date",
"description": "日历日,格式 YYYY-MM-DD"
},
"publishAt": {
"type": "number",
"title": "发布时间",
"display_type": "datetime_unix_seconds",
"description": "提交值为 Unix 秒"
},
"homepage": {
"type": "string",
"title": "主页链接",
"display_type": "url"
}
}
}
@@ -455,6 +609,9 @@ HINT: <next step>
- `boolean` 在宿主中渲染为开关
- `enum` 在宿主中渲染为选择框
- `type: number` 渲染为数字输入
- `display_type`/`format: date` 渲染为日期(日);`datetime_unix_seconds` 渲染为日期时间并按 **Unix 秒**提交
- `display_type`/`format: url` / `email` 为薄层输入类型(校验从宽,勿当强校验网关)
- `required` 字段在提交前校验
- `sensitive` 字段不得出现在低信任入口
- `readOnly: true` 时数据管理表单只读展示该字段(仍随提交传入);适合行内/勾选预填的主键、编号列表
@@ -462,8 +619,7 @@ HINT: <next step>
- 需要确认的副作用操作使用 `confirmation.message`
- 模板参数使用 `{{fieldName}}`,且必须在 `inputSchema.properties` 中声明
- `entrypoint.args` 项当前只允许 `string`、`number`、`boolean`;不允许对象或 `null`
- **不要**在当前模板中宣传 `nested object`、`oneOf`、复杂数组等尚未稳定支持的能力
- **不要**在当前模板中宣传文件上传、多选数组、`oneOf`、复杂 nested object 等尚未稳定支持的能力
## 风险与入口配置
- 查询、诊断、版本检查可以放到 `skill-detail` 或 `agent`

View File

@@ -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)。

View File

@@ -45,6 +45,7 @@ python {baseDir}/scripts/main.py init-db
- **`config-path`**:输出 JSON,包含 `skill`、`env_path`、`example_path`。
- **`version`**:输出 JSON(`version`、`skill`)。
- **`init-db`**:幂等创建/迁移本地 SQLite(含 `_jiangchang_*` 展示元数据)。成功输出一行 JSON(`ok` / `skill` / `db_path`),退出码 `0`。宿主可在技能安装或更新成功后静默调用;不要并入 `health`。
- **`policy`(可选)**:仅当 `actions.json` 顶层启用 Data Policy 时需要;向 stdout 输出 Snapshot JSON(见 `references/POLICY.md`)。模板默认不实现;多数技能保持 L0 即可。
- **`run` / RPA / 长任务**:CLI 仍是同一内核;**Agent 禁止**用 bash 干等长任务结束。须走宿主 Skill Action(`run_skill_action`),且该 Action 一般为 `executionProfile: "async"` → **任务中心**。长时间无 stdout **不代表卡死**;进度看任务中心 / Run Journal。
- **排查顺序**(由快到慢):
1. `python {baseDir}/scripts/main.py logs`

382
references/POLICY.md Normal file
View File

@@ -0,0 +1,382 @@
# Skill Data Policy 契约(v1.0)
> **状态:正式契约 v1.0(`schemaVersion` 1)。**
> **目标:** 冻结「启用方式 + Snapshot 最小字段 + L2 scope 白名单」,使多数技能(L0)零配置,需要权限的技能(L1/L2)有稳定扩展面。
> **非目标:** 宿主实现业务用户/角色库;规定飞书/HTTP/本地表等权限数据源;本期不强制 L3(`visibleWhen` 等)。
> **配套 Schema:** [`../assets/schemas/skill-policy-snapshot.schema.json`](../assets/schemas/skill-policy-snapshot.schema.json);启用块见 [`../assets/schemas/skill-actions.schema.json`](../assets/schemas/skill-actions.schema.json)。
相关正交轴见 [`ACTIONS.md`](ACTIONS.md)。本契约增加第六轴:**policy(当前身份允许什么)**。
---
## 0. 能力档位(必须按此理解)
| 档位 | 技能配置 | 宿主行为 |
|------|----------|----------|
| **L0** | 无 `policy`,或 `policy.enabled !== true` | **与今日完全一致**:不拉 Snapshot,不按身份过滤按钮/行/列 |
| **L1** | `policy.enabled: true`,Snapshot 至少给出 Action 授权 | 数据管理展示 skill Action 前与 allowlist 求交;可选限制宿主内置能力 |
| **L2** | 在 L1 基础上 Snapshot 带 `data.tables[table]` | 列表查询应用声明式 `rows.scope`;列按 `columns` 投影 |
| **L3** | (预留,本契约不冻结行为) | 如行态 `visibleWhen`、菜单级藏表、`rows.mode=delegate` 等 |
**80% 技能停留 L0。** 启用 policy 的技能必须实现约定 CLI,并在写路径自行再鉴权(见 §7)。
---
## 0.1 已冻结最佳实践(v1.0)
| 议题 | 决定 | 技能开发者怎么做 |
|------|------|------------------|
| Skill Action 授权模式 | **仅** `allowlist` / `open`;v1 **禁止** `denylist` | 有角色就算出允许的 action id 列表;只控行不控按钮时用 `open` |
| `hostCapabilities` | **省略 = 不限制**宿主内置;写出则按 allowlist(`[]` = 全禁内置) | 只控业务按钮时**不要写**该字段;要限导出/删除时再显式列出 |
| scope 运算符 | 保留小白名单(见 §3.4);**禁止** like/raw/嵌套 | 日常优先 `eq` / `in` / `is_null`;比较符按需 |
| `valueFrom` | v1 **仅** `actor.userId` | 角色只在技能内用来算 Snapshot,**不要**用 `actor.roles` 做行过滤 |
---
## 1. 启用字段(冻在 `assets/actions.json` 顶层)
`actions.json` 根对象增加**可选**字段 `policy`(与 `schemaVersion` / `skill` / `actions` 同级)。
### 1.1 形状(L0/L1 必冻)
```json
{
"schemaVersion": 1,
"skill": "your-skill-slug",
"policy": {
"enabled": true,
"entrypoint": {
"type": "cli",
"command": "policy",
"args": ["--table", "{{table}}"]
},
"appliesTo": ["data-management"]
},
"actions": []
}
```
| 字段 | 必填 | 冻结说明 |
|------|------|----------|
| `policy` | 否 | 缺省 = L0 |
| `policy.enabled` | 若存在 `policy` 则**必填** | 仅 `true` 启用;`false` 等同 L0 |
| `policy.entrypoint` | `enabled: true` 时必填 | 与 Action `entrypoint` 同构:`type` 目前仅 `"cli"` |
| `policy.entrypoint.command` | 是 | 子命令名;**推荐**固定为 `policy` |
| `policy.entrypoint.args` | 是 | JSON 数组;可含占位符,由宿主替换后再拼接 CLI |
| `policy.appliesTo` | 否 | 缺省 `["data-management"]`。本期宿主**只实现** `data-management`;其它值忽略但不报错 |
### 1.2 占位符白名单(冻)
| 占位符 | 含义 | 例 |
|--------|------|----|
| `{{table}}` | 当前数据管理选中的**物理表名** | `customs_timelines` |
| `{{skill}}` | 当前技能 slug | 与 manifest `skill` 一致 |
| `{{userId}}` | 当前宿主用户 id | 与注入的 `JIANGCHANG_USER_ID` 一致 |
未列出的 `{{…}}`:**禁止**;宿主应视为契约错误(fail-closed,见 §6)。
### 1.3 显式不放进启用块的内容(防推翻)
以下**不得**出现在 `actions.json.policy` 中(属于技能内部或 Snapshot):
- 角色列表、人员表、飞书 app id、外部 IAM URL
- 具体 allowlist / scope(那是 Snapshot 的事)
- 按角色复制的 Action 定义
### 1.4 Action 级可选提示(不冻为运行时权威)
允许(文档/静态检查用),**宿主不得仅凭此显隐按钮**:
```json
"authz": { "capability": "optional.skill.defined.string" }
```
运行时权威仅为 Snapshot §2 的 `actions`。
---
## 2. Snapshot 最小字段集(`policy` CLI 标准输出)
- 编码:UTF-8 JSON 对象,写在 **stdout**(可先打日志到 stderr/文件,但 stdout 必须是单 JSON)。
- 成功退出码 `0`;业务上「无权限看任何东西」仍是 `0` + 合法 Snapshot(例如空 allowlist),不是靠非 0 表示未授权。
- 非 0 或非合法 JSON → §6 fail-closed。
### 2.1 根对象(L0 不出现;L1 必冻)
| 字段 | 必填 | 类型 | 说明 |
|------|------|------|------|
| `schemaVersion` | 是 | `integer` | 本期冻结为 **`1`** |
| `skill` | 是 | `string` | 必须等于 manifest `skill` |
| `actor` | 是 | `object` | 见 §2.2 |
| `actions` | 是 | `object` | 见 §2.3 |
| `hostCapabilities` | 否 | `string[]` | 见 §2.4;**省略 = 不限制宿主内置按钮**(兼容) |
| `data` | 否 | `object` | 见 §3;省略 = 不限制行/列(L1-only) |
| `extensions` | 否 | `object` | 技能私有扩展;宿主**必须忽略**未知键 |
根级未知字段:宿主**忽略**;**不得**依赖未写入本契约的字段做安全决策。
### 2.2 `actor`(L1 必冻)
| 字段 | 必填 | 说明 |
|------|------|------|
| `userId` | 是 | 非空;应与 `JIANGCHANG_USER_ID` / `{{userId}}` 对齐 |
| `displayName` | 否 | 展示用 |
| `roles` | 否 | `string[]`,角色**码**;仅供展示 / 技能自检。**不得**用于 v1 scope 的 `valueFrom` |
| `attributes` | 否 | `object`,仅允许 JSON 标量值;本期 scope **不**解引用 |
### 2.3 `actions`(L1 必冻)— Skill Action 授权
**最佳实践:** 角色 → 技能内计算 → `allowlist` 的 id 列表。宿主不理解角色名。
```json
"actions": {
"mode": "allowlist",
"ids": ["whoami", "create-record", "register-submit"]
}
```
| 字段 | 必填 | 冻结取值 |
|------|------|----------|
| `mode` | 是 | **仅两值**:`"allowlist"` \| `"open"` |
| `ids` | `mode=allowlist` 时必填 | `string[]`;元素为 **`actions.json` 里的 action.id** |
| `ids` | `mode=open` 时 | 必须省略或 `[]`;宿主视为**不限制** skill Action |
规则:
1. 宿主先按现有规则得到候选集:`placements` ∩ `bind.tables`(及 batch 等)。
2. 若 `mode=allowlist`:候选 ∩ `ids`。漏配 id = 少按钮(安全侧失败)。
3. `ids` 中的未知 id:**忽略**(不报错)。
4. `mode=open`:等价「L1 不限制按钮」,可与 L2 行列限制并存。
5. **`denylist` 不是 v1 合法值**;出现则 §6 fail-closed。
`replaces` 接管的默认新增/编辑/删除:视为对应 Action 的 id 是否在 allowlist 中;不在则宿主隐藏该接管按钮。`mode=open` 时不按 id 限制 replaces。
### 2.4 `hostCapabilities`(L1 可选冻)
控制数据管理**宿主内置**能力,与 skill `action.id` **命名空间分离**。
本期枚举冻结:`refresh` | `export` | `view` | `edit` | `delete` | `import`。
| 写法 | 含义 | 何时用 |
|------|------|--------|
| **省略字段** | **不限制**(与今日一致) | **默认推荐**:只控 skill 按钮 / 行数据时 |
| 写出非空数组 | 仅允许列出的内置能力(allowlist) | 要限导出、删除等时 |
| 写出 `[]` | 禁止全部上表内置能力 | 极少见;慎用 |
未知字符串:忽略。**禁止**把「省略」解释成全禁。
---
## 3. L2:`data` 与 scope 语法白名单
### 3.1 `data` 形状
```json
"data": {
"tables": {
"customs_timelines": {
"accessible": true,
"columns": {
"visible": ["id", "contract_no", "status"],
"writable": ["status"]
},
"rows": {
"mode": "scope",
"scope": {
"all": [
{ "field": "owner_user_id", "op": "eq", "valueFrom": "actor.userId" }
]
}
}
}
}
}
```
| 字段 | 必填 | 说明 |
|------|------|------|
| `data.tables` | 若存在 `data` 则建议有 | `key` = 物理表名 |
| `accessible` | 否 | 缺省 `true`。`false`:宿主可拒绝打开该表 |
| `columns.visible` / `writable` | 否 | 物理列名;省略 = 不额外限制 |
| `rows` | 否 | 见下 |
`writable` 应 ⊆ `visible`;若违规,宿主按交集降级,不 fail-closed。
### 3.2 `rows.mode`(冻)
| 值 | 本期 | 含义 |
|----|------|------|
| `scope` | **实现目标** | 宿主将 `scope` 编译为安全查询约束 |
| `open` | 允许 | 显式不限制行 |
| `delegate` | **仅占位** | 本期未实现时 fail-closed / 提示;**禁止**当 `open` |
缺省:无 `rows` = `open`。
### 3.3 `scope` 布尔结构(冻)
```text
Scope =
| { "all": Predicate[] } // AND,长度 ≥ 1
| { "any": Predicate[] } // OR,长度 ≥ 1
| Predicate // 单个谓词的语法糖
```
**禁止**:嵌套超过一层。
### 3.4 `Predicate` 白名单(冻)
**运算符(宿主必须实现):** `eq` | `ne` | `in` | `not_in` | `gt` | `gte` | `lt` | `lte` | `is_null` | `is_not_null`
**推荐日常只用:** `eq` / `in` / `is_null`(及 `is_not_null`)。
**明确排除:** `like` / `contains` / `sql` / `raw` / `expr`。
**`valueFrom` v1 仅:** `actor.userId`。
`actor.roles` / `actor.attributes.*` **非法** → fail-closed。
`value` 与 `valueFrom` 互斥。标量:`string` | `number` | `boolean` | `null`(与 null 比较优先用 `is_null`)。
### 3.5 与宿主原有过滤的关系
最终行集 = **原有表查询**(搜索、排序、分页、用户 Filter) **AND** policy `scope`。宿主必须把 scope 下推到查询层。
---
## 4. 宿主消费算法
```text
IF manifest.policy?.enabled !== true:
→ L0;END
调用 entrypoint(替换占位符),解析 Snapshot
IF 失败 / 非法 / skill 不匹配 / mode 非法:
→ fail-closed(§6);END
候选 = filter(placements, bind.tables, …)
IF mode == allowlist: 候选 ∩ ids
IF mode == open: 候选不变
渲染 skill 按钮 ← 候选
渲染宿主内置 ← hostCapabilities(省略则不限制)
应用 data.tables[T](若有)
```
拉取时机(建议):进入表、切换表、刷新。短缓存不得跨 `userId`。
---
## 5. 身份与环境
| 项 | 约定 |
|----|------|
| 用户主键 | `JIANGCHANG_USER_ID` |
| 技能解析 | 飞书 / 本地表 / HTTP IAM 等,**契约不规定** |
---
## 6. Fail-closed(启用后必冻)
当且仅当 `policy.enabled === true`:
| 情况 | 宿主行为 |
|------|----------|
| CLI 非 0 / 超时 / 无 JSON | 不渲染该表 skill 写操作与(建议)导出;明确错误;**禁止**回退 L0 全开 |
| `schemaVersion` ≠ 1 / `skill` 不匹配 / `actions.mode` 非法 | 同上 |
| `rows.mode=scope` 但 predicate 非法 | 不返回行或整表不可用;**禁止**忽略 scope |
| `rows.mode=delegate`(未实现) | 提示不支持;**禁止**当 `open` |
---
## 7. 安全边界
1. Snapshot **只**服务宿主 UI 与查询约束。
2. 写 Action / 敏感读的 **domain service 必须再次鉴权**。
3. 禁止「只藏按钮」宣称为完整权限方案。
4. Cron / Agent **不**自动套用本 Snapshot。
---
## 8. 最小示例
模板仓库另有可复制示例:
- [`../assets/examples/skill-policy-snapshot.allowlist.example.json`](../assets/examples/skill-policy-snapshot.allowlist.example.json)
- [`../assets/examples/skill-policy-snapshot.scope.example.json`](../assets/examples/skill-policy-snapshot.scope.example.json)
### 8.1 L0
不写 `policy` 字段。
### 8.2 L1 allowlist(不碰宿主内置)
```json
{
"schemaVersion": 1,
"skill": "your-skill-slug",
"actor": { "userId": "u1", "displayName": "张三", "roles": ["clerk"] },
"actions": {
"mode": "allowlist",
"ids": ["whoami", "create-record", "register-submit"]
}
}
```
### 8.3 L1 + 限制导出
```json
{
"schemaVersion": 1,
"skill": "your-skill-slug",
"actor": { "userId": "u1", "roles": ["clerk"] },
"actions": { "mode": "allowlist", "ids": ["whoami"] },
"hostCapabilities": ["refresh", "view"]
}
```
### 8.4 L1 open + L2 scope
```json
{
"schemaVersion": 1,
"skill": "your-skill-slug",
"actor": { "userId": "u1" },
"actions": { "mode": "open" },
"data": {
"tables": {
"orders": {
"rows": {
"mode": "scope",
"scope": {
"all": [
{ "field": "owner_user_id", "op": "eq", "valueFrom": "actor.userId" }
]
}
}
}
}
}
}
```
---
## 9. 刻意延后
| 项 | 备注 |
|----|------|
| `actions.mode: denylist` | 另开 `schemaVersion` |
| `valueFrom: actor.roles` | **明确不做** |
| `visibleWhen` / 嵌套 scope / `like` | L3 或永不做 |
| `rows.mode: delegate` 宿主实现 | 字段名已占坑 |
| 平台全局 Role UI / cron·agent 套用 Snapshot | 另议 |
---
## 10. 冻结检查清单
- [x] L0 缺省无歧义
- [x] 启用字段仅 `enabled` + `entrypoint` + 可选 `appliesTo`
- [x] `actions.mode` 仅 `allowlist` / `open`
- [x] `hostCapabilities` 省略 = 不限制
- [x] scope 小白名单;`valueFrom` 仅 `actor.userId`
- [x] fail-closed;写路径技能自检

View File

@@ -19,6 +19,8 @@
| [`CLI.md`](CLI.md) | 共享 Python、命令行入口、参数与短操作契约 |
| [`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`。
按需阅读:短操作优先 `CLI.md`;Action / 任务中心读 `ACTIONS.md`;结构化字段读 `SCHEMA.md`;权限 opt-in 读 `POLICY.md`;格子标色读 `CELL_PRESENTATION.md`。

View File

@@ -10,9 +10,11 @@
- 定义真实 SQLite 表结构
- 使用英文 `snake_case` 表名和字段名
- 使用 `_jiangchang_tables` 提供表级展示元数据
- 使用 `_jiangchang_columns` 提供字段级展示元数据
- 使用 `_jiangchang_columns` 提供字段级展示元数据(含可选 `description` 字段说明)
- 需要「新增表单预填」时,在物理列上声明 SQLite `DEFAULT`(勿把默认值只写在文案里)
- 定义哪些字段可见、可搜索、可创建、可编辑
- 定义字段展示类型和枚举选项
- (可选)按业务规则维护单元格呈现旁路数据(语义 tone;见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md))
- 保证元数据与真实数据库结构一致
- 保证元数据初始化幂等
@@ -22,11 +24,14 @@
- 读取 SQLite `PRAGMA table_info`
- 根据 schema 渲染数据管理页面
- 根据 `editable` 推导字段是否可创建或编辑
- 在**新增 / 编辑**表单中,若列有 `_jiangchang_columns.description`,在标签下方展示该说明(灰字;空则不显示)
- 打开**新增**表单时,对可创建字段用 SQLite 列 `DEFAULT`(`PRAGMA table_info.dflt_value`)预填;无 DEFAULT 则留空
- 执行通用查询、新增、编辑、删除、导入、导出
- 对字段名、表名、类型和权限进行最终校验
- 不猜测技能业务含义
- 不根据具体技能名称写特殊逻辑
- 可在技能安装/更新成功后静默调用 `python scripts/main.py init-db` 做幂等 ensure(内部即 `init_db()`);失败不影响安装结果
- 若技能启用了单元格呈现(见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)),读取旁路呈现数据并为单元格映射语义样式(`tone`);不猜测业务规则
## 初始化入口
@@ -179,17 +184,101 @@ upsert_table_metadata(
|------|------|------|
| `table_name` | 是 | 所属物理表 |
| `column_name` | 是 | 真实英文字段名 |
| `display_name` | 是 | 用户可见的中文字段名 |
| `description` | 否 | 字段说明 |
| `display_name` | 是 | 用户可见的中文字段名(表头与表单标签);保持简短 |
| `description` | 否 | 字段说明(给人看的短文案);**宿主在新增/编辑表单标签下方展示**(见下节);不要塞进 `display_name` |
| `display_order` | 否 | 兼容字段,默认保持 `NULL` |
| `visible` | 是 | 是否在数据管理表格中展示 |
| `searchable` | 是 | 是否参与搜索 |
| `editable` | 是 | `1` 业务字段允许宿主用于新增和编辑;`0` 系统/只读字段 |
| `display_type` | 否 | 字段渲染类型(如 `text`、`textarea`、`datetime_unix_seconds`) |
| `display_type` | 否 | 字段渲染类型;稳定取值见下节「表单与展示:稳定字段类型」 |
| `options_json` | 否 | 枚举选项 JSON |
这两张表是**数据字典**,不是业务数据;宿主不会把它们当作普通业务表展示。
#### 表单与展示:稳定字段类型(宿主支持,须与 Action 对称)
数据管理「新增 / 编辑」表单与 Skill Action 参数表单应使用**同一套控件语义**(配置入口不同,见 [`ACTIONS.md`](ACTIONS.md))。新技能只使用下表;宿主须两边都实现,避免「Action 有、CRUD 没有」。
**本版稳定支持(须齐):**
| `display_type`(CRUD) | 物理类型建议 | 列表展示 | 新增/编辑控件 | 存库约定 |
|------------------------|--------------|----------|---------------|----------|
| `text`(或缺省) | `TEXT` / 一般标量 | 原文 | 单行文本 | 原文 |
| `textarea` | `TEXT` | 原文(可截断) | 多行文本 | 原文 |
| `json` | `TEXT` | 原文 | 多行文本(JSON 字符串) | 原文;提交时宿主可做基础 JSON 合法性校验 |
| `number` | `INTEGER` / `REAL` / `NUMERIC` | 原文 | **数字输入**(`type=number`) | 数字;无 `display_type` 时,物理类型为 INT/REAL/NUMERIC 也应走数字控件 |
| `boolean` | `INTEGER` 0/1 或 BOOL | 开关语义展示 | 开关 | `0` / `1` |
| (枚举) | 任意 + `options_json` **数组** | 标签映射 | 下拉选择 | 选项 `value` |
| `lookup` | 外键式标量 + `options_json` **对象** | 标签映射 | 下拉选择(查关联表) | 关联键值 |
| (敏感) | 任意;列 `sensitive=1` | 掩码/隐藏策略由宿主定 | 新建可用密码框;编辑策略与既有宿主一致 | 原文(勿日志明文) |
| `date` | `TEXT` 推荐 `YYYY-MM-DD`,或可安全归一的值 | 日期 | **日期选择**(日) | `YYYY-MM-DD` |
| `datetime_unix_seconds` | `INTEGER` | `YYYY-MM-DD HH:mm:ss` | **日期时间选择**;提交写回 **Unix 秒** | Unix **秒**整数;禁止改成 ISO 字符串 |
| `url` | `TEXT` | 原文(可作链接) | 单行输入 `type=url`(薄层;校验从宽) | 原文 |
| `email` | `TEXT` | 原文 | 单行输入 `type=email`(薄层;校验从宽) | 原文 |
说明灰字与创建默认值见下一小节;与 Action 的对照写在 [`ACTIONS.md`](ACTIONS.md)「表单字段类型对照」。
**明确不在本版稳定范围(勿在新技能宣传为已支持):**
- 文件 / 图片上传
- 多选、标签数组、复杂 `oneOf` / nested object 表单
- 把业务可编辑时间存成毫秒、ISO 文本或 SQLite `datetime` 文本(审计字段规范见「标准时间字段」)
`datetime_unix_milliseconds`:仅兼容**列表展示**旧数据;**新技能不要用**;可编辑时间一律 `datetime_unix_seconds`。
#### 新增/编辑表单:字段说明与默认值(宿主支持)
数据管理「新增记录 / 编辑记录」与 Skill Action 弹窗是**两套表单**;CRUD 侧约定如下(交互对齐 Action 的 `inputSchema.description` / `default`,但**配置入口不同**)。
| 能力 | 技能写什么 | 宿主做什么 |
|------|------------|------------|
| 字段说明 | `_jiangchang_columns.description`(可选) | 新增/编辑表单中,在标签下方以灰字展示;空则不显示 |
| 创建默认值 | 物理列 SQLite `DEFAULT`(如 `limit_n INTEGER NOT NULL DEFAULT 0`) | 打开「新增」时,对可创建字段预填该默认值;无 DEFAULT 则留空 |
| 编辑已有行 | — | **不**用 DEFAULT 覆盖已有行数据 |
| 表格列头 | 只用 `display_name` | 长说明**不**进入列头 |
规则与禁止:
- **推荐**对易填错、语义特殊的可编辑字段写 `description`(示例链接、`0` 表示不限制等);非强制每个字段都有
- **禁止**把说明硬塞进 `display_name`(如 `评论上限(0不限)`);表头会变脏,也无法与 Action 说明对齐
- 业务语义(如「0 = 不限制」)写在 `description` 里;宿主只展示,**不**解读业务含义
- 第一期**不**新增元数据「表单默认值」列;CRUD 默认值 = DDL `DEFAULT`,与 Action 的 `inputSchema.default` 分开配置
- 同一业务字段若同时出现在 Action 与新增表单,**文案应对齐**(短标题 + 说明),但分别写在 `actions.json` 与 `_jiangchang_columns` / DDL
- 数字类可编辑列推荐 `display_type: "number"`(或依赖 INT/REAL/NUMERIC 推断)并在 DDL 写清 `DEFAULT`(如条数上限默认 `0`)
示例(可编辑列种子 + DDL 默认):
```sql
-- 物理列:创建默认 0
comment_limit INTEGER NOT NULL DEFAULT 0
```
```python
seed_column_metadata_batch(
cursor,
table_name="videos",
columns=(
{
"column_name": "video_url",
"display_name": "视频链接",
"editable": 1,
"display_type": "url",
"description": "填平台视频页链接;需包含可识别的视频路径片段。",
},
{
"column_name": "comment_limit",
"display_name": "采集评论条数",
"editable": 1,
"display_type": "number",
"description": "最多采多少条。填 0 = 不限制;填正整数则按该数量封顶。",
},
),
valid_column_names=physical,
)
```
Action 参数表单的 `title` / `description` / `default` / `display_type` 见 [`ACTIONS.md`](ACTIONS.md);勿与本节 CRUD 配置入口混用。
## editable 与 createable 的关系
技能模板只维护 `editable` 这一份字段权限声明:
@@ -294,10 +383,22 @@ readonly = 0
| 存储格式 | Unix 秒级整数 |
| 展示元数据 `display_type` | `datetime_unix_seconds` |
| 中文名 | `created_at` → **创建时间**;`updated_at` → **更新时间** |
| 用户编辑 | `editable = 0`(系统字段,宿主只展示格式化结果) |
| 用户编辑 | **审计字段** `editable = 0`(系统维护,列表只展示格式化结果,**不**进可编辑表单) |
| 字段顺序 | 位于业务字段末尾:`… → created_at → updated_at` |
| 排序 | **不使用** `display_order`;顺序仅来自 `PRAGMA table_info` |
### 业务可编辑时间 vs 审计时间
| 场景 | `editable` | 列表 | 新增/编辑表单 |
|------|------------|------|----------------|
| `created_at` / `updated_at` 等审计列 | `0` | 格式化为 `YYYY-MM-DD HH:mm:ss` | 不出现(或只读,由宿主既有规则) |
| 业务时间列(预约、截止、活动开始等) | `1` | 同上格式化 | **日期时间控件**;提交写入 Unix **秒**整数 |
宿主对 `display_type = datetime_unix_seconds`:
- **列表**:格式化为 `YYYY-MM-DD HH:mm:ss` 展示,**不**改库内原始整数
- **可编辑表单**:提供日期时间选择;保存时写回 Unix **秒**(不要写 ISO 字符串、不要写毫秒)
### 自动生成与维护
- **created_at**:`INTEGER NOT NULL DEFAULT (unixepoch())`;插入时可省略,由数据库生成。
@@ -310,8 +411,6 @@ readonly = 0
- 中文元数据种子:`scripts/db/display_metadata.py`
- 自检:`scripts/db/display_metadata_validator.py`
宿主只负责把 `display_type = datetime_unix_seconds` 格式化为 `YYYY-MM-DD HH:mm:ss` 展示,**不修改**数据库原始值。
### 模板默认:`task_logs`
`task_logs` 是技能内部**系统自动写入**的任务审计日志(由 `task_logs_repository` INSERT/UPDATE),不是供用户在宿主数据管理中手工维护的业务表。

View File

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

View File

@@ -255,6 +255,30 @@ class TestActionsManifest(unittest.TestCase):
{"create", "update", "delete"},
)
def test_schema_defines_host_effects_and_steps(self) -> None:
schema = _load_actions_schema()
action_def = schema["$defs"]["action"]
props = action_def["properties"]
self.assertIn("hostEffects", props)
self.assertEqual(props["hostEffects"].get("$ref"), "#/$defs/hostEffects")
self.assertIn("steps", props)
effects = schema["$defs"]["hostEffects"]
self.assertIs(effects.get("additionalProperties"), False)
for key in ("onSuccess", "onFailure"):
self.assertIn(key, effects.get("properties") or {})
effect_ids = set(schema["$defs"]["hostEffectId"].get("enum") or [])
self.assertEqual(effect_ids, {"reloadTable", "refreshCatalog", "clearSelection"})
step = schema["$defs"]["actionStep"]
self.assertEqual(
set(step["properties"]["when"].get("enum") or []),
{"before", "afterSuccess", "afterFailure"},
)
self.assertEqual(
set(step["properties"]["do"].get("enum") or []),
{"deleteByFk", "deleteRows", "reloadTable", "refreshCatalog", "clearSelection"},
)
self.assertEqual(props["steps"].get("maxItems"), 8)
def test_manifest_validates_against_json_schema(self) -> None:
try:
import jsonschema
@@ -514,6 +538,17 @@ class TestActionsManifest(unittest.TestCase):
"数组相对顺序",
"全部直接展示",
"左右分栏",
"行级入口",
"行右键",
"六职责正交",
"POLICY.md",
"hostEffects",
"steps",
"reloadTable",
"afterSuccess",
"afterFailure",
"cellPresentation",
"CELL_PRESENTATION.md",
):
self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}")
self.assertNotIn(
@@ -521,6 +556,17 @@ class TestActionsManifest(unittest.TestCase):
text,
msg="ACTIONS.md must not document removed overflow more-menu truncation",
)
self.assertIn(
"新增 `row-context-menu` 等 placement",
text,
msg="ACTIONS.md must forbid inventing a separate row-context-menu placement",
)
placement_enum = _load_actions_schema().get("$defs", {}).get("placement", {}).get("enum") or []
self.assertNotIn(
"row-context-menu",
placement_enum,
msg="skill-actions.schema must not add row-context-menu to placements enum",
)
for forbidden in (
"数据管理 toolbar " + "只挂",
"toolbar " + "只放 async",

View File

@@ -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()

View File

@@ -299,9 +299,21 @@ class TestDocsStandards(unittest.TestCase):
root = get_skill_root()
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"))
)
self.assertTrue(
os.path.isfile(
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")))
@@ -325,9 +337,28 @@ class TestDocsStandards(unittest.TestCase):
"options_json",
"demo_items",
"readonly",
"新增/编辑表单:字段说明与默认值(宿主支持)",
"表单与展示:稳定字段类型(宿主支持,须与 Action 对称)",
"datetime_unix_seconds",
"display_type: \"number\"",
"DEFAULT",
"description",
"文件 / 图片上传",
):
self.assertIn(marker, text, msg=f"SCHEMA.md missing {marker!r}")
def test_actions_md_documents_form_schema_split(self) -> None:
text = self._read("references/ACTIONS.md")
for marker in (
"inputSchema 与参数表单",
"表单字段类型对照(须与 CRUD 对称)",
"数据管理新增/编辑",
"_jiangchang_columns.description",
"datetime_unix_seconds",
"SCHEMA.md",
):
self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}")
def test_guarded_docs_do_not_reference_amazon_implementation(self) -> None:
forbidden = (
"download-settlement-report-amazon",