Precipitate host contracts from task-plaza: visibleWhen equalsFrom, replaces gates, url openExternal, feishu data-sources (v1.0.128)
All checks were successful
技能自动化发布 / release (push) Successful in 14s

This commit is contained in:
2026-11-01 16:30:46 +08:00
parent d911bd28cd
commit 90eefc8653
15 changed files with 412 additions and 32 deletions

View File

@@ -9,6 +9,13 @@
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
## 1.0.128
- 沉淀 `bind.visibleWhen`:`equalsFrom` / `notEqualsFrom`(`currentUser.userId` / `username`)与 `all` / `any`;Schema 对齐
- 沉淀 `replaces: update|delete` 的 `visibleWhen` **同步控制宿主行「编辑/删除」**;新增 `replaces-delete-owner-visible` 示例;POLICY 补状态机表配方(`hostCapabilities` 不含 `edit`)
- 沉淀 `displayType: url` 列表/详情 **系统浏览器外开**(SCHEMA);CHECKLIST A6-18
- 沉淀 `data-sources.json`:`kind: feishu_bitable` + 源无关 `columns` 覆盖(`searchable` / `editable` / `defaultValueFrom` / `optionsWhen` 等);新增飞书示例;CHECKLIST A4-15 / A6-17
## 1.0.127
- 沉淀第三种执行画像 `executionProfile: stream`(与 sync/async 并列):页内 LLM/长生成流式,不进任务中心主路径

View File

@@ -1,7 +1,7 @@
---
name: 技能开发模板(通用业务版)
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
version: 1.0.127
version: 1.0.128
author: 深圳匠厂科技有限公司
metadata:
openclaw:

View File

@@ -1,6 +1,7 @@
# 示例 JSON
- `data-sources.mysql.example.json`:MySQL 云库 `data-sources.json` 形状参考(复制到用户数据目录并配 `.env` 中 `MYSQL_DSN`;见 `references/DATA_SOURCES.md`)。
- `data-sources.feishu_bitable.example.json`:飞书多维表源 + `columns` 展示覆盖(`searchable` / `editable` / `displayType: url` / `defaultValueFrom` / `optionsWhen`;见同文档)。
- `version-response.json`:`python main.py version` 成功时 stdout 单行对象的形状参考。
- `skill-policy-snapshot.*.example.json`:Data Policy Snapshot 形状参考(见 `references/POLICY.md`)。
- `skill-cell-presentation.example.json`:数据管理单元格呈现文档形状参考(见 `references/CELL_PRESENTATION.md`)。
@@ -15,3 +16,4 @@
- `selection-label-column.action.example.json`:选择摘要 `bind.labelColumn` 形状参考(见 `references/ACTIONS.md`「选择摘要」)。
- `selection-mode-optional.action.example.json`:`bind.selectionMode: "optional"` 智能勾选/按数量合并形状参考(含 `cron`:配置页按无勾选分支,只见数量字段);用户可见文案为口语中文(见 `references/ACTIONS.md`「勾选驱动模式」「用户可见文案」)。
- `replaces-create.action.example.json` / `replaces-update.action.example.json` / `replaces-delete.action.example.json` / `replaces-import.action.example.json`:数据管理默认按钮接管(`replaces`)形状参考;**import 时 UI 仍用宿主「导入数据」面板**,确认后调技能写库(见 `references/ACTIONS.md`「`replaces`」)。实现 import 解析时复用 `scripts/service/table_file_import.py`(兼容宿主前缀 `*` 表头与 `{value} · {label}` lookup 单元格)。
- `replaces-delete-owner-visible.action.example.json`:`replaces: delete` + `visibleWhen.equalsFrom: currentUser.userId`——宿主行「删除」仅所有者可见(与技能行按钮同一求值;见 `references/ACTIONS.md`「行菜单显隐」)。

View File

@@ -0,0 +1,50 @@
{
"version": 1,
"sources": [
{
"id": "primary",
"kind": "feishu_bitable",
"displayName": "飞书业务表(示例)",
"appIdEnv": "FEISHU_APP_ID",
"appSecretEnv": "FEISHU_APP_SECRET",
"appTokenEnv": "FEISHU_BITABLE_APP_TOKEN",
"tableIdEnv": "FEISHU_TASK_TABLE_ID",
"columns": {
"record_id": {
"searchable": false
},
"标题": {
"requiredOnCreate": true,
"searchable": true
},
"状态": {
"editable": false,
"searchable": true,
"defaultValue": "可接"
},
"结果链接": {
"displayType": "url",
"editable": false,
"searchable": false
},
"发单人": {
"displayType": "userId",
"editable": false,
"formVisible": false,
"defaultValueFrom": "currentUser.userId",
"searchable": true
},
"平台": {
"searchable": true,
"optionsWhen": {
"dependsOn": "任务类型",
"map": {
"文章发布": ["头条号", "搜狐号"],
"视频发布": ["抖音", "视频号"]
}
}
}
}
}
]
}

View File

@@ -0,0 +1,53 @@
{
"id": "item-delete-own",
"label": "删除",
"description": "删除自己拥有的记录。用户点宿主「删除」:行上仅所有者=当前用户时显示;勾选批量时他人记录由技能跳过。",
"replaces": "delete",
"placements": ["agent"],
"executionProfile": "sync",
"interaction": "confirm",
"interactionByPlacement": {
"delete": "confirm",
"agent": "form"
},
"confirmation": {
"message": "确认删除?仅删除你拥有的记录,他人记录会跳过;删除后不可恢复。"
},
"bind": {
"tables": ["business_items"],
"selectionMode": "required",
"visibleWhen": {
"field": "owner_user_id",
"equalsFrom": "currentUser.userId"
},
"inputMapping": {
"itemId": "$row.$pk",
"itemIds": "$selection.ids"
}
},
"hostEffects": {
"onSuccess": ["reloadTable", "clearSelection"]
},
"inputSchema": {
"type": "object",
"properties": {
"itemId": {
"type": "string",
"title": "编号",
"description": "行内触发时自动填入",
"readOnly": true
},
"itemIds": {
"type": "string",
"title": "编号列表",
"description": "勾选时自动填入",
"readOnly": true
}
}
},
"entrypoint": {
"type": "cli",
"command": "item-delete",
"args": ["--id", "{{itemId}}", "--ids", "{{itemIds}}"]
}
}

View File

@@ -91,7 +91,7 @@
"type": ["string", "number", "boolean"]
},
"visibleWhenRule": {
"description": "条件显隐规则。支持 {field,equals} / {field,in:[...]},或单键简写 {status:\"approved\"} / {status:[\"a\",\"b\"]}。",
"description": "条件显隐规则。支持 {field,equals} / {field,in} / {field,equalsFrom} / {field,notEqualsFrom} / {all:[...]} / {any:[...]},或单键简写 {status:\"approved\"} / {status:[\"a\",\"b\"]}。",
"oneOf": [
{
"type": "object",
@@ -115,6 +115,56 @@
}
}
},
{
"type": "object",
"required": ["field", "equalsFrom"],
"additionalProperties": false,
"properties": {
"field": { "type": "string", "minLength": 1 },
"equalsFrom": {
"type": "string",
"enum": ["currentUser.userId", "currentUser.username"],
"description": "与宿主当前用户上下文比较(与 data-sources columns.defaultValueFrom 同键族)。"
}
}
},
{
"type": "object",
"required": ["field", "notEqualsFrom"],
"additionalProperties": false,
"properties": {
"field": { "type": "string", "minLength": 1 },
"notEqualsFrom": {
"type": "string",
"enum": ["currentUser.userId", "currentUser.username"],
"description": "字段值不等于宿主当前用户上下文时才显示。"
}
}
},
{
"type": "object",
"required": ["all"],
"additionalProperties": false,
"properties": {
"all": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/visibleWhenRule" }
}
}
},
{
"type": "object",
"required": ["any"],
"additionalProperties": false,
"properties": {
"any": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/$defs/visibleWhenRule" }
}
}
},
{
"type": "object",
"minProperties": 1,

View File

@@ -113,10 +113,11 @@ checklist_version: 1.0
- [ ] A4-8 **P1** 行内/勾选打开的 `form`:有「已选择」时参数区不应再重复映射主键(`inputMapping` + `readOnly`);需要强制露出时用 `revealInForm`,**禁止**用表单 `visibleWhen` 藏主键(会不提交)
- [ ] A4-9 **P1** 若同一能力既支持「勾选处理」又支持「按数量从队列领取」:用一个 Action + `bind.selectionMode: "optional"`(勿拆两个同义顶栏 / cron 按钮);无勾选可点、有勾选显示「已选择」、空选数量字段用 `pickCount` 等约定名;**定时任务配置页**只应出现数量字段、不应出现空的「编号列表」(见 `references/ACTIONS.md`)
- [ ] A4-10 **P0** 数据管理弹窗与任务失败文案:全中文产品用语;禁止在 `description` / `confirmation.message` / 参数 `description` / 用户向 `message` 写英文字段名、CLI 子命令、`async`/`pending`/`status=` 等(Agent 路由放 `SKILL.md`;见 `references/ACTIONS.md`「用户可见文案」)
- [ ] A4-11 **P1** 若行操作应按业务状态显隐(如仅待确认可「采用」、仅已采用可「加入待写」):用 **`bind.visibleWhen`**(对当前行列值求值;存库原始值如 `approved`);勿指望 Policy L3;CLI 侧仍须校验状态(见 `references/ACTIONS.md`「行菜单显隐」)
- [ ] A4-11 **P1** 若行操作应按业务状态显隐(如仅待确认可「采用」、仅已采用可「加入待写」):用 **`bind.visibleWhen`**(对当前行列值求值;存库原始值如 `approved`;可用 `equals` / `in` / `equalsFrom` / `notEqualsFrom` / `all` / `any`);勿指望 Policy L3;CLI 侧仍须校验状态(见 `references/ACTIONS.md`「行菜单显隐」)
- [ ] A4-12 **P1** 若要在表单里翻页勾选一批业务行(文章、平台等):用 Lookup **`presentation: "paged_table"`**(仍是 `display_type: lookup`,勿新造类型);`skill_cli` 的 stdout 为一行 `{"items","total"}`,0 条也成功退出;该列表命令不要在入口加载整包 platform-kit 或 Playwright(见 `references/ACTIONS.md`「分页勾选小表」)
- [ ] A4-13 **P1** 若 Action 有固定小枚举(状态/船司/优先级等)需中文展示:用 `enum` + **`enumNames`**,或绑列复用列 `options_json` 数组;宿主优先序为绑列 options → enumNames → 纯 enum;**禁止**为中文展示造假 Lookup(见 `references/ACTIONS.md`「静态枚举 label」)
- [ ] A4-14 **P0** 若声明 `replaces: import`:表头清洗须兼容宿主**前缀**必填星(`*显示名`)与旧后缀(`显示名*`);lookup 列须能解析 `{value} · {label}`;推荐复用 `scripts/service/table_file_import.py`;**禁止**只 `removesuffix("*")` 导致「字段不能为空」(见 `references/ACTIONS.md`「导入表头与 lookup 单元格」)
- [ ] A4-15 **P1** 若仅所有者可删/可改:`replaces: delete|update` + `bind.visibleWhen` + **`equalsFrom: currentUser.userId`**(宿主行「删除/编辑」与技能按钮同一规则);状态机表另用 Policy **`hostCapabilities` 不含 `edit`** + 列 `editable: false`(见 `ACTIONS.md` / `POLICY.md`「状态机表配方」)
---
## 5. 账号与浏览器会话(网页 RPA 适用;其他形态标 N/A 或改测等价项)
@@ -156,6 +157,8 @@ checklist_version: 1.0
- [ ] A6-14 **P0** 废弃用户可见表时:`drop_deprecated_user_table` 物理删表 + 清元数据;**禁止**只删元数据留物理表;有迁移时须提供旧库回归测试(见 `SCHEMA.md`、`tests/samples/test_table_deprecation_migration.py.sample`)
- [ ] A6-15 **P1** 若启用 `cellPresentation`:旁路表不得出现在数据管理侧栏;整行着色不得铺到详情每个字段;禁止 `all`×`all`;tone 闭集。未启用 → **N/A**(见 `references/CELL_PRESENTATION.md`)
- [ ] A6-16 **P0** 若走 **MySQL 云库**:用户目录 `data-sources.json` + `.env` DSN 已配置;Host ≥ 6.9.28;CLI / Action 与数据管理 UI 读写**同一库**(抽测 list/create);业务 DDL 支持所选后端。纯 SQLite → **N/A**(见 `references/DATA_SOURCES.md`)
- [ ] A6-17 **P0** 若走 **飞书多维表**(`kind: feishu_bitable`):`data-sources.json` + `.env` 凭证齐全;`columns` 收紧 `searchable`/`editable`;结果链接等用 **`displayType: url`**(表格可点外开);CLI / Action 与数据管理抽测同一表。非飞书 → **N/A**
- [ ] A6-18 **P1** 有 `display_type` / `displayType: url` 的列:数据管理表格或详情中合法 http(s) 可点击并用系统浏览器打开(见 `references/SCHEMA.md`「url 外开」)。无 url 列 → **N/A**
---

View File

@@ -21,7 +21,7 @@
1. **宿主管调度,技能管业务** — 不自行实现任务中心 / Job 轮询 / 后台守护进程。
2. **单业务内核、多入口** — 数据管理、定时任务、Agent、技能详情与直接 CLI 复用同一 domain service;禁止按入口复制业务逻辑。
3. **五职责正交** — `placements` / `bind.tables` / `executionProfile` / `entrypoint` / domain service 各管一事(见 ACTIONS.md)。可选第六轴 **policy**(见 `references/POLICY.md`):未启用 = L0 与今日一致。可选 **`hostEffects` / `steps`**(见 ACTIONS.md):数据管理交互前后的 UI/级联编排;省略 = 不额外改 UI(匠厂宿主 ≥2.8.5)。可选 **`hoverHint`**(见 ACTIONS.md):数据管理工具栏悬停;省略 = 不显示,不回退说明或确认黄条。可选 **表单 `visibleWhen`**(见 ACTIONS.md):Action 参数表单字段条件显隐;省略 = 字段始终平铺;**勿与** **`bind.visibleWhen`**(行菜单)或 Policy L3 混淆;**勿**用其藏映射主键。可选 **`pathRoot`**(见 ACTIONS.md):`file`/`directory` 原生选路对话框起始目录(相对技能数据根;省略默认 `{skill_data}/input`)。可选 **`bind.labelColumn`**(见 ACTIONS.md):选择摘要可读标签列;省略 = 宿主常见列名启发。可选 **`bind.selectionMode`**(见 ACTIONS.md):表勾选如何驱动 Action(`required` / `none` / `optional`);省略 = 宿主按 `$selection.ids` / `batch` 推断;**定时任务等无表勾选入口按无勾选分支**(隐藏 `$selection.ids`,保留 `pickCount`)。可选 **`bind.visibleWhen`**(见 ACTIONS.md「行菜单显隐」):`row` 菜单按当前行列值显隐;省略 = 始终显示。可选 **`revealInForm`**(见 ACTIONS.md):有「已选择」时仍强制显示某 `readOnly` 字段;省略 = 映射只读默认不占 form 参数区(仍提交)。可选 **`cellPresentation`**(见 `references/CELL_PRESENTATION.md`):单元格/整行/整列/区域语义标色;省略 = P0。**用户可见文案**(见 ACTIONS.md「用户可见文案」):弹窗 `description` / 确认黄条 / 参数说明 / 任务失败 `message` 须全中文产品用语;Agent 路由勿塞进弹窗。
3. **五职责正交** — `placements` / `bind.tables` / `executionProfile` / `entrypoint` / domain service 各管一事(见 ACTIONS.md)。可选第六轴 **policy**(见 `references/POLICY.md`):未启用 = L0 与今日一致。可选 **`hostEffects` / `steps`**(见 ACTIONS.md):数据管理交互前后的 UI/级联编排;省略 = 不额外改 UI(匠厂宿主 ≥2.8.5)。可选 **`hoverHint`**(见 ACTIONS.md):数据管理工具栏悬停;省略 = 不显示,不回退说明或确认黄条。可选 **表单 `visibleWhen`**(见 ACTIONS.md):Action 参数表单字段条件显隐;省略 = 字段始终平铺;**勿与** **`bind.visibleWhen`**(行菜单)或 Policy L3 混淆;**勿**用其藏映射主键。可选 **`pathRoot`**(见 ACTIONS.md):`file`/`directory` 原生选路对话框起始目录(相对技能数据根;省略默认 `{skill_data}/input`)。可选 **`bind.labelColumn`**(见 ACTIONS.md):选择摘要可读标签列;省略 = 宿主常见列名启发。可选 **`bind.selectionMode`**(见 ACTIONS.md):表勾选如何驱动 Action(`required` / `none` / `optional`);省略 = 宿主按 `$selection.ids` / `batch` 推断;**定时任务等无表勾选入口按无勾选分支**(隐藏 `$selection.ids`,保留 `pickCount`)。可选 **`bind.visibleWhen`**(见 ACTIONS.md「行菜单显隐」):`row` 菜单按当前行列值显隐(含 `equalsFrom: currentUser.userId`);`replaces: update|delete` 时宿主内置编辑/删除同步读同一规则;省略 = 始终显示。可选 **`revealInForm`**(见 ACTIONS.md):有「已选择」时仍强制显示某 `readOnly` 字段;省略 = 映射只读默认不占 form 参数区(仍提交)。可选 **`cellPresentation`**(见 `references/CELL_PRESENTATION.md`):单元格/整行/整列/区域语义标色;省略 = P0。**用户可见文案**(见 ACTIONS.md「用户可见文案」):弹窗 `description` / 确认黄条 / 参数说明 / 任务失败 `message` 须全中文产品用语;Agent 路由勿塞进弹窗。
4. **长耗时建议 async;页内生成建议 stream** — 开浏览器、RPA、HTTP 大文件下载等建议 `executionProfile: "async"`,进**任务中心**;HTML 内 LLM/长生成要边出边看时用 `"stream"`(静默 Job)。这是对**方法**的选择,**不**限制可以出现在哪些 placement / 入口。
5. **Agent:短 CLI / 长 Action** — 短查询可用共享 Python bash CLI;长任务 / RPA **禁止** `exec` + `process poll`,必须 `run_skill_action`。
6. **进度走 Journal** — `emit` / `video.add_step` → Run Journal(任务中心 / Run Card);`stream` 的增量走 Journal `stream_delta`(经 `stream_channel`),由 HTML `jiangchang.stream` 消费;**不要**靠 stdout 假流式。

View File

@@ -170,14 +170,25 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
"visibleWhen": { "field": "status", "in": ["pending_review", "skipped"] }
```
也支持 `{ "field": "status", "equals": "approved" }`。规则缺失或无法解析时宿主视为始终可见(兼容旧技能)。
也支持:
| 形状 | 含义 |
|------|------|
| `{ "field": "status", "equals": "approved" }` | 等于常量 |
| `{ "field": "status", "in": ["a", "b"] }` | 属于集合 |
| `{ "field": "owner_user_id", "equalsFrom": "currentUser.userId" }` | 等于宿主当前用户(`userId` / `username`) |
| `{ "field": "owner_user_id", "notEqualsFrom": "currentUser.userId" }` | 不等于当前用户 |
| `{ "all": [ … ] }` / `{ "any": [ … ] }` | 组合 AND / OR |
规则缺失或无法解析时宿主视为始终可见(兼容旧技能)。
**要点:**
1. 只影响 **UI 是否展示**;CLI / Agent 仍可直接调同一 entrypoint,技能侧仍应校验状态。
2. 比较用行的**存库原始值**(如 `approved`),不是界面中文「已采用」。
3. 顶栏 / batch 按钮不受 `bind.visibleWhen` 约束(无「当前行」);批量入口靠勾选 + 技能跳过非法状态。
4. 示例:[`assets/examples/bind-visible-when-row.action.example.json`](../assets/examples/bind-visible-when-row.action.example.json)。
4. **宿主内置「编辑 / 删除」也会读同一套规则**:若本表声明了 `replaces: update` / `replaces: delete`,宿主行菜单的编辑/删除按钮按**该 replaces Action** 的 `bind.visibleWhen` 显隐(与技能 `row` 按钮同一求值器)。无 `visibleWhen` / 无 replaces → 行为与旧宿主一致(只看表级可否编辑/删除)。示例:[`assets/examples/replaces-delete-owner-visible.action.example.json`](../assets/examples/replaces-delete-owner-visible.action.example.json)。
5. 示例(技能行按钮):[`assets/examples/bind-visible-when-row.action.example.json`](../assets/examples/bind-visible-when-row.action.example.json)。
### 数据管理:勾选驱动模式 `bind.selectionMode`
@@ -453,6 +464,7 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
- `update` / `delete` 可用 `inputMapping`(`$row.$pk`、`$selection.ids`)预填主键。
- `import` 约定:`filePath`(或 `file`)+ 可选 `onError`(`skip`/`abort` 或上例中文枚举);技能自行解析文件与表头(须兼容宿主前缀 `*` 与 lookup `编号 · 名称`,见上文「导入表头与 lookup 单元格」)。
- 整表 `_jiangchang_tables.readonly=1` 时仍隐藏宿主默认新增/编辑/导入与直写删除;**若声明了 `replaces: delete`,宿主仍显示「删除」并走技能 CLI**(不直写 SQLite)。
- **按行显隐宿主编辑/删除**:`replaces: update` / `replaces: delete` 的 `bind.visibleWhen` 同时约束宿主行菜单「编辑」「删除」(见上文「行菜单显隐」第 4 点)。典型:仅本人可删 → `equalsFrom: currentUser.userId`;队列状态机表还常配合 Policy **`hostCapabilities` 不含 `edit`**,禁止裸改状态/结果链接(见 [`POLICY.md`](POLICY.md)「状态机表配方」)。
- **列元数据复用**:Action 表单(含 toolbar/batch 弹窗与 create/update 抽屉)可将 `inputSchema` 字段对齐业务列,复用中文名与列上 **options**(静态枚举 `options_json` **数组**,或已解析的 lookup):
- 优先让属性名 = 物理列名(如 `carrier_code` / `platform_key`);
- 或 `bind.columnMapping`:`{ "carrierCode": "carrier_code" }`;

View File

@@ -1,12 +1,20 @@
# 多数据源声明(SQLite / MySQL 云库)
# 多数据源声明(SQLite / MySQL / 飞书多维表)
本文说明技能如何向匠厂宿主**声明**业务数据库,以及技能 CLI / Action 如何与宿主读写**同一物理库**。
本文说明技能如何向匠厂宿主**声明**业务数据库,以及技能 CLI / Action 如何与宿主读写**同一物理库**(或同一飞书表)。
**产品定位:** 默认仍是本地 SQLite(`{skill_slug}.db`)。需要多用户共享、集中运维或云库时,可显式声明 **MySQL**。UI / API 契约不变;`_jiangchang_*` 展示元数据仍写在同一逻辑库中。
**产品定位:** 默认仍是本地 SQLite(`{skill_slug}.db`)。需要多用户共享、集中运维或云库时,可显式声明 **MySQL**。需要以飞书多维表格为权威数据时,声明 **`feishu_bitable`**。UI / API 契约尽量一致;SQLite/MySQL 的 `_jiangchang_*` 展示元数据写在同一逻辑库中,飞书源用本文件的 **`columns` 覆盖**声明展示。
**宿主版本:** 匠厂 Host **≥ 6.9.28** 才支持 `data-sources.json` 与 MySQL 连接器。
**宿主版本:**
**完整参考实现:** [`process-work-order`](https://git.jc2009.com/client-commons/process-work-order) @ v1.0.2(工单管理 + MySQL 云库试点)。
| 能力 | 最低 Host |
|------|-----------|
| `data-sources.json` + MySQL | **≥ 6.9.28** |
| `kind: feishu_bitable` + `columns` 展示覆盖 | 以当前匠厂 Host 为准(task-plaza 等已落地;发布说明见 Host changelog) |
**完整参考实现:**
- MySQL:[`process-work-order`](https://git.jc2009.com/client-commons/process-work-order) @ v1.0.2
- 飞书:业务技能 `task-plaza`(外包任务板)
---
@@ -16,8 +24,9 @@
|------|------|
| 单机、离线、小数据量、便携部署 | **SQLite**(默认,不配声明文件亦可) |
| 多用户共享、运维集中管库、与外部系统共库 | **MySQL 云库**(显式 opt-in) |
| 权威数据已在飞书多维表、多人在线协作 | **`feishu_bitable`**(显式 opt-in) |
**硬规则:一个 `sourceId` 对应一个物理库。** 禁止双写、禁止 SQLite 与 MySQL 同步镜像。
**硬规则:一个 `sourceId` 对应一个物理库 / 一张飞书业务表声明。** 禁止双写、禁止 SQLite 与 MySQL / 飞书同步镜像。
---
@@ -28,13 +37,13 @@
```
{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/{skill_slug}/
├── data-sources.json # 声明式数据源(可选;见下)
├── .env # MYSQL_DSN 等连接配置
├── .env # MYSQL_DSN / 飞书凭证等连接配置
└── {skill_slug}.db # SQLite 模式下的本地库文件
```
安装/升级时,若用户目录尚无 `data-sources.json` 且技能包内有 `assets/data-sources.example.json`,宿主会**复制一次**示例(不覆盖已有文件)。
模板默认示例为 **SQLite 显式声明**(与自动扫描 `{skill_slug}.db` 等价)。改走 MySQL 时,请参照 `assets/examples/data-sources.mysql.example.json` 手工替换用户目录中的 `data-sources.json`,并在 `.env` 配置 DSN。
模板默认示例为 **SQLite 显式声明**(与自动扫描 `{skill_slug}.db` 等价)。改走 MySQL / 飞书时,请参照 `assets/examples/` 下对应示例手工替换用户目录中的 `data-sources.json`,并在 `.env` 配置密钥。
若不需要声明式接入,可删除用户目录中的 `data-sources.json`,宿主恢复自动扫描 `.db` 文件。
@@ -54,10 +63,13 @@
| 字段 | 必填 | 说明 |
|------|------|------|
| `id` | 是 | 英文标识,如 `primary` |
| `kind` | 是 | `sqlite` 或 `mysql` |
| `kind` | 是 | `sqlite` \| `mysql` \| `feishu_bitable` |
| `displayName` | 否 | 侧栏展示名;多库时建议填写 |
| `path` | sqlite 必填 | 相对技能数据目录的路径;可用 `{skillSlug}` 占位 |
| `connectionEnv` | mysql 必填 | `.env` 中 DSN 的环境变量名,如 `MYSQL_DSN` |
| `appIdEnv` / `appSecretEnv` / `appTokenEnv` | feishu 必填 | `.env` 中飞书应用与多维表 app_token 的变量名 |
| `tableIdEnv` | feishu 常用 | `.env` 中业务表 table_id 的变量名(或宿主约定的等价字段) |
| `columns` | 否 | **列展示覆盖**(源无关;见下节) |
### SQLite 示例(模板默认)
@@ -67,20 +79,79 @@
见 [`../assets/examples/data-sources.mysql.example.json`](../assets/examples/data-sources.mysql.example.json)。
### 飞书多维表示例
见 [`../assets/examples/data-sources.feishu_bitable.example.json`](../assets/examples/data-sources.feishu_bitable.example.json)。
---
## `.env` 与 DSN
## `columns` 展示覆盖(源无关)
MySQL 连接串写在用户 `.env`(或进程环境变量),**不要**写进 `data-sources.json`:
连接器先按后端生成列 schema(SQLite `_jiangchang_columns` / MySQL 元数据 / 飞书字段映射),再叠加 `sources[].columns`:
```json
"columns": {
"状态": { "editable": false, "searchable": true },
"结果链接": { "displayType": "url", "editable": false, "searchable": false },
"发单人": {
"displayType": "userId",
"editable": false,
"formVisible": false,
"defaultValueFrom": "currentUser.userId",
"searchable": true
},
"平台": {
"searchable": true,
"optionsWhen": {
"dependsOn": "任务类型",
"map": { "文章发布": ["头条号", "搜狐号"], "视频发布": ["抖音", "视频号"] }
}
}
}
```
| 字段 | 类型 | 作用 |
|------|------|------|
| `displayType` | string | 覆盖展示类型(如 `url`、`textarea`、`userId`、`currency`) |
| `displayName` | string | 覆盖列显示名 |
| `defaultValue` | string/number/boolean/null | 新增表单默认值 |
| `defaultValueFrom` | string | 从宿主会话预填:`currentUser.userId` / `currentUser.username` / `currentUser.usernameOrUserId` |
| `optionsWhen` | object | 依赖另一字段当前值过滤下拉:`dependsOn` + `map` |
| `createable` | boolean | `false` = 不进新增表单 / 不进 create payload |
| `editable` | boolean | `false` = 不进编辑表单 / 不进 update payload(状态机字段常用) |
| `formVisible` | boolean | `false` = 表单不展示但仍可按 createable/editable 写入(如静默 `defaultValueFrom`) |
| `requiredOnCreate` | boolean | 新增必填 |
| `searchable` | boolean | 是否进**列筛选**与**顶栏全局搜索**(覆盖连接器默认) |
**与 `_jiangchang_columns` 的关系:**
| 后端 | 主声明位置 |
|------|------------|
| SQLite / MySQL | `_jiangchang_columns`(`searchable` / `editable` / `display_type` 等);`data-sources.columns` 可再覆盖 |
| 飞书 | **无** `_jiangchang_*` 表;**以 `data-sources.json` 的 `columns` 为准**覆盖飞书字段映射默认值 |
飞书连接器默认:明文与单选等常见字段可筛;技能用 `searchable: true/false` 收紧或放开筛选列(避免「全表可筛」噪音)。详见 [`SCHEMA.md`](SCHEMA.md)「列筛选与全局搜索」与「`display_type: url` 外开」。
---
## `.env` 与连接串
密钥写在用户 `.env`(或进程环境变量),**不要**写进 `data-sources.json`:
```env
# MySQL 云库连接(不配则走本地 SQLite)
MYSQL_DSN=mysql://user:password@192.168.0.20:3306/your_database
# 飞书多维表(变量名须与 data-sources.json 中 *Env 一致)
FEISHU_APP_ID=cli_xxx
FEISHU_APP_SECRET=xxx
FEISHU_BITABLE_APP_TOKEN=bascnxxx
FEISHU_TASK_TABLE_ID=tblxxx
```
格式:`mysql://` 或 `mysql2://`,须含库名。注释写法须符合 [`../development/CONFIG.md`](../development/CONFIG.md)(配置管理会展示给用户)。
MySQL 格式:`mysql://` 或 `mysql2://`,须含库名。注释写法须符合 [`../development/CONFIG.md`](../development/CONFIG.md)。
技能 Python 侧:`scripts/db/backend.py` 读取同一 `MYSQL_DSN`;有值则 `resolve_backend()` 为 `mysql`,否则 `sqlite`。
技能 Python 侧(SQLite/MySQL):`scripts/db/backend.py` 读取同一 `MYSQL_DSN`;有值则 `resolve_backend()` 为 `mysql`,否则 `sqlite`。飞书读写由技能自备 SDK / HTTP,与宿主连接器并行遵守同一张表、同一凭证 env。
---
@@ -88,13 +159,13 @@ MYSQL_DSN=mysql://user:password@192.168.0.20:3306/your_database
### 宿主 UI(数据管理)
宿主读取用户目录 `data-sources.json` + `.env`,经 Host 内置连接器访问库。**不依赖**技能 `connection.py`。
宿主读取用户目录 `data-sources.json` + `.env`,经 Host 内置连接器访问库 / 飞书表。**不依赖**技能 `connection.py`。
### CLI / Action / `init-db`
**必须**与宿主使用同一后端策略,否则会出现「UI 看云库、CLI 写本地 `.db`」的分裂。
模板已提供:
模板已提供(SQLite / MySQL):
| 模块 | 作用 |
|------|------|
@@ -102,23 +173,31 @@ MYSQL_DSN=mysql://user:password@192.168.0.20:3306/your_database
| `scripts/db/cursor_compat.py` | 统一 `?` 占位符与 `executescript` |
| `scripts/db/connection.py` | `get_conn()` 双分支;`init_db()` 幂等建 `_jiangchang_*` |
业务表 DDL / 迁移仍在技能自己的 `schema` / `init_db()` 中按 `conn.backend` 分支处理(见工单参考实现)。
业务表 DDL / 迁移仍在技能自己的 `schema` / `init_db()` 中按 `conn.backend` 分支处理(见工单参考实现)。飞书技能无本地业务 DDL,但仍须提供 `health` / Action 与宿主同一表。
可选:`describe_active_store()` 用于 health / 排障,输出当前连的是 sqlite 还是 mysql。
可选:`describe_active_store()` 用于 health / 排障。
---
## MySQL 当前限制(Host)
## MySQL / 飞书 当前限制(Host)
**MySQL:**
- 不支持 `tableAdmin`(建表/改表 UI)
- 不支持 `cellPresentation`
- 元数据与 CRUD 与 SQLite 路径一致
表结构变更仍由技能 `init-db` / 迁移脚本负责。
**飞书:**
- 不支持本地式 `tableAdmin` / 随意改物理 DDL(字段以飞书后台为准)
- 筛选/排序能力受飞书 API 约束;用 `columns.searchable` 控制筛选项
- 附件 / 人员等字段依赖宿主映射的 `displayType`(如 `attachment`、`userId`)
表结构变更:SQLite/MySQL 由技能 `init-db` / 迁移负责;飞书在飞书后台改字段后,同步更新 `columns` 覆盖与技能读写字段名。
---
## 开发 Checklist(云库)
## 开发 Checklist(云库 / 飞书)
复制新业务技能并选择 MySQL 时:
@@ -130,10 +209,18 @@ MYSQL_DSN=mysql://user:password@192.168.0.20:3306/your_database
6. **抽测**:数据管理 UI 与 CLI(如 list / create)读写同一库
7. 见 [`../development/CHECKLIST.md`](../development/CHECKLIST.md) A6-16
选择飞书时:
1. Host 已支持 `feishu_bitable`
2. `data-sources.json` + `.env` 凭证齐全
3. `columns` 收紧 `searchable` / `editable`,结果链接用 `displayType: url`
4. CLI / Action 与数据管理抽测同一 `app_token` + `table_id`
5. 见 CHECKLIST **A6-17**
---
## 与 SCHEMA / ACTIONS 的关系
- [`SCHEMA.md`](SCHEMA.md):`_jiangchang_*` 契约适用于**已注册数据源**上的表,不限于本地文件。
- [`ACTIONS.md`](ACTIONS.md):无 `replaces` 时宿主直写**已注册数据源**(默认本地 SQLite)。
- [`CLI.md`](CLI.md):`init-db` 对当前 active backend 幂等初始化元数据。
- [`SCHEMA.md`](SCHEMA.md):`_jiangchang_*` 适用于 SQLite/MySQL 已注册表;飞书用本文件 `columns`。`url` 外开、列筛选语义两边对齐。
- [`ACTIONS.md`](ACTIONS.md):无 `replaces` 时宿主直写**已注册数据源**;状态机表常用 Policy 关裸编辑 + `replaces: delete` + `visibleWhen`。
- [`CLI.md`](CLI.md):`init-db` 对当前 active backend(sqlite/mysql)幂等初始化元数据。

View File

@@ -168,6 +168,26 @@
未知字符串:忽略。**禁止**把「省略」解释成全禁。
### 2.5 状态机 / 队列表配方(推荐)
业务状态只能走领取、提交、验收、结算等 Action,**禁止**用户在数据管理里裸改状态 / 结果链接时:
```json
{
"schemaVersion": 1,
"skill": "your-skill-slug",
"actor": { "userId": "u1" },
"actions": { "mode": "open" },
"hostCapabilities": ["refresh", "view", "export", "import"]
}
```
要点:
1. **`hostCapabilities` 不含 `edit`(也不含直写 `delete`)** → 宿主隐藏裸「编辑」;状态机字段同时建议列级 `editable: false`(SQLite `_jiangchang_columns` 或飞书 `data-sources.columns`)。
2. 删除仍要走技能校验时:声明 **`replaces: delete`**,并用 `bind.visibleWhen` + `equalsFrom: currentUser.userId` 控制「仅本人可见删除」(宿主行菜单与技能按钮同一规则;见 [`ACTIONS.md`](ACTIONS.md))。
3. `mode: open` 不挡业务 Action;需要收紧技能按钮时再改 `allowlist`。
---
## 3. L2:`data` 与 scope 语法白名单

View File

@@ -346,11 +346,24 @@ upsert_column_metadata(
| `year_month` | `TEXT` 推荐 `YYYY-MM` | 原文 | **年月选择** | `YYYY-MM` 字符串;别名 `month` |
| `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`(薄层;校验从宽) | 原文 |
| `url` | `TEXT` | **可点击链接**(见下) | 单行输入 `type=url`(薄层;校验从宽) | 原文 |
| `email` | `TEXT` | 原文 | 单行输入 `type=email`(薄层;校验从宽) | 原文 |
说明灰字与创建默认值见下一小节;与 Action 的对照写在 [`ACTIONS.md`](ACTIONS.md)「表单字段类型对照」。
#### display_type: url 列表 / 详情外开(宿主契约)
声明为 `url`(SQLite `_jiangchang_columns.display_type`,或 `data-sources.json` 的 `columns.*.displayType`)时:
| 行为 | 约定 |
|------|------|
| 表格单元格 / 详情只读展示 | 合法 **`http://` / `https://`** 显示为可点击链接 |
| 点击 | 用**系统默认浏览器**打开(Electron `openExternal`);非 Electron 环境可降级为新标签页 |
| 非法或非 http(s) | 按普通文本展示,不可点 |
| 表单编辑 | 仍是单行 URL 输入;**不**在输入框内嵌浏览器 |
技能只需声明类型;**不要**再为「外开链接」单独做 Action。飞书等多维表源同样认 `columns.*.displayType: "url"`。
**明确不在本版稳定范围(勿在新技能宣传为已支持):**
- 文件 / 图片上传
@@ -359,7 +372,9 @@ upsert_column_metadata(
> **Lookup 多选**已纳入本版契约(见下文 `multiple`),与「任意多选/标签数组」不是同一能力。
`datetime_unix_milliseconds`:仅兼容**列表展示**旧数据;**新技能不要用**;可编辑时间一律 `datetime_unix_seconds`。
`datetime_unix_milliseconds`:仅兼容**列表展示**旧数据(含部分飞书时间列映射);**新 SQLite 技能不要用**;可编辑时间一律 `datetime_unix_seconds`。
远程源(如飞书)还可能出现宿主映射的 `userId` / `currency` / `attachment` 等展示类型:用于列表/表单呈现;**新本地 SQLite 技能仍优先使用上表稳定类型**,勿把飞书专用类型抄进 DDL 当默认。
#### 新增/编辑表单:字段说明与默认值(宿主支持)

View File

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

View File

@@ -564,6 +564,9 @@ class TestActionsManifest(unittest.TestCase):
bind_props = schema["$defs"]["bind"]["properties"]
self.assertIn("visibleWhen", bind_props)
self.assertIn("visibleWhenRule", schema["$defs"])
rule_desc = schema["$defs"]["visibleWhenRule"].get("description") or ""
self.assertIn("equalsFrom", rule_desc)
self.assertIn("notEqualsFrom", rule_desc)
manifest = {
"schemaVersion": 1,
"skill": "your-skill-slug",
@@ -600,6 +603,54 @@ class TestActionsManifest(unittest.TestCase):
}
jsonschema.validate(instance=manifest, schema=schema)
def test_schema_allows_visible_when_equals_from_and_all(self) -> None:
try:
import jsonschema
except ImportError:
self.fail("jsonschema is required")
schema = _load_actions_schema()
manifest = {
"schemaVersion": 1,
"skill": "your-skill-slug",
"actions": [
{
"id": "probe-owner-delete",
"label": "删除",
"description": "验证 equalsFrom 与 all 可通过 Schema",
"replaces": "delete",
"placements": [],
"executionProfile": "sync",
"interaction": "confirm",
"confirmation": {"message": "确认删除?"},
"bind": {
"tables": ["demo_items"],
"visibleWhen": {
"all": [
{
"field": "owner_user_id",
"equalsFrom": "currentUser.userId",
},
{"field": "status", "in": ["open", "draft"]},
]
},
"inputMapping": {"itemId": "$row.$pk"},
},
"inputSchema": {
"type": "object",
"properties": {
"itemId": {"type": "string", "readOnly": True},
},
},
"entrypoint": {
"type": "cli",
"command": "item-delete",
"args": ["--id", "{{itemId}}"],
},
}
],
}
jsonschema.validate(instance=manifest, schema=schema)
def test_action_examples_user_facing_copy_avoids_jargon(self) -> None:
"""示例 Action 的用户可见字符串不得含常见开发黑话(属性名本身除外)。"""
examples_dir = os.path.join(get_skill_root(), "assets", "examples")

View File

@@ -386,6 +386,8 @@ class TestDocsStandards(unittest.TestCase):
"*显示名",
"table_file_import.py",
"导入模板 lookup 单元格",
"url 列表 / 详情外开",
"系统默认浏览器",
):
self.assertIn(marker, text, msg=f"SCHEMA.md missing {marker!r}")
@@ -399,16 +401,31 @@ class TestDocsStandards(unittest.TestCase):
"一个 `sourceId`",
"process-work-order",
"tableAdmin",
"feishu_bitable",
"columns",
"searchable",
"defaultValueFrom",
"optionsWhen",
"displayType",
):
self.assertIn(marker, text, msg=f"DATA_SOURCES.md missing {marker!r}")
self.assertTrue(os.path.isfile(os.path.join(get_skill_root(), "assets/data-sources.example.json")))
self.assertTrue(
os.path.isfile(os.path.join(get_skill_root(), "assets/examples/data-sources.mysql.example.json"))
)
self.assertTrue(
os.path.isfile(
os.path.join(
get_skill_root(),
"assets/examples/data-sources.feishu_bitable.example.json",
)
)
)
env_example = self._read(".env.example")
self.assertIn("MYSQL_DSN", env_example)
checklist = self._read("development/CHECKLIST.md")
self.assertIn("A6-16", checklist)
self.assertIn("A6-17", checklist)
self.assertIn("DATA_SOURCES.md", checklist)
def test_actions_md_documents_form_schema_split(self) -> None:
@@ -519,6 +536,9 @@ class TestDocsStandards(unittest.TestCase):
self.assertIn("A4-10", checklist)
self.assertIn("A4-11", checklist)
self.assertIn("bind.visibleWhen", checklist)
self.assertIn("A4-15", checklist)
self.assertIn("equalsFrom", checklist)
self.assertIn("A6-18", checklist)
self.assertIn("用户可见文案", checklist)
runtime = self._read("development/SKILL_ACTION_RUNTIME.md")
self.assertIn("visibleWhen", runtime)
@@ -532,8 +552,18 @@ class TestDocsStandards(unittest.TestCase):
self.assertIn("双受众", actions)
self.assertIn("行菜单显隐", actions)
self.assertIn("bind.visibleWhen", actions)
self.assertIn("equalsFrom", actions)
self.assertIn("currentUser.userId", actions)
self.assertIn("宿主内置「编辑 / 删除」", actions)
self.assertIn("无表勾选入口", actions)
self.assertIn("编号列表", actions)
self.assertIn(
"replaces-delete-owner-visible.action.example.json",
self._read("assets/examples/README.md"),
)
policy = self._read("references/POLICY.md")
self.assertIn("状态机", policy)
self.assertIn("hostCapabilities", policy)
def test_guarded_docs_do_not_reference_amazon_implementation(self) -> None:
forbidden = (