From 90eefc8653e96e723f9cf8dc45bb3b5b83c4decd Mon Sep 17 00:00:00 2001 From: chendelian <116870791@qq.com> Date: Sun, 1 Nov 2026 16:30:46 +0800 Subject: [PATCH] Precipitate host contracts from task-plaza: visibleWhen equalsFrom, replaces gates, url openExternal, feishu data-sources (v1.0.128) --- CHANGELOG.md | 7 + SKILL.md | 2 +- assets/examples/README.md | 2 + .../data-sources.feishu_bitable.example.json | 50 +++++++ ...s-delete-owner-visible.action.example.json | 53 +++++++ assets/schemas/skill-actions.schema.json | 52 ++++++- development/CHECKLIST.md | 5 +- development/SKILL_ACTION_RUNTIME.md | 2 +- references/ACTIONS.md | 16 ++- references/DATA_SOURCES.md | 133 +++++++++++++++--- references/POLICY.md | 20 +++ references/SCHEMA.md | 19 ++- scripts/util/constants.py | 2 +- tests/test_actions_manifest.py | 51 +++++++ tests/test_docs_standards.py | 30 ++++ 15 files changed, 412 insertions(+), 32 deletions(-) create mode 100644 assets/examples/data-sources.feishu_bitable.example.json create mode 100644 assets/examples/replaces-delete-owner-visible.action.example.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 98af46c..cdd1842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/长生成流式,不进任务中心主路径 diff --git a/SKILL.md b/SKILL.md index e904fbc..eee8b68 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 技能开发模板(通用业务版) description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。" -version: 1.0.127 +version: 1.0.128 author: 深圳匠厂科技有限公司 metadata: openclaw: diff --git a/assets/examples/README.md b/assets/examples/README.md index 48cb5e5..8589422 100644 --- a/assets/examples/README.md +++ b/assets/examples/README.md @@ -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`「行菜单显隐」)。 diff --git a/assets/examples/data-sources.feishu_bitable.example.json b/assets/examples/data-sources.feishu_bitable.example.json new file mode 100644 index 0000000..7448707 --- /dev/null +++ b/assets/examples/data-sources.feishu_bitable.example.json @@ -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": { + "文章发布": ["头条号", "搜狐号"], + "视频发布": ["抖音", "视频号"] + } + } + } + } + } + ] +} diff --git a/assets/examples/replaces-delete-owner-visible.action.example.json b/assets/examples/replaces-delete-owner-visible.action.example.json new file mode 100644 index 0000000..232b0d0 --- /dev/null +++ b/assets/examples/replaces-delete-owner-visible.action.example.json @@ -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}}"] + } +} diff --git a/assets/schemas/skill-actions.schema.json b/assets/schemas/skill-actions.schema.json index 8d88398..d0e63c0 100644 --- a/assets/schemas/skill-actions.schema.json +++ b/assets/schemas/skill-actions.schema.json @@ -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, diff --git a/development/CHECKLIST.md b/development/CHECKLIST.md index 7bbaa9a..ce8dc91 100644 --- a/development/CHECKLIST.md +++ b/development/CHECKLIST.md @@ -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** --- diff --git a/development/SKILL_ACTION_RUNTIME.md b/development/SKILL_ACTION_RUNTIME.md index 5a0c358..9e7faa5 100644 --- a/development/SKILL_ACTION_RUNTIME.md +++ b/development/SKILL_ACTION_RUNTIME.md @@ -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 假流式。 diff --git a/references/ACTIONS.md b/references/ACTIONS.md index d15feea..1550300 100644 --- a/references/ACTIONS.md +++ b/references/ACTIONS.md @@ -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" }`; diff --git a/references/DATA_SOURCES.md b/references/DATA_SOURCES.md index ea2a60b..e548768 100644 --- a/references/DATA_SOURCES.md +++ b/references/DATA_SOURCES.md @@ -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)幂等初始化元数据。 diff --git a/references/POLICY.md b/references/POLICY.md index 166d20b..b2e61e2 100644 --- a/references/POLICY.md +++ b/references/POLICY.md @@ -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 语法白名单 diff --git a/references/SCHEMA.md b/references/SCHEMA.md index 204ca1f..ebe6ce7 100644 --- a/references/SCHEMA.md +++ b/references/SCHEMA.md @@ -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 当默认。 #### 新增/编辑表单:字段说明与默认值(宿主支持) diff --git a/scripts/util/constants.py b/scripts/util/constants.py index 6c136fa..8057a81 100644 --- a/scripts/util/constants.py +++ b/scripts/util/constants.py @@ -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" diff --git a/tests/test_actions_manifest.py b/tests/test_actions_manifest.py index f9527d4..347ecca 100644 --- a/tests/test_actions_manifest.py +++ b/tests/test_actions_manifest.py @@ -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") diff --git a/tests/test_docs_standards.py b/tests/test_docs_standards.py index dc50080..f423f07 100644 --- a/tests/test_docs_standards.py +++ b/tests/test_docs_standards.py @@ -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 = (