Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db2325dbe4 | |||
| 16ee121ab3 | |||
| 139d176841 | |||
| 0b6bf0b2fa | |||
| 51be1517c0 | |||
| 98f057e26b | |||
| 8878a90315 | |||
| 2ecb1172f4 | |||
| d2c8c3240c | |||
| 2c9f610a8e | |||
| 2c9bf56054 | |||
| 633ecff4e2 | |||
| 46bc573777 |
18
.env.example
18
.env.example
@@ -33,21 +33,19 @@ HUMAN_WAIT_TIMEOUT=180 # 单位:秒;来不及处理时可调
|
||||
|
||||
# 以下为可选目录配置。一般不用改;取消注释并填写后,才覆盖默认子文件夹。
|
||||
# 填写相对路径时,相对于本技能的数据目录(不是当前工作目录)。
|
||||
# 用户可见只有 input(上行)与 output(下行);见 development/DATA_PATHS.md。
|
||||
|
||||
# 下载目录:技能保存下载文件的位置
|
||||
# SKILL_DOWNLOAD_DIR=downloads # 相对本技能数据目录;留空或保持注释则用默认 downloads
|
||||
# 输入目录:用户/Agent 放入的待处理文件(清单、CSV、产品表等)
|
||||
# SKILL_INPUT_DIR=input # 相对本技能数据目录;留空或保持注释则用默认 input
|
||||
|
||||
# 导入目录:待导入文件放置的位置
|
||||
# SKILL_IMPORT_DIR=imports # 相对本技能数据目录;留空或保持注释则用默认 imports
|
||||
# 输出目录:技能/宿主交给用户拿走的结果(视频、报表、导出文件等)
|
||||
# SKILL_OUTPUT_DIR=output # 相对本技能数据目录;留空或保持注释则用默认 output
|
||||
|
||||
# 导出目录:技能导出文件的保存位置
|
||||
# SKILL_EXPORT_DIR=exports # 相对本技能数据目录;留空或保持注释则用默认 exports
|
||||
|
||||
# 上传目录:待上传文件放置的位置
|
||||
# 上传目录:待发布到外部平台的 staging(可选,不上侧栏)
|
||||
# SKILL_UPLOAD_DIR=uploads # 相对本技能数据目录;留空或保持注释则用默认 uploads
|
||||
|
||||
# 缓存目录:临时缓存文件的位置
|
||||
# 缓存目录:临时缓存文件的位置(不上侧栏)
|
||||
# SKILL_CACHE_DIR=cache # 相对本技能数据目录;留空或保持注释则用默认 cache
|
||||
|
||||
# 临时目录:运行中临时文件的位置
|
||||
# 临时目录:运行中临时文件的位置(不上侧栏)
|
||||
# SKILL_TEMP_DIR=temp # 相对本技能数据目录;留空或保持注释则用默认 temp
|
||||
|
||||
62
CHANGELOG.md
62
CHANGELOG.md
@@ -9,6 +9,68 @@
|
||||
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
|
||||
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
|
||||
|
||||
## 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`
|
||||
- 更新 `SKILL_ACTION_RUNTIME.md`
|
||||
|
||||
## 1.0.70
|
||||
|
||||
- 对齐宿主:数据管理表顶栏技能按钮全部直接展示,取消「更多操作」溢出菜单
|
||||
- 补充宽窄布局说明:够宽左右横排一行;不够宽左右分栏、栏内竖排
|
||||
- 更新 ACTIONS.md 与 skill-actions.schema.json
|
||||
|
||||
## 1.0.69
|
||||
|
||||
- 用户数据目录收敛为上行 `input/` 与下行 `output/`(取代 downloads / imports / exports 三分法)
|
||||
- `runtime_paths` 提供 `get_input_dir()` / `get_output_dir()`;相对输入路径解析到 `input/`
|
||||
- 更新 DATA_PATHS.md、CONFIG、`.env.example`;缓存 / 临时 / RPA 录屏目录仍为内部目录
|
||||
|
||||
## 1.0.68
|
||||
|
||||
- 明确数据管理技能按钮顺序契约:同一 placement(toolbar / row / batch)下按 `actions` 数组相对顺序展示;调整数组先后即可改序
|
||||
- 说明宿主内置刷新 / 导出 / 查看等不参与该顺序;无独立 `sortOrder` 字段
|
||||
- 更新 ACTIONS.md 与 skill-actions.schema.json
|
||||
|
||||
## 1.0.67
|
||||
|
||||
- 移除模板内嵌的 `tools/ui_inspector`;UI 录制器产品迁至独立仓库 `jiangchang-ui-recorder`
|
||||
- 相关开发文档改为指向 Gitea 产品仓库入口
|
||||
|
||||
## 1.0.66
|
||||
|
||||
- 数据管理支持表级默认排序:`_jiangchang_tables.default_sort_json`(多字段 JSON);宿主优先采用,未配置则仍按 updated_at → created_at → 主键
|
||||
- 更新 SCHEMA.md 与 `upsert_table_metadata`;旧库幂等补列
|
||||
|
||||
## 1.0.65
|
||||
|
||||
- `replaces` create/update 可复用表列 lookup:字段名对齐列名,或使用 `bind.columnMapping` / 字段 `column`
|
||||
|
||||
8
SKILL.md
8
SKILL.md
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: 技能开发模板(通用业务版)
|
||||
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
|
||||
version: 1.0.65
|
||||
version: 1.0.76
|
||||
author: 深圳匠厂科技有限公司
|
||||
metadata:
|
||||
openclaw:
|
||||
@@ -32,9 +32,11 @@ allowed-tools:
|
||||
### 架构铁律(先读)
|
||||
|
||||
- **单业务内核、多入口**:数据管理 / 定时任务 / Agent / 技能详情 / 直接 CLI 必须复用同一 domain service;禁止按入口复制业务逻辑,禁止按 `source` 分叉业务行为。
|
||||
- **`placements`**:只决定 Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail)。
|
||||
- **`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(必读)
|
||||
|
||||
|
||||
@@ -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`)。
|
||||
|
||||
29
assets/examples/skill-cell-presentation.example.json
Normal file
29
assets/examples/skill-cell-presentation.example.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
13
assets/examples/skill-policy-snapshot.allowlist.example.json
Normal file
13
assets/examples/skill-policy-snapshot.allowlist.example.json
Normal 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"]
|
||||
}
|
||||
}
|
||||
28
assets/examples/skill-policy-snapshot.scope.example.json
Normal file
28
assets/examples/skill-policy-snapshot.scope.example.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -19,14 +19,68 @@
|
||||
"actions": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/action" }
|
||||
"items": { "$ref": "#/$defs/action" },
|
||||
"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",
|
||||
@@ -144,6 +198,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"],
|
||||
@@ -166,7 +289,7 @@
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
"items": { "$ref": "#/$defs/placement" },
|
||||
"description": "决定 Action 出现在哪里;与 executionProfile 正交。仅 replaces 接管默认 CRUD 时可为空数组。"
|
||||
"description": "决定 Action 出现在哪里;与 executionProfile 正交。仅 replaces 接管默认 CRUD 时可为空数组。数据管理 toolbar/row/batch 的顺序由顶层 actions 数组相对顺序决定。表顶栏 toolbar/batch 全部直接展示(无溢出菜单);row 为行级入口(操作列/行右键/⋯),不在本字段配置排序、截断或菜单树。"
|
||||
},
|
||||
"replaces": { "$ref": "#/$defs/mutationKind" },
|
||||
"entrypoint": { "$ref": "#/$defs/entrypoint" },
|
||||
@@ -198,7 +321,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": {
|
||||
|
||||
78
assets/schemas/skill-cell-presentation.schema.json
Normal file
78
assets/schemas/skill-cell-presentation.schema.json
Normal 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);填完数据并重算后该项消失则提醒自然关闭"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
196
assets/schemas/skill-policy-snapshot.schema.json
Normal file
196
assets/schemas/skill-policy-snapshot.schema.json
Normal 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" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,15 +108,15 @@ HUMAN_WAIT_TIMEOUT=180 # 单位:秒;来不及处理时可调
|
||||
|
||||
四档运行模式的**实现与测试约定**见 [`ADAPTER.md`](ADAPTER.md);录屏管线见 [`RPA.md`](RPA.md)。这些内容**不要**写回 `.env.example` 注释。
|
||||
|
||||
**业务专属配置必须带技能前缀**(如 `DEMO_XXX`、`MY_SKILL_XXX`、`SKILL_DOWNLOAD_DIR`),**不要污染全局命名空间**(禁止 `SCRAPE_1688_*`、`RECEIVE_ORDER_*` 等跨技能前缀写入模板)。
|
||||
**业务专属配置必须带技能前缀**(如 `DEMO_XXX`、`MY_SKILL_XXX`、`SKILL_OUTPUT_DIR`),**不要污染全局命名空间**(禁止 `SCRAPE_1688_*`、`RECEIVE_ORDER_*` 等跨技能前缀写入模板)。
|
||||
|
||||
### 数据目录子路径(下载 / 导入 / 导出)
|
||||
### 数据目录子路径(输入 / 输出)
|
||||
|
||||
技能**自己写入**的文件默认在 `{JIANGCHANG_DATA_ROOT}/{USER_ID}/{slug}/` 下标准子目录(`downloads/`、`imports/`、`exports/` 等)。须通过 `util.runtime_paths.resolve_data_path()` 或 `get_*_dir()` 解析,**禁止** `os.path.abspath(config.get(...))` 相对 CWD。
|
||||
用户可见目录只有 **`input/`(上行)** 与 **`output/`(下行)**。技能写入的业务产出默认落在 `output/`;用户放入的待处理文件落在 `input/`。须通过 `util.runtime_paths.resolve_data_path()` 或 `get_input_dir()` / `get_output_dir()` 解析,**禁止** `os.path.abspath(config.get(...))` 相对 CWD。
|
||||
|
||||
- 权威说明(开发):[`DATA_PATHS.md`](DATA_PATHS.md)
|
||||
- `.env.example` 中路径类配置**留空或保持注释即用默认**;若写相对路径,必须相对 **skill 数据目录**,**禁止** `./outputs/...` 这类相对 workspace 的写法
|
||||
- 可选 env:`SKILL_DOWNLOAD_DIR`、`SKILL_IMPORT_DIR`、`SKILL_EXPORT_DIR` 等;若作为活跃 key 出现在配置管理中,同样须有用户向行头/行尾(见模板根 `.env.example`)
|
||||
- 可选 env:`SKILL_INPUT_DIR`、`SKILL_OUTPUT_DIR` 等;若作为活跃 key 出现在配置管理中,同样须有用户向行头/行尾(见模板根 `.env.example`)
|
||||
|
||||
### 录屏开关(`OPENCLAW_RECORD_VIDEO`)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 用户数据目录与文件路径黄金标准
|
||||
|
||||
> 本文是 skill **读写本地文件** 的权威约定。凡涉及下载、导入、导出、缓存、RPA 存证等路径,复制模板后须遵守本文;Agent 实现业务 skill 时也应先读本文。
|
||||
> 本文是 skill **读写本地文件** 的权威约定。凡涉及输入、输出、缓存、RPA 存证等路径,复制模板后须遵守本文;Agent 实现业务 skill 时也应先读本文。
|
||||
|
||||
---
|
||||
|
||||
@@ -30,11 +30,17 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
||||
|
||||
## 2. 标准子目录树
|
||||
|
||||
用户可见目录(数据管理侧栏只挂这两个)按 **上行 / 下行** 划分:
|
||||
|
||||
| 子目录 | 方向 | 用途 | 默认 helper |
|
||||
|--------|------|------|-------------|
|
||||
| `input/` | 上行 | 用户/Agent 放入的待处理材料(urls.txt、batch.json、CSV、产品表…) | `get_input_dir()` |
|
||||
| `output/` | 下行 | 技能/宿主交给用户拿走的结果(视频、回单、xlsx/csv/json…) | `get_output_dir()` |
|
||||
|
||||
内部 / 可选目录(**不上侧栏**):
|
||||
|
||||
| 子目录 | 用途 | 默认 helper |
|
||||
|--------|------|-------------|
|
||||
| `downloads/` | 从外部拉取的原始文件(视频、回单、PDF…) | `get_downloads_dir()` |
|
||||
| `imports/` | 用户/Agent 放入的待处理清单(urls.txt、batch.json、CSV) | `get_imports_dir()` |
|
||||
| `exports/` | 导出给用户的结果(xlsx/csv/json) | `get_exports_dir()` |
|
||||
| `uploads/` | 待发布到外部平台的 staging(可选) | `get_uploads_dir()` |
|
||||
| `cache/` | 可再生缓存(页面快照、解析中间结果) | `get_cache_dir()` |
|
||||
| `temp/` | 单次任务 scratch,任务结束可删 | `get_temp_dir()` |
|
||||
@@ -45,6 +51,15 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
||||
|
||||
复制后若业务不需要某目录,可不创建;**但一旦写入该类文件,必须走对应 helper**。
|
||||
|
||||
**开发者硬规则:**
|
||||
|
||||
- 用户/Agent **放进来**的文件 → `input/`(`get_input_dir()`)
|
||||
- 技能/宿主 **交给用户拿走**的文件 → `output/`(`get_output_dir()`)
|
||||
- 禁止再引入业务侧栏第三目录(如旧的 `downloads` / `imports` / `exports` 三分法)
|
||||
- 缓存、临时、RPA 录屏用 `cache` / `temp` / `videos`,不要当成用户可见的「输出目录」
|
||||
|
||||
宿主约定(与本模板对齐):工具栏「导入」默认从 `input/` 取文件;「导出」默认写到 `output/`。
|
||||
|
||||
---
|
||||
|
||||
## 3. 两类路径
|
||||
@@ -54,6 +69,7 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
||||
技能**创建或更新**的文件:下载结果、导出报表、失败截图、队列 sidecar 等。
|
||||
|
||||
- **默认**必须在 `{skill_data_dir}/<子目录>/` 下。
|
||||
- 用户可拿走的业务产出 → **`output/`**。
|
||||
- **必须**通过 `resolve_data_path()` 或 `get_*_dir()` 解析,**禁止** `os.path.abspath(config.get(...))` 相对 CWD。
|
||||
|
||||
### B. 用户显式输入(user-owned)
|
||||
@@ -61,7 +77,7 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
||||
用户在 CLI / Agent 对话中给出的**已有文件**绝对路径,例如「发布这个视频 `D:\素材\x.mp4`」。
|
||||
|
||||
- 技能**只读**,路径可以是用户磁盘任意位置。
|
||||
- 使用 `resolve_input_path()`:绝对路径原样;**相对路径**解析为 `{skill_data_dir}/imports/<相对路径>`,**不**相对 workspace/CWD。
|
||||
- 使用 `resolve_input_path()`:绝对路径原样;**相对路径**解析为 `{skill_data_dir}/input/<相对路径>`,**不**相对 workspace/CWD。
|
||||
|
||||
---
|
||||
|
||||
@@ -72,9 +88,8 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
||||
```ini
|
||||
# ── 数据目录子路径(见 development/DATA_PATHS.md)──
|
||||
# 相对路径相对于 {JIANGCHANG_DATA_ROOT}/{USER_ID}/{slug}/,不是 workspace
|
||||
# SKILL_DOWNLOAD_DIR=downloads/videos
|
||||
# SKILL_IMPORT_DIR=imports
|
||||
# SKILL_EXPORT_DIR=exports
|
||||
# SKILL_INPUT_DIR=input
|
||||
# SKILL_OUTPUT_DIR=output/videos
|
||||
# SKILL_UPLOAD_DIR=uploads
|
||||
# SKILL_CACHE_DIR=cache
|
||||
# SKILL_TEMP_DIR=temp
|
||||
@@ -83,8 +98,8 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 未配置 | `{skill_data_dir}/{default_subdir}` |
|
||||
| 相对值 `downloads/videos` | `{skill_data_dir}/downloads/videos` |
|
||||
| 绝对值 `D:\nas\exports` | 高级用户/NAS 挂载,原样使用 |
|
||||
| 相对值 `output/videos` | `{skill_data_dir}/output/videos` |
|
||||
| 绝对值 `D:\nas\output` | 高级用户/NAS 挂载,原样使用 |
|
||||
| **禁止** `./outputs/...` | 会随 CWD 漂移,不得作为 `.env.example` 活跃配置行 |
|
||||
|
||||
业务专属路径 env key 须带 **`SKILL_` 前缀**(或技能 slug 前缀),避免污染全局命名空间。
|
||||
@@ -95,20 +110,20 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
||||
|
||||
```python
|
||||
from util.runtime_paths import (
|
||||
get_downloads_dir,
|
||||
get_imports_dir,
|
||||
get_input_dir,
|
||||
get_output_dir,
|
||||
resolve_data_path,
|
||||
resolve_input_path,
|
||||
list_resolved_data_paths,
|
||||
)
|
||||
|
||||
# 下载保存(A 类)
|
||||
dest = os.path.join(get_downloads_dir(), f"{vid}.mp4")
|
||||
# 下行产出(A 类)——视频、报表等
|
||||
dest = os.path.join(get_output_dir(), f"{vid}.mp4")
|
||||
|
||||
# 或带 env 覆盖
|
||||
out_dir = resolve_data_path("SKILL_DOWNLOAD_DIR", "downloads")
|
||||
out_dir = resolve_data_path("SKILL_OUTPUT_DIR", "output")
|
||||
|
||||
# CLI -i urls.txt(B 类,相对路径 → imports/)
|
||||
# CLI -i urls.txt(B 类,相对路径 → input/)
|
||||
manifest = resolve_input_path(args.input_path)
|
||||
|
||||
# health 诊断
|
||||
@@ -123,6 +138,8 @@ os.path.abspath(config.get("DOWNLOAD_OUTPUT_DIR"))
|
||||
|
||||
# ❌ .env.example 里写 ./outputs/videos
|
||||
DOWNLOAD_OUTPUT_DIR=./outputs/videos
|
||||
|
||||
# ❌ 再拆 downloads / imports / exports 三个用户目录
|
||||
```
|
||||
|
||||
---
|
||||
@@ -138,7 +155,7 @@ DOWNLOAD_OUTPUT_DIR=./outputs/videos
|
||||
- 失败截图、录屏中间文件:`rpa-artifacts/{batch_id}/`(见 [`RPA.md`](RPA.md) §5.1)
|
||||
- 录屏成片:`videos/`(见 [`RPA.md`](RPA.md) §5.3)
|
||||
|
||||
业务 `downloads/` 与 RPA `videos/` **分工不同**:前者是业务产出文件,后者是操作过程录屏。
|
||||
业务 `output/` 与 RPA `videos/` **分工不同**:前者是业务产出文件,后者是操作过程录屏。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 | 长任务出现进度 / 可取消;来源标签与触发入口一致 |
|
||||
|
||||
@@ -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 已约束 |
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
|
||||
未完成第 2~6 步时,代码只能保留明确的 `TODO: NEED_REAL_UI_INSPECTION`,不得写“看起来可能正确”的 selector。
|
||||
|
||||
开发期推荐使用独立仓 [jiangchang-ui-recorder](https://git.jc2009.com/client-jiangchang/jiangchang-ui-recorder)(本机常见路径 `D:\OpenClaw\client-jiangchang\jiangchang-ui-recorder`,`python run.py`):以 **Raw Event Journal → Action → Element Catalog → 异步 Observation** 录制,优先把 `action_flow.json` 与 `element_catalog.json` 交给 AI 编程工具;原始事件可重新归并,tree/index 作为诊断证据。默认不再让完整页面 dump 阻塞动作采集。本模板仓内的 `tools/ui_inspector` 已移除,勿再从 skill-template 启动。
|
||||
|
||||
## 3. 元素清单
|
||||
|
||||
每个关键输入、按钮、列表、弹窗、成功状态和错误状态都必须登记。推荐 YAML:
|
||||
|
||||
@@ -104,7 +104,7 @@ RPA 录屏成片(`RpaVideoSession`)、ffmpeg 路径、背景音乐探测均
|
||||
{...}/{skill_slug}.db
|
||||
```
|
||||
|
||||
**业务文件(下载、导入、导出、缓存等)** 须落在上述数据目录的标准子目录下,**不得**相对 workspace/CWD 写入。完整子目录树、env 覆盖规则、`resolve_data_path()` 用法见 **[`DATA_PATHS.md`](DATA_PATHS.md)**。
|
||||
**业务文件(输入、输出、缓存等)** 须落在上述数据目录的标准子目录下,**不得**相对 workspace/CWD 写入。用户可见目录为 `input/`(上行)与 `output/`(下行)。完整子目录树、env 覆盖规则、`resolve_data_path()` 用法见 **[`DATA_PATHS.md`](DATA_PATHS.md)**。
|
||||
|
||||
- 业务表使用英文 snake_case;中文表名/字段名写入 `_jiangchang_tables` / `_jiangchang_columns`(见 `references/SCHEMA.md`)。
|
||||
- 界面字段顺序由 `CREATE TABLE` 列定义顺序决定,**不要**维护 `display_order` 来调整顺序。
|
||||
|
||||
@@ -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 流**。
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
| 值 | 宿主行为 | 超时 | 任务中心 |
|
||||
|----|----------|------|----------|
|
||||
| **`sync`**(或省略时的宿主缺省) | 调用方等待 CLI 结束,直接返回结构化结果或结构化错误;**不**创建后台 Job | 默认约 **30s**(`SKILL_ACTION_SYNC_TIMEOUT_MS`) | ❌ 不进 |
|
||||
| **`sync`**(或省略时的宿主缺省) | 调用方等待 CLI 结束,直接返回结构化结果或结构化错误;**不**创建后台 Job | 默认约 **60s**(`SKILL_ACTION_SYNC_TIMEOUT_MS`) | ❌ 不进 |
|
||||
| **`async`**(须显式声明) | 后台 spawn Job,立即返回 `jobId`;`emit` / `checkpoint` / `finish`;长任务应支持暂停/停止/取消检查 | Job 本身无默认墙钟上限 | ✅ 进入 |
|
||||
|
||||
直接执行 CLI:终端等待进程结束。宿主经 Action 调用同一 CLI:再按 `executionProfile` 分流。技能不得自建任务中心。
|
||||
@@ -48,7 +48,7 @@
|
||||
| 场景 | 建议 |
|
||||
|------|------|
|
||||
| 浏览器 / Playwright / RPA 主路径 | `async` + 建议 `confirmation` |
|
||||
| 单次可能超过 ~20s 的网络/下载/同步 | `async` |
|
||||
| 单次可能接近或超过约 60s 的网络/下载/同步 | `async` |
|
||||
| `health` / `version` / `config-path` / 纯统计 / 秒级查询 | `sync` |
|
||||
| 任意 placement(含 toolbar) | 允许 sync 或 async;以 manifest 为准 |
|
||||
|
||||
@@ -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 |
|
||||
@@ -141,7 +141,7 @@ cmd_stats() # 只读 sync:JSON stdout,无 job_context
|
||||
| 能力 | 约定 |
|
||||
|------|------|
|
||||
| 队列表 | `status`: `pending` → `processing` → `success` / `failed` |
|
||||
| 入库 | 数据管理 CRUD,或 `import-*`(相对 `{skill_data}/imports/`,见 [`DATA_PATHS.md`](DATA_PATHS.md)) |
|
||||
| 入库 | 数据管理 CRUD,或 `import-*`(相对 `{skill_data}/input/`,见 [`DATA_PATHS.md`](DATA_PATHS.md)) |
|
||||
| 消费 | `run --pick N`,默认仅 `pending` |
|
||||
| 重试 | `reset` 或数据管理改 `pending`;**failed 不要自动重试** |
|
||||
| 批次 | 可选 `batch_id`,便于筛选与任务中心对照 |
|
||||
|
||||
@@ -72,39 +72,46 @@ Action 是**可选能力**:
|
||||
|------|------|------|
|
||||
| `schemaVersion` | 是 | 当前固定为 `1` |
|
||||
| `skill` | 是 | 必须与 `SKILL.md` 的 `metadata.openclaw.slug` 一致 |
|
||||
| `actions` | 是 | Action 数组,每项 `id` 唯一 |
|
||||
| `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`(行内/批量预填,见下节) |
|
||||
| **policy** | 可选;`enabled` + `entrypoint`(推荐 `policy` CLI);见 POLICY.md。未启用 = L0 |
|
||||
| **cellPresentation** | 可选;`{ "enabled": true }` 时宿主应加载单元格呈现;见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)。未启用 = P0 |
|
||||
| inputSchema | 根 `type: object`;properties 使用 `string` / `number` / `boolean`;支持 `enum`、`default`、`required`、`sensitive`、`readOnly` |
|
||||
| confirmation | `{ "message": "..." }` — **风险文案**(confirm/form 弹窗黄条;agent 元数据);**不是**「要不要弹窗」的开关 |
|
||||
| **executionProfile** | `"sync"` \| `"async"`(**必须显式写出**,禁止依赖缺省) |
|
||||
| **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,44 @@ 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` 过滤)的相对先后**。
|
||||
|
||||
规则:
|
||||
|
||||
1. 想调整顺序 → **调整 `actions` 数组先后**(不必改 `id` / `label`)。
|
||||
2. 无关 placement 的 Action(如仅 `skill-detail` 的 `health`)夹在中间**不影响** toolbar/row 的相对顺序;宿主按匹配子集保序渲染。
|
||||
3. 同一 Action 可同时挂多个 placement;它在各入口的位置,分别是该入口匹配子集里的相对位置。
|
||||
4. **宿主内置按钮不参与本契约、位置固定**:
|
||||
- 表顶栏右侧:删除(有勾选时)/ 刷新 / 新增 / 导入 / 导出 —— 技能 toolbar/batch 按钮在左侧,不能与右侧内置按钮混排。
|
||||
- 行级:技能 `row` 项按本顺序出现在操作列 / 行右键(及可选「⋯」)中;宿主「查看」→(非只读)「编辑」→「删除」为内置项,位置由宿主固定(操作列常在技能项之后;右键中可与技能项分组)。
|
||||
5. **表顶栏全部直接展示,无「更多操作」溢出菜单**:匹配到的 toolbar/batch 技能按钮一律渲染(顺序仍按本契约)。**行级**不以「顶栏溢出菜单」模型截断;行右键是 `row` 的正式呈现壳之一,不是 toolbar 式「更多操作」截断。
|
||||
6. **表顶栏宽窄布局(宿主行为,技能无需配置)**:
|
||||
- 够宽:左右各横排一行(左技能按钮 + 右宿主工具)。
|
||||
- 不够宽:保持左右分栏,栏内按钮上下竖排(左栏技能、右栏刷新/导出等)。
|
||||
|
||||
禁止:
|
||||
|
||||
- 臆造 `sortOrder` / `order` / `priority` 等字段指望宿主排序(模板 Schema `additionalProperties: false` 会拒收)。
|
||||
- 依赖或宣传表顶栏「更多操作」溢出菜单(宿主已取消该能力)。
|
||||
- 把「表格行数据默认排序」与按钮顺序混淆——行排序见 [`SCHEMA.md`](SCHEMA.md) 的 `default_sort_json`。
|
||||
- 新增 `row-context-menu` 等 placement,或要求技能再维护一份与 `row` 平行的菜单清单。
|
||||
|
||||
### Action 类型 × placements(常见示例,不是限制)
|
||||
|
||||
下表只表示**常见用法示例**。实际 `placements` 以 manifest 配置为准;**`sync` / `async` 不影响展示入口**。已声明的入口都要在宿主自测,不要只测其中一条。
|
||||
@@ -390,8 +507,18 @@ HINT: <next step>
|
||||
|
||||
- `object` / `string` / `number` / `boolean`
|
||||
- `enum` / `default` / `required` / `sensitive` / `readOnly`
|
||||
- 属性级 `description`(表单标签下方灰字说明)
|
||||
- 简单模板替换(`{{fieldName}}`)
|
||||
|
||||
**与数据管理「新增 / 编辑」表单的分工(勿混用配置入口):**
|
||||
|
||||
| 表单 | 短标题 | 说明灰字 | 默认值 |
|
||||
|------|--------|----------|--------|
|
||||
| Skill Action 弹窗 | `inputSchema.title` | `inputSchema.description` | `inputSchema.default` |
|
||||
| 数据管理新增/编辑 | `_jiangchang_columns.display_name` | `_jiangchang_columns.description` | 物理列 SQLite `DEFAULT` |
|
||||
|
||||
CRUD 侧宿主义务与禁止事项见 [`SCHEMA.md`](SCHEMA.md)「新增/编辑表单:字段说明与默认值(宿主支持)」。同一业务含义两边文案应对齐,但分别配置。
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
@@ -402,11 +529,13 @@ HINT: <next step>
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"title": "查询条件"
|
||||
"title": "查询条件",
|
||||
"description": "支持关键词或完整业务编号"
|
||||
},
|
||||
"limit": {
|
||||
"type": "number",
|
||||
"title": "返回数量",
|
||||
"description": "最多返回多少条;填 0 表示不限制(若业务支持)",
|
||||
"default": 50
|
||||
}
|
||||
}
|
||||
|
||||
210
references/CELL_PRESENTATION.md
Normal file
210
references/CELL_PRESENTATION.md
Normal 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)。
|
||||
@@ -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
382
references/POLICY.md
Normal 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;写路径技能自检
|
||||
@@ -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`。
|
||||
|
||||
@@ -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`);不猜测业务规则
|
||||
|
||||
## 初始化入口
|
||||
|
||||
@@ -133,6 +138,38 @@ metadata:
|
||||
| `sort_order` | 否 | 兼容保留字段,不用于替代物理字段顺序 |
|
||||
| `visible` | 是 | `1` 在宿主数据管理中展示 / `0` 隐藏 |
|
||||
| `readonly` | 是 | `1` 整张表只读;`0` 允许宿主根据字段权限执行写操作 |
|
||||
| `default_sort_json` | 否 | 数据管理**默认行排序**(见下节);空则宿主用内置默认 |
|
||||
|
||||
#### 表级默认排序 `default_sort_json`(宿主支持)
|
||||
|
||||
技能可声明该表在数据管理中的默认排序;宿主优先采用,未声明或声明无效时回退:
|
||||
|
||||
`updated_at DESC` → `created_at DESC` → 主键 DESC。
|
||||
|
||||
JSON 数组(可多字段,按顺序):
|
||||
|
||||
```json
|
||||
[{"column":"sent_at","direction":"desc"},{"column":"id","direction":"desc"}]
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- `column` 必须是该表真实列名;敏感列会被宿主忽略
|
||||
- `direction` 仅 `asc` / `desc`(缺省按 `desc`)
|
||||
- 用户在 UI 上手动改排序后,以用户选择为准;声明只影响未选择时的默认
|
||||
- 通过 `upsert_table_metadata(..., default_sort_json=...)` 写入;旧库由 `init_display_metadata_tables` 幂等 `ALTER` 补列
|
||||
|
||||
示例(消息时间线):
|
||||
|
||||
```python
|
||||
upsert_table_metadata(
|
||||
cursor,
|
||||
table_name="messages",
|
||||
display_name="消息",
|
||||
readonly=0,
|
||||
default_sort_json='[{"column":"sent_at","direction":"desc"},{"column":"id","direction":"desc"}]',
|
||||
)
|
||||
```
|
||||
|
||||
禁止:
|
||||
|
||||
@@ -147,8 +184,8 @@ metadata:
|
||||
|------|------|------|
|
||||
| `table_name` | 是 | 所属物理表 |
|
||||
| `column_name` | 是 | 真实英文字段名 |
|
||||
| `display_name` | 是 | 用户可见的中文字段名 |
|
||||
| `description` | 否 | 字段说明 |
|
||||
| `display_name` | 是 | 用户可见的中文字段名(表头与表单标签);保持简短 |
|
||||
| `description` | 否 | 字段说明(给人看的短文案);**宿主在新增/编辑表单标签下方展示**(见下节);不要塞进 `display_name` |
|
||||
| `display_order` | 否 | 兼容字段,默认保持 `NULL` |
|
||||
| `visible` | 是 | 是否在数据管理表格中展示 |
|
||||
| `searchable` | 是 | 是否参与搜索 |
|
||||
@@ -158,6 +195,56 @@ metadata:
|
||||
|
||||
这两张表是**数据字典**,不是业务数据;宿主不会把它们当作普通业务表展示。
|
||||
|
||||
#### 新增/编辑表单:字段说明与默认值(宿主支持)
|
||||
|
||||
数据管理「新增记录 / 编辑记录」与 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
|
||||
|
||||
示例(可编辑列种子 + 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,
|
||||
"description": "填平台视频页链接;需包含可识别的视频路径片段。",
|
||||
},
|
||||
{
|
||||
"column_name": "comment_limit",
|
||||
"display_name": "采集评论条数",
|
||||
"editable": 1,
|
||||
"description": "最多采多少条。填 0 = 不限制;填正整数则按该数量封顶。",
|
||||
},
|
||||
),
|
||||
valid_column_names=physical,
|
||||
)
|
||||
```
|
||||
|
||||
Action 参数表单的 `title` / `description` / `default` 见 [`ACTIONS.md`](ACTIONS.md);勿与本节 CRUD 配置入口混用。
|
||||
|
||||
## editable 与 createable 的关系
|
||||
|
||||
技能模板只维护 `editable` 这一份字段权限声明:
|
||||
|
||||
@@ -24,7 +24,8 @@ CREATE TABLE IF NOT EXISTS _jiangchang_tables (
|
||||
description TEXT,
|
||||
sort_order INTEGER,
|
||||
visible INTEGER NOT NULL DEFAULT 1,
|
||||
readonly INTEGER NOT NULL DEFAULT 0
|
||||
readonly INTEGER NOT NULL DEFAULT 0,
|
||||
default_sort_json TEXT
|
||||
)
|
||||
"""
|
||||
|
||||
@@ -82,23 +83,50 @@ TASK_LOGS_COLUMN_METADATA: tuple[dict[str, object], ...] = (
|
||||
)
|
||||
|
||||
|
||||
def _ensure_jiangchang_tables_columns(cursor) -> None:
|
||||
"""旧库补列:CREATE IF NOT EXISTS 不会给已有表加新字段。"""
|
||||
cursor.execute("PRAGMA table_info('_jiangchang_tables')")
|
||||
cols = {str(row[1]) for row in cursor.fetchall()}
|
||||
if "default_sort_json" not in cols:
|
||||
cursor.execute("ALTER TABLE _jiangchang_tables ADD COLUMN default_sort_json TEXT")
|
||||
|
||||
|
||||
def init_display_metadata_tables(cursor) -> None:
|
||||
cursor.execute(_JIANGCHANG_TABLES_DDL)
|
||||
cursor.execute(_JIANGCHANG_COLUMNS_DDL)
|
||||
_ensure_jiangchang_tables_columns(cursor)
|
||||
|
||||
|
||||
def upsert_table_metadata(cursor, *, table_name: str, display_name: str, description: str | None = None, visible: int = 1, readonly: int = 0) -> None:
|
||||
def upsert_table_metadata(
|
||||
cursor,
|
||||
*,
|
||||
table_name: str,
|
||||
display_name: str,
|
||||
description: str | None = None,
|
||||
visible: int = 1,
|
||||
readonly: int = 0,
|
||||
default_sort_json: str | None = None,
|
||||
) -> None:
|
||||
"""写入表级元数据。
|
||||
|
||||
default_sort_json: 宿主数据管理默认排序,JSON 数组,例如
|
||||
'[{"column":"sent_at","direction":"desc"},{"column":"id","direction":"desc"}]'
|
||||
为空则宿主回退到 updated_at → created_at → 主键。
|
||||
"""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO _jiangchang_tables (table_name, display_name, description, visible, readonly)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
INSERT INTO _jiangchang_tables (
|
||||
table_name, display_name, description, visible, readonly, default_sort_json
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(table_name) DO UPDATE SET
|
||||
display_name = excluded.display_name,
|
||||
description = excluded.description,
|
||||
visible = excluded.visible,
|
||||
readonly = excluded.readonly
|
||||
readonly = excluded.readonly,
|
||||
default_sort_json = excluded.default_sort_json
|
||||
""",
|
||||
(table_name, display_name, description, visible, readonly),
|
||||
(table_name, display_name, description, visible, readonly, default_sort_json),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。"""
|
||||
|
||||
SKILL_SLUG = "your-skill-slug"
|
||||
SKILL_VERSION = "1.0.61"
|
||||
SKILL_VERSION = "1.0.76"
|
||||
LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug"
|
||||
PLATFORM_KIT_MIN_VERSION = "1.2.2"
|
||||
|
||||
@@ -10,18 +10,19 @@ from util.constants import SKILL_SLUG
|
||||
|
||||
_SCRIPTS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
STANDARD_SUBDIR_DOWNLOADS = "downloads"
|
||||
STANDARD_SUBDIR_IMPORTS = "imports"
|
||||
STANDARD_SUBDIR_EXPORTS = "exports"
|
||||
# 用户可见的上行/下行目录(数据管理侧栏只挂这两个)
|
||||
STANDARD_SUBDIR_INPUT = "input"
|
||||
STANDARD_SUBDIR_OUTPUT = "output"
|
||||
|
||||
# 内部/可选目录(不上侧栏)
|
||||
STANDARD_SUBDIR_UPLOADS = "uploads"
|
||||
STANDARD_SUBDIR_CACHE = "cache"
|
||||
STANDARD_SUBDIR_TEMP = "temp"
|
||||
STANDARD_SUBDIR_RPA_ARTIFACTS = "rpa-artifacts"
|
||||
STANDARD_SUBDIR_VIDEOS = "videos"
|
||||
|
||||
CONFIG_KEY_DOWNLOAD_DIR = "SKILL_DOWNLOAD_DIR"
|
||||
CONFIG_KEY_IMPORT_DIR = "SKILL_IMPORT_DIR"
|
||||
CONFIG_KEY_EXPORT_DIR = "SKILL_EXPORT_DIR"
|
||||
CONFIG_KEY_INPUT_DIR = "SKILL_INPUT_DIR"
|
||||
CONFIG_KEY_OUTPUT_DIR = "SKILL_OUTPUT_DIR"
|
||||
CONFIG_KEY_UPLOAD_DIR = "SKILL_UPLOAD_DIR"
|
||||
CONFIG_KEY_CACHE_DIR = "SKILL_CACHE_DIR"
|
||||
CONFIG_KEY_TEMP_DIR = "SKILL_TEMP_DIR"
|
||||
@@ -81,27 +82,25 @@ def resolve_data_path(
|
||||
|
||||
|
||||
def resolve_input_path(raw_path: str, *, create_parent: bool = False) -> str:
|
||||
"""解析 CLI 输入路径:绝对路径原样;相对路径相对 {skill_data_dir}/imports/。"""
|
||||
"""解析 CLI 输入路径:绝对路径原样;相对路径相对 {skill_data_dir}/input/。"""
|
||||
text = (raw_path or "").strip()
|
||||
if not text:
|
||||
return text
|
||||
expanded = os.path.expanduser(text)
|
||||
if os.path.isabs(expanded):
|
||||
return os.path.abspath(expanded)
|
||||
imports_dir = get_imports_dir(create=create_parent)
|
||||
return os.path.abspath(os.path.join(imports_dir, os.path.normpath(expanded)))
|
||||
input_dir = get_input_dir(create=create_parent)
|
||||
return os.path.abspath(os.path.join(input_dir, os.path.normpath(expanded)))
|
||||
|
||||
|
||||
def get_downloads_dir(*, create: bool = True) -> str:
|
||||
return resolve_data_path(CONFIG_KEY_DOWNLOAD_DIR, STANDARD_SUBDIR_DOWNLOADS, create=create)
|
||||
def get_input_dir(*, create: bool = True) -> str:
|
||||
"""上行:用户/Agent 放入的待处理文件(清单、CSV、产品表等)。"""
|
||||
return resolve_data_path(CONFIG_KEY_INPUT_DIR, STANDARD_SUBDIR_INPUT, create=create)
|
||||
|
||||
|
||||
def get_imports_dir(*, create: bool = True) -> str:
|
||||
return resolve_data_path(CONFIG_KEY_IMPORT_DIR, STANDARD_SUBDIR_IMPORTS, create=create)
|
||||
|
||||
|
||||
def get_exports_dir(*, create: bool = True) -> str:
|
||||
return resolve_data_path(CONFIG_KEY_EXPORT_DIR, STANDARD_SUBDIR_EXPORTS, create=create)
|
||||
def get_output_dir(*, create: bool = True) -> str:
|
||||
"""下行:技能/宿主交给用户拿走的文件(视频、报表、导出 CSV 等)。"""
|
||||
return resolve_data_path(CONFIG_KEY_OUTPUT_DIR, STANDARD_SUBDIR_OUTPUT, create=create)
|
||||
|
||||
|
||||
def get_uploads_dir(*, create: bool = True) -> str:
|
||||
@@ -135,9 +134,8 @@ def list_resolved_data_paths() -> dict[str, str]:
|
||||
"""供 health / config-path 输出已解析的数据子目录(不创建目录)。"""
|
||||
return {
|
||||
"skill_data_dir": get_skill_data_dir(),
|
||||
"downloads_dir": get_downloads_dir(create=False),
|
||||
"imports_dir": get_imports_dir(create=False),
|
||||
"exports_dir": get_exports_dir(create=False),
|
||||
"input_dir": get_input_dir(create=False),
|
||||
"output_dir": get_output_dir(create=False),
|
||||
"uploads_dir": get_uploads_dir(create=False),
|
||||
"cache_dir": get_cache_dir(create=False),
|
||||
"temp_dir": get_temp_dir(create=False),
|
||||
|
||||
@@ -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
|
||||
@@ -510,8 +534,39 @@ class TestActionsManifest(unittest.TestCase):
|
||||
"executionProfile",
|
||||
"interaction",
|
||||
"confirmation",
|
||||
"数据管理技能按钮顺序",
|
||||
"数组相对顺序",
|
||||
"全部直接展示",
|
||||
"左右分栏",
|
||||
"行级入口",
|
||||
"行右键",
|
||||
"六职责正交",
|
||||
"POLICY.md",
|
||||
"hostEffects",
|
||||
"steps",
|
||||
"reloadTable",
|
||||
"afterSuccess",
|
||||
"afterFailure",
|
||||
"cellPresentation",
|
||||
"CELL_PRESENTATION.md",
|
||||
):
|
||||
self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}")
|
||||
self.assertNotIn(
|
||||
"其余进「更多操作」",
|
||||
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",
|
||||
|
||||
93
tests/test_cell_presentation_contract.py
Normal file
93
tests/test_cell_presentation_contract.py
Normal 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()
|
||||
@@ -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,22 @@ class TestDocsStandards(unittest.TestCase):
|
||||
"options_json",
|
||||
"demo_items",
|
||||
"readonly",
|
||||
"新增/编辑表单:字段说明与默认值(宿主支持)",
|
||||
"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 与参数表单",
|
||||
"数据管理新增/编辑",
|
||||
"_jiangchang_columns.description",
|
||||
"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",
|
||||
|
||||
@@ -89,11 +89,11 @@ class TestRuntimePaths(unittest.TestCase):
|
||||
import util.runtime_paths as rp
|
||||
|
||||
importlib.reload(rp)
|
||||
path = rp.resolve_data_path(None, "downloads", create=True)
|
||||
path = rp.resolve_data_path(None, "output", create=True)
|
||||
norm = os.path.normcase(os.path.normpath(path))
|
||||
norm_tmp = os.path.normcase(os.path.normpath(tmp))
|
||||
self.assertTrue(norm.startswith(norm_tmp))
|
||||
self.assertTrue(norm.endswith(os.path.normcase(os.path.join("downloads"))))
|
||||
self.assertTrue(norm.endswith(os.path.normcase(os.path.join("output"))))
|
||||
|
||||
def test_resolve_data_path_relative_uses_data_dir_not_cwd(self) -> None:
|
||||
with IsolatedDataRoot() as tmp:
|
||||
@@ -102,20 +102,20 @@ class TestRuntimePaths(unittest.TestCase):
|
||||
import util.runtime_paths as rp
|
||||
|
||||
importlib.reload(rp)
|
||||
os.environ["SKILL_DOWNLOAD_DIR"] = "downloads/videos"
|
||||
os.environ["SKILL_OUTPUT_DIR"] = "output/videos"
|
||||
from jiangchang_skill_core import config
|
||||
|
||||
config.reset_cache()
|
||||
importlib.reload(rp)
|
||||
path = rp.resolve_data_path("SKILL_DOWNLOAD_DIR", "downloads", create=False)
|
||||
path = rp.resolve_data_path("SKILL_OUTPUT_DIR", "output", create=False)
|
||||
norm = os.path.normcase(os.path.normpath(path))
|
||||
self.assertIn(os.path.normcase("downloads"), norm)
|
||||
self.assertIn(os.path.normcase("output"), norm)
|
||||
self.assertIn(os.path.normcase("videos"), norm)
|
||||
self.assertTrue(norm.startswith(os.path.normcase(os.path.normpath(tmp))))
|
||||
os.environ.pop("SKILL_DOWNLOAD_DIR", None)
|
||||
os.environ.pop("SKILL_OUTPUT_DIR", None)
|
||||
config.reset_cache()
|
||||
|
||||
def test_resolve_input_path_relative_under_imports(self) -> None:
|
||||
def test_resolve_input_path_relative_under_input(self) -> None:
|
||||
with IsolatedDataRoot() as tmp:
|
||||
import importlib
|
||||
|
||||
@@ -124,9 +124,24 @@ class TestRuntimePaths(unittest.TestCase):
|
||||
importlib.reload(rp)
|
||||
resolved = rp.resolve_input_path("urls.txt", create_parent=True)
|
||||
norm = os.path.normcase(os.path.normpath(resolved))
|
||||
self.assertIn(os.path.normcase("imports"), norm)
|
||||
self.assertIn(os.path.normcase("input"), norm)
|
||||
self.assertTrue(norm.startswith(os.path.normcase(os.path.normpath(tmp))))
|
||||
|
||||
def test_get_input_and_output_dirs(self) -> None:
|
||||
with IsolatedDataRoot() as tmp:
|
||||
import importlib
|
||||
|
||||
import util.runtime_paths as rp
|
||||
|
||||
importlib.reload(rp)
|
||||
input_dir = rp.get_input_dir(create=True)
|
||||
output_dir = rp.get_output_dir(create=True)
|
||||
norm_tmp = os.path.normcase(os.path.normpath(tmp))
|
||||
self.assertTrue(os.path.normcase(os.path.normpath(input_dir)).startswith(norm_tmp))
|
||||
self.assertTrue(os.path.normcase(os.path.normpath(output_dir)).startswith(norm_tmp))
|
||||
self.assertTrue(input_dir.replace("\\", "/").endswith("/input"))
|
||||
self.assertTrue(output_dir.replace("\\", "/").endswith("/output"))
|
||||
|
||||
def test_list_resolved_data_paths_keys(self) -> None:
|
||||
with IsolatedDataRoot():
|
||||
import importlib
|
||||
@@ -137,12 +152,14 @@ class TestRuntimePaths(unittest.TestCase):
|
||||
paths = rp.list_resolved_data_paths()
|
||||
for key in (
|
||||
"skill_data_dir",
|
||||
"downloads_dir",
|
||||
"imports_dir",
|
||||
"exports_dir",
|
||||
"input_dir",
|
||||
"output_dir",
|
||||
):
|
||||
self.assertIn(key, paths)
|
||||
self.assertTrue(paths[key])
|
||||
self.assertNotIn("downloads_dir", paths)
|
||||
self.assertNotIn("imports_dir", paths)
|
||||
self.assertNotIn("exports_dir", paths)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# skill-template 工具
|
||||
|
||||
## UI 拾取(开发期)
|
||||
|
||||
`tools/ui_inspector` 已迁出本模板仓。请使用独立产品仓:
|
||||
|
||||
- Gitea:https://git.jc2009.com/client-jiangchang/jiangchang-ui-recorder
|
||||
- 本机常见路径:`D:\OpenClaw\client-jiangchang\jiangchang-ui-recorder`
|
||||
- 启动:`python run.py`
|
||||
|
||||
模板仓历史中仍保留删除前的归档提交(可按需从 Git 恢复旧副本);日常开发与运行以独立仓为准。
|
||||
|
||||
## 业务仓也可以直接 clone(来源 A)
|
||||
|
||||
若项目经理已在**公司自建 Gitea**([https://git.jc2009.com/](https://git.jc2009.com/),基于开源 Gitea 自建,**不是** Gitea 官网)开好业务技能仓库并授权,技术人员应直接:
|
||||
|
||||
Reference in New Issue
Block a user