Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 295b2356ac | |||
| 7257d09813 | |||
| 9d33c5ea2c | |||
| b5d120469b | |||
| d35d04f779 | |||
| cbebd19ab3 | |||
| 05764cac15 | |||
| 0a6b0171d8 | |||
| 43486afec4 | |||
| 225d5c0777 | |||
| fbf326d525 | |||
| 28c3c8d8ab | |||
| 571c135114 | |||
| d840745758 | |||
| cf3f0ae990 | |||
| a11cdf4e7e | |||
| 81096051dd | |||
| f221f81d4e | |||
| 3738f0bea2 | |||
| 6ebc570632 | |||
| 2d02ccff5d |
119
CHANGELOG.md
119
CHANGELOG.md
@@ -9,6 +9,125 @@
|
||||
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
|
||||
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
|
||||
|
||||
## 1.0.102
|
||||
|
||||
- 沉淀数据管理 Action「已选择」摘要契约:confirm/form 弹层由宿主默认展示勾选对象(#id + 可读标签);技能零改动即可用
|
||||
- 新增可选 `bind.labelColumn`(仅影响展示,不改提交 payload,不决定业务跳过逻辑)
|
||||
- 更新 `skill-actions.schema.json`、`ACTIONS.md`、`SKILL_ACTION_RUNTIME.md`;新增 `selection-label-column.action.example.json`
|
||||
|
||||
## 1.0.101
|
||||
|
||||
- 沉淀 Action 表单本地路径契约:`file` / `directory` 可选 **`pathRoot`**(相对技能数据根的起始目录;省略则宿主默认打开本技能 `input/`)
|
||||
- 更新 `skill-actions.schema.json`、`ACTIONS.md`、`DATA_PATHS.md`;新增 `assets/examples/path-root-file-action.example.json`
|
||||
- 与宿主导入面板默认 `input/` 划清边界;禁止技能写死用户 ID 或绝对盘符
|
||||
|
||||
## 1.0.100
|
||||
|
||||
- 沉淀用户市场文档契约 `development/TUTORIAL_SURFACES.md`:**说明 Tab ≠ 教程 Tab**;教程按数据管理 ∥ 对话、定时可选三入口结构
|
||||
- 重写根 `README.md` / `TUTORIAL.md` 模板骨架;CHECKLIST 增 A2-7~A2-11;`actions.json` 准备类优先 toolbar、skill-detail 不可作教程主路径
|
||||
- 新增 `tests/test_tutorial_surfaces.py`(POLICY-TUTORIAL-001);REQUIREMENTS §0.1 增教程章节列与用户文档对齐要求
|
||||
|
||||
## 1.0.99
|
||||
|
||||
- 沉淀数据管理**表注册门禁**契约:存在 `_jiangchang_tables` 时,宿主仅展示注册且 `visible=1` 的表;孤儿物理表不展示(匠厂宿主 ≥6.8.25)
|
||||
- 新增「表增删改与退役」:`drop_deprecated_user_table()` 标准迁移;禁止只删元数据留物理表;新技能禁止 `task_logs`
|
||||
- `LOGGING.md` 可观测性三分法;`DATA_PATHS.md` 卸载不删用户数据;Checklist A6-13 / A6-14;`tests/samples/test_table_deprecation_migration.py.sample`
|
||||
|
||||
## 1.0.98
|
||||
|
||||
- 沉淀数据管理**列筛选与全局搜索**契约:`searchable` 同时控制列筛选字段列表与顶栏全局搜索;存库 value / 展示 label 分工;筛选栏枚举/boolean/lookup 用下拉 Select(与表单开关/分段单选区分)
|
||||
- 全局搜索:options label 映射、boolean 别名、lookup 多选 token、`datetime_unix_seconds` 日期区间
|
||||
- `ACTIONS.md` 表单对照表补充「列筛选」脚注;`CHECKLIST` A6-11 / A6-12;`display_metadata_validator` 对未开 searchable 的枚举/boolean 列软 warning
|
||||
|
||||
## 1.0.97
|
||||
|
||||
- 沉淀数据管理 HTML 预览**布局契约**:`references/ACTIONS.md` 新增「宿主预览布局」(流体铺满 iframe、禁横向滚动、表格断行等)
|
||||
- 升级 `assets/html/example-report.html` 为通用布局基线;`test_html_page_service` 守护不退化
|
||||
- `DEVELOPMENT.md` / `CHECKLIST`(S0-12、A6-10)/ `TESTING.md` 交叉引用
|
||||
|
||||
## 1.0.96
|
||||
|
||||
- 在 `references/ACTIONS.md` 沉淀数据管理技能按钮中文名(`label`)参考表:同类通用动作用同一动词,不是白名单
|
||||
- 打开类改为「打开目录 / 打开浏览器」,一点即开、成功不弹「执行成功」;示例不再使用「打开文件位置」
|
||||
- 交活 Checklist A7-7;POLICY_MATRIX 软规则:不扫描 label 关键字
|
||||
|
||||
## 1.0.95
|
||||
|
||||
- 正式沉淀「业务行生命周期」通用契约(`references/WORK_ITEM_STATUS.md`):用可操作/进行中/成功·失败·跳过类终态等**角色**约束处理队列,不绑死某一技能的状态名
|
||||
- 默认只处理可操作态;开始抢占进行中;结束回写成功或失败(失败带原因);重做靠人工改回可操作
|
||||
- REQUIREMENTS §0.3 状态→角色映射;CHECKLIST S0-11 / A6-7~A6-9;SKILL / ACTIONS / SCHEMA / references 索引交叉引用
|
||||
|
||||
## 1.0.94
|
||||
|
||||
- 修正 1.0.93 更新日志重复小节
|
||||
|
||||
## 1.0.93
|
||||
|
||||
- 沉淀 Lookup **多选**契约:同一 `lookup` / `options_json` 对象上加 `multiple: true`(平行扩展单选,不另造 display_type)
|
||||
- 多选存 `TEXT` 逗号分隔 id;列表展示映射为多个 label;控件由宿主按选项数量选择(短列表勾选 / 长列表 Chip+下拉)
|
||||
- 更新 SCHEMA / ACTIONS / skill-actions.schema / lookup 示例与 options_json 校验
|
||||
|
||||
## 1.0.92
|
||||
|
||||
- 沉淀数据管理 `replaces: import`:导入 UI 统一宿主面板,确认后由技能写库
|
||||
- ACTIONS / schema / 守护测试对齐 create·update·delete·import;补充可抄示例 `replaces-*.action.example.json`
|
||||
- 修正旧文档「宿主导入仍直写」的过时说明
|
||||
|
||||
## 1.0.91
|
||||
|
||||
- 沉淀「表与表之间禁止 SQLite 外键」契约:只存业务 id,关联与级联在 repository/service;界面用 `lookup`
|
||||
- `display_metadata_validator` 扫描业务表 DDL,含 `FOREIGN KEY` / `REFERENCES` 时报错
|
||||
- 交活 Checklist 新增 A6-6;`lookup` 文档用语改为「跨表引用 id」,避免与 SQLite 外键混淆
|
||||
|
||||
## 1.0.90
|
||||
|
||||
- 沉淀数据管理表格说明契约:`_jiangchang_tables.grid_help` / `_jiangchang_columns.grid_help`
|
||||
- 有内容才显示表上方说明和列头「?」;不写则与今日一致
|
||||
- 与新增/编辑表单的 `description` 独立,禁止互拷(见 `references/SCHEMA.md`)
|
||||
|
||||
## 1.0.89
|
||||
|
||||
- 仓库增加 MIT 许可证,版权归属深圳匠厂科技有限公司
|
||||
- 增加 SECURITY.md:未公开漏洞请通过匠厂官网联系,不要发在公开 Issue
|
||||
- 模板 developer_ids 改为占位 12345,复制后请换成自己的匠厂用户 ID
|
||||
|
||||
## 1.0.88
|
||||
|
||||
- `release.ps1` 默认 tag 说明改为 UTF-8 字节写入,不再依赖脚本源码里的中文默认参数(Windows PowerShell 5.1 会把无 BOM 的 UTF-8 按系统代码页解析)
|
||||
- 不改写已发布 tag(v1.0.86 / v1.0.87 说明仍可能乱码)
|
||||
|
||||
## 1.0.87
|
||||
|
||||
- `release.ps1` 打 annotated tag / 自动 commit 改为 UTF-8 消息文件 + `git -F`,避免 `-m` 走控制台代码页
|
||||
- 默认说明仍可能乱码(见 1.0.88)
|
||||
|
||||
## 1.0.86
|
||||
|
||||
- 去掉 `release.ps1` 里通过 `cmd /c` 调 git 的写法;中文 tag 说明在部分 Windows 控制台代码页下仍可能乱码,见 1.0.87
|
||||
|
||||
## 1.0.85
|
||||
|
||||
- 新增开发主文 `development/PRODUCT_SURFACES.md`:说明对话 / 数据管理 / 定时任务平级关系、何时走哪个入口、如何做入口定位
|
||||
- 需求与交活清单增加入口定位表;纠正「只做对话即可」与「Action 一概可选」的误导语气
|
||||
- 数据管理明确为 UI 执行面 + 过程可见,与对话同内核
|
||||
|
||||
## 1.0.84
|
||||
|
||||
- 沉淀 Action 参数表单字段条件显隐契约 `visibleWhen`(先选模式再出对应参数)
|
||||
- 文档入口:`ACTIONS.md` / `SKILL_ACTION_RUNTIME.md` / `CHECKLIST` A4-7;与 Policy L3 行态 `visibleWhen` 消歧
|
||||
- 新增示例 `assets/examples/visible-when-action.example.json`;Schema 已支持该字段
|
||||
|
||||
## 1.0.83
|
||||
|
||||
- 沉淀跨表 Lookup 契约:Action 可用内联 `lookup` 或绑列下拉,禁止让用户手填内部编号
|
||||
- 统一展示:`编号 · 名称`,可选 `extra_column` 时为 `编号 · 名称(额外)`
|
||||
- 更新 `ACTIONS.md` / `SCHEMA.md` / `skill-actions.schema.json`、示例与验收清单
|
||||
|
||||
## 1.0.82
|
||||
|
||||
- 沉淀数据管理技能按钮可选悬停字段 `hoverHint`:短名看不懂或会覆盖数据时再写;不回退说明或确认黄条
|
||||
- 宿主内置删除/刷新/新增/导入/导出不走该字段;默认 health/version/config-path 不加悬停
|
||||
|
||||
## 1.0.81
|
||||
|
||||
- 模板默认不再创建或写入 `task_logs`:过程看运行日志,进度看任务中心,业务结果看业务表
|
||||
|
||||
10
DEMO.md
10
DEMO.md
@@ -15,9 +15,13 @@ demo_video_url: ""
|
||||
|
||||
## 建议观看顺序
|
||||
|
||||
1. 如何发起一次典型任务
|
||||
2. 执行过程中会看到什么提示 / 进度
|
||||
3. 成功后的结果长什么样
|
||||
建议与 `TUTORIAL.md` 三入口结构对齐(有则拍、无则跳过):
|
||||
|
||||
1. 数据管理:准备数据 → 主流程按钮 → 结果表/报告
|
||||
2. 对话任务(若有):一句自然语言触发同一能力
|
||||
3. 定时任务(若有):到点自动跑;若本技能无定时,演示中可说明「不需要定时」
|
||||
|
||||
成功后的结果长什么样(表、任务中心、HTML 等)
|
||||
|
||||
## 没有视频时
|
||||
|
||||
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 深圳匠厂科技有限公司
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
60
README.md
60
README.md
@@ -1,12 +1,13 @@
|
||||
---
|
||||
description: "用用户能理解的方式说明这个技能能做什么、适合什么场景,以及如何开始使用。"
|
||||
description: "用用户能理解的方式说明这个技能能做什么、适合什么场景,以及边界与结果形态。"
|
||||
---
|
||||
|
||||
> **模板说明(复制后删除本段)**
|
||||
> 本文是技能市场详情与普通用户说明,**不是** Agent 参考索引、CLI 契约或开发文档。
|
||||
> frontmatter 的 `description` 必须是用户能看懂的一句话能力描述;**不得**写成 CLI、schema、references、development、契约、数据结构、开发规范或 Agent 参考索引。
|
||||
> 正文只回答用户关心的问题,**不要**把 Playwright、DOM、Python、RPA、references、development、schema 等作为面向用户的主说明内容;若业务上必须出现技术词,须用用户能理解的方式解释,且不能喧宾夺主。
|
||||
> **复制后验收章节(标题可微调,建议保留):**「能帮你做什么」「适合什么场景」「使用前需要准备什么」「你可以这样告诉 Agent」「执行后你会得到什么」「注意事项」「常见问题」。
|
||||
> 本文是技能市场 **「说明」** Tab(`readme_md`),**不是**使用教程。
|
||||
> frontmatter 的 `description` 必须是用户能看懂的一句话能力描述;**不得**写成 CLI、schema、references、development、契约或「如何开始使用」类教程承诺。
|
||||
> **禁止**写入:分步操作(1/2/3)、Agent 示例话术块、「入口一/入口二」专章、逐步排障。这些一律写在根目录 `TUTORIAL.md`。
|
||||
> 正文只回答:是什么、能做什么、适合谁、准备什么、得到什么、注意事项。完整契约见 `development/TUTORIAL_SURFACES.md` §1。
|
||||
> **复制后验收章节(标题可微调):**「能帮你做什么」「适合什么场景」「使用前需要准备什么」「怎么使用」「执行后你会得到什么」「注意事项」「常见问题」。
|
||||
|
||||
# 【技能名称】
|
||||
|
||||
@@ -17,7 +18,7 @@ description: "用用户能理解的方式说明这个技能能做什么、适合
|
||||
|
||||
## 这个技能能帮你做什么
|
||||
|
||||
用 3 到 6 条业务能力说明技能价值,例如:
|
||||
用 3 到 6 条业务能力说明技能价值(**不要**写成操作步骤),例如:
|
||||
|
||||
- 【自动整理/查询/生成/同步/核对/提交】某类业务数据
|
||||
- 帮你减少重复操作,降低人工录入或核对成本
|
||||
@@ -38,42 +39,33 @@ description: "用用户能理解的方式说明这个技能能做什么、适合
|
||||
|
||||
## 使用前需要准备什么
|
||||
|
||||
说明用户在使用前要准备的东西,例如:
|
||||
说明**前置条件**(装什么、要不要账号、要不要外站登录),**不要**写「去哪张表点哪个按钮」:
|
||||
|
||||
- **网页自动化类:** 首次运行可由技能自动登记平台账号(浏览器会话由系统隔离管理)。也可在「账号管理」里指定登录标识。登记账号 ≠ 已登录目标站
|
||||
- **账号在哪里看:** 已登记账号可在「数据管理 / 账号管理」中查看;每个账号使用独立浏览器会话,批量或多账号任务不会共用登录状态
|
||||
- **是否要登录目标站:** 按本技能说明(有的平台必须登录,有的公开页可不登录);与「是否有账号记录」不是一回事
|
||||
- **网页自动化类:** 可能需要在账号管理中登记平台账号;登记账号 ≠ 已登录目标站(细节见使用教程)
|
||||
- 相关业务权限(查询、导出、提交等)已具备
|
||||
- 需要处理的文件、时间范围、订单号、客户名称、项目名称等信息
|
||||
- 如果涉及验证码或人工确认,请保证能及时在弹出的浏览器窗口中操作
|
||||
- 需要处理的文件、时间范围、单号等信息(若适用)
|
||||
- 若涉及验证码或人工确认,使用时请能及时在弹出窗口中操作
|
||||
|
||||
---
|
||||
|
||||
## 你可以这样告诉 Agent
|
||||
## 怎么使用
|
||||
|
||||
用自然语言示例展示用户可以怎么发起任务,例如:
|
||||
本技能支持哪些宿主入口,用**一两句话**概括即可,例如:
|
||||
|
||||
> “帮我查询今天的【业务对象】,整理成表格。”
|
||||
>
|
||||
> “帮我核对这些【数据/单据】是否有异常,并告诉我问题在哪里。”
|
||||
>
|
||||
> “帮我把【时间范围】内的【业务数据】导出并生成处理报告。”
|
||||
>
|
||||
> “帮我根据这个文件里的内容,逐条完成【业务动作】。”
|
||||
- 支持 **数据管理**(点表操作)与 **对话任务**(自然语言);是否提供定时任务按本技能说明。
|
||||
|
||||
> 注意:示例应是自然语言,不要写命令行命令。
|
||||
**逐步操作、示例话术与排障,请查看技能市场「使用教程」Tab。**
|
||||
(完整写法见 `development/TUTORIAL_SURFACES.md`。)
|
||||
|
||||
---
|
||||
|
||||
## 执行后你会得到什么
|
||||
|
||||
说明用户会看到什么结果,例如:
|
||||
说明用户会看到什么**结果形态**(不要教如何点出来),例如:
|
||||
|
||||
- 任务是否成功完成
|
||||
- 处理了多少条数据
|
||||
- 哪些内容成功、哪些失败
|
||||
- 失败原因和建议处理方式
|
||||
- 生成的文件、表格、报告或记录
|
||||
- 处理了多少条数据;哪些成功、哪些失败
|
||||
- 生成的文件、表格、报告或业务表记录
|
||||
|
||||
---
|
||||
|
||||
@@ -90,18 +82,16 @@ description: "用用户能理解的方式说明这个技能能做什么、适合
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 为什么任务没有查到数据?
|
||||
> FAQ 写**概念与边界**;需要「点哪里」的排障写到「使用教程」。
|
||||
|
||||
> 可能是时间范围、关键词、账号权限或系统筛选条件不正确。请先确认输入信息是否准确。
|
||||
### 这个技能适合我吗?
|
||||
|
||||
### 为什么任务执行到一半需要我操作?
|
||||
> 若你的场景与上文「适合什么场景」一致,可以安装后按「使用教程」完成第一次操作。
|
||||
|
||||
> 可能遇到了登录、验证码、权限确认、二次审批或系统异常。请根据 Agent 的提示完成必要操作。
|
||||
### 我不会操作,从哪里开始?
|
||||
|
||||
### 结果文件在哪里查看?
|
||||
|
||||
> 请根据 Agent 返回的结果说明查看生成文件、任务记录或处理报告。
|
||||
> 请打开技能市场 **「使用教程」** Tab,按「入口一:数据管理」或「入口二:对话任务」任选一种跟做。
|
||||
|
||||
### 已经登记的账号在哪里查看?
|
||||
|
||||
> 请到匠厂的「数据管理 / 账号管理」查看账号记录和状态。若任务仍提示账号异常,请把账号编号、任务时间和界面提示原文提供给支持人员;不要发送密码、Cookie 或验证码。
|
||||
> 请到匠厂的「数据管理 / 账号管理」查看账号记录和状态。不要发送密码、Cookie 或验证码。
|
||||
|
||||
20
SECURITY.md
Normal file
20
SECURITY.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 安全漏洞披露
|
||||
|
||||
本仓库是匠厂技能开发模板。请把**未公开的安全漏洞**发给深圳匠厂科技有限公司,不要在公开 Issue、PR 或讨论区里贴完整复现步骤、PoC 或利用细节。
|
||||
|
||||
## 怎么报告
|
||||
|
||||
请通过匠厂官网联系渠道提交:[https://www.jc2009.com/](https://www.jc2009.com/)
|
||||
|
||||
建议在联系时注明:
|
||||
|
||||
- 标题或开头带 `[Security]`
|
||||
- 影响的仓库 / 版本(例如 `skill-template`、tag `v1.0.x`)
|
||||
- 问题类型、影响范围、复现步骤
|
||||
- 若已有修复思路,可一并说明
|
||||
|
||||
我们会确认收悉,并在有结论后告知是否修复、是否公开披露。
|
||||
|
||||
## 不在本页范围
|
||||
|
||||
本页只说明**如何向我们报告漏洞**。技能运行时的权限、RPA 合规、加密发布等产品安全设计,见 `development/` 与 `references/`,不要写进本文件。
|
||||
24
SKILL.md
24
SKILL.md
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: 技能开发模板(通用业务版)
|
||||
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
|
||||
version: 1.0.81
|
||||
version: 1.0.102
|
||||
author: 深圳匠厂科技有限公司
|
||||
metadata:
|
||||
openclaw:
|
||||
@@ -10,8 +10,7 @@ metadata:
|
||||
emoji: "📦"
|
||||
category: "通用"
|
||||
developer_ids:
|
||||
- 10032
|
||||
- 12428
|
||||
- 12345 # 占位,请换成你的匠厂用户 ID
|
||||
allowed-tools:
|
||||
- bash
|
||||
---
|
||||
@@ -31,12 +30,15 @@ allowed-tools:
|
||||
|
||||
### 架构铁律(先读)
|
||||
|
||||
- **宿主产品入口平级**:对话任务 / 数据管理 / 定时任务是同一技能能力的不同触发场景,**同等重要**;挂哪些由本技能 UX 定义(入口定位表),详见 `development/PRODUCT_SURFACES.md`。以后宿主若增加新入口,仍是「新触发场景 + 同一内核」。
|
||||
- **单业务内核、多入口**:数据管理 / 定时任务 / Agent / 技能详情 / 直接 CLI 必须复用同一 domain service;禁止按入口复制业务逻辑,禁止按 `source` 分叉业务行为。
|
||||
- **数据管理**:既是 **UI 执行面**(点按钮走与对话相同的闭环),也是数据与过程可见;不是「只读库」。
|
||||
- **`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`。
|
||||
- **业务行生命周期(有处理队列时)**:对某一行做生成/触达/私信等处理时,默认只操作「可操作」角色状态;开始抢占「进行中」;结束回写成功或失败(失败带原因);重做靠人工改回可操作。状态名由本技能自定。详见 `references/WORK_ITEM_STATUS.md`。
|
||||
- **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;文件型特殊报告才另做 `export-*`(落 `output/`);**HTML 整页报告**写 `html/` 并用固定 Action id `render-html`(见 `references/ACTIONS.md`)。
|
||||
- **准备 vs 执行(适用则拆)**:若技能存在「先写入/准备本地数据」与「再对外产生副作用」两段,Agent 必须能分开触发;禁止默认绑成不可拆的一步。一键无准备态的技能可跳过本条。
|
||||
|
||||
@@ -66,11 +68,12 @@ allowed-tools:
|
||||
1. **禁止**对长任务使用 `exec`/`bash` 跑 `…/scripts/main.py run`(或等价业务命令)再 `process poll` 干等。
|
||||
2. **禁止**用 `uv run python …` 跑技能脚本(会建临时环境,缺 `jiangchang_skill_core`)。
|
||||
3. **禁止** Agent 自行 `pip install`。
|
||||
4. Action **可选**:无数据管理 / 定时 / Agent 结构化调用需求时可不提供 `actions.json`;有长任务 / RPA 时**必须**提供至少一条 `executionProfile: "async"` 且 `placements` 含 `agent` 的 Action。
|
||||
4. **入口按定位表挂载**:复制业务技能后须在 `development/REQUIREMENTS.md` 填写入口定位表(见 `development/PRODUCT_SURFACES.md`)。定位需要数据管理 / 定时 / Agent 结构化调用时,必须提供 `actions.json` 并声明对应 `placements`;定位确认无需任何宿主结构化入口时可不提供。有长任务 / RPA 时**必须**提供至少一条 `executionProfile: "async"` 且 `placements` 含 `agent` 的 Action。
|
||||
5. **一个业务能力一个 Action**,用多个 `placements` 挂入口;底层仍是同一 `scripts/main.py` CLI。
|
||||
6. 是否进任务中心**只**看该 Action 的 `executionProfile`,与数据管理 / 定时 / Agent 入口无关。
|
||||
7. **处理业务行**时遵守状态角色契约(可操作 / 进行中 / 终态);禁止对已成功/失败/跳过默认重复处理。详见 `references/WORK_ITEM_STATUS.md`。
|
||||
|
||||
Agent 执行契约细节:`references/CLI.md`、`references/ACTIONS.md`、`references/SCHEMA.md`、按需 `references/POLICY.md`(不要把 `development/` 当 Agent 主读路径)。
|
||||
Agent 执行契约细节:`references/CLI.md`、`references/ACTIONS.md`、`references/SCHEMA.md`、按需 `references/POLICY.md` / `references/WORK_ITEM_STATUS.md`(不要把 `development/` 当 Agent 主读路径)。
|
||||
|
||||
## 共享 Python(必读)
|
||||
|
||||
@@ -106,10 +109,11 @@ python {baseDir}/scripts/main.py init-db
|
||||
|
||||
**回答「怎么用 / 能做什么」时的要求:**
|
||||
|
||||
1. 「能做什么 / 场景 / FAQ」以 `README.md` 为主;「第一次怎么用完」以 `TUTORIAL.md` 为主。
|
||||
1. 「能做什么 / 场景 / 概念类 FAQ」以 `README.md`(说明 Tab)为主;「点哪里 / 说什么 / 排障」以 `TUTORIAL.md`(教程 Tab)为主。说明 Tab **不应**含分步操作;若 README 仍有旧式步骤,以教程为准并提示开发者按 `development/TUTORIAL_SURFACES.md` 修正。
|
||||
2. **禁止**在回答中粘贴 `python …/scripts/main.py …` 或其它命令行(除非用户明确索要命令,或自称技术人员/开发者)。
|
||||
3. **禁止**为答用法而去 `dir` / `ls` / 列技能目录,或默认打开 `references/`、`development/`;仅当用户明确问命令、参数、集成或排错时再读 `references/`。
|
||||
4. **不要**向普通用户暴露 Playwright、DOM、Python、RPA、库表名、`error_code` 等实现细节,除非用户明确询问技术实现。
|
||||
5. 用户问「怎么用」时:**优先读 `TUTORIAL.md`**;不要只根据 README 臆造按钮路径。
|
||||
|
||||
**文档读取边界:**
|
||||
|
||||
@@ -122,13 +126,13 @@ python {baseDir}/scripts/main.py init-db
|
||||
|
||||
| 文档 | 读者 | 用途 |
|
||||
|------|------|------|
|
||||
| 根目录 `README.md` | 普通用户 | 市场 **说明** Tab(`readme_md`) |
|
||||
| 根目录 `TUTORIAL.md` | 普通用户 | 市场 **教程** Tab(`tutorial_md`) |
|
||||
| 根目录 `README.md` | 普通用户 | 市场 **说明** Tab(`readme_md`):能力/场景/边界;**非教程** |
|
||||
| 根目录 `TUTORIAL.md` | 普通用户 | 市场 **教程** Tab(`tutorial_md`):分入口操作;结构见 `development/TUTORIAL_SURFACES.md` |
|
||||
| 根目录 `DEMO.md` | 普通用户 | 市场 **演示** Tab(`demo_video_url` + `demo_md`) |
|
||||
| 根目录 `CHANGELOG.md` | 普通用户 | 市场 **更新日志** Tab(按版本小节 → `changelog`) |
|
||||
| `SKILL.md`(本文) | LLM / OpenClaw 平台 | 技能入口、触发与运行契约摘要 |
|
||||
| `references/` | Agent 编排 / 调用 | CLI、Action、schema 契约(Agent 主读) |
|
||||
| `development/` | 开发者 / AI 编程代理 | 需求、开发教程、测试、深度技术规范 |
|
||||
| `development/` | 开发者 / AI 编程代理 | 需求、开发教程、测试、深度技术规范;**产品入口**见 `development/PRODUCT_SURFACES.md`;**用户文档契约**见 `development/TUTORIAL_SURFACES.md` |
|
||||
|
||||
## 目录约定
|
||||
|
||||
@@ -143,7 +147,7 @@ python {baseDir}/scripts/main.py init-db
|
||||
1. 从本模板创建新业务技能时,**不得保留模板 `.git`**;优先使用 [`tools/scaffold_skill.ps1`](tools/scaffold_skill.ps1)(见 [`tools/README.md`](tools/README.md))。
|
||||
2. 业务技能仓库中**不得**存在 `.openclaw-skill-template`(仅 template 源仓库保留)。
|
||||
3. 复制目录为你的新 skill 仓库,全局替换 `your-skill-slug` 等占位词。
|
||||
4. 按 `development/DEVELOPMENT.md` 完成开发与文档定制。
|
||||
4. 按 `development/DEVELOPMENT.md` 完成开发与文档定制;开发前先读 `development/PRODUCT_SURFACES.md` 并填写入口定位表。
|
||||
5. 用户市场四 Tab 材料写入根 `README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md`;Agent 调用契约见 `references/CLI.md`、`references/SCHEMA.md`、`references/ACTIONS.md`。
|
||||
6. 同步修改 `scripts/util/constants.py` 中的 `SKILL_SLUG` / `SKILL_VERSION`。
|
||||
7. 本地 SQLite 的中文表名/字段名、标准时间字段与字段顺序规范见 `references/SCHEMA.md`。
|
||||
|
||||
144
TUTORIAL.md
144
TUTORIAL.md
@@ -1,12 +1,34 @@
|
||||
> **模板说明(复制后删除本段)**
|
||||
> 本文对应技能市场 **「使用教程」** Tab(发布时同步为 `tutorial_md`)。
|
||||
> 面向普通用户写**可跟着做的步骤**,不要写 CLI、目录结构、release、开发规范。
|
||||
> 与根 `README.md`(技能说明)分工:README 讲「是什么 / 能做什么」;本文讲「第一次怎么用完」。
|
||||
> **复制后验收章节(标题可微调):**「开始前」「第一步…」「第二步…」「常见卡住怎么办」。
|
||||
> 与根 `README.md`(技能说明)分工:README 讲「是什么 / 能做什么 / 边界」;本文讲「从哪个入口怎么用完」。
|
||||
> **完整契约:** `development/TUTORIAL_SURFACES.md`(三入口结构、说明≠教程、skill-detail 不可作主路径)。
|
||||
> **复制后验收章节(标题可微调,语义不可缺):**「本技能做什么」「开始前」「入口一:数据管理」「入口二:对话任务」「入口三:定时任务」「主能力对照表」「常见卡住怎么办」。
|
||||
> 若本技能 §0.1 未挂数据管理 / 对话 / 定时,对应入口节改为「不适用」并写一句理由(与 REQUIREMENTS 一致)。
|
||||
|
||||
# 【技能名称】使用教程
|
||||
# 【技能名称】· 使用教程
|
||||
|
||||
用最短路径带用户完成一次成功使用。步骤要具体,可用「打开匠厂 → …」这类操作语言。
|
||||
本技能有两种**平行**的使用方式(按本技能入口定位填写;未挂的入口删掉或写不适用):
|
||||
|
||||
- **数据管理**:在左侧表上点工具栏 / 行内按钮
|
||||
- **对话任务**:用自然语言让 Agent 完成同一能力
|
||||
|
||||
两种方式触发同一套业务能力,结果一致,选你习惯的即可。
|
||||
定时任务是否提供:见下方「入口三」(没有则写「本技能不需要定时」+ 理由)。
|
||||
|
||||
---
|
||||
|
||||
## 本技能做什么
|
||||
|
||||
用业务语言画清闭环(**不要**写库表英文名或命令行),例如:
|
||||
|
||||
```text
|
||||
【输入/准备】 → 【核心动作】 → 【结果可见处】 → 【可选报告/导出】
|
||||
```
|
||||
|
||||
- **输入**:……
|
||||
- **输出**:……
|
||||
- **说明**:边界(教学/正式、是否接外站等)
|
||||
|
||||
---
|
||||
|
||||
@@ -14,43 +36,105 @@
|
||||
|
||||
确认:
|
||||
|
||||
1. 已安装并登录匠厂客户端
|
||||
2. 已在技能市场安装本技能(或更新到最新版)
|
||||
3. 【网页自动化类技能】首次运行一般会自动登记本平台账号;也可在「账号管理」中指定登录标识。浏览器会话由系统隔离管理——**不要**用本机日常浏览器用户目录去跑真实站
|
||||
4. 【按平台】若本技能说明「需要先登录目标站」,请在弹出的有头浏览器中按提示登录或扫码;「登记账号」不等于「已登录目标站」。若说明「可不登录也能用」,仍须有可用的账号会话目录
|
||||
5. 【网页自动化类技能】已登记账号可在「数据管理 / 账号管理」查看;每个账号都有独立浏览器会话
|
||||
6. 【如需】需要的文件 / 时间范围 / 单号等已准备好
|
||||
1. 已安装并登录匠厂客户端
|
||||
2. 已在技能市场安装本技能(建议更新到最新版)
|
||||
3. 【网页自动化类】账号登记 / 目标站登录要求(按本技能;登记 ≠ 已登录)
|
||||
4. 【如需】文件 / 时间范围 / 单号等已准备好
|
||||
5. 【如需】首次须先完成「载入样例 / 导入 / 同步」等准备动作(见入口一或入口二)
|
||||
|
||||
---
|
||||
|
||||
## 第一步:打开技能并说清目标
|
||||
## 入口一:数据管理
|
||||
|
||||
在匠厂里新建任务或打开对话,用自然语言说明要做什么,例如:
|
||||
> 若本技能无业务表、无 toolbar/row/batch:整节改为「不适用」+ 理由。
|
||||
|
||||
> “帮我【完成某件业务事】,范围是【时间 / 对象 / 条件】。”
|
||||
适合:对照数据、喜欢点界面、机房/运营用户。
|
||||
|
||||
建议一次只交代一件事;范围说清楚,减少来回确认。
|
||||
### 左侧各表是干什么的
|
||||
|
||||
| 表名(中文) | 作用 |
|
||||
|--------------|------|
|
||||
| 【表 A】 | …… |
|
||||
| 【表 B】 | …… |
|
||||
|
||||
### 首次准备(若需要)
|
||||
|
||||
1. 进入 **数据管理** → **【主表】**
|
||||
2. 点击表顶 **「【准备类按钮 label】」**(若为空表可点,写明是否须选中行)
|
||||
3. 成功后应看到:……
|
||||
|
||||
> **注意:** 准备/载入/导入类按钮须挂在数据管理 **toolbar**(用户看得见)。不要把「技能详情页点按钮」写成主路径——详情页常只显示四 Tab,不一定渲染 Action 按钮。
|
||||
|
||||
### 主流程
|
||||
|
||||
1. 进入 **【主表】**
|
||||
2. 【是否选中行】
|
||||
3. 点击 **「【主能力 label】」**
|
||||
4. 到 **【结果表 / 任务中心 / 报告】** 核对结果
|
||||
|
||||
### 行级 / 批量(若有)
|
||||
|
||||
| 你想做什么 | 操作 |
|
||||
|------------|------|
|
||||
| …… | **【表】** 行内 → **「【label】」** |
|
||||
| …… | 勾选多行 → **「【label】」** |
|
||||
|
||||
---
|
||||
|
||||
## 第二步:按提示补充信息
|
||||
## 入口二:对话任务
|
||||
|
||||
如果 Agent 问你缺什么(账号、文件、筛选条件),按提示补充即可。
|
||||
> 若本技能不挂 `agent`:整节改为「不适用」+ 理由。
|
||||
|
||||
- 有文件时:按界面提示选择或拖入
|
||||
- 有账号时:确认已在对应平台登录,且具备操作权限
|
||||
适合:临时提问、要解释、不想切换表。
|
||||
|
||||
### 什么时候用对话
|
||||
|
||||
- ……
|
||||
- 与入口一效果相同;习惯看表时优先用数据管理
|
||||
|
||||
### 示例话术
|
||||
|
||||
| 你想做什么 | 可以这样说 |
|
||||
|------------|------------|
|
||||
| …… | 「……」 |
|
||||
| …… | 「……」 |
|
||||
|
||||
### Agent 可能追问什么
|
||||
|
||||
- 缺参数时:……
|
||||
- 失败时:可改用数据管理点按钮
|
||||
|
||||
---
|
||||
|
||||
## 第三步:等待结果并核对
|
||||
## 入口三:定时任务
|
||||
|
||||
任务结束后,请核对:
|
||||
> **有 `cron`:** 写如何新建定时、选哪个 Action、默认参数、预期结果。
|
||||
> **无 `cron`:** 用下面「不需要」模板,删掉「如何配置」小节。
|
||||
|
||||
- 是否成功完成
|
||||
- 处理了多少条 / 哪些失败
|
||||
- 生成的文件、表格或记录是否在预期位置
|
||||
### 本技能不需要定时时(示例文案,按需保留/改写)
|
||||
|
||||
若失败,先看失败原因说明,再按下一节排查。
|
||||
本技能**未提供定时任务入口**,原因:【一次性 / 须人指定对象 / 须当场确认 / ……】。
|
||||
请使用 **数据管理** 或 **对话任务**。
|
||||
|
||||
### 本技能需要定时时(示例结构)
|
||||
|
||||
1. 打开侧栏 **定时任务** → 新建
|
||||
2. 选择本技能 → Action **「【label】」**
|
||||
3. 填写参数 → 保存;可「立即运行」验证
|
||||
4. 到点后在【结果表 / 任务中心】查看
|
||||
|
||||
---
|
||||
|
||||
## 主能力对照表
|
||||
|
||||
同一能力,两种主入口(按本技能实际填写;只挂一端的注明):
|
||||
|
||||
| 想做什么 | 数据管理 | 对话任务 |
|
||||
|----------|----------|----------|
|
||||
| …… | **【表】** → **「【label】」** | 「……」 |
|
||||
| …… | …… | …… |
|
||||
|
||||
按钮名必须与 `assets/actions.json` 的 `label` 一致。
|
||||
|
||||
---
|
||||
|
||||
@@ -58,10 +142,10 @@
|
||||
|
||||
| 现象 | 你可以先试 |
|
||||
|------|------------|
|
||||
| 提示未登录 / 无权限 | 确认匠厂已登录;业务账号是否在线、权限是否足够 |
|
||||
| 提示账号不存在 / 被占用 | 到「数据管理 / 账号管理」确认账号记录和状态,稍后再试;不要重复创建账号 |
|
||||
| 提示缺少必要信息 | 把时间范围、单号、文件等说完整后再试一次 |
|
||||
| 一直没有结果 | 到任务中心查看进度或失败原因;必要时重新发起 |
|
||||
| 结果不对 | 用更明确的条件再说一次,或缩小范围重试 |
|
||||
| 主表是空的 | 先做「首次准备」或对话载入/导入 |
|
||||
| 按钮点不了 | 是否须先选中行;是否装错版本 |
|
||||
| 一直没有结果 | 看任务中心 / 运行日志;async 任务看进度 |
|
||||
| 【网页自动化】未登录 / 无权限 | 确认账号在线与目标站登录状态 |
|
||||
| 结果不对 | 缩小范围重试,或改用数据管理核对表数据 |
|
||||
|
||||
仍无法解决时,把「你说了什么 + 界面提示原文」发给支持人员,便于排查。
|
||||
仍无法解决时,把「你用的入口 + 操作步骤 + 界面提示原文」发给支持人员。
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# assets
|
||||
|
||||
- `actions.json`:Skill Action manifest 最小示例(**可选**;不需要宿主入口时可删除)。每个 action **必须**显式声明 `executionProfile`(`sync` \| `async`)。默认仅 `health` / `version` / `config-path`,不放 toolbar,故无 `bind` 示例。
|
||||
- `html/example-report.html`:HTML 报告页最小壳(`{{TITLE}}` / `{{GENERATED_AT}}` / `{{BODY}}`);渲染后须写入数据目录 `html/`,见 `references/ACTIONS.md`「HTML 页面报告」。
|
||||
- `examples/`:CLI 成功输出形状示例、以及 `render-html.action.example.json`(虚构路径与数据)。
|
||||
- `actions.json`:Skill Action manifest 最小示例(按入口定位表决定是否保留/扩展;见 `development/PRODUCT_SURFACES.md`)。每个 action **必须**显式声明 `executionProfile`(`sync` \| `async`)。默认仅 `health` / `version` / `config-path`,不放 toolbar,故无 `bind` 示例。
|
||||
- **业务技能注意:** 若主路径在数据管理,准备/载入/导入类 Action 应挂 `toolbar` + `bind.tables`(用户看得见),**不要**只挂 `skill-detail` 却在教程里写成「技能详情点按钮」。见 `development/TUTORIAL_SURFACES.md` §5、`references/ACTIONS.md`「用户可见性」。
|
||||
- `html/example-report.html`:HTML 报告页最小壳(`{{TITLE}}` / `{{GENERATED_AT}}` / `{{BODY}}`);含数据管理 iframe 预览布局基线(流体铺满、禁横向滚动);渲染后须写入数据目录 `html/`,见 `references/ACTIONS.md`「HTML 页面报告」。
|
||||
- `examples/`:CLI 成功输出形状示例、Action 形状参考(含 `path-root-file-action.example.json`、`selection-label-column.action.example.json`、`render-html.action.example.json` 等;虚构路径与数据)。
|
||||
- `schemas/`:轻量 JSON Schema(`skill-actions.schema.json` 为**新技能严格规范**)。
|
||||
|
||||
`skill-actions.schema.json` 使用 `additionalProperties: false`。正式支持 `bind.tables`(toolbar Action 必填)与可选 `bind.inputMapping`;placements 含 `toolbar` / `row` / `batch` / `cron` / `agent` / `skill-detail`(对齐匠厂宿主)。`concurrency`/`locks` 仍为预留。`placements` 与 `executionProfile` 正交——进任务中心只看 `async`,与入口无关。表级 Action 与 Agent 契约见 [`references/ACTIONS.md`](../references/ACTIONS.md);开发深度规范见 [`development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md)。
|
||||
`skill-actions.schema.json` 使用 `additionalProperties: false`。正式支持 `bind.tables`(toolbar Action 必填)与可选 `bind.inputMapping` / `columnMapping` / **`labelColumn`**(选择摘要标签列);placements 含 `toolbar` / `row` / `batch` / `cron` / `agent` / `skill-detail`(对齐匠厂宿主)。数据管理 confirm/form 弹层默认展示「已选择」摘要(宿主能力,技能可不改)。可选 `hoverHint` 为数据管理工具栏悬停(省略不显示,不回退)。`concurrency`/`locks` 仍为预留。`placements` 与 `executionProfile` 正交——进任务中心只看 `async`,与入口无关。表级 Action 与 Agent 契约见 [`references/ACTIONS.md`](../references/ACTIONS.md);开发深度规范见 [`development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md)。
|
||||
|
||||
- 用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.md)
|
||||
- 用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.md);写法契约见 [`development/TUTORIAL_SURFACES.md`](../development/TUTORIAL_SURFACES.md)
|
||||
- Agent 调用/编排参考见 [`references/`](../references/)(含 [`ACTIONS.md`](../references/ACTIONS.md))
|
||||
- 开发规范见 [`development/`](../development/)
|
||||
|
||||
@@ -4,3 +4,6 @@
|
||||
- `skill-policy-snapshot.*.example.json`:Data Policy Snapshot 形状参考(见 `references/POLICY.md`)。
|
||||
- `skill-cell-presentation.example.json`:数据管理单元格呈现文档形状参考(见 `references/CELL_PRESENTATION.md`)。
|
||||
- `render-html.action.example.json`:HTML 报告「重新生成」Action 形状参考(固定 id `render-html`;见 `references/ACTIONS.md`)。
|
||||
- `lookup-action.example.json`:Action 内联 Lookup 形状参考(单选 + `multiple: true` 多选并列;跨表引用勿填编号;见 `references/ACTIONS.md`「跨表引用:Lookup」)。
|
||||
- `visible-when-action.example.json`:Action 参数字段条件显隐 `visibleWhen` 形状参考(互斥模式联动;见 `references/ACTIONS.md`「字段条件显隐」;≠ Policy L3)。
|
||||
- `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`」)。
|
||||
|
||||
83
assets/examples/lookup-action.example.json
Normal file
83
assets/examples/lookup-action.example.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"_comment": "形状参考:Action 参数引用同库另一张表时用 Lookup。单选与多选同一 lookup 对象,多选只加 multiple:true。完整契约见 references/ACTIONS.md「跨表引用:Lookup」与 references/SCHEMA.md。",
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actions": [
|
||||
{
|
||||
"id": "send-with-template",
|
||||
"label": "按模板发送(示例)",
|
||||
"description": "演示内联 lookup 单选:从 message_templates 选模板;-- 表示自动挑选。",
|
||||
"placements": ["toolbar"],
|
||||
"executionProfile": "async",
|
||||
"interaction": "form",
|
||||
"bind": {
|
||||
"tables": ["leads"]
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "engage",
|
||||
"args": ["--template-id", "{{templateId}}", "--content", "{{content}}"]
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"title": "正文",
|
||||
"description": "填了就按这里发;留空则用下面的话术模板。",
|
||||
"display_type": "textarea"
|
||||
},
|
||||
"templateId": {
|
||||
"type": "number",
|
||||
"title": "话术模板",
|
||||
"description": "仅在正文留空时生效。选 -- = 自动挑选。",
|
||||
"display_type": "lookup",
|
||||
"lookup": {
|
||||
"ref_table": "message_templates",
|
||||
"value_column": "id",
|
||||
"label_column": "name",
|
||||
"extra_column": "channel",
|
||||
"filter_column": "is_active",
|
||||
"filter_value": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "probe-with-engines",
|
||||
"label": "按引擎抽检(多选示例)",
|
||||
"description": "演示 lookup.multiple:从 engines 多选,提交为逗号分隔 id。",
|
||||
"placements": ["toolbar"],
|
||||
"executionProfile": "async",
|
||||
"interaction": "form",
|
||||
"bind": {
|
||||
"tables": ["queries"]
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "query",
|
||||
"args": ["batch-probe", "--engine-ids", "{{engineIds}}"]
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"engineIds": {
|
||||
"type": "string",
|
||||
"title": "将使用的引擎",
|
||||
"description": "可多选;提交值为逗号分隔编号,如 1,2。",
|
||||
"display_type": "lookup",
|
||||
"lookup": {
|
||||
"ref_table": "engines",
|
||||
"value_column": "id",
|
||||
"label_column": "display_name",
|
||||
"filter_column": "enabled",
|
||||
"filter_value": 1,
|
||||
"multiple": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
36
assets/examples/path-root-file-action.example.json
Normal file
36
assets/examples/path-root-file-action.example.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"_comment": "形状参考:Action 表单 file/directory 用 pathRoot 指定原生对话框起始目录(相对技能数据根)。省略则宿主默认 {skill_data}/input。完整契约见 references/ACTIONS.md「本地路径选择与 pathRoot」。勿与宿主导入面板(replaces:import)混用本示例。",
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actions": [
|
||||
{
|
||||
"id": "import-pack",
|
||||
"label": "导入数据包",
|
||||
"description": "从本技能输入目录子文件夹选择 JSON 并写入业务表。",
|
||||
"placements": ["toolbar", "agent"],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "form",
|
||||
"bind": { "tables": ["business_items"] },
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["file"],
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": "string",
|
||||
"title": "数据包文件",
|
||||
"description": "请选择 input/数据包/ 下的 JSON。",
|
||||
"format": "file",
|
||||
"display_type": "file",
|
||||
"pathRoot": "input/数据包"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "import-pack",
|
||||
"args": ["--file", "{{file}}"]
|
||||
},
|
||||
"steps": [{ "when": "afterSuccess", "do": "reloadTable" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
28
assets/examples/replaces-create.action.example.json
Normal file
28
assets/examples/replaces-create.action.example.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"id": "item-create",
|
||||
"label": "新增记录",
|
||||
"description": "新增业务记录并执行领域副作用(走技能 CLI,非宿主直写库)。",
|
||||
"replaces": "create",
|
||||
"placements": [],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "form",
|
||||
"bind": {
|
||||
"tables": ["items"]
|
||||
},
|
||||
"hostEffects": {
|
||||
"onSuccess": ["reloadTable"]
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": { "type": "string", "title": "名称" },
|
||||
"note": { "type": "string", "title": "备注" }
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "item",
|
||||
"args": ["create", "--name", "{{name}}", "--note", "{{note}}"]
|
||||
}
|
||||
}
|
||||
33
assets/examples/replaces-delete.action.example.json
Normal file
33
assets/examples/replaces-delete.action.example.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"id": "item-delete",
|
||||
"label": "删除记录",
|
||||
"description": "删除业务记录并清理关联副作用。",
|
||||
"replaces": "delete",
|
||||
"placements": [],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "confirm",
|
||||
"confirmation": {
|
||||
"message": "确定删除该记录?关联数据将一并清理。"
|
||||
},
|
||||
"bind": {
|
||||
"tables": ["items"],
|
||||
"inputMapping": {
|
||||
"id": "$row.$pk"
|
||||
}
|
||||
},
|
||||
"hostEffects": {
|
||||
"onSuccess": ["reloadTable", "clearSelection"]
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": { "type": "number", "title": "编号", "readOnly": true }
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "item",
|
||||
"args": ["delete", "--id", "{{id}}"]
|
||||
}
|
||||
}
|
||||
38
assets/examples/replaces-import.action.example.json
Normal file
38
assets/examples/replaces-import.action.example.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "item-import",
|
||||
"label": "导入记录",
|
||||
"description": "由宿主「导入数据」面板调用:解析 Excel/CSV 并写入业务表(含领域校验与派生字段)。",
|
||||
"replaces": "import",
|
||||
"placements": [],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "form",
|
||||
"bind": {
|
||||
"tables": ["items"]
|
||||
},
|
||||
"hostEffects": {
|
||||
"onSuccess": ["reloadTable"]
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["filePath"],
|
||||
"properties": {
|
||||
"filePath": {
|
||||
"type": "string",
|
||||
"title": "导入文件",
|
||||
"description": "由宿主导入面板传入的 Excel/CSV 路径。",
|
||||
"format": "file"
|
||||
},
|
||||
"onError": {
|
||||
"type": "string",
|
||||
"title": "导入规则(出错时)",
|
||||
"enum": ["跳过该行,继续导入", "立即停止,不再导入后续行"],
|
||||
"default": "跳过该行,继续导入"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "item",
|
||||
"args": ["import", "--file", "{{filePath}}", "--on-error", "{{onError}}"]
|
||||
}
|
||||
}
|
||||
34
assets/examples/replaces-update.action.example.json
Normal file
34
assets/examples/replaces-update.action.example.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "item-update",
|
||||
"label": "编辑记录",
|
||||
"description": "编辑业务记录并重算派生字段。",
|
||||
"replaces": "update",
|
||||
"placements": [],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "form",
|
||||
"bind": {
|
||||
"tables": ["items"],
|
||||
"inputMapping": {
|
||||
"id": "$row.$pk",
|
||||
"name": "$row.name",
|
||||
"note": "$row.note"
|
||||
}
|
||||
},
|
||||
"hostEffects": {
|
||||
"onSuccess": ["reloadTable"]
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": { "type": "number", "title": "编号", "readOnly": true },
|
||||
"name": { "type": "string", "title": "名称" },
|
||||
"note": { "type": "string", "title": "备注" }
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "item",
|
||||
"args": ["update", "--id", "{{id}}", "--name", "{{name}}", "--note", "{{note}}"]
|
||||
}
|
||||
}
|
||||
38
assets/examples/selection-label-column.action.example.json
Normal file
38
assets/examples/selection-label-column.action.example.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "process-selected",
|
||||
"label": "处理勾选",
|
||||
"description": "对勾选行执行业务处理。示例:声明 labelColumn,让确认弹层「已选择」摘要显示可读名称。",
|
||||
"placements": ["toolbar", "batch"],
|
||||
"executionProfile": "async",
|
||||
"interaction": "confirm",
|
||||
"confirmation": {
|
||||
"message": "将按技能业务规则处理勾选记录(具体是否跳过某些状态由 CLI 决定)。"
|
||||
},
|
||||
"bind": {
|
||||
"tables": ["business_items"],
|
||||
"inputMapping": {
|
||||
"ids": "$selection.ids"
|
||||
},
|
||||
"labelColumn": "display_name"
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["ids"],
|
||||
"properties": {
|
||||
"ids": {
|
||||
"type": "string",
|
||||
"title": "编号列表",
|
||||
"description": "逗号分隔主键;宿主会预填勾选行",
|
||||
"readOnly": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "process",
|
||||
"args": ["--ids", "{{ids}}"]
|
||||
},
|
||||
"hostEffects": {
|
||||
"onSuccess": ["reloadTable"]
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.92",
|
||||
"skill": "your-skill-slug"
|
||||
}
|
||||
|
||||
72
assets/examples/visible-when-action.example.json
Normal file
72
assets/examples/visible-when-action.example.json
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"_comment": "形状参考:互斥模式用 inputSchema.properties.*.visibleWhen 联动显隐,勿只靠 description 灰字。完整契约见 references/ACTIONS.md「字段条件显隐」。勿与 POLICY.md L3 行态 visibleWhen 混淆。",
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actions": [
|
||||
{
|
||||
"id": "scrape-links-demo",
|
||||
"label": "采集链接(示例)",
|
||||
"description": "演示按采集方式切换参数:关键词路径 vs 作者主页路径。",
|
||||
"placements": ["toolbar"],
|
||||
"executionProfile": "async",
|
||||
"interaction": "form",
|
||||
"bind": {
|
||||
"tables": ["demo_items"]
|
||||
},
|
||||
"confirmation": {
|
||||
"message": "将按所选方式采集链接(示例确认文案)。是否继续?"
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "run",
|
||||
"args": [
|
||||
"--collect-mode",
|
||||
"{{collectMode}}",
|
||||
"--keyword",
|
||||
"{{keyword}}",
|
||||
"--author-url",
|
||||
"{{authorUrl}}",
|
||||
"--max-count",
|
||||
"{{maxCount}}"
|
||||
]
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["collectMode"],
|
||||
"properties": {
|
||||
"collectMode": {
|
||||
"type": "string",
|
||||
"title": "采集方式",
|
||||
"description": "先选方式,下方只出现对应参数。",
|
||||
"enum": ["按关键词采集", "按作者主页采集"],
|
||||
"default": "按关键词采集"
|
||||
},
|
||||
"keyword": {
|
||||
"type": "string",
|
||||
"title": "关键词",
|
||||
"description": "例如 rpa。",
|
||||
"visibleWhen": {
|
||||
"field": "collectMode",
|
||||
"equals": "按关键词采集"
|
||||
}
|
||||
},
|
||||
"authorUrl": {
|
||||
"type": "string",
|
||||
"title": "作者主页链接",
|
||||
"description": "例如 https://example.com/author/...",
|
||||
"display_type": "url",
|
||||
"visibleWhen": {
|
||||
"collectMode": "按作者主页采集"
|
||||
}
|
||||
},
|
||||
"maxCount": {
|
||||
"type": "number",
|
||||
"title": "数量上限",
|
||||
"default": 50,
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -11,17 +11,31 @@
|
||||
--muted: #64748b;
|
||||
--card: #ffffff;
|
||||
--line: #e2e8f0;
|
||||
--page-pad-x: clamp(14px, 2.5vw, 24px);
|
||||
--page-pad-y: clamp(12px, 2vw, 20px);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html { overflow-y: auto; }
|
||||
body {
|
||||
html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
body {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
line-height: 1.5;
|
||||
}
|
||||
header {
|
||||
padding: 20px 24px 8px;
|
||||
padding: var(--page-pad-y) var(--page-pad-x) 8px;
|
||||
background: var(--card);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
header h1 {
|
||||
margin: 0 0 6px;
|
||||
@@ -34,7 +48,23 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
main {
|
||||
padding: 8px 24px 28px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: var(--page-pad-y) var(--page-pad-x) 28px;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.section h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
color: var(--muted);
|
||||
}
|
||||
.meta {
|
||||
margin: 0 0 10px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
.card {
|
||||
background: var(--card);
|
||||
@@ -44,6 +74,35 @@
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
}
|
||||
th, td {
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
word-break: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
th {
|
||||
background: #f8fafc;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
tr:last-child td { border-bottom: none; }
|
||||
/*
|
||||
宿主数据管理在 iframe 内预览本页:应横向铺满可用宽度,仅纵向滚动。
|
||||
复杂看板可在 {{BODY}} 内使用 .section / table;布局底线见 references/ACTIONS.md「宿主预览布局」。
|
||||
*/
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -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 接受部分缺省字段,但新技能不应依赖缺省行为。含可选 hostEffects/steps(匠厂宿主 ≥2.8.5)。",
|
||||
"description": "匠厂 skill-template 对新技能的严格 Action manifest 规范(schemaVersion 1)。additionalProperties=false 表示模板推荐契约;宿主运行时可能为兼容历史 manifest 接受部分缺省字段,但新技能不应依赖缺省行为。含可选 hostEffects/steps(匠厂宿主 ≥2.8.5)与可选 hoverHint(数据管理工具栏悬停,需宿主已支持该字段)。",
|
||||
"type": "object",
|
||||
"required": ["schemaVersion", "skill", "actions"],
|
||||
"additionalProperties": false,
|
||||
@@ -84,8 +84,8 @@
|
||||
},
|
||||
"mutationKind": {
|
||||
"type": "string",
|
||||
"enum": ["create", "update", "delete"],
|
||||
"description": "可选。接管数据管理默认按钮:create=新增、update=编辑、delete=删除。须配合非空 bind.tables;无配置时宿主仍直写 SQLite。"
|
||||
"enum": ["create", "update", "delete", "import"],
|
||||
"description": "可选。接管数据管理默认按钮:create=新增、update=编辑、delete=删除、import=导入。须配合非空 bind.tables;无配置时宿主仍直写 SQLite。import 时 UI 仍用宿主「导入数据」面板,确认后调用本 Action 写库。"
|
||||
},
|
||||
"scalarArg": {
|
||||
"type": ["string", "number", "boolean"]
|
||||
@@ -116,20 +116,117 @@
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-z][a-z0-9_]*$",
|
||||
"description": "可选。绑定业务表物理列名;replaces create/update 时宿主可复用该列的中文名与 lookup 下拉"
|
||||
"description": "可选。绑定业务表物理列名;Action 弹窗(toolbar/batch)与 replaces create/update 抽屉均可复用该列的中文名与 lookup 下拉"
|
||||
},
|
||||
"display_type": {
|
||||
"type": "string",
|
||||
"enum": ["textarea", "date", "datetime_unix_seconds", "url", "email"],
|
||||
"description": "可选。表单控件提示;与 format 二选一即可。须与 CRUD display_type 语义对齐(见 ACTIONS.md / SCHEMA.md)。"
|
||||
"enum": ["textarea", "date", "datetime_unix_seconds", "url", "email", "lookup", "directory", "file"],
|
||||
"description": "可选。表单控件提示;与 format 二选一即可。lookup=关联下拉(须配 lookup 对象或绑到已有 lookup 列)。file/directory=本地路径选择(可选 pathRoot 指定起始目录,见 ACTIONS.md)。须与 CRUD display_type 语义对齐。"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"enum": ["textarea", "date", "datetime_unix_seconds", "url", "email"],
|
||||
"enum": ["textarea", "date", "datetime_unix_seconds", "url", "email", "lookup", "directory", "file"],
|
||||
"description": "可选。display_type 别名,语义相同。"
|
||||
},
|
||||
"pathRoot": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "可选。仅对 display_type/format 为 file 或 directory 有意义:原生选文件/目录对话框的起始文件夹,相对当前技能数据根(如 input/库存数据)。省略则宿主默认打开 {skill_data}/input。禁止绝对路径、盘符与 ..。Agent/CLI 忽略。见 references/ACTIONS.md「本地路径选择与 pathRoot」。"
|
||||
},
|
||||
"lookup": {
|
||||
"$ref": "#/$defs/lookupConfig",
|
||||
"description": "可选。内联同库表关联 Lookup(与列 options_json lookup 对象同构)。用于当前表没有对应列时;推荐同时设 display_type=lookup。见 ACTIONS.md「跨表引用:Lookup」。"
|
||||
},
|
||||
"visibleWhen": {
|
||||
"description": "可选。表单字段条件显隐。宿主在数据管理/定时任务参数表单中:规则不满足则隐藏该字段、提交时省略、且不校验 required。支持 {field,equals} / {field,in:[...]},或单键简写 {collectMode:\"按关键词采集\"} / {collectMode:[\"a\",\"b\"]}。Agent/CLI 无 UI 时同样按当前 input 求值后剥离隐藏字段。",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["field", "equals"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"field": { "type": "string", "minLength": 1 },
|
||||
"equals": { "type": ["string", "number", "boolean"] }
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["field", "in"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"field": { "type": "string", "minLength": 1 },
|
||||
"in": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "type": ["string", "number", "boolean"] }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"maxProperties": 1,
|
||||
"additionalProperties": {
|
||||
"oneOf": [
|
||||
{ "type": ["string", "number", "boolean"] },
|
||||
{
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "type": ["string", "number", "boolean"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"lookupConfig": {
|
||||
"type": "object",
|
||||
"required": ["ref_table", "value_column", "label_column"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"ref_table": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
|
||||
"description": "同库被引用表名"
|
||||
},
|
||||
"value_column": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
|
||||
"description": "提交值列(编号 / 外键)"
|
||||
},
|
||||
"label_column": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
|
||||
"description": "主文案列"
|
||||
},
|
||||
"extra_column": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
|
||||
"description": "可选。第三展示列;有值时宿主拼成「value · label(extra)」"
|
||||
},
|
||||
"filter_column": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
|
||||
"description": "可选。等值过滤列"
|
||||
},
|
||||
"filter_value": {
|
||||
"type": ["string", "number", "boolean", "null"],
|
||||
"description": "可选。与 filter_column 配对的等值"
|
||||
},
|
||||
"multiple": {
|
||||
"type": "boolean",
|
||||
"description": "可选。true=多选 Lookup(存/提交 TEXT 逗号分隔 id);省略或 false=单选(默认)。与 CRUD options_json lookup 同构。"
|
||||
}
|
||||
},
|
||||
"description": "同库 FK 式 Lookup。展示:value · label;有 extra 时 value · label(extra)。multiple 时多选。宿主亦接受 camelCase 键名。"
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["type", "properties"],
|
||||
@@ -203,10 +300,16 @@
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-z][a-z0-9_]*$"
|
||||
},
|
||||
"description": "可选。将 inputSchema 属性名映射到业务表物理列名,供 replaces create/update 抽屉复用列 displayName / lookup options。"
|
||||
"description": "可选。将 inputSchema 属性名映射到业务表物理列名,供 Action 弹窗与 replaces create/update 抽屉复用列 displayName / lookup options。"
|
||||
},
|
||||
"labelColumn": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-z][a-z0-9_]*$",
|
||||
"description": "可选。数据管理 Action 确认/表单弹层「已选择」摘要用的可读标签列(#id + 标签)。仅影响展示,不改变提交 payload。省略时宿主按常见列名启发(title/name 等),再不行只显示编号。"
|
||||
}
|
||||
},
|
||||
"description": "表级绑定;toolbar 必填。可选 inputMapping / columnMapping。concurrency/locks 勿写入。"
|
||||
"description": "表级绑定;toolbar 必填。可选 inputMapping / columnMapping / labelColumn。concurrency/locks 勿写入。"
|
||||
},
|
||||
"hostEffectId": {
|
||||
"type": "string",
|
||||
@@ -295,6 +398,11 @@
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"hoverHint": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "可选。数据管理工具栏悬停文案。与 description / confirmation.message 独立;省略则不显示悬停,不回退。需宿主已支持该字段。当前实现为顶栏技能按钮,不含宿主内置删除/刷新/新增/导入/导出。"
|
||||
},
|
||||
"placements": {
|
||||
"type": "array",
|
||||
"minItems": 0,
|
||||
@@ -327,7 +435,8 @@
|
||||
"skill-detail": { "type": "string", "enum": ["direct", "confirm", "form"] },
|
||||
"create": { "type": "string", "enum": ["direct", "confirm", "form"] },
|
||||
"update": { "type": "string", "enum": ["direct", "confirm", "form"] },
|
||||
"delete": { "type": "string", "enum": ["direct", "confirm", "form"] }
|
||||
"delete": { "type": "string", "enum": ["direct", "confirm", "form"] },
|
||||
"import": { "type": "string", "enum": ["direct", "confirm", "form"] }
|
||||
},
|
||||
"description": "按 placement / replaces 覆盖 interaction(只能写 concrete 模式,不能写 auto)。"
|
||||
},
|
||||
|
||||
@@ -57,6 +57,10 @@ checklist_version: 1.0
|
||||
- [ ] S0-6 是否需要在任务中心查看进度或结果:是 / 否
|
||||
- [ ] S0-7 外部系统账号、数据、设备与操作权限的授权来源:
|
||||
- [ ] S0-8 是否包含提交、付款、删除、审批、发布、发送等不可逆副作用:是 / 否
|
||||
- [ ] S0-9 **入口定位表**(见 `PRODUCT_SURFACES.md` / `REQUIREMENTS` §0.1):已填写 / 未填写
|
||||
- [ ] S0-10 本技能宣称的主用户路径涉及:对话 / 数据管理(执行或仅展示) / 定时(可多选;与定位表一致)
|
||||
- [ ] S0-11 **业务行生命周期**(见 `references/WORK_ITEM_STATUS.md` / `REQUIREMENTS` §0.3):有处理队列且已声明状态→角色映射 / 不适用
|
||||
- [ ] S0-12 **HTML 页面报告**(见 `references/ACTIONS.md`「HTML 页面报告」):有侧栏可预览的 `html/*.html` + `render-html` / 不适用
|
||||
|
||||
---
|
||||
|
||||
@@ -73,10 +77,15 @@ checklist_version: 1.0
|
||||
|
||||
- [ ] A2-1 **P0** 市场「说明」能让非开发者理解:做什么、给谁用、开始前准备什么
|
||||
- [ ] A2-2 **P0** 「说明/教程」不以开发目录、内部实现细节作为用户主路径
|
||||
- [ ] A2-3 **P1** 「教程」可按真实顺序跟做(准备 → 步骤 → 失败怎么办)
|
||||
- [ ] A2-3 **P1** 「教程」可按真实顺序跟做(准备 → 分入口步骤 → 失败怎么办);结构见 `TUTORIAL_SURFACES.md`
|
||||
- [ ] A2-4 **P1** 「演示」结构完整(有演示信息位;无视频则标明暂无,不假装有)
|
||||
- [ ] A2-5 **P0** 「更新日志」能对应到当前交付版本
|
||||
- [ ] A2-6 **P0** 平台/Agent 侧说明中,常见用户意图的触发方式与真实能力一致(短操作 vs 长任务不混淆)
|
||||
- [ ] A2-7 **P0** 「说明」Tab(`README.md`)**不含**分步操作(1/2/3)、Agent 话术块、「入口一/二」专章;操作指引指向「使用教程」
|
||||
- [ ] A2-8 **P0** 「教程」Tab 覆盖 `REQUIREMENTS` §0.1 / `actions.json` 中所有「挂」的入口(数据管理 / 对话;结构见 `TUTORIAL_SURFACES.md`)
|
||||
- [ ] A2-9 **P0** 无 `cron` 时教程有「不需要定时」+ 理由;有 `cron` 时教程写清如何配置
|
||||
- [ ] A2-10 **P1** 教程主能力对照表与 `actions.json` 的 `label` 一致
|
||||
- [ ] A2-11 **P1** 若有数据管理主路径:准备/载入/导入类能力在对应表 **toolbar** 可见;教程**不以** skill-detail 为唯一主路径
|
||||
|
||||
---
|
||||
|
||||
@@ -86,6 +95,8 @@ checklist_version: 1.0
|
||||
- [ ] A3-2 **P0** 缺参或非法输入时稳定失败(可读错误,不假成功)
|
||||
- [ ] A3-3 **P1** 失败可诊断:能从任务记录/提示判断失败阶段,而非只有“失败了”
|
||||
- [ ] A3-4 **P0** 同一能力从已声明的不同入口触发时,业务结果一致(不出现入口分叉成两套逻辑)
|
||||
- [ ] A3-5 **P0** `REQUIREMENTS` 入口定位表已填写;定位为「挂」的入口在 `actions.json`(及必要表元数据)中有对应声明
|
||||
- [ ] A3-6 **P1** 定位为「不挂」的入口有书面理由(不是空白遗漏)
|
||||
|
||||
---
|
||||
|
||||
@@ -96,6 +107,8 @@ checklist_version: 1.0
|
||||
- [ ] A4-3 **P0** 异步任务在任务中心能看到进行中与结束状态;结束有成功或失败结论
|
||||
- [ ] A4-4 **P1** 进行中的进度/步骤信息对用户可理解
|
||||
- [ ] A4-5 **P1** 已对外暴露的动作参数覆盖主路径关键输入(无“命令行能做、动作入口不能做”的关键缺口)
|
||||
- [ ] A4-6 **P0** 若 Action 需引用同库另一张业务表(话术/平台/账号等):参数为 Lookup 下拉(内联 `lookup` 或绑列),**不是**手填内部编号;可选「自动」可留空提交
|
||||
- [ ] A4-7 **P1** 若 Action 有互斥采集/操作模式(如「按关键词 / 按主页」):用 `inputSchema` 字段级 **`visibleWhen`** 联动显隐,勿只靠 `description` 灰字说明「仅某某时填写」(见 `references/ACTIONS.md`;勿与 Policy L3 行态 `visibleWhen` 混淆)
|
||||
|
||||
---
|
||||
|
||||
@@ -123,6 +136,15 @@ checklist_version: 1.0
|
||||
- [ ] A6-3 **P1** 重复执行同一业务键时,结果符合宣称(更新/去重/追加),无莫名脏数据或静默丢数据
|
||||
- [ ] A6-4 **P1** 「零结果成功」与「失败」可区分
|
||||
- [ ] A6-5 **P1** 若产出文件:落在约定数据位置且可找到,不丢在不明临时路径
|
||||
- [ ] A6-6 **P0** 有业务表时:`sqlite_master` 中业务表 DDL **不得**含 SQLite `FOREIGN KEY` / `REFERENCES`;跨表只存业务 id,关联与级联在 repository/service;界面名称用 `lookup`(见 `references/SCHEMA.md`);可跑 `validate_template_database` / `init-db` 后自检
|
||||
- [ ] A6-7 **P0** 有「处理业务行」类 Action 时(见 `WORK_ITEM_STATUS.md`):默认只处理**可操作**角色状态;成功/失败/跳过/进行中再点处理应拒绝或跳过且不产生重复副作用;重做须人工改回可操作。无处理队列 → **N/A**
|
||||
- [ ] A6-8 **P0** 同上适用时:开始处理后业务表进入**进行中**;成功回写成功类终态;失败回写失败类终态且**失败原因**可读(任务成功但业务行仍停在可操作/进行中 → **FAIL**)
|
||||
- [ ] A6-9 **P1** 同上适用时:`REQUIREMENTS` §0.3(或等价)已填写本技能状态→角色映射;处理类 Action 确认文案写清仅可操作态
|
||||
- [ ] A6-10 **P1** 有 HTML 侧栏预览时(S0-12):预览区横向铺满、无横向滚动条;重新生成后内容与页头时间更新;布局符合 `references/ACTIONS.md`「宿主预览布局」。无 HTML 报告 → **N/A**
|
||||
- [ ] A6-11 **P1** 业务**状态 / 启用类**列若用户需要筛表或顶栏搜中文:`searchable: 1` 且 `options_json`(或 `boolean` + `options_json`)的 **value 与存库一致**;仅 `searchable: 0` 则宿主不提供列筛与全局搜索。见 `references/SCHEMA.md`「列筛选与全局搜索」
|
||||
- [ ] A6-12 **P2** `display_type: boolean` 的展示列推荐同时配置 `options_json` 中文 label(如 停用/启用),避免仅靠宿主默认「是/否」;无 boolean 列 → **N/A**
|
||||
- [ ] A6-13 **P0** 新技能**不得**创建 `task_logs`;过程/进度/业务结果走运行日志、任务中心、业务表(见 `LOGGING.md`、`SCHEMA.md`)
|
||||
- [ ] A6-14 **P0** 废弃用户可见表时:`drop_deprecated_user_table` 物理删表 + 清元数据;**禁止**只删元数据留物理表;有迁移时须提供旧库回归测试(见 `SCHEMA.md`、`tests/samples/test_table_deprecation_migration.py.sample`)
|
||||
|
||||
---
|
||||
|
||||
@@ -134,6 +156,7 @@ checklist_version: 1.0
|
||||
- [ ] A7-4 **P1** 关键步骤有据可查(能对应到开始/关键节点/结束;RPA 宜有多个 `stage=`)
|
||||
- [ ] A7-5 **P1** 若宣称支持录屏或截图存证:打开开关后关键步骤或失败能留下可查看产物;宣称支持却完全无产物 → **FAIL**
|
||||
- [ ] A7-6 **P2** 用户可见文案偏业务语言,而非大段开发黑话
|
||||
- [ ] A7-7 **P2** 数据管理技能按钮(toolbar / row / batch)中文名对照 `references/ACTIONS.md`「Action 按钮中文名」;不与宿主「查看 / 编辑 / 删除」抢名;打开类成功不弹成功条。行业专有按钮名可自创
|
||||
|
||||
---
|
||||
|
||||
@@ -162,7 +185,7 @@ checklist_version: 1.0
|
||||
|
||||
- [ ] A9-1 **P0** 仓库默认测试套件通过(执行技能约定的一键测试命令,如 `python tests/run_tests.py -v`)
|
||||
- [ ] A9-2 **P0** 不以「仅模拟档通过」作为交活依据(有真实档要求时必须真实跑通或书面阶段性豁免)
|
||||
- [ ] A9-3 **P0** **NEED_MANUAL** 已声明的宿主入口(详情/Agent/工具栏/定时等)按声明抽测可通过
|
||||
- [ ] A9-3 **P0** **NEED_MANUAL** 入口定位表中定为「挂」的宿主入口(详情/Agent/工具栏/定时等)按声明抽测可通过;未在定位表中规划且未声明的入口可标 N/A(须引用定位表理由),**禁止**用「未声明」掩盖「从未做入口定位」
|
||||
- [ ] A9-4 **P1** 对外版本一致:详情、version、更新日志、发布标签能对上
|
||||
- [ ] A9-5 **P1** 开发者按平台规则能安装并测到该技能(若平台有开发者可见性要求)
|
||||
|
||||
|
||||
@@ -67,7 +67,9 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
||||
|
||||
宿主约定(与本模板对齐):
|
||||
|
||||
- 工具栏「导入」默认从 `input/` 取文件;「导出」默认写到 `output/`
|
||||
- 工具栏「导入」**UI 一律**打开宿主「导入数据」面板(默认从本技能 `input/` 选文件);「导出」默认写到 `output/`
|
||||
- **Action 表单**里 `display_type`/`format` 为 `file` 或 `directory` 时:宿主原生对话框默认打开**当前技能** `{skill_data}/input`;若字段声明可选 **`pathRoot`**(相对技能数据根,如 `input/库存数据`),则打开该子目录。禁止在技能里写用户 ID 或绝对盘符。契约见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「本地路径选择与 pathRoot」
|
||||
- 若表声明了 `replaces: import`,确认导入后由技能 CLI 写库(见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「`replaces`」);仅有 `replaces: create` 时宿主可按行调 create(兼容)
|
||||
- 数据管理扫描 `{skill_data}/html/*.html`(一层、非递归);`<title>` → 侧栏显示名;「重新生成」调用 Action id **`render-html`**(见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「HTML 页面报告」)
|
||||
|
||||
---
|
||||
@@ -175,6 +177,10 @@ DOWNLOAD_OUTPUT_DIR=./outputs/videos
|
||||
- `tests/test_runtime_paths.py` 覆盖 `resolve_data_path` / `resolve_input_path`。
|
||||
- `POLICY-DATA-PATH-001`:禁止 `.env.example` 中 `./` 路径;禁止业务代码 `abspath(config.get(...))`。
|
||||
|
||||
### 卸载技能与数据保留
|
||||
|
||||
宿主**卸载技能只删除安装目录**(如 `.openclaw/skills/{slug}`),**不会**删除 `{JIANGCHANG_DATA_ROOT}/{user_id}/{skill_slug}/` 下的 SQLite、输入输出与日志。重装会复用同一用户库;schema 迁移须在 `init_db()` 中幂等完成(见 [`../references/SCHEMA.md`](../references/SCHEMA.md)「表增删改与退役」)。
|
||||
|
||||
---
|
||||
|
||||
## 9. 相关文档
|
||||
|
||||
@@ -352,9 +352,9 @@ git remote -v
|
||||
| `DEMO.md` | 演示 | `demo_md` + frontmatter `demo_video_url` |
|
||||
| `CHANGELOG.md` | 更新日志 | 匹配当前 tag 版本的 `##` 小节 → `changelog` |
|
||||
|
||||
**`README.md`**:能力与场景说明。含 frontmatter `description`;写能做什么、场景、准备、如何告诉 Agent、结果、注意事项、FAQ。不要写开发教程、目录规范、release、requirements。
|
||||
**`README.md`(说明 Tab)**:能力、场景、前置条件、结果形态、注意事项、概念类 FAQ。含 frontmatter `description`(**不要**写「如何开始使用」)。**禁止**分步操作、Agent 话术块、「入口一/二」专章、逐步排障——这些属教程。完整契约见 [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md) §1。
|
||||
|
||||
**`TUTORIAL.md`**:第一次怎么用完的逐步操作。写「开始前 → 步骤 → 卡住怎么办」;不要写 CLI / 目录 / 开发规范。
|
||||
**`TUTORIAL.md`(教程 Tab)**:按 [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md) 写三入口结构(数据管理 ∥ 对话;定时有则教、无则 N/A)+ 主能力对照表 + 常见卡住。步骤必须与 `REQUIREMENTS` §0.1、`actions.json` 及宿主真实 UI 对齐;**禁止**把仅 `skill-detail` 的 Action 写成用户主路径。不要写 CLI / 目录 / 开发规范。
|
||||
|
||||
**`DEMO.md`**:演示视频与补充说明。frontmatter 必须含 `demo_video_url`(无视频时用 `""`);正文可选。
|
||||
|
||||
@@ -527,6 +527,7 @@ assets/actions.json → placements / bind / executionProfile / entry
|
||||
|
||||
- `db/connection.py`
|
||||
只做连接和建表(模板默认只建 `_jiangchang_*` 元数据表;业务表按需 CREATE)
|
||||
**业务表之间禁止 SQLite `FOREIGN KEY` / `REFERENCES`**:只存业务 id,存在性 / 级联 / 删除在 repository/service 处理(见 [`references/SCHEMA.md`](../references/SCHEMA.md)「表与表之间:禁止 SQLite 外键」)
|
||||
|
||||
- `db/<domain>_repository.py`
|
||||
只做业务表增删查改(有业务表才建;模板不再默认提供任务审计表)
|
||||
@@ -543,17 +544,20 @@ assets/actions.json → placements / bind / executionProfile / entry
|
||||
- 打印用户提示
|
||||
- 拼接业务流程
|
||||
|
||||
## 11.5 Skill Action manifest(可选)
|
||||
## 11.5 Skill Action manifest(按入口定位挂载)
|
||||
|
||||
若技能需要出现在宿主**数据管理按钮**、**技能详情**、**定时任务**或 **Agent 直接调用**中,提供 `assets/actions.json`。
|
||||
先读 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md):对话 / 数据管理 / 定时任务**平级**;哪个主能力出现在哪个入口,由本技能 UX 在 `REQUIREMENTS` 入口定位表中定义。
|
||||
|
||||
若定位表要求出现在宿主**数据管理按钮**、**技能详情**、**定时任务**或 **Agent 直接调用**中,提供 `assets/actions.json` 并写入对应 `placements`。
|
||||
|
||||
- **产品入口与何时走哪个**见 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md)
|
||||
- **运行时金标准**见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md)
|
||||
- 字段契约见 [`references/ACTIONS.md`](../references/ACTIONS.md)(`placements` / `bind.tables` / `executionProfile` 正交)
|
||||
- JSON Schema 见 [`assets/schemas/skill-actions.schema.json`](../assets/schemas/skill-actions.schema.json)
|
||||
- 模板最小示例仅暴露 `health` / `version` / `config-path`(均显式 `executionProfile: "sync"`;**不**自动加 toolbar);CLI 另含 `init-db` 供宿主静默建库(不必做成 Action)
|
||||
- 含 `toolbar` 的 Action:**必须**声明合法 `bind.tables`(`POLICY-SKILL-ACTION-003`)
|
||||
- 含浏览器 RPA / 长耗时的业务技能:**必须**增加至少一条 `"executionProfile": "async"` 且 `placements` 含 `agent` 的 action(`POLICY-SKILL-ACTION-001`)
|
||||
- 没有宿主直接操作需求时,**可以删除** `actions.json`;删除后勿保留失效引用
|
||||
- 入口定位表确认无任何宿主结构化入口需求时,**可以删除** `actions.json`;删除后勿保留失效引用
|
||||
- scaffold **不会**移除 Schema 中的 `bind` 支持,**不会**生成平台业务 Action 或业务表名;仅替换占位 slug
|
||||
|
||||
宿主执行方式:
|
||||
@@ -586,7 +590,7 @@ cmd_stats() # 只读 sync:JSON,无 job_context
|
||||
- 批量数量用 **`pickCount` / `--pick N` 参数**(任意正整数),不写死条数。
|
||||
- **顺序执行、不并发**同一浏览器 Profile / account lease。
|
||||
- **同步数据**与**宿主表导出**职责分离:同步不写 CSV/Excel;表导出交给数据管理。
|
||||
- **HTML 页面报告**(可选):需要侧栏预览整页时,写入 `html/` + Action `render-html`;契约见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「HTML 页面报告」、[`DATA_PATHS.md`](DATA_PATHS.md);薄写入 helper 见 `scripts/service/html_page_service.py`。
|
||||
- **HTML 页面报告**(可选):需要侧栏预览整页时,写入 `html/` + Action `render-html`;契约见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「HTML 页面报告」(含**宿主预览布局**底线)、[`DATA_PATHS.md`](DATA_PATHS.md);薄写入 helper 见 `scripts/service/html_page_service.py`。建议从 `assets/html/example-report.html` 复制壳;`{{BODY}}` 内数据须转义;样式升级后提醒用户重新生成。
|
||||
- 详情见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md);契约样例见 `tests/samples/test_sync_contract.py.sample`、`tests/samples/test_action_core_contract.py.sample`。
|
||||
|
||||
### 队列 + pick(可选)
|
||||
@@ -818,9 +822,14 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
|
||||
- 安装后状态正常
|
||||
- 不会出现缺文件、缺入口或安装失败的问题
|
||||
|
||||
### 第七步:按声明做宿主多入口验收(已声明的入口都要测;须看用户体验)
|
||||
### 第七步:按入口定位与声明做宿主多入口验收(须看用户体验)
|
||||
|
||||
安装完成后,不要只停留在“已安装”状态,也**不要**只在本地 mock 通就认为完成。宿主侧栏有多条与技能相关的入口;**按本技能 `assets/actions.json` 的 `placements` / `executionProfile` 声明逐项自测**(契约见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md)、[`../references/ACTIONS.md`](../references/ACTIONS.md))。未声明的入口可以跳过;**已声明却测不到,视为验收失败**。不要只测对话、忽略其它入口。
|
||||
安装完成后,不要只停留在“已安装”状态,也**不要**只在本地 mock 通就认为完成。对话任务 / 数据管理 / 定时任务在宿主上**平级**(见 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md))。先核对本技能 `REQUIREMENTS` **入口定位表**,再按 `assets/actions.json` 的 `placements` / `executionProfile` 对「定位为挂」的入口逐项自测(字段契约见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md)、[`../references/ACTIONS.md`](../references/ACTIONS.md))。
|
||||
|
||||
- 定位为「挂」却未声明或测不到 → **验收失败**
|
||||
- 定位为「不挂」且写了理由 → 可跳过该入口
|
||||
- **禁止**从未做入口定位、却用「未声明所以跳过」交差
|
||||
- 不要只测对话、忽略定位表中已挂的其它入口
|
||||
|
||||
**用户体验(与功能入口并列,后台通了不算完):**
|
||||
|
||||
@@ -828,7 +837,7 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
|
||||
- 长任务(async)是否有可见进度,能否取消或等待说明
|
||||
- 网页 RPA 是否有头运行;需登录/验证码时是否停下等人,而不是静默失败
|
||||
- 数据管理:表名/字段中文、按钮文案是否清楚;结果是否写回用户看得见的地方
|
||||
- 市场说明 / 教程是否与真实行为一致
|
||||
- 市场说明 / 教程是否与真实行为一致;**说明 Tab 不含分步操作**,教程覆盖已挂入口(见 [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md))
|
||||
|
||||
| 宿主入口(侧栏 / 界面) | 技能侧如何挂上 | 建议验收什么 |
|
||||
|-------------------------|----------------|--------------|
|
||||
|
||||
@@ -122,9 +122,15 @@ RPA 等待用 `interruptible_sleep`,**不要**裸 `asyncio.sleep`(否则暂
|
||||
|
||||
Journal 会写入 `type: lifecycle` 事件(`paused` / `resumed` / `stopping`),供宿主任务中心显示状态。
|
||||
|
||||
### 2.3 业务表(可选,不是默认排障通道)
|
||||
### 2.3 可观测性三分法(禁止 task_logs)
|
||||
|
||||
模板**默认不建**任务审计表。业务结果写入各技能自己的业务表;过程看 `jiangchang.log`,进度/终态看任务中心。
|
||||
| 信息类型 | 去哪里 | 技能怎么做 |
|
||||
|----------|--------|------------|
|
||||
| **过程**(步骤、异常栈、排查) | `jiangchang.log` / 宿主「运行日志」 | `log.info` / `log.exception`,关键节点见 §3 |
|
||||
| **进度 / 终态**(任务中心) | Run Journal:`emit` / `finish` | `job_context` + `finish(status=...)` |
|
||||
| **业务结果** | 业务表字段(status、产物路径、统计等) | repository 回写;见 [`WORK_ITEM_STATUS.md`](../references/WORK_ITEM_STATUS.md) |
|
||||
|
||||
模板**默认不建** `task_logs` 等通用任务审计表。**不要**用业务表冒充任务进度,也不要用任务表代替业务状态机。
|
||||
|
||||
### 2.4 RPA video step / artifacts(用户可见动作与存证)
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
| POLICY-SKILL-ACTION-002 | 每个 Action 必须显式声明 `executionProfile`,且只能是 `sync` 或 `async` | development/SKILL_ACTION_RUNTIME.md;references/ACTIONS.md;assets/schemas/skill-actions.schema.json | hard | schema required + manifest 扫描 | `tests/test_development_policy_guard.py::TestPolicySkillAction002`;`tests/test_actions_manifest.py` |
|
||||
| POLICY-SKILL-ACTION-003 | toolbar Action 必须声明合法非空 `bind.tables`(snake_case、唯一) | references/ACTIONS.md;assets/schemas/skill-actions.schema.json | hard | schema if/then + manifest 扫描 | `tests/test_development_policy_guard.py::TestPolicySkillAction003`;`tests/test_actions_manifest.py` |
|
||||
| POLICY-SKILL-ACTION-004 | `placements` 与 `executionProfile` 正交;文档不得把数据管理入口与异步执行方式硬绑定;Schema 须接受 toolbar/row/batch/cron/agent/skill-detail × sync/async 全部组合 | development/SKILL_ACTION_RUNTIME.md;references/ACTIONS.md;assets/schemas/skill-actions.schema.json | hard | Schema 6×2 矩阵校验 + 文档禁止短语扫描 | `tests/test_actions_manifest.py::test_schema_allows_all_placement_execution_profile_combinations`;`tests/test_development_policy_guard.py::TestPolicySkillAction004` |
|
||||
| POLICY-TUTORIAL-001 | 存在 `development/TUTORIAL_SURFACES.md`;`TUTORIAL.md` 须含数据管理/对话/定时/对照表骨架;`README.md` 禁止「告诉 Agent」专章与入口专章;业务技能教程与 `actions.json` placements 对齐(无 cron 须声明不需要定时) | development/TUTORIAL_SURFACES.md;development/CHECKLIST.md A2-7~A2-11 | hard | 文档存在性 + 章节/反模式扫描 + placements 对齐 | `tests/test_tutorial_surfaces.py`;`tests/test_docs_standards.py::test_tutorial_surfaces_contract_doc_and_checklist` |
|
||||
| POLICY-ARCH-CORE-001 | 同一业务能力的 Action、CLI、Agent、cron、数据管理入口复用同一业务内核(文档要求已声明) | development/DEVELOPMENT.md;development/SKILL_ACTION_RUNTIME.md | hard(文档标记) / soft(语义) | 文档关键表述存在性;真实复用靠 sample/review | `tests/test_development_policy_guard.py::TestPolicyArchCore001`;`tests/samples/test_action_core_contract.py.sample` |
|
||||
| POLICY-DATA-SYNC-001 | 同步型业务须遵循幂等、事务、完整性与空结果保护(文档已声明) | references/SCHEMA.md;development/SKILL_ACTION_RUNTIME.md | hard(文档标记) / soft(语义) | 文档关键表述存在性;完整语义靠 sample/review | `tests/test_development_policy_guard.py::TestPolicyDataSync001`;`tests/samples/test_sync_contract.py.sample` |
|
||||
|
||||
@@ -67,6 +68,7 @@
|
||||
| `SKILL.md` / `constants.SKILL_SLUG` 一致性 | development/DEVELOPMENT.md §16 | 已有 `tests/test_skill_metadata.py` |
|
||||
| platform_kit_min_version >= 1.2.11 | development/RUNTIME.md | 已有 `tests/test_platform_import.py` |
|
||||
| 宿主按 `bind.tables` 过滤 toolbar 按钮 | references/ACTIONS.md | 待宿主升级后完整生效;模板侧已强制显式绑定 |
|
||||
| 数据管理技能按钮中文名(`label`)对照 ACTIONS.md 建议表;打开类成功静默 | references/ACTIONS.md「Action 按钮中文名」 | 参考非白名单;行业词允许自创;勿做 label 关键字扫描 |
|
||||
| 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 已约束 |
|
||||
|
||||
188
development/PRODUCT_SURFACES.md
Normal file
188
development/PRODUCT_SURFACES.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# 宿主产品入口(对话 / 数据管理 / 定时任务)
|
||||
|
||||
本文给**技能开发者**与 **AI 编程助手**读:说明匠厂宿主上几个**平级**业务入口各自干什么、**什么时候走哪个**、以及如何为本技能做入口定位。
|
||||
|
||||
字段怎么写见 [`../references/ACTIONS.md`](../references/ACTIONS.md)、[`../references/SCHEMA.md`](../references/SCHEMA.md);任务中心 / sync·async 见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md)。
|
||||
|
||||
**与其它分类正交:**
|
||||
|
||||
| 文档维度 | 回答的问题 |
|
||||
|----------|------------|
|
||||
| 本文(产品入口) | 用户从宿主哪条路径触发同一能力 |
|
||||
| Type A–D / 目录分层 | 技能有没有库、会不会调兄弟技能 |
|
||||
| RPA 四象限 | 外部系统怎么接 |
|
||||
|
||||
不要把三者合并成一个大枚举。
|
||||
|
||||
---
|
||||
|
||||
## 1. 入口是什么(先立规矩)
|
||||
|
||||
技能跑在**匠厂宿主**上,不是独立软件。宿主侧栏里的入口,是同一技能能力的不同**触发场景**:
|
||||
|
||||
| 今日常见入口 | 触发方式 | 底层 |
|
||||
|--------------|----------|------|
|
||||
| **对话任务**(新建任务) | 自然语言 → Agent | 短 CLI,或 `run_skill_action`(`placements` 含 `agent`) |
|
||||
| **数据管理** | 点工具栏 / 行 / 批量 / 表单(UI) | `run_skill_action`(`source=data-management`)→ **同一 CLI** |
|
||||
| **定时任务** | 到点「技能直调」或定时 Agent | `run_skill_action`(`cron` / `cron-agent`)→ **同一 CLI** |
|
||||
|
||||
**铁律:**
|
||||
|
||||
1. **入口平级**:没有「对话主、数据管理/定时辅」。只是触发场景与交互方式不同,各自有优势。
|
||||
2. **同一业务能力、同一 domain service**:禁止按入口复制逻辑,禁止按 `source` 分叉业务结果。
|
||||
3. **同能力可挂多个入口**:不是三选一;按本技能 UX 决定挂哪些。
|
||||
4. **可扩展**:以后若宿主增加新入口,仍是「新触发场景 + 同一内核」,定位方法不变(定位表多一列即可)。
|
||||
5. **任务中心不是业务入口**:只表示 `executionProfile: async` 的执行壳;与从哪个入口点进来无关。
|
||||
|
||||
---
|
||||
|
||||
## 2. 什么时候走哪个入口
|
||||
|
||||
### 2.1 对话任务——「用嘴发起」
|
||||
|
||||
**适合:**
|
||||
|
||||
- 用户用自然语言描述意图,条件一次说清或还要追问
|
||||
- 探索式:「能不能帮我……」「先看看再决定」
|
||||
- 对象临时指定(贴链接、说一个范围)
|
||||
- 需要 Agent 解释结果、失败原因、下一步
|
||||
- 用户不想打开数据管理,只想在聊天里办完
|
||||
|
||||
**技术自问:** 用户会不会用一句话说清这件事?需要模型帮理解和确认吗?——会,就为该能力挂 `agent`(长任务 / RPA 必须 Action + 通常 `async`;短查询可用共享 Python CLI)。
|
||||
|
||||
**注意:** 若技能还有「对着表运营 / 定时啃队列」等场景,**不要把对话当成唯一路径**,否则传统用户与省 token 路径会丢掉。
|
||||
|
||||
### 2.2 数据管理——「用界面发起 + 看着数据做」
|
||||
|
||||
数据管理有两层,**同等重要**:
|
||||
|
||||
1. **执行面**:工具栏 / 行级 / 批量 / `replaces` 接管新增·编辑·删除·**导入** —— 与对话一样触发同一 CLI,走完整业务闭环(传统用户可不聊天)。导入按钮的 UX 始终是宿主「导入数据」面板;有 `replaces:import` 时确认后由技能写库。
|
||||
2. **运营面**:业务表、状态流向、配置、input/output、日志、HTML —— 看清数据从哪来、到哪去、卡在哪。
|
||||
|
||||
**适合:**
|
||||
|
||||
- 在业务数据上操作:看队列、选一行重试、勾选批量、顶栏「处理 N 条 / 同步」
|
||||
- 已知流程、参数结构化(表单 / 确认 / 一点即跑)——**省 token**,不必每次过模型
|
||||
- 准备与执行拆开:表里先准备,再在表上点执行
|
||||
- 服务不愿用 AI 对话的用户
|
||||
|
||||
**技术自问:** 用户会不会打开这张表,靠点选就把主流程跑完?——要,就挂 `toolbar` / `row` / `batch` / `replaces`,并做好 `_jiangchang_*` 中文元数据。
|
||||
|
||||
**不要:** 把数据管理理解成「只给开发者看 SQLite」;也不要在完全没有结构化操作对象时硬造空表按钮。
|
||||
|
||||
### 2.3 定时任务——「到点自动发起」
|
||||
|
||||
**适合:**
|
||||
|
||||
- 同一能力需按节奏重复:消费队列、定期同步、巡检
|
||||
- 无人值守:默认参数即可跑,人不在场也正确、安全
|
||||
- 与对话 / 数据管理挂**同一个** Action,只改触发方式
|
||||
|
||||
**技术自问:** 这件事明天能不能在人不盯着时自动再跑一遍?——能,挂 `cron`;不能,定位表写明不挂及理由。
|
||||
|
||||
**慎挂:** 每次对象都不同须人指定;强不可逆且须当场确认;主路径依赖验证码 / 人工介入。
|
||||
|
||||
### 2.4 场景对照(贴需求用)
|
||||
|
||||
| 若用户场景是… | 优先考虑 | 说明 |
|
||||
|---------------|----------|------|
|
||||
| 「帮我处理这个……」(临时、口语) | 对话 | 灵活、可追问 |
|
||||
| 「对着表点一下就跑」 | 数据管理 | 结构化、省 token、传统用户 |
|
||||
| 「每天自动跑,不用我管」 | 定时 | 无人值守 |
|
||||
| 「偶尔对话下发,平时表上运营,夜里自动啃队列」 | **多个入口都挂同一能力** | 平级互补,不是重复开发 |
|
||||
| 「只看结果,从不在表上触发」 | 表可仅展示;执行挂对话/定时 | 由本技能 UX 定 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 如何为本技能做入口定位(必做)
|
||||
|
||||
模板**不**规定「某类技能必须三入口都挂满」。由技术按**本技能干什么 + 用户体验**填写定位表(写入 [`REQUIREMENTS.md`](REQUIREMENTS.md))。
|
||||
|
||||
对每个**主能力**(一条业务 CLI / 一个 Action)填一行:
|
||||
|
||||
| 主能力(Action id / CLI) | 对话 | 数据管理 | 定时 | 不挂理由(若不挂) |
|
||||
|---------------------------|------|----------|------|--------------------|
|
||||
| (例)`download-run-pick` | 挂 | toolbar | 挂 | — |
|
||||
| (例)`download-video` | 挂 | 不挂 | 不挂 | 对象每次不同,须人指定链接 |
|
||||
|
||||
**数据管理列怎么写:** `不挂` / `toolbar` / `row` / `batch` / `replaces` / `仅展示表(无业务按钮)`(可组合)。
|
||||
|
||||
**规则只有三条:**
|
||||
|
||||
1. 定位为「挂」的入口:必须在 `actions.json`(及必要元数据)中声明,且宿主可测通,并走同一内核。
|
||||
2. 定位为「不挂」的入口:必须有一句理由;禁止空白「忘了」。
|
||||
3. 未来新入口:表上多一列,方法不变。
|
||||
|
||||
完整 ≠ 每个命令塞满所有入口;完整 = **想清楚用户路径后,该出现的入口都挂上了,且同内核**。
|
||||
|
||||
---
|
||||
|
||||
## 4. 怎么挂(最小路径)
|
||||
|
||||
| 我想让用户… | 主要改什么 |
|
||||
|-------------|------------|
|
||||
| 能在对话里触发 | `SKILL.md` 意图路由 + `actions.json` 的 `placements` 含 `agent`(长任务 `async`) |
|
||||
| 能在数据管理看见结果 / 过程 | 业务表 + `init-db` + `_jiangchang_*`(见 SCHEMA) |
|
||||
| 能在表上点按钮走闭环 | `placements` 含 `toolbar`/`row`/`batch`,`toolbar` 须 `bind.tables` |
|
||||
| 能设定时自动跑 | 同一 Action 的 `placements` 含 `cron`;参数适合无人值守 |
|
||||
|
||||
细节与 Schema:[`../references/ACTIONS.md`](../references/ACTIONS.md)。运行时:[`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 反例(不要这样做)
|
||||
|
||||
- 只做对话 Action,从未填写入口定位表,也未考虑数据管理 / 定时用户路径
|
||||
- 为 toolbar / cron / agent 复制三套业务实现,或按 `source` 分叉结果
|
||||
- 把数据管理当成只读调试窗,主闭环只能聊天完成(若 UX 上本应支持 UI)
|
||||
- 不可逆、须确认的主路径默认挂 `cron` 且无产品确认
|
||||
- 用「Action 可选 / 定时以后再说」当作「可以不思考入口」
|
||||
|
||||
---
|
||||
|
||||
## 6. 附录:定位表示例(参考,非强制)
|
||||
|
||||
### 示例 A:队列消费类(对话 + 表上处理 + 定时)
|
||||
|
||||
| 主能力 | 对话 | 数据管理 | 定时 | 理由摘要 |
|
||||
|--------|------|----------|------|----------|
|
||||
| 入队 / 准备 | 挂 | 可 replaces 或表新增 | 不挂 | 准备态由人或对话写入 |
|
||||
| 处理 N 条 pending | 挂 | toolbar | 挂 | 运营点选与无人值守同一内核 |
|
||||
| 单条立即执行 | 挂 | 可选 row | 不挂 | 对象临时指定 |
|
||||
|
||||
### 示例 B:单次确认发布类
|
||||
|
||||
| 主能力 | 对话 | 数据管理 | 定时 | 理由摘要 |
|
||||
|--------|------|----------|------|----------|
|
||||
| 发布一条 | 挂 | 按是否落草稿表决定 | 不挂 | 不可逆,需人确认;不做无人值守 |
|
||||
|
||||
### 示例 C:无库短查询
|
||||
|
||||
| 主能力 | 对话 | 数据管理 | 定时 | 理由摘要 |
|
||||
|--------|------|----------|------|----------|
|
||||
| 查询 | 挂(或短 CLI) | 不挂 | 不挂 | 无本地业务表与重复批跑需求 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 用户教程如何投影本文
|
||||
|
||||
入口定位表填完后,**必须**落到用户市场文档(见 [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md)):
|
||||
|
||||
| 定位结果 | `TUTORIAL.md` | `README.md` |
|
||||
|----------|---------------|-------------|
|
||||
| 挂数据管理 | 「入口一」专章 + 表/按钮步骤 | 仅一句「支持数据管理」 |
|
||||
| 挂对话 | 「入口二」专章 + 话术 | 仅一句「支持对话」;**禁止**话术块 |
|
||||
| 挂定时 | 「入口三」教怎么建 | 可选一句边界 |
|
||||
| 不挂定时 | 「入口三」写不需要 + 理由 | 可选一句「不提供定时」 |
|
||||
|
||||
准备/载入类 Action:优先 `toolbar` + `bind.tables`,避免教程写「技能详情点按钮」而宿主详情页不渲染。
|
||||
|
||||
---
|
||||
|
||||
## 8. 相关文档
|
||||
|
||||
- 需求里填定位表:[`REQUIREMENTS.md`](REQUIREMENTS.md)
|
||||
- **用户说明 / 教程怎么写:** [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md)
|
||||
- 交活怎么验:[`CHECKLIST.md`](CHECKLIST.md)
|
||||
- 开发步骤中的 Action 挂载:[`DEVELOPMENT.md`](DEVELOPMENT.md) §11.5、§15
|
||||
- Agent 侧字段契约:[`../references/ACTIONS.md`](../references/ACTIONS.md)
|
||||
@@ -9,13 +9,15 @@
|
||||
| 步骤 | 做什么 | 详见 |
|
||||
|------|--------|------|
|
||||
| 1 | **拿到仓库**:① 项目经理在 [自建 Gitea](https://git.jc2009.com/)(`git.jc2009.com`,非 Gitea 官网)开仓并授权后 `git clone`;或 ② 本地已有 `skill-template` 时用 scaffold / 复制出新目录(遵守 Git 红线) | [`DEVELOPMENT.md`](DEVELOPMENT.md) §1、§4 |
|
||||
| 2 | **先读 AI + RPA 总纲**;界面技能先完成元素拾取/element map,再按浏览器、桌面或手机读分端手册 | [`AI_RPA.md`](AI_RPA.md)、[`RPA_ELEMENT_LOCATING.md`](RPA_ELEMENT_LOCATING.md) |
|
||||
| 3 | 按 [`NAMING.md`](NAMING.md) 确认 slug;**先填** [`REQUIREMENTS.md`](REQUIREMENTS.md),再写业务代码 | [`REQUIREMENTS.md`](REQUIREMENTS.md)、[`NAMING.md`](NAMING.md) |
|
||||
| 4 | 替换标识与占位,按四象限 / examples 实现 `scripts/service/` | [`DEVELOPMENT.md`](DEVELOPMENT.md) §4~§14 |
|
||||
| 5 | **配置 `developer_ids`(开发自测必做)**:开发期技能在匠厂默认不公开;不加则开发者自己也装测不了。从宿主设置复制用户 ID 写入 `SKILL.md` | [`DEVELOPMENT.md`](DEVELOPMENT.md) §6「关于 developer_ids」 |
|
||||
| 6 | 本地单测(mock)通过后,按业务默认 **`real_rpa`** 真实跑通;再 `release.ps1`;看 Gitea CI。**mock 通 ≠ 完成** | [`ADAPTER.md`](ADAPTER.md);[`DEVELOPMENT.md`](DEVELOPMENT.md) §15 |
|
||||
| 7 | 匠厂安装后,按已声明入口自测,并看**用户体验**(进度、失败提示、有头 RPA 等) | [`DEVELOPMENT.md`](DEVELOPMENT.md) §15;[`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) |
|
||||
| 8 | **交活验收(第三方)**:对照 [`CHECKLIST.md`](CHECKLIST.md) 验收;报告写入仓库根目录 [`acceptance-reports/`](../acceptance-reports/) | [`CHECKLIST.md`](CHECKLIST.md) |
|
||||
| 2 | **先读宿主产品入口**:对话 / 数据管理 / 定时任务平级;何时走哪个;为本技能填入口定位表 | [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md) |
|
||||
| 2b | **用户市场文档**:说明 Tab ≠ 教程 Tab;教程按三入口写;与 §0.1 / actions 对齐 | [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md) |
|
||||
| 3 | **先读 AI + RPA 总纲**;界面技能先完成元素拾取/element map,再按浏览器、桌面或手机读分端手册 | [`AI_RPA.md`](AI_RPA.md)、[`RPA_ELEMENT_LOCATING.md`](RPA_ELEMENT_LOCATING.md) |
|
||||
| 4 | 按 [`NAMING.md`](NAMING.md) 确认 slug;**先填** [`REQUIREMENTS.md`](REQUIREMENTS.md)(含入口定位表),再写业务代码 | [`REQUIREMENTS.md`](REQUIREMENTS.md)、[`NAMING.md`](NAMING.md) |
|
||||
| 5 | 替换标识与占位,按四象限 / examples 实现 `scripts/service/` | [`DEVELOPMENT.md`](DEVELOPMENT.md) §4~§14 |
|
||||
| 6 | **配置 `developer_ids`(开发自测必做)**:开发期技能在匠厂默认不公开;不加则开发者自己也装测不了。从宿主设置复制用户 ID 写入 `SKILL.md` | [`DEVELOPMENT.md`](DEVELOPMENT.md) §6「关于 developer_ids」 |
|
||||
| 7 | 本地单测(mock)通过后,按业务默认 **`real_rpa`** 真实跑通;再 `release.ps1`;看 Gitea CI。**mock 通 ≠ 完成** | [`ADAPTER.md`](ADAPTER.md);[`DEVELOPMENT.md`](DEVELOPMENT.md) §15 |
|
||||
| 8 | 匠厂安装后,按**入口定位表已挂**的入口自测,并看**用户体验**(进度、失败提示、有头 RPA 等) | [`DEVELOPMENT.md`](DEVELOPMENT.md) §15;[`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md);[`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) |
|
||||
| 9 | **交活验收(第三方)**:对照 [`CHECKLIST.md`](CHECKLIST.md) 验收;报告写入仓库根目录 [`acceptance-reports/`](../acceptance-reports/) | [`CHECKLIST.md`](CHECKLIST.md) |
|
||||
|
||||
脚手架与 Git 防串库:[`../tools/README.md`](../tools/README.md)(`scaffold_skill.ps1`)。业务仓也可直接从自建 Gitea clone,见 [`DEVELOPMENT.md`](DEVELOPMENT.md) §4 来源 A。
|
||||
|
||||
@@ -25,23 +27,25 @@
|
||||
|
||||
开始定制 skill 前,建议按以下顺序阅读:
|
||||
|
||||
1. [`AI_RPA.md`](AI_RPA.md) — **AI Agent + RPA 总纲**:职责边界、合规红线、风控、操作节奏与完整闭环
|
||||
2. [`ACCOUNT_MANAGER.md`](ACCOUNT_MANAGER.md) — **浏览器账号标准**:一账号一 Profile、租约、多账号、数据查看、验收与排障
|
||||
3. [`SHARED_REPOS.md`](SHARED_REPOS.md) — **公共仓库目录**(模板 / 账号管理 / platform-kit 等,可扩展)
|
||||
4. [`REQUIREMENTS.md`](REQUIREMENTS.md) — 需求文档模板与验收标准
|
||||
5. [`NAMING.md`](NAMING.md) — slug / 仓库名命名规范(复制模板前必读)
|
||||
6. [`DEVELOPMENT.md`](DEVELOPMENT.md) — 完整开发步骤与目录规范
|
||||
7. [`TESTING.md`](TESTING.md) — 测试分层、隔离数据根与档位开关
|
||||
8. [`LOGGING.md`](LOGGING.md) — 日志分层、必打节点、敏感信息红线(**涉及长任务、RPA、外部系统时必读**)
|
||||
9. [`ADAPTER.md`](ADAPTER.md) — 涉及外部系统对接时
|
||||
10. [`RPA.md`](RPA.md) — 涉及浏览器 / 桌面 / 手机自动化时的会话、风控和工程总纲
|
||||
11. [`RPA_ELEMENT_LOCATING.md`](RPA_ELEMENT_LOCATING.md) — **所有界面技能必读**:真实拾取、element map、唯一性、降级和 AI 编程约束
|
||||
12. 按执行端选读:[`RPA_BROWSER.md`](RPA_BROWSER.md) / [`RPA_DESKTOP.md`](RPA_DESKTOP.md) / [`RPA_MOBILE.md`](RPA_MOBILE.md)
|
||||
13. [`CONFIG.md`](CONFIG.md) — `.env` 规范与 bootstrap 机制
|
||||
14. [`DATA_PATHS.md`](DATA_PATHS.md) — 下载/导入/导出等本地文件路径标准(涉及文件读写时必读)
|
||||
15. [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) — Skill Action sync/async、任务中心、Agent 禁令、队列 pick(涉及 RPA / 长任务 / 数据管理按钮 / Cron / Agent 时必读)
|
||||
16. [`RUNTIME.md`](RUNTIME.md) — 共享 runtime、数据路径、发布打包与编码约定
|
||||
17. [`CHECKLIST.md`](CHECKLIST.md) — **交活验收清单(第三方视角)**;报告输出到 [`../acceptance-reports/`](../acceptance-reports/)
|
||||
1. [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md) — **宿主产品入口**:对话 / 数据管理 / 定时任务平级;何时走哪个;入口定位表
|
||||
1b. [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md) — **用户说明 / 教程契约**:README≠TUTORIAL;三入口结构;skill-detail 不可作主路径
|
||||
2. [`AI_RPA.md`](AI_RPA.md) — **AI Agent + RPA 总纲**:职责边界、合规红线、风控、操作节奏与完整闭环
|
||||
3. [`ACCOUNT_MANAGER.md`](ACCOUNT_MANAGER.md) — **浏览器账号标准**:一账号一 Profile、租约、多账号、数据查看、验收与排障
|
||||
4. [`SHARED_REPOS.md`](SHARED_REPOS.md) — **公共仓库目录**(模板 / 账号管理 / platform-kit 等,可扩展)
|
||||
5. [`REQUIREMENTS.md`](REQUIREMENTS.md) — 需求文档模板与验收标准(含入口定位表)
|
||||
6. [`NAMING.md`](NAMING.md) — slug / 仓库名命名规范(复制模板前必读)
|
||||
7. [`DEVELOPMENT.md`](DEVELOPMENT.md) — 完整开发步骤与目录规范
|
||||
8. [`TESTING.md`](TESTING.md) — 测试分层、隔离数据根与档位开关
|
||||
9. [`LOGGING.md`](LOGGING.md) — 日志分层、必打节点、敏感信息红线(**涉及长任务、RPA、外部系统时必读**)
|
||||
10. [`ADAPTER.md`](ADAPTER.md) — 涉及外部系统对接时
|
||||
11. [`RPA.md`](RPA.md) — 涉及浏览器 / 桌面 / 手机自动化时的会话、风控和工程总纲
|
||||
12. [`RPA_ELEMENT_LOCATING.md`](RPA_ELEMENT_LOCATING.md) — **所有界面技能必读**:真实拾取、element map、唯一性、降级和 AI 编程约束
|
||||
13. 按执行端选读:[`RPA_BROWSER.md`](RPA_BROWSER.md) / [`RPA_DESKTOP.md`](RPA_DESKTOP.md) / [`RPA_MOBILE.md`](RPA_MOBILE.md)
|
||||
14. [`CONFIG.md`](CONFIG.md) — `.env` 规范与 bootstrap 机制
|
||||
15. [`DATA_PATHS.md`](DATA_PATHS.md) — 下载/导入/导出等本地文件路径标准(涉及文件读写时必读)
|
||||
16. [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) — Skill Action sync/async、任务中心、Agent 禁令、队列 pick(涉及 RPA / 长任务 / 数据管理按钮 / Cron / Agent 时必读)
|
||||
17. [`RUNTIME.md`](RUNTIME.md) — 共享 runtime、数据路径、发布打包与编码约定
|
||||
18. [`CHECKLIST.md`](CHECKLIST.md) — **交活验收清单(第三方视角)**;报告输出到 [`../acceptance-reports/`](../acceptance-reports/)
|
||||
|
||||
Agent 调用契约见 [`../references/CLI.md`](../references/CLI.md)、[`../references/SCHEMA.md`](../references/SCHEMA.md)、[`../references/ACTIONS.md`](../references/ACTIONS.md)。
|
||||
用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.md),不要写进本目录。
|
||||
|
||||
@@ -31,7 +31,30 @@
|
||||
- **登记 ≠ 登录**:ensure/add-web 只建账号与 Profile;站点登录由本技能在有头浏览器中完成
|
||||
- **反模式**:把「需要登录态才能采到数据」的需求写成 `optional`,再省略 `ensure_logged_in`
|
||||
|
||||
## 0.1 AI + RPA 协作与授权边界(涉及外部系统时必填)
|
||||
## 0.1 宿主入口定位(必填)
|
||||
|
||||
先读 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md)。对话任务 / 数据管理 / 定时任务在宿主上**平级**;挂哪些由本技能业务与 UX 定,模板不规定「必须三入口都挂满」。用户文档如何写见 [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md)。
|
||||
|
||||
对每个主能力填一行(可增行;「不挂」必须写理由):
|
||||
|
||||
| 主能力(Action id / CLI) | 对话 | 数据管理 | 定时 | 教程章节 | 不挂理由(若不挂) |
|
||||
|---------------------------|------|----------|------|----------|--------------------|
|
||||
| (例)`<action-or-cli>` | 挂 / 不挂 | 不挂 / toolbar / row / batch / replaces / 仅展示表 | 挂 / 不挂 | 入口一 / 入口二 / 入口三 / N/A | |
|
||||
|
||||
说明:
|
||||
|
||||
- 数据管理列可写组合(如 `toolbar + row`)
|
||||
- 「仅展示表」= 用户要看过程/结果,但不在表上触发该能力
|
||||
- 定位为「挂」的入口须在 `assets/actions.json`(及必要表元数据)中落实,并与对话路径共用同一 domain service
|
||||
- **教程章节**:该能力在 `TUTORIAL.md` 中落在哪一节;整技能无定时时,入口三仍须写「不需要 + 理由」
|
||||
|
||||
### 0.1.1 用户市场文档(与入口表一致)
|
||||
|
||||
- `README.md`(说明 Tab):只写能力 / 场景 / 边界;**禁止**分步操作与 Agent 话术(见 `TUTORIAL_SURFACES.md` §1)
|
||||
- `TUTORIAL.md`(教程 Tab):覆盖 §0.1 中所有「挂」的入口;准备/载入类优先数据管理 toolbar,**不要**把 skill-detail 当主路径
|
||||
- 定位为「不挂」定时:教程须有 N/A 声明,与本表理由一致
|
||||
|
||||
## 0.2 AI + RPA 协作与授权边界(涉及外部系统时必填)
|
||||
|
||||
### Agent 设计
|
||||
|
||||
@@ -69,6 +92,20 @@
|
||||
- 需要保存哪些日志、截图、视频和业务结果:
|
||||
- 定位失败时如何保存 DOM/UIA/XML、候选数量和环境版本:
|
||||
|
||||
## 0.3 业务行生命周期(有「处理某一行」类能力时必填)
|
||||
|
||||
若本技能存在对业务表某一行做生成、触达、私信、发布等**有副作用**的处理,须遵守 [`../references/WORK_ITEM_STATUS.md`](../references/WORK_ITEM_STATUS.md),并在此声明**本技能状态 → 角色**映射(状态名自定,角色固定):
|
||||
|
||||
| 角色 | 本技能状态值(可多个) | 说明 |
|
||||
|------|------------------------|------|
|
||||
| 可操作(operable) | (例)新建 / 待生成 / … | 默认唯一可被处理 Action 捞起 |
|
||||
| 进行中(in_progress) | (例)生成中 / 触达中 / … | 抢占后、结束前 |
|
||||
| 成功类终态 | (例)已生成 / 已私信 / … | 默认不可再处理 |
|
||||
| 失败类终态 | (例)失败 / 触达失败 / … | 默认不可再处理;失败原因字段: |
|
||||
| 跳过类终态 | (例)已跳过 / … | 默认不可再处理 |
|
||||
|
||||
无此类处理队列时整节写「不适用」即可。
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
说明这份需求文档的作用,以及它解决什么问题。
|
||||
@@ -131,12 +168,13 @@
|
||||
- 支持 `python scripts/main.py <your-main-command>`
|
||||
- 支持核心业务编排(HTTP / 批处理 / 可选 RPA,按四档 adapter 选型)
|
||||
- 支持失败可诊断(统一文件日志 + 任务中心 `finish`)
|
||||
- 完成 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md) 入口定位表,并按表落实已挂入口(对话 / 数据管理 / 定时按本技能 UX)
|
||||
|
||||
### 可后续扩展
|
||||
|
||||
- 支持定时批跑
|
||||
- 支持失败自动重试
|
||||
- 支持多 `<目标系统>` 轮询
|
||||
- 入口定位表中已标明「本期不挂」且写了理由的能力(例如暂不挂定时)
|
||||
|
||||
## 5. 非功能要求
|
||||
|
||||
@@ -214,7 +252,7 @@
|
||||
- 真实联调样例仍放 `tests/integration/`,默认套件不包含真实外联(与业务 `.env` 默认 `real_rpa` 不冲突)
|
||||
- `SKILL.md` 的 `developer_ids` 已配置为开发者本人匠厂用户 ID(开发期不公开时,否则本人无法在市场安装自测;取 ID 步骤见 `development/DEVELOPMENT.md` §6)
|
||||
- 发布后 Gitea 工作流成功;匠厂技能市场对开发者账号可见最新版本并可安装
|
||||
- 安装后按本技能 `actions.json` 声明完成宿主多入口验收(见 `development/DEVELOPMENT.md` §15);**须看用户体验**(进度可见、失败可读、RPA 有头、文案与真实行为一致等),后台通了不算完
|
||||
- 安装后按本技能入口定位表与 `actions.json` 声明完成宿主多入口验收(见 `development/DEVELOPMENT.md` §15、`PRODUCT_SURFACES.md`);**须看用户体验**(进度可见、失败可读、RPA 有头、文案与真实行为一致等),后台通了不算完
|
||||
- 已人工核验授权与技术路线:无破解、逆向、私有协议、验证码绕过、Hook / 注入、自动换号或其他规避平台限制的实现
|
||||
- RPA 真实主路径已验证状态等待、`1~3` 秒正常操作节奏、有限重试、账号 / 设备串行、人工介入、结果核验和结束存证
|
||||
- RPA 关键元素已在真实界面拾取并登记 element map;关键动作唯一定位,重启会话后复测通过
|
||||
@@ -304,7 +342,7 @@
|
||||
- 命令可运行;tests/run_tests.py -v 通过
|
||||
- 业务表(若有)符合 SCHEMA;过程看运行日志,进度看任务中心
|
||||
- `developer_ids` 已配置为「你的用户ID」(匠厂设置中复制的正整数)
|
||||
- 正式环境安装验证通过;已声明的宿主入口(新建任务 / 数据管理 / 定时任务 / 任务中心 / 技能详情等)按 DEVELOPMENT §15 自测
|
||||
- 正式环境安装验证通过;已声明的宿主入口(新建任务 / 数据管理 / 定时任务 / 任务中心 / 技能详情等)按 DEVELOPMENT §15 与入口定位表自测
|
||||
## 10. 开发注意事项
|
||||
|
||||
- 不修改无关项目;不引入旧模板结构
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
本文是匠厂宿主 **Skill Action / 任务中心** 的技能侧金标准。涉及浏览器 RPA、长耗时任务、数据管理按钮、定时任务或 Agent 直接调用时**必读**。
|
||||
|
||||
**产品入口(平级关系、何时挂哪个)**先读 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md);本文管 sync/async、任务中心与编排。
|
||||
|
||||
**边界:**
|
||||
|
||||
| 文档 | 职责 |
|
||||
|------|------|
|
||||
| 本文 | sync/async、任务中心、Agent 禁令、单内核编排、队列 pick、同步 vs 导出 |
|
||||
| [`../references/ACTIONS.md`](../references/ACTIONS.md) | `actions.json` 字段、placements、`bind.tables`、inputSchema |
|
||||
| [`../references/ACTIONS.md`](../references/ACTIONS.md) | `actions.json` 字段、placements、`bind.tables`、inputSchema、跨表 Lookup、字段条件显隐 `visibleWhen`、本地路径 `file`/`directory` + 可选 **`pathRoot`**、**按钮中文名 `label` 参考** |
|
||||
| [`LOGGING.md`](LOGGING.md) | `job_context` / `emit` / `finish` / Run Journal |
|
||||
| [`DATA_PATHS.md`](DATA_PATHS.md) | 下载/导入等落盘路径 |
|
||||
|
||||
@@ -19,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)。可选 **`cellPresentation`**(见 `references/CELL_PRESENTATION.md`):单元格语义标色;省略 = P0。
|
||||
3. **五职责正交** — `placements` / `bind.tables` / `executionProfile` / `entrypoint` / domain service 各管一事(见 ACTIONS.md)。可选第六轴 **policy**(见 `references/POLICY.md`):未启用 = L0 与今日一致。可选 **`hostEffects` / `steps`**(见 ACTIONS.md):数据管理交互前后的 UI/级联编排;省略 = 不额外改 UI(匠厂宿主 ≥2.8.5)。可选 **`hoverHint`**(见 ACTIONS.md):数据管理工具栏悬停;省略 = 不显示,不回退说明或确认黄条。可选 **`visibleWhen`**(见 ACTIONS.md):Action 参数表单字段条件显隐;省略 = 字段始终平铺;**勿与** Policy L3 行态 `visibleWhen` 混淆。可选 **`pathRoot`**(见 ACTIONS.md):`file`/`directory` 原生选路对话框起始目录(相对技能数据根;省略默认 `{skill_data}/input`)。可选 **`bind.labelColumn`**(见 ACTIONS.md):选择摘要可读标签列;省略 = 宿主常见列名启发。可选 **`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 流**。
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
- **发布打包守护**:[`tests/test_release_packaging_constraints.py`](../tests/test_release_packaging_constraints.py) ——`scripts/**/*.py` 单文件 < 1000 行、文本文件 UTF-8 without BOM。
|
||||
- **开发规范守护(hard policy)**:[`tests/test_development_policy_guard.py`](../tests/test_development_policy_guard.py) ——规则索引见 [`development/POLICY_MATRIX.md`](POLICY_MATRIX.md),只执行可机器检测的 hard 规则。
|
||||
- **slug 语义**:[`tests/test_slug_naming.py`](../tests/test_slug_naming.py) ——verb-noun-platform 命名规范校验。
|
||||
- **HTML 报告壳**:[`tests/test_html_page_service.py`](../tests/test_html_page_service.py) 守护 `assets/html/example-report.html` 的数据管理 iframe 预览布局基线;业务技能若有自定义 `assets/html/*.html`,建议在自有仓库加同类静态断言(见 `references/ACTIONS.md`「宿主预览布局」)。
|
||||
|
||||
**原则:`tests/test_*.py` 不允许隐形访问外网、不允许拉起真实浏览器、不允许读写开发者机器的真实数据根**。如果需要仿真服务器,也应仅在 integration(并由档位变量放行)。
|
||||
|
||||
|
||||
206
development/TUTORIAL_SURFACES.md
Normal file
206
development/TUTORIAL_SURFACES.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# 用户市场文档契约(说明 Tab / 教程 Tab)
|
||||
|
||||
本文给**技能开发者**与 **AI 编程助手**读:规定根目录 `README.md`(技能说明)与 `TUTORIAL.md`(使用教程)**写什么、不写什么**,以及如何与宿主三入口、入口定位表、`actions.json` 对齐。
|
||||
|
||||
**单一真相链:**
|
||||
|
||||
```text
|
||||
REQUIREMENTS.md §0.1 入口定位表
|
||||
↓
|
||||
assets/actions.json(placements / label / bind)
|
||||
↓
|
||||
TUTORIAL.md(用户可读、分入口操作)
|
||||
README.md(能力与边界;不写步骤)
|
||||
```
|
||||
|
||||
产品入口「挂什么」见 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md)。本文只管**用户市场文档怎么投影**。
|
||||
|
||||
---
|
||||
|
||||
## 0. 与四 Tab 的关系
|
||||
|
||||
| 文件 | 市场 Tab | 本文角色 |
|
||||
|------|----------|----------|
|
||||
| `README.md` | **说明** | §1:是什么 / 给谁 / 边界 |
|
||||
| `TUTORIAL.md` | **教程** | §2~§6:怎么用(分入口) |
|
||||
| `DEMO.md` | 演示 | 观看顺序可对齐三入口,不另造标准 |
|
||||
| `CHANGELOG.md` | 更新日志 | 与文档契约无关 |
|
||||
|
||||
**铁律:** 说明就是说明,教程就是教程。**禁止**在 README 里写分步操作、Agent 话术块、排障「点哪里」;这些一律进 `TUTORIAL.md`。
|
||||
|
||||
---
|
||||
|
||||
## 1. README.md(说明 Tab)
|
||||
|
||||
### 1.1 必须回答
|
||||
|
||||
| 问题 | 写法 |
|
||||
|------|------|
|
||||
| 这是什么 | 标题 + 一两句价值 |
|
||||
| 能做什么 | 3~6 条能力 bullet(**无步骤编号**) |
|
||||
| 适合谁 / 场景 | 业务语言 |
|
||||
| 使用前准备 | 安装、账号、权限等**前置条件**;不写「去哪张表点哪」 |
|
||||
| 执行后得到什么 | 结果形态(表、报告、文件),**不教怎么生成** |
|
||||
| 注意事项 | 边界、风险、人工复核 |
|
||||
| 怎么使用(可选,短) | **一两句**:支持哪些入口 + **「步骤见使用教程 Tab」** |
|
||||
|
||||
### 1.2 禁止写入 README
|
||||
|
||||
- `1.` `2.` `3.` 操作步骤(如「选品任务 → 点某某按钮」)
|
||||
- 「你可以这样告诉 Agent」整节或大块 `>` 示例话术
|
||||
- 「入口一 / 入口二」专章结构
|
||||
- 逐步排障(「先点 A 再点 B」)
|
||||
- frontmatter `description` 不要写「以及如何开始使用」类教程承诺
|
||||
|
||||
### 1.3 FAQ 边界
|
||||
|
||||
- README FAQ:**概念类**(和真实系统区别、从哪开始学、是否支持定时)→ 可指向「使用教程」Tab
|
||||
- **操作类**排障 → 只写在 `TUTORIAL.md`「常见卡住怎么办」
|
||||
|
||||
### 1.4 建议章节(标题可微调)
|
||||
|
||||
```text
|
||||
# 【技能中文名】
|
||||
## 这个技能能帮你做什么
|
||||
## 适合什么场景
|
||||
## 使用前需要准备什么
|
||||
## 怎么使用 ← 仅短摘要 + 指向教程
|
||||
## 执行后你会得到什么
|
||||
## 注意事项
|
||||
## 常见问题 ← 概念类
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. TUTORIAL.md(教程 Tab)必选结构
|
||||
|
||||
复制业务技能后,**验收章节(标题可微调,语义不可缺)**:
|
||||
|
||||
```text
|
||||
# 【技能中文名】· 使用教程
|
||||
(开篇:数据管理与对话平行;定时有/无一句)
|
||||
|
||||
## 本技能做什么 ← 业务数据流(非 CLI/SQL)
|
||||
## 开始前
|
||||
## 入口一:数据管理 ← §0.1 有 toolbar/row/batch/replaces 则必填
|
||||
## 入口二:对话任务 ← §0.1 有 agent 则必填
|
||||
## 入口三:定时任务 ← 有 cron:怎么用;无 cron:不需要 + 理由
|
||||
## 主能力对照表 ← 双挂能力必填
|
||||
## 常见卡住怎么办
|
||||
```
|
||||
|
||||
### 2.1 与入口定位表的关系
|
||||
|
||||
| §0.1 / actions | TUTORIAL 要求 |
|
||||
|----------------|---------------|
|
||||
| 任一能力挂数据管理(toolbar/row/batch/replaces) | **必须**有「入口一:数据管理」专章 |
|
||||
| 任一能力挂 `agent` | **必须**有「入口二:对话任务」专章(**禁止**用「或者你也可以说…」替代专章) |
|
||||
| 任一能力挂 `cron` | **必须**有「入口三」且写清如何建定时、选哪个 Action |
|
||||
| **无** `cron` | **必须**写「本技能不需要定时」+ 一句理由(与 §0.1 一致) |
|
||||
| 某能力只挂一端 | 对照表注明「仅数据管理」或「仅对话」 |
|
||||
|
||||
### 2.2 平行关系(数据管理 ∥ 对话)
|
||||
|
||||
- 两者是**平行主入口**,不是「对话主、点表辅」或反过来
|
||||
- 禁止全文只有「第一步:打开对话说清目标」且无数据管理专章
|
||||
- 允许用户只用其中一种;教程须写清「两种效果一致,选你习惯的」
|
||||
|
||||
---
|
||||
|
||||
## 3. 入口一:数据管理(怎么写)
|
||||
|
||||
适合写:
|
||||
|
||||
1. 左侧有哪些业务表、各干什么(业务语言)
|
||||
2. 首次准备(载入/导入/新建)——**按钮必须出现在用户能看到的地方**(见 §5)
|
||||
3. 主流程:进哪张表 → 是否要选中行 → 点哪个 `label` → 去哪看结果
|
||||
4. 行级 / 批量操作(若有 `row` / `batch`)
|
||||
|
||||
按钮中文名 = `assets/actions.json` 的 `label`,不要另造别名。
|
||||
|
||||
---
|
||||
|
||||
## 4. 入口二:对话任务(怎么写)
|
||||
|
||||
适合写:
|
||||
|
||||
1. 什么时候用对话(与数据管理互补)
|
||||
2. 示例话术表(与主能力一一对应)
|
||||
3. Agent 可能追问什么、用户怎么答
|
||||
|
||||
**不要**把对话写成唯一路径;也不要把话术塞进 README。
|
||||
|
||||
---
|
||||
|
||||
## 5. 准确性铁律(用户看得见才写)
|
||||
|
||||
教程步骤必须对应 **宿主真实 UI**,不是只对应 manifest 声明。
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| **placements 对齐** | 教程写的入口 ∈ `actions.json` 已挂 placement |
|
||||
| **skill-detail 不可作主路径** | 声明了 `skill-detail` **不等于**详情页一定渲染出按钮;当前宿主常只显示四 Tab。**禁止**把「技能详情 → 点某某」写成用户主路径 |
|
||||
| **准备/载入/导入类** | 优先挂 **数据管理 `toolbar`** + `bind.tables`(空表也可点的能力尤其如此);可保留 `agent`;`skill-detail` 仅作可选兼容 |
|
||||
| **写入后刷新** | 建议 `steps: [{ "when": "afterSuccess", "do": "refreshCatalog" }]`,避免用户点完表不刷新 |
|
||||
| **对照表** | 「想做什么 \| 数据管理 \| 对话」覆盖双挂主能力;`label` 与界面一致 |
|
||||
|
||||
字段细节见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「用户可见性与教程准确性」。
|
||||
|
||||
---
|
||||
|
||||
## 6. 入口三:定时任务(可选)
|
||||
|
||||
### 6.1 何时挂 `cron`(教程教「怎么用定时」)
|
||||
|
||||
- 参数相对固定、按节奏重复(同步、巡检、消费队列)
|
||||
- 无人值守仍安全、可幂等或可失败重试
|
||||
- 与数据管理是「白天点选 + 夜里自动啃」组合
|
||||
|
||||
教程写:侧栏定时任务 → 新建 → 选本技能 → 选 Action → 填参 → 保存/立即运行。
|
||||
|
||||
### 6.2 何时不挂(教程写「不需要」)
|
||||
|
||||
- 一次性 / 课堂演示,对象当场指定
|
||||
- 强不可逆、须当场确认
|
||||
- 每次对象不同、须人指定链接或范围
|
||||
- 主路径依赖验证码 / 人工介入
|
||||
|
||||
决策三问(与 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md) §2.3 一致):
|
||||
|
||||
1. 明天人不盯着、同样参数再跑一遍,业务上合理吗?
|
||||
2. 失败后自动重跑可接受吗?
|
||||
3. 有没有须人当场确认或动态指定的输入?
|
||||
|
||||
①是 且 ②可接受 且 ③否 → 可挂 cron;否则不挂,§0.1 写理由,教程声明 N/A。
|
||||
|
||||
---
|
||||
|
||||
## 7. 复制后自检(发布前)
|
||||
|
||||
- [ ] README 无分步操作、无 Agent 话术块
|
||||
- [ ] TUTORIAL 有三入口结构(定时有或 N/A)
|
||||
- [ ] 对照表与 `actions.json` `label` 一致
|
||||
- [ ] 准备类按钮在数据管理表顶可见(若技能有数据管理主路径)
|
||||
- [ ] 教程未把 skill-detail 当唯一载入/主流程入口
|
||||
- [ ] §0.1「挂」的入口在 TUTORIAL 有专章;「不挂」有理由且教程一致
|
||||
|
||||
交活清单见 [`CHECKLIST.md`](CHECKLIST.md) A2-7~A2-11。
|
||||
|
||||
---
|
||||
|
||||
## 8. 参考样例
|
||||
|
||||
已验收参考(业务技能,非模板正文):`client-gdcm/review-sku-selection-default`
|
||||
(说明 Tab 瘦身 + 教程三入口 + 「载入课堂样例」挂 `selection_cases` toolbar。)
|
||||
|
||||
复制模板后**不要**照搬选品表名与样例编码;只复用**章节结构与职责划分**。
|
||||
|
||||
---
|
||||
|
||||
## 9. 相关文档
|
||||
|
||||
- 宿主入口何时挂:[`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md)
|
||||
- 入口定位表:[`REQUIREMENTS.md`](REQUIREMENTS.md) §0.1
|
||||
- Action placements / 可见性:[`../references/ACTIONS.md`](../references/ACTIONS.md)
|
||||
- 市场四 Tab 同步:[`DEVELOPMENT.md`](DEVELOPMENT.md) §6 附近「用户市场四 Tab」
|
||||
- 交活验收:[`CHECKLIST.md`](CHECKLIST.md) §2
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
本文件说明 `assets/actions.json` 与 `scripts/main.py` CLI 的对应关系,供 **Agent 编排**与宿主 **Skill Action Runtime** 使用。这是 Agent 侧关于 Action / 任务中心的**主契约**;实现细节与开发规范见 [`../development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md)。
|
||||
|
||||
**先读产品入口:** 对话 / 数据管理 / 定时任务在宿主上**平级**,何时挂哪个入口见 [`../development/PRODUCT_SURFACES.md`](../development/PRODUCT_SURFACES.md)(本文只讲字段与执行模型)。
|
||||
|
||||
**边界:** 用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.md);CLI 参数见 [`CLI.md`](CLI.md)。
|
||||
|
||||
## Agent 何时用 Action(与 CLI 的分工)
|
||||
@@ -27,6 +29,7 @@
|
||||
3. 有可感知副作用时:提供 `confirmation.message`
|
||||
4. 需要用户/Agent 入参时:简单 `inputSchema` + entrypoint 中的 `{{fieldName}}`
|
||||
5. `SKILL.md`「用户意图路由表」与 `CLI.md`「Action 强制黑名单」**出现同一 action id / 命令**
|
||||
6. 若处理的是**业务队列表中的一行**:遵守 [`WORK_ITEM_STATUS.md`](WORK_ITEM_STATUS.md)——默认只捞**可操作**角色;开始抢占为**进行中**;结束回写成功/失败(失败带原因);确认文案写清仅可操作态可处理
|
||||
|
||||
默认模板 `assets/actions.json` 仅含诊断类 sync Action,**不要**为通过本 checklist 而塞假业务 Action;业务技能按需添加。完整字段见下文 Phase 1。
|
||||
|
||||
@@ -48,10 +51,10 @@ JSON Schema 使用 `additionalProperties: false` 表达模板严格边界;这
|
||||
|
||||
## Action 是否必需
|
||||
|
||||
Action 是**可选能力**:
|
||||
Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**决定(见 [`../development/PRODUCT_SURFACES.md`](../development/PRODUCT_SURFACES.md)),不是「可有可无的附属能力」:
|
||||
|
||||
- 没有宿主直接操作需求的技能,**可以不提供** `actions.json`。
|
||||
- 需要出现在**数据管理按钮**、**技能详情**、**定时任务**或 **Agent 直接调用**中的技能,**必须**提供 `assets/actions.json`。
|
||||
- 定位需要出现在**数据管理按钮**、**技能详情**、**定时任务**或 **Agent 直接调用**中时,**必须**提供 `assets/actions.json` 并声明对应入口。
|
||||
- 定位确认无任何宿主结构化入口时,**可以不提供** `actions.json`。
|
||||
- **含浏览器 RPA / 长耗时任务**的技能:**必须**提供至少一条 `executionProfile: "async"` 且 `placements` 含 `agent` 的 action(见 `POLICY-SKILL-ACTION-001`)。
|
||||
|
||||
## 文件位置
|
||||
@@ -81,10 +84,12 @@ Action 是**可选能力**:
|
||||
| 字段 / 层 | 只决定什么 | 不决定什么 |
|
||||
|-----------|------------|------------|
|
||||
| **`placements`** | Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail;`row`=行级入口,壳由宿主呈现) | 不决定 sync/async,不决定点了是否弹窗,不决定行级用操作列还是右键,不决定谁能点 |
|
||||
| **`replaces`** | **可选**:接管数据管理默认「新增 / 编辑 / 删除」 | 不强制;无配置时宿主仍直写 SQLite |
|
||||
| **`replaces`** | **可选**:接管数据管理默认「新增 / 编辑 / 删除 / **导入**」 | 不强制;无配置时宿主仍直写 SQLite;**import 时 UI 仍用宿主「导入数据」面板** |
|
||||
| **`bind.tables`** | 数据管理里出现在**哪些业务表** | 不决定业务实现 |
|
||||
| **`executionProfile`** | 宿主是**同步等待**还是**异步 Job** | 不限制可展示入口,不决定是否弹窗 |
|
||||
| **`interaction`** | 可点击入口(数据管理 toolbar/row/batch、默认 CRUD 接管、未来 skill-detail)**点了怎么交互** | 不决定 sync/async;**cron 触发与 agent 忽略** |
|
||||
| **`hoverHint`** | **可选**:数据管理**工具栏悬停**文案 | 不决定短按钮名;不决定点下去弹不弹窗;**不回退** `description` / `confirmation.message`;省略 = 不显示悬停。当前宿主实现为顶栏技能按钮 |
|
||||
| **`inputSchema.visibleWhen`** | **可选**:Action **参数表单**字段条件显隐(先选模式再出对应参数) | **不是** Data Policy L3 行态 `visibleWhen`;不决定 placements;旧宿主忽略则字段平铺全显 |
|
||||
| **`hostEffects` / `steps`** | **可选**:数据管理交互跑完前后,宿主要不要刷新表 / 清选区 / 级联删等 | 不决定业务写什么;**省略 = 宿主不额外改 UI**(≥2.8.5 起不再硬编码 mutation 后刷新);cron/agent 忽略 |
|
||||
| **`cellPresentation`** | **可选**:某表某行某列的语义标色/强调(tone + 说明) | 不替代业务列;不下发 CSS;用途不限(超时、预警、驳回等);见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md) |
|
||||
| **`policy` / Snapshot** | **可选**:当前身份在数据管理下允许哪些 Action / 宿主内置能力 / 行列范围 | 不替代 placements;不替代 domain 写路径鉴权;多数技能不配 = L0 全开 |
|
||||
@@ -98,15 +103,17 @@ Action 是**可选能力**:
|
||||
| 层级 | 稳定字段 / 能力 |
|
||||
|------|----------------|
|
||||
| 顶层 | `schemaVersion`、`skill`、`actions`;可选 **`policy`**(Data Policy 启用块);可选 **`cellPresentation`**(单元格呈现启用块) |
|
||||
| action | `id`、`label`、`description`、`placements`、`entrypoint`、**`executionProfile`(必填)**;可选 `inputSchema`、`confirmation`、**`interaction`**、**`interactionByPlacement`**、**`bind`**、**`replaces`**、**`hostEffects`**、**`steps`**、**`authz`**(仅文档提示) |
|
||||
| action | `id`、`label`、`description`、`placements`、`entrypoint`、**`executionProfile`(必填)**;可选 `hoverHint`、`inputSchema`、`confirmation`、**`interaction`**、**`interactionByPlacement`**、**`bind`**、**`replaces`**、**`hostEffects`**、**`steps`**、**`authz`**(仅文档提示) |
|
||||
| entrypoint | `type = cli`、`command`、`args`(**必填** JSON 数组,项为 `string` / `number` / `boolean`) |
|
||||
| placements | `toolbar`、`row`、`batch`、`skill-detail`、`agent`、`cron`;仅 `replaces` 时可为空数组 |
|
||||
| **replaces** | 可选 `"create"` \| `"update"` \| `"delete"` — 接管默认新增/编辑/删除(须 `bind.tables`) |
|
||||
| **bind** | `{ "tables": ["business_table"] }`;可选 `inputMapping`(行内/批量预填,见下节) |
|
||||
| **replaces** | 可选 `"create"` \| `"update"` \| `"delete"` \| `"import"` — 接管默认新增/编辑/删除/导入(须 `bind.tables`) |
|
||||
| **bind** | `{ "tables": ["business_table"] }`;可选 `inputMapping`、`columnMapping`、**`labelColumn`**(行内/批量预填与选择摘要标签,见下节) |
|
||||
| **policy** | 可选;`enabled` + `entrypoint`(推荐 `policy` CLI);见 POLICY.md。未启用 = L0 |
|
||||
| **cellPresentation** | 可选;`{ "enabled": true }` 时宿主应加载单元格呈现;见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)。未启用 = P0 |
|
||||
| inputSchema | 根 `type: object`;properties 使用 `string` / `number` / `boolean`;支持 `enum`、`default`、`required`、`sensitive`、`readOnly`;可选 `display_type` / `format`(`textarea` / `date` / `datetime_unix_seconds` / `url` / `email`) |
|
||||
| inputSchema | 根 `type: object`;properties 使用 `string` / `number` / `boolean`;支持 `enum`、`default`、`required`、`sensitive`、`readOnly`;可选 `display_type` / `format`(含 `file` / `directory` 等);可选 **`pathRoot`**(file/directory 起始目录,相对技能数据根);可选 **`visibleWhen`**(字段条件显隐,见专节) |
|
||||
| confirmation | `{ "message": "..." }` — **风险文案**(confirm/form 弹窗黄条;agent 元数据);**不是**「要不要弹窗」的开关 |
|
||||
| **hoverHint** | 可选字符串。数据管理**工具栏悬停**文案;与 `description` / `confirmation.message` **独立**。省略则不显示悬停,不回退。短名已够懂时不要写。需宿主已支持该字段;当前实现为顶栏技能按钮,不含宿主内置删除/刷新/新增/导入/导出 |
|
||||
| **visibleWhen** | 可选。写在 **property** 上(非 action 顶层):依赖同表单其它字段值决定是否展示;隐藏字段不提交、不校验 `required`。**勿与** [`POLICY.md`](POLICY.md) L3 行态 `visibleWhen` 混淆 |
|
||||
| **executionProfile** | `"sync"` \| `"async"`(**必须显式写出**,禁止依赖缺省) |
|
||||
| **interaction** | `"auto"` \| `"direct"` \| `"confirm"` \| `"form"`(可选;省略≈`auto`) |
|
||||
| **interactionByPlacement** | 可选;按 placement 覆盖为 `direct`/`confirm`/`form`(不可写 `auto`) |
|
||||
@@ -125,41 +132,185 @@ Action 是**可选能力**:
|
||||
|
||||
规则:
|
||||
|
||||
- `bind` 为 object,`additionalProperties: false`;当前允许 `tables`(必填键当存在 bind 时)与可选 `inputMapping`。
|
||||
- `bind` 为 object,`additionalProperties: false`;当前允许 `tables`(必填键当存在 bind 时)与可选 `inputMapping`、`columnMapping`、**`labelColumn`**。
|
||||
- `tables` 非空数组、元素唯一、英文 **snake_case**、不得为空字符串。
|
||||
- **`placements` 含 `toolbar` 时必须显式声明非空 `bind.tables`**(模板 Schema `if/then` + 自动测试强制)。
|
||||
- `row` / `batch` **建议**同样声明 `bind.tables`,避免在无关表上露出按钮;未声明时宿主可能按兼容逻辑在更多表上展示。
|
||||
- 表名须真实存在于技能 SQLite 与 `_jiangchang_tables`。
|
||||
- 希望出现在多张表时,显式列出所有表;**不要**依赖「无 bind 则全表展示」——那是宿主对**旧技能**的兼容,不是新技能标准。
|
||||
- **`inputMapping`(可选)**:把行/勾选映射到 `inputSchema` 字段,例如 `"leadId": "$row.$pk"`、`"ids": "$selection.ids"`。适合 `row` / `batch`。宿主仍会提交这些字段;可用 `inputSchema.properties.*.readOnly: true` 在表单中只读展示。
|
||||
- **`columnMapping`(可选)**:把 inputSchema 属性名映射到物理列名,供表单复用列展示名 / lookup。
|
||||
- **`labelColumn`(可选)**:物理列名,供数据管理 Action 弹层「已选择」摘要展示 `#id + 标签`。**仅影响展示,不改变 CLI/提交 payload,也不决定业务是否跳过某些状态。** 省略时宿主用常见列名启发(如 `title` / `name` / `nickname` 等);再没有则只显示编号。列名不常见、或启发会选错列时再显式声明。
|
||||
- `concurrency` / `locks` 仍未稳定,**不要**写入新技能 manifest。
|
||||
|
||||
## `replaces`(可选:接管默认新增 / 编辑 / 删除)
|
||||
### 数据管理:选择摘要(宿主默认能力)
|
||||
|
||||
有领域副作用的表(建目录、加密、级联清理)不要依赖宿主直写 SQLite。此时可为该表声明:
|
||||
当用户在数据管理**勾选行**后打开 `confirm` / `form` 弹层时,宿主会展示通用 **「已选择 · N 条」** 摘要(`#主键` + 可读标签;超过约 3 条可展开查看全部)。这是**宿主 UX**,对所有技能默认生效:
|
||||
|
||||
- **技能零改动即可受益**;不要在 `confirmation.message` 里手写「已选 id 列表」,也不要为了面板去拼业务状态跳过文案。
|
||||
- 摘要与执行解耦:勾选了谁由 UI 说明;**实际处理哪些行、是否跳过某些状态,仍由技能 CLI / domain service 决定**。
|
||||
- 需要稳定标签列时,在 `bind` 上声明可选 `labelColumn`(见上)。示例:[`assets/examples/selection-label-column.action.example.json`](../assets/examples/selection-label-column.action.example.json)。
|
||||
|
||||
## `replaces`(可选:接管默认新增 / 编辑 / 删除 / 导入)
|
||||
|
||||
有领域副作用的表(归一化字段、建目录、加密、级联清理)不要依赖宿主直写 SQLite。此时可为该表声明 `replaces`,**用户仍点宿主默认按钮**,由技能 CLI 完成写入。
|
||||
|
||||
### 怎么选
|
||||
|
||||
| 场景 | 建议 |
|
||||
|------|------|
|
||||
| 纯表字段、无副作用 | 不配 `replaces`,宿主直写即可 |
|
||||
| 单条新增/编辑/删除有副作用 | `replaces: create` / `update` / `delete` |
|
||||
| 批量导入也有副作用(推荐) | 另配 `replaces: import`(与 create 可同表并存) |
|
||||
| 只有 create、暂无 import | 宿主「导入」面板确认后会**逐行调 create**(兼容);正式产品应补 `import` |
|
||||
|
||||
### 导入分流(必读)
|
||||
|
||||
点工具栏「导入」时:
|
||||
|
||||
1. **UI 一律**打开宿主「导入数据」面板(下载模板 → 选文件 → 映射 → 出错规则 → 预览)。
|
||||
2. **写库**按表能力分流:
|
||||
- 有 `replaces: import` → 把选中文件路径 + 出错规则交给该 Action(技能解析并写入)
|
||||
- 仅有 `replaces: create` → 宿主按行调用 create(兼容)
|
||||
- 都没有 → 宿主直写 SQLite
|
||||
|
||||
**不要**再做一张技能自己的「开始执行」导入表单来冒充导入体验。`placements` 对 import 保持 `[]`;`description` 写明「由宿主导入面板调用」。
|
||||
|
||||
可抄文件见 [`assets/examples/`](../assets/examples/):`replaces-*.action.example.json`。
|
||||
|
||||
### 示例:create
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "account-add-web",
|
||||
"label": "新增账号",
|
||||
"description": "登记网页账号并初始化 profile 目录。",
|
||||
"id": "item-create",
|
||||
"label": "新增记录",
|
||||
"description": "新增业务记录并执行领域副作用(走技能 CLI,非宿主直写库)。",
|
||||
"replaces": "create",
|
||||
"placements": [],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "form",
|
||||
"bind": { "tables": ["accounts"] },
|
||||
"bind": { "tables": ["items"] },
|
||||
"hostEffects": { "onSuccess": ["reloadTable"] },
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["platform_key", "account_label"],
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"platform_key": { "type": "string", "title": "平台" },
|
||||
"account_label": { "type": "string", "title": "账号名称" }
|
||||
"name": { "type": "string", "title": "名称" },
|
||||
"note": { "type": "string", "title": "备注" }
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "account",
|
||||
"args": ["add-web", "--platform", "{{platform_key}}", "--label", "{{account_label}}"]
|
||||
"command": "item",
|
||||
"args": ["create", "--name", "{{name}}", "--note", "{{note}}"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例:update
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "item-update",
|
||||
"label": "编辑记录",
|
||||
"description": "编辑业务记录并重算派生字段。",
|
||||
"replaces": "update",
|
||||
"placements": [],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "form",
|
||||
"bind": {
|
||||
"tables": ["items"],
|
||||
"inputMapping": {
|
||||
"id": "$row.$pk",
|
||||
"name": "$row.name",
|
||||
"note": "$row.note"
|
||||
}
|
||||
},
|
||||
"hostEffects": { "onSuccess": ["reloadTable"] },
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": { "type": "number", "title": "编号", "readOnly": true },
|
||||
"name": { "type": "string", "title": "名称" },
|
||||
"note": { "type": "string", "title": "备注" }
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "item",
|
||||
"args": ["update", "--id", "{{id}}", "--name", "{{name}}", "--note", "{{note}}"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例:delete
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "item-delete",
|
||||
"label": "删除记录",
|
||||
"description": "删除业务记录并清理关联副作用。",
|
||||
"replaces": "delete",
|
||||
"placements": [],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "confirm",
|
||||
"confirmation": { "message": "确定删除该记录?关联数据将一并清理。" },
|
||||
"bind": {
|
||||
"tables": ["items"],
|
||||
"inputMapping": { "id": "$row.$pk" }
|
||||
},
|
||||
"hostEffects": { "onSuccess": ["reloadTable", "clearSelection"] },
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": { "type": "number", "title": "编号", "readOnly": true }
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "item",
|
||||
"args": ["delete", "--id", "{{id}}"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例:import
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "item-import",
|
||||
"label": "导入记录",
|
||||
"description": "由宿主「导入数据」面板调用:解析 Excel/CSV 并写入业务表(含领域校验与派生字段)。",
|
||||
"replaces": "import",
|
||||
"placements": [],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "form",
|
||||
"bind": { "tables": ["items"] },
|
||||
"hostEffects": { "onSuccess": ["reloadTable"] },
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["filePath"],
|
||||
"properties": {
|
||||
"filePath": {
|
||||
"type": "string",
|
||||
"title": "导入文件",
|
||||
"description": "由宿主导入面板传入的 Excel/CSV 路径。",
|
||||
"format": "file"
|
||||
},
|
||||
"onError": {
|
||||
"type": "string",
|
||||
"title": "导入规则(出错时)",
|
||||
"enum": ["跳过该行,继续导入", "立即停止,不再导入后续行"],
|
||||
"default": "跳过该行,继续导入"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "item",
|
||||
"args": ["import", "--file", "{{filePath}}", "--on-error", "{{onError}}"]
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -167,18 +318,20 @@ Action 是**可选能力**:
|
||||
规则:
|
||||
|
||||
- **全可选**:无 `replaces` 时,宿主默认按钮仍直写库,行为与以前一致。
|
||||
- 取值:`create`(新增)/ `update`(编辑)/ `delete`(删除)。
|
||||
- 取值:`create`(新增)/ `update`(编辑)/ `delete`(删除)/ `import`(导入写库)。
|
||||
- **必须**非空 `bind.tables`;同一技能同一表同一 `replaces` 至多一个 Action。
|
||||
- 与 `placements` 正交:可只接管默认按钮(`placements: []`),不必再挂 toolbar「第二个新增」。
|
||||
- 点默认按钮时走该 Action 的 `interaction`(create/update 常用 `form`;delete 常用 `confirm`)。
|
||||
- 与 `placements` 正交:可只接管默认按钮(`placements: []`),不必再挂 toolbar「第二个新增/导入」。
|
||||
- 点默认「新增/编辑/删除」时走该 Action 的 `interaction`(create/update 常用 `form`;delete 常用 `confirm`)。
|
||||
- 点默认「导入」时**固定**打开宿主导入面板;确认后若有 `replaces:import` 则调用该 Action(`interaction` 不决定是否再弹技能表单)。
|
||||
- `update` / `delete` 可用 `inputMapping`(`$row.$pk`、`$selection.ids`)预填主键。
|
||||
- 宿主导入仍直写:有 `replaces:create` 的表慎用宿主导入,或另做 import Action。
|
||||
- `import` 约定:`filePath`(或 `file`)+ 可选 `onError`(`skip`/`abort` 或上例中文枚举);技能自行解析文件与表头。
|
||||
- 整表 `_jiangchang_tables.readonly=1` 时仍隐藏写按钮(优先于 `replaces`)。
|
||||
- **列元数据复用(lookup)**:create/update 抽屉可将 `inputSchema` 字段对齐业务列,复用中文名与 lookup 下拉:
|
||||
- **列元数据复用(lookup)**:Action 表单(含 toolbar/batch 弹窗与 create/update 抽屉)可将 `inputSchema` 字段对齐业务列,复用中文名与 lookup 下拉:
|
||||
- 优先让属性名 = 物理列名(如 `platform_key`);
|
||||
- 或 `bind.columnMapping`:`{ "platform": "platform_key" }`;
|
||||
- 或字段上写 `"column": "platform_key"`。
|
||||
- **不要**在 actions.json 里写死会过期的全量平台 `enum`。
|
||||
- 当前表没有对应列时,用字段内联 `lookup`(见下文「跨表引用:Lookup」)。
|
||||
- **不要**在 actions.json 里写死会过期的全量平台 `enum`,也**不要**让用户手填内部编号。
|
||||
|
||||
## `interaction`(点击交互方式)
|
||||
|
||||
@@ -186,16 +339,16 @@ Action 是**可选能力**:
|
||||
|
||||
| 值 | 数据管理体验 | 典型场景 |
|
||||
|----|--------------|----------|
|
||||
| `direct` | **一点即跑**,无弹窗 | 打开文件位置、无参只读、行内主键已映射 |
|
||||
| `confirm` | 轻量确认弹窗(无参数区;文案优先 `confirmation.message`) | 打开浏览器、有副作用但无可编辑入参 |
|
||||
| `form` | 完整参数表单(现状默认路径) | 导入、查询条件、需用户改写的字段 |
|
||||
| `direct` | **一点即跑**,无弹窗 | 打开目录、打开浏览器、无参只读、行内主键已映射 |
|
||||
| `confirm` | 轻量确认弹窗(无参数区;文案优先 `confirmation.message`;有勾选时宿主展示「已选择」摘要) | 处理待发送、删除前确认、有副作用但无可编辑入参 |
|
||||
| `form` | 完整参数表单(有勾选时同样展示「已选择」摘要) | 导入、查询条件、需用户改写的字段 |
|
||||
| `auto` / 省略 | 宿主推断:有**可编辑**参数 → `form`;仅有 `confirmation` → `confirm`;否则 → `direct` | 旧技能兼容;新技能建议显式写出 |
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "reveal-profile",
|
||||
"label": "打开文件位置",
|
||||
"description": "在资源管理器中打开 Profile 目录。",
|
||||
"label": "打开目录",
|
||||
"description": "打开该账号的浏览器 Profile 目录。",
|
||||
"placements": ["row"],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "direct",
|
||||
@@ -222,10 +375,10 @@ Action 是**可选能力**:
|
||||
{
|
||||
"id": "open-browser",
|
||||
"label": "打开浏览器",
|
||||
"description": "用该账号持久化 Profile 打开系统浏览器,仅供肉眼核对。",
|
||||
"placements": ["row"],
|
||||
"executionProfile": "async",
|
||||
"interaction": "confirm",
|
||||
"confirmation": { "message": "将用该账号 Profile 打开浏览器,仅供查看。" },
|
||||
"executionProfile": "sync",
|
||||
"interaction": "direct",
|
||||
"bind": {
|
||||
"tables": ["accounts"],
|
||||
"inputMapping": { "accountId": "$row.$pk" }
|
||||
@@ -234,7 +387,7 @@ Action 是**可选能力**:
|
||||
"type": "object",
|
||||
"required": ["accountId"],
|
||||
"properties": {
|
||||
"accountId": { "type": "number", "readOnly": true }
|
||||
"accountId": { "type": "number", "title": "账号 ID", "readOnly": true }
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
@@ -253,6 +406,63 @@ Action 是**可选能力**:
|
||||
- **cron 触发 / agent**:忽略 `interaction`(无人值守 / 模型填参);`confirmation` 对 agent 仍作 `requiresConfirmation` 元数据。
|
||||
- 不要用 `interaction: "direct"` 掩盖需要用户填写的可编辑字段——宿主在 `auto` 下会因可编辑参数回落到 `form`;显式 `direct` + 可编辑字段时参数取 default/预填,作者须自觉。
|
||||
|
||||
## Action 按钮中文名(`label`,参考)
|
||||
|
||||
`label` 是数据管理 **toolbar / row / batch** 以及技能详情直调上用户看见的短按钮名。本节是**参考契约,不是白名单**:行业专有动作(私信、审单、报关登记等)按业务起名。同类通用动作尽量用同一动词,避免每个技能一套叫法。
|
||||
|
||||
**写法:** 短中文,动词或「动词 + 对象」(约 2~8 字)。同一能力用**一个** Action 换 `placements`,不要为入口复制同义按钮。
|
||||
|
||||
以后增补:在下表加行,并在模板 `CHANGELOG.md` 记一笔。废弃说法挪到「不要叫」,不要直接删行。
|
||||
|
||||
### 宿主已有,技能不要再做一套同义按钮
|
||||
|
||||
数据管理宿主已提供:**查看 / 编辑 / 删除 / 新增 / 导入 / 导出 / 刷新**。
|
||||
|
||||
技能要用 CLI 接管增删改导入时用 `replaces`(见上文),**不要**再把同义按钮挂到 `placements: ["row"]` 或 `toolbar`(例如「编辑平台」「删除 Key」与底下宿主「编辑」「删除」叠两套)。用户仍点宿主那几个字。
|
||||
|
||||
### 建议动词(有则用之)
|
||||
|
||||
| 类型 | 建议 `label` | 不要叫 | 说明 |
|
||||
|------|----------------|--------|------|
|
||||
| 基础设施 | 运行检查、查看版本、查看配置位置、初始化数据 | 健康检查、版本信息、配置路径、初始化数据库 | 多挂 `skill-detail` |
|
||||
| 打开 | 打开浏览器、打开目录、打开文件 | 打开文件位置、在资源管理器中打开、用浏览器打开 | 见下节静默成功 |
|
||||
| 改库 | 新增…、编辑…、删除…、启用/停用、设置默认…、加入待…、重置为待… | 表记录用「添加」;无必要拆成「启用」「停用」两个按钮 | 对象名可带,如「新增账号」 |
|
||||
| 外源入库 | 同步数据、全量同步、采集…、扫描… | 把宿主「刷新」叫成同步;把导出 CSV 叫成同步 | 默认用「同步数据」;全量才写「全量同步」 |
|
||||
| 队列与探测 | 处理待…、测试 API、测试网页、抽检本条、抽检所选、批量抽检 | 行 / 勾选 / 工具栏三个入口都叫「抽检」 | 行=本条,勾选=所选,不管勾选的批量=批量 |
|
||||
| 生成发布 | 生成 HTML、生成…、发布到… | 用「导出」代替宿主表导出 | 固定 id `render-html` 的 label 用「生成 HTML」 |
|
||||
|
||||
### 打开类:一点即开,成功不提示
|
||||
|
||||
打开目录 / 打开浏览器 / 打开文件:`interaction: "direct"`,`executionProfile` 通常 `"sync"`。
|
||||
|
||||
- **成功**:不要输出用户可见成功文案。若打印 JSON,令 `"message": ""`(宿主据此不弹「执行成功」)。操作系统已经弹出目录或浏览器,就是反馈。
|
||||
- **失败**:仍 `ERROR:<CODE>:`(可加 `HINT:`)。
|
||||
- **不要**用宿主「查看」表示打开目录或浏览器——「查看」是只读详情。
|
||||
|
||||
## `hoverHint`(工具栏悬停,可选)
|
||||
|
||||
与 `label` / `description` / `confirmation.message` **独立**。只决定数据管理**顶栏技能按钮**鼠标悬停时显示什么。
|
||||
|
||||
- **省略 / 空白** ⇒ 不显示悬停,**不回退**灰色说明或确认黄条。
|
||||
- 短按钮名已经够懂时**不要写**,避免噪音。适合:业务黑话、会覆盖本机/远程、不点进去看不出后果。
|
||||
- 悬停文案不要写成「确认开始?」;那是点下去之后黄条的口吻。
|
||||
- 当前宿主实现为**工具栏技能按钮**;宿主内置删除/刷新/新增/导入/导出不走本字段。
|
||||
- 需宿主已支持 `hoverHint`;更旧宿主会忽略未知字段(模板 Schema 仍会校验新技能)。
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "kb-sync-feishu",
|
||||
"label": "拉取到本机",
|
||||
"description": "把飞书知识库覆盖到本机工作区。未发布的本地修改会丢失。",
|
||||
"hoverHint": "将用飞书覆盖本机知识库,未发布的本地修改会丢失。",
|
||||
"placements": ["toolbar"],
|
||||
"executionProfile": "async",
|
||||
"interaction": "confirm",
|
||||
"confirmation": { "message": "将用飞书覆盖本机知识库,未发布的本地修改会丢失。确认开始?" },
|
||||
"bind": { "tables": ["kb_sources"] }
|
||||
}
|
||||
```
|
||||
|
||||
## `hostEffects` / `steps`(执行前后宿主编排,≥2.8.5)
|
||||
|
||||
与 `placements` / `executionProfile` / `interaction` **正交**。决定的是:数据管理里点完 Action 之后(或之前),宿主要不要**刷新当前表、刷新目录、清空多选、级联删子表**等。
|
||||
@@ -417,6 +627,7 @@ python {skill_root}/scripts/main.py <command> <args...>
|
||||
|
||||
- 使用稳定的退出码
|
||||
- 成功时输出可解析的结构化结果
|
||||
- **例外(打开类)**:打开目录 / 打开浏览器 / 打开文件成功时不要输出用户可见成功文案;若打印 JSON,令 `"message": ""`。失败仍走下一款错误约定
|
||||
- 失败时输出稳定错误信息
|
||||
- 对可预期错误提供稳定错误码
|
||||
- 对用户可处理的问题提供 `HINT`
|
||||
@@ -443,6 +654,18 @@ HINT: <next step>
|
||||
| `agent` | Agent 可直接调用(`run_skill_action`) |
|
||||
| `cron` | 定时任务「技能直调」 |
|
||||
|
||||
### 用户可见性与教程准确性
|
||||
|
||||
`placements` 决定 Action **可以**出现在哪;教程只能写用户在宿主里**实际能点到**的路径。完整用户文档契约见 [`../development/TUTORIAL_SURFACES.md`](../development/TUTORIAL_SURFACES.md)。
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| **skill-detail ≠ 一定可见** | 声明 `skill-detail` 不保证详情页渲染出按钮;当前宿主详情常只有说明/教程/演示/更新日志四 Tab。**禁止**把「技能详情 → 点某某」写成用户主路径或教程唯一入口 |
|
||||
| **准备 / 载入 / 导入 / 重置** | 若技能以数据管理为主路径,此类能力优先 `toolbar` + 非空 `bind.tables`(空表也可点的准备动作尤其如此);可同时挂 `agent`;`skill-detail` 仅作可选兼容 |
|
||||
| **写入后刷新** | 会写入业务表的 Action 建议 `steps: [{ "when": "afterSuccess", "do": "refreshCatalog" }]`,避免用户点完表不更新 |
|
||||
| **教程按钮名** | 必须等于本文件 `label`;对照表与数据管理顶栏/行内文案一致 |
|
||||
| **README vs 教程** | 说明 Tab 不写分步;操作步骤只写 `TUTORIAL.md` |
|
||||
|
||||
### 行级入口 `row`(宿主呈现契约)
|
||||
|
||||
- **技能决定有什么**:`placements` 含 `"row"`(且通过 `bind.tables` / policy 等过滤)的 Action,构成该表行的技能菜单项集合;配什么就出现什么。
|
||||
@@ -471,6 +694,7 @@ HINT: <next step>
|
||||
禁止:
|
||||
|
||||
- 臆造 `sortOrder` / `order` / `priority` 等字段指望宿主排序(模板 Schema `additionalProperties: false` 会拒收)。
|
||||
- 臆造 `tooltip` / `title` 等字段当悬停(用可选 `hoverHint`;不要把 `confirmation.message` 复用成悬停)。
|
||||
- 依赖或宣传表顶栏「更多操作」溢出菜单(宿主已取消该能力)。
|
||||
- 把「表格行数据默认排序」与按钮顺序混淆——行排序见 [`SCHEMA.md`](SCHEMA.md) 的 `default_sort_json`。
|
||||
- 把「默认展示列」与 `visible=0` 硬隐藏混淆——默认展示列见 [`SCHEMA.md`](SCHEMA.md) 的 `default_visible_columns_json`。
|
||||
@@ -487,7 +711,7 @@ HINT: <next step>
|
||||
| 数据同步(外源 → 本地库) | sync 或 async | toolbar + cron + agent + skill-detail(按需) | 可选 |
|
||||
| 单行重试 / 行内处理 | sync 或 async | row(可加 agent) | 可选 |
|
||||
| 批量处理勾选行 | sync 或 async | batch(可加 toolbar/cron) | 可选 |
|
||||
| 导入 / 重置 | sync 或 async | skill-detail, agent | 可选 |
|
||||
| 导入 / 重置 | sync 或 async | **优先** toolbar(须 `bind.tables`)+ agent;skill-detail 仅可选 | 可选 |
|
||||
| 单条副作用 RPA | async(建议) | agent, skill-detail | **建议** |
|
||||
| 批量顺序 pick RPA | async(建议) | toolbar, cron, agent | **建议** |
|
||||
## 同步数据 vs 导出当前表 vs 特殊业务报告 vs HTML 页面
|
||||
@@ -538,7 +762,25 @@ HINT: <next step>
|
||||
| `steps` | `afterSuccess` → **`refreshCatalog`**(重扫 `htmlPages`;不要只 `reloadTable`) |
|
||||
| `entrypoint` | CLI 子命令(如 `render-html`)调用同一写入内核 |
|
||||
|
||||
完整 JSON 形状见 [`../assets/examples/render-html.action.example.json`](../assets/examples/render-html.action.example.json)。最小页面壳见 [`../assets/html/example-report.html`](../assets/html/example-report.html)。
|
||||
完整 JSON 形状见 [`../assets/examples/render-html.action.example.json`](../assets/examples/render-html.action.example.json)。最小页面壳见 [`../assets/html/example-report.html`](../assets/html/example-report.html)(已含数据管理 iframe 预览的布局基线;复制后可按需扩展 `{{BODY}}` 内结构)。
|
||||
|
||||
### 宿主预览布局
|
||||
|
||||
数据管理在 **iframe** 内预览 `html/*.html`;宿主 iframe 已占满右侧预览区,**技能页不应再人为收窄正文**(尤其数据管理窗口通常较窄)。
|
||||
|
||||
| 级别 | 约定 |
|
||||
|------|------|
|
||||
| **必须** | 页面横向**铺满** iframe 可用宽度:`main { width: 100%; min-width: 0; }` |
|
||||
| **必须** | **禁止** `main`(或等价正文容器)使用 `max-width: Npx` 且**无** `margin: 0 auto`(宽屏会左对齐留白;窄宿主窗浪费横向空间) |
|
||||
| **必须** | `html` / `body` 设 `overflow-x: hidden`;滚动以**纵向**为主,避免横向滚动条 |
|
||||
| **必须** | 写入 `{{BODY}}` 的业务数据经 `html.escape`(或等价)转义,防 XSS |
|
||||
| **应当** | `header` 与 `main` 水平内边距一致(如共用 CSS 变量或 `clamp()`) |
|
||||
| **应当** | 多列表格:`width: 100%` + 单元格 `word-break` / `overflow-wrap`,长文本在格内换行 |
|
||||
| **应当** | KPI / 卡片网格用 `repeat(auto-fit, minmax(…, 1fr))` 等流体列,勿死写固定列数 |
|
||||
| **禁止** | 在 `<td>` 上用 `max-width` 限制「说明列」等业务列(易导致列宽异常或横向溢出) |
|
||||
| **禁止** | 依赖外链 CSS/JS 或 `networkidle` 式加载(iframe `sandbox` 仅 `allow-scripts`;页面应自包含) |
|
||||
|
||||
长文阅读型报告若确需版心,可对整个 `main` 使用 `max-width` **且** `margin: 0 auto`,并令 `header` 与 `main` 同宽;**数据看板类**默认不设 `max-width`。
|
||||
|
||||
### 实现要点
|
||||
|
||||
@@ -551,11 +793,83 @@ HINT: <next step>
|
||||
### 验收(复制后自测)
|
||||
|
||||
- [ ] `html/` 下出现 `.html`,数据管理侧栏能看到报告项,预览正常
|
||||
- [ ] 预览区**横向铺满**、**无横向滚动条**;拖窄侧栏后内容自适应(见上节「宿主预览布局」)
|
||||
- [ ] 表顶栏(若声明 toolbar)或报告右键「重新生成」能跑通;成功后预览刷新
|
||||
- [ ] 未登录/无权限用户被拒绝,且有可读错误
|
||||
- [ ] 成功路径带 `refreshCatalog`(或等价让侧栏/预览看到更新)
|
||||
|
||||
## inputSchema 与参数表单
|
||||
|
||||
### 跨表引用:Lookup(宿主已支持;勿让用户填编号)
|
||||
|
||||
当 Action 参数要引用**同库另一张业务表**的一行(话术模板、平台、账号等)时,必须用 Lookup 下拉,**禁止**做成「请填写编号」的数字框(用户记不住内部 id,体验差)。
|
||||
|
||||
| 场景 | 怎么配 | 说明 |
|
||||
|------|--------|------|
|
||||
| 当前表已有同含义列,且列上已是 CRUD lookup | `column` / `bind.columnMapping`,或属性名 = 列名 | toolbar/batch 弹窗与 create/update 抽屉都会复用该列已解析的选项 |
|
||||
| 当前表没有该列(如在「潜客」表上选「话术模板」) | `display_type: "lookup"` + 字段内联 `lookup` 对象 | 打开表单时宿主按当前技能库解析选项 |
|
||||
|
||||
内联 `lookup` 与列元数据 `options_json` **同构**(见 [`SCHEMA.md`](SCHEMA.md)「跨表引用 lookup」):
|
||||
|
||||
```json
|
||||
{
|
||||
"templateId": {
|
||||
"type": "number",
|
||||
"title": "话术模板",
|
||||
"description": "仅在正文留空时生效;选 -- 表示自动挑选。",
|
||||
"display_type": "lookup",
|
||||
"lookup": {
|
||||
"ref_table": "message_templates",
|
||||
"value_column": "id",
|
||||
"label_column": "name",
|
||||
"extra_column": "channel",
|
||||
"filter_column": "is_active",
|
||||
"filter_value": 1
|
||||
}
|
||||
},
|
||||
"engineIds": {
|
||||
"type": "string",
|
||||
"title": "监测引擎",
|
||||
"description": "可多选;提交为逗号分隔编号。",
|
||||
"display_type": "lookup",
|
||||
"lookup": {
|
||||
"ref_table": "engines",
|
||||
"value_column": "id",
|
||||
"label_column": "display_name",
|
||||
"filter_column": "enabled",
|
||||
"filter_value": 1,
|
||||
"multiple": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `ref_table` | 是 | 同库被引用表 |
|
||||
| `value_column` | 是 | 提交给 CLI 的值(业务 id / 编号) |
|
||||
| `label_column` | 是 | 主文案列 |
|
||||
| `extra_column` | 否 | 第三展示列(任意列名,不限「备注」) |
|
||||
| `filter_column` / `filter_value` | 否 | 等值过滤 |
|
||||
| `multiple` | 否 | `true` = 多选;省略/`false` = 单选(默认) |
|
||||
|
||||
**下拉/选项展示(宿主统一,技能只配列):** 单选为 `{value} · {label}`;有非空 `extra_column` 时为 `{value} · {label}({extra})`。例:`2 · 通用私信-留资`、`2 · 通用私信-留资(dm)`。
|
||||
|
||||
**多选存值与模板:** Action / CRUD 存 **`TEXT` 逗号分隔 id**(如 `1,2,5`);`{{engineIds}}` 原样替换进 CLI。多选字段在 `inputSchema` 里用 `type: "string"`(逗号串),不要用尚未稳定的 JSON array 当业务控件。
|
||||
|
||||
**多选控件(宿主策略):** 选项少(建议 ≤6)用勾选列表;选项多用字段内 Chip + 可搜索下拉(不是再开业务弹窗)。
|
||||
|
||||
**可选「自动」(单选常见):** 字段不要放进 `required`;不要用 `minimum: 1` 卡死留空。宿主会省略空的可选数字参数,避免「提交的数据无效」。
|
||||
|
||||
**反模式(勿做):**
|
||||
|
||||
- 标题写成「某某编号」,说明里教用户去左侧表抄 id
|
||||
- 用静态 `enum` 罗列会增长/变更的业务行(会过期)
|
||||
- 在技能里自己拼假 label 列代替宿主统一格式(难维护)
|
||||
- 另造 `lookup_multi` / 第二套 display_type(应只加 `multiple: true`)
|
||||
|
||||
形状参考:[`assets/examples/lookup-action.example.json`](../assets/examples/lookup-action.example.json)。
|
||||
|
||||
### 表单字段类型对照(须与 CRUD 对称)
|
||||
|
||||
Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件语义**。CRUD 侧配置见 [`SCHEMA.md`](SCHEMA.md)「表单与展示:稳定字段类型」;本表只写 Action 怎么配。
|
||||
@@ -568,17 +882,24 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件
|
||||
| 数字 | `type: number` | `display_type: number` 或 INT/REAL/NUMERIC |
|
||||
| 开关 | `type: boolean` | `display_type: boolean` |
|
||||
| 枚举下拉 | `enum: [...]` | `options_json` 数组 |
|
||||
| 关联下拉 | 绑列 lookup(`column` / `columnMapping`) | `display_type: lookup` |
|
||||
| 关联下拉(Lookup) | 内联 `display_type: lookup` + `lookup`(可选 `multiple`),或绑列 | 同左;多选存 `TEXT` 逗号分隔 id |
|
||||
| 密码 | `sensitive: true` | 列 `sensitive` |
|
||||
| 日期(日) | `display_type` 或 `format`: `date` | `display_type: date` → 存 `YYYY-MM-DD` |
|
||||
| 日期时间 | `display_type` 或 `format`: `datetime_unix_seconds` | 同左;**存/传 Unix 秒** |
|
||||
| URL(薄层) | `display_type` 或 `format`: `url` | `display_type: url` |
|
||||
| Email(薄层) | `display_type` 或 `format`: `email` | `display_type: email` |
|
||||
| **本地文件路径** | `display_type`/`format`: **`file`**;可选 **`pathRoot`** | CRUD 一般不用;见下节 |
|
||||
| **本地目录路径** | `display_type`/`format`: **`directory`**;可选 **`pathRoot`** | CRUD 一般不用;见下节 |
|
||||
| 说明灰字 | `description` | `_jiangchang_columns.description` |
|
||||
| 表格横幅 / 列头「?」 | (无;Action 不用) | `_jiangchang_tables.grid_help` / `_jiangchang_columns.grid_help` |
|
||||
| 默认值 | `default` | SQLite 列 `DEFAULT` |
|
||||
| 只读 | `readOnly: true` | 列 `editable: 0` / 宿主只读规则 |
|
||||
| **条件显隐** | **`visibleWhen`**(见下节) | **本版 CRUD 无对称**;仅 Action / 定时任务参数表单 |
|
||||
| **列筛选「值」** | **无对称**(Action 无表格筛选) | 见 [`SCHEMA.md`](SCHEMA.md)「数据管理:列筛选与全局搜索」:枚举 / boolean / lookup 在筛选栏用**下拉 Select**(非开关、非分段单选);存库仍用 value,展示与搜索认 label |
|
||||
|
||||
**本版不做(勿宣传):** 文件上传、多选/标签数组、复杂 `oneOf` / nested object 专用表单。
|
||||
**本版不做(勿宣传):** 浏览器内文件上传控件、复杂 `oneOf` / nested object 专用表单;`visibleWhen` 多条件 AND/OR 树、跨 Action 依赖、CRUD 新增/编辑对称显隐;Action 参数表单的列筛选对称。
|
||||
(**Lookup 多选**已支持:同一 `lookup` + `multiple: true`,见上文;不是任意标签数组。)
|
||||
(**本地路径选择**已支持:`file` / `directory` + 可选 `pathRoot`,见下节;提交值仍是本机绝对路径字符串,不是 multipart 上传。)
|
||||
|
||||
日期时间约定:
|
||||
|
||||
@@ -586,13 +907,115 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件
|
||||
- `date` 只表示日历日,值为 `YYYY-MM-DD`
|
||||
- 脏/无法归一的旧预设:日期控件可回退为普通文本,避免卡死表单
|
||||
|
||||
### 本地路径选择与 `pathRoot`
|
||||
|
||||
用于数据管理 / 定时任务 **Action 参数表单**里选本机文件或目录(原生对话框)。与宿主工具栏内置「导入」面板(默认打开本技能 `input/`,见 [`../development/DATA_PATHS.md`](../development/DATA_PATHS.md))是**两条 UI 路径**,不要混为一谈。
|
||||
|
||||
| 字段 | 含义 |
|
||||
|------|------|
|
||||
| `display_type` / `format`: `file` \| `directory` | 渲染「选文件 / 选文件夹」控件;提交值为本机**绝对路径**字符串 |
|
||||
| **`pathRoot`(可选)** | 对话框**起始目录**,相对**当前技能数据根** `{JIANGCHANG_DATA_ROOT}/{USER_ID}/{slug}/` |
|
||||
|
||||
宿主行为(匠厂已支持):
|
||||
|
||||
| `pathRoot` | 打开对话框时 |
|
||||
|------------|--------------|
|
||||
| 省略 / 空 | `{skill_data}/input` |
|
||||
| `input/库存数据` 等 | `{skill_data}/` + 该相对路径 |
|
||||
| `output` / `output/…` | 落在技能 `output` 下 |
|
||||
| 裸 `库存数据` | 视为 `input/库存数据` |
|
||||
| 绝对路径、盘符、`..` | 非法 → 回退 `{skill_data}/input` |
|
||||
|
||||
硬规则:
|
||||
|
||||
1. **禁止**写用户 ID、盘符、绝对路径;用户与数据根由宿主按当前登录解析。
|
||||
2. **`description` 只给人看**,宿主**不会**解析文案来决定起始目录;要默认进子目录必须写 `pathRoot`。
|
||||
3. Agent / CLI **忽略** `pathRoot`(仍直接传路径字符串)。
|
||||
4. 输入框**已有路径**时,宿主优先用已有值作为起始(便于再次浏览)。
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "refresh-inventory",
|
||||
"label": "刷新库存数据",
|
||||
"description": "从输入目录选择 JSON 快照并更新库存。",
|
||||
"placements": ["toolbar", "agent"],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "form",
|
||||
"bind": { "tables": ["sku_inventory"] },
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["file"],
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": "string",
|
||||
"title": "库存数据文件",
|
||||
"description": "请选择 input/库存数据/ 下的 JSON。",
|
||||
"format": "file",
|
||||
"display_type": "file",
|
||||
"pathRoot": "input/库存数据"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "refresh-inventory",
|
||||
"args": ["--file", "{{file}}"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
形状参考:[`assets/examples/path-root-file-action.example.json`](../assets/examples/path-root-file-action.example.json)。
|
||||
|
||||
### 当前宿主稳定支持(摘要)
|
||||
|
||||
- 根:`object`;属性:`string` / `number` / `boolean`(`object`/`array` 仅作 JSON 多行编辑兼容,新技能慎用)
|
||||
- `enum` / `default` / `required` / `sensitive` / `readOnly`
|
||||
- `display_type` / `format`:`textarea` | `date` | `datetime_unix_seconds` | `url` | `email`
|
||||
- `display_type` / `format`:`textarea` | `date` | `datetime_unix_seconds` | `url` | `email` | **`lookup`** | **`file`** | **`directory`**
|
||||
- 可选 **`pathRoot`**(仅 `file` / `directory`):相对技能数据根的起始目录;省略默认 `{skill_data}/input`
|
||||
- 字段内联 `lookup`(与 CRUD `options_json` lookup 对象同构;可选 `extra_column`、`multiple`)
|
||||
- 属性级 `description`(表单标签下方灰字说明)
|
||||
- 简单模板替换(`{{fieldName}}`)
|
||||
- 可选空数字字段提交时由宿主省略,避免 `""` 触发 number 校验失败
|
||||
- **字段条件显隐 `visibleWhen`**(可选):依赖同表单其它字段值决定是否展示;隐藏字段不进提交、不校验 `required`
|
||||
|
||||
### 字段条件显隐(`visibleWhen`)
|
||||
|
||||
用于「先选模式,再出对应参数」类表单。写在 `inputSchema.properties.<field>` 上(**不是** action 顶层字段)。
|
||||
|
||||
> **消歧义:** 本文的 `visibleWhen` = **Action 参数表单控件显隐**。
|
||||
> [`POLICY.md`](POLICY.md) L3 预留的 `visibleWhen` = **行态/权限策略**(未冻结),**不是**同一能力。勿混用、勿互相替代。
|
||||
|
||||
```json
|
||||
"keyword": {
|
||||
"type": "string",
|
||||
"title": "关键词",
|
||||
"visibleWhen": { "field": "collectMode", "equals": "按关键词采集" }
|
||||
},
|
||||
"authorUrl": {
|
||||
"type": "string",
|
||||
"title": "作者主页链接",
|
||||
"visibleWhen": { "collectMode": "按作者主页采集" }
|
||||
}
|
||||
```
|
||||
|
||||
支持形态:
|
||||
|
||||
| 写法 | 含义 |
|
||||
|------|------|
|
||||
| `{ "field": "x", "equals": "a" }` | `x === a` 时显示 |
|
||||
| `{ "field": "x", "in": ["a", "b"] }` | `x` 属于集合时显示 |
|
||||
| `{ "x": "a" }` / `{ "x": ["a","b"] }` | 单键简写,同上 |
|
||||
|
||||
宿主行为(需已支持该字段的匠厂宿主;更旧版本会忽略未知字段 → 参数平铺全显):
|
||||
|
||||
- 数据管理 / 定时任务参数表单按当前值求值并隐藏
|
||||
- 若依赖字段自身因 `visibleWhen` 隐藏,则下游字段一并隐藏(避免切模式后残留)
|
||||
- 提交与服务端 `validateActionInput` 会剥离隐藏字段;隐藏字段不校验 `required`
|
||||
- 无 `visibleWhen` 的字段保持始终可见(兼容旧技能)
|
||||
|
||||
形状参考:[`assets/examples/visible-when-action.example.json`](../assets/examples/visible-when-action.example.json)。
|
||||
|
||||
互斥模式**优先**用 `visibleWhen`,不要只靠 `description` 灰字写「仅某某时填写」。
|
||||
|
||||
**与数据管理「新增 / 编辑」表单的分工(勿混用配置入口):**
|
||||
|
||||
@@ -601,6 +1024,8 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件
|
||||
| Skill Action 弹窗 | `inputSchema.title` | `inputSchema.description` | `inputSchema.default` | `type` + `display_type`/`format` |
|
||||
| 数据管理新增/编辑 | `_jiangchang_columns.display_name` | `_jiangchang_columns.description` | 物理列 SQLite `DEFAULT` | `display_type` + 物理类型推断 |
|
||||
|
||||
数据管理**表格**上方说明与列头「?」用 `_jiangchang_tables.grid_help` / `_jiangchang_columns.grid_help`(空则不渲染),**不要**用 `description`。见 [`SCHEMA.md`](SCHEMA.md)「表格说明 `grid_help`」。
|
||||
|
||||
CRUD 侧宿主义务与禁止事项见 [`SCHEMA.md`](SCHEMA.md)「表单与展示:稳定字段类型」「新增/编辑表单:字段说明与默认值」。同一业务含义两边文案与控件应对齐,但分别配置。
|
||||
|
||||
示例:
|
||||
@@ -651,13 +1076,15 @@ CRUD 侧宿主义务与禁止事项见 [`SCHEMA.md`](SCHEMA.md)「表单与展
|
||||
"pickCount": {
|
||||
"type": "number",
|
||||
"title": "处理数量",
|
||||
"description": "从队列选取 pending 记录条数",
|
||||
"default": 1,
|
||||
"minimum": 1
|
||||
"description": "从队列选取「可操作」状态记录的条数(具体状态名由本技能定义,见 WORK_ITEM_STATUS)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
默认**不要**把失败/成功/跳过混进该 pick;重做靠人工改回可操作态。详见 [`WORK_ITEM_STATUS.md`](WORK_ITEM_STATUS.md)。
|
||||
|
||||
若还需要默认篇数,可在同一 `properties` 里为 `pickCount` 增加 `"default": 1`、`"minimum": 1`。
|
||||
|
||||
约定:
|
||||
|
||||
- `boolean` 在宿主中渲染为开关
|
||||
@@ -670,9 +1097,10 @@ CRUD 侧宿主义务与禁止事项见 [`SCHEMA.md`](SCHEMA.md)「表单与展
|
||||
- `readOnly: true` 时数据管理表单只读展示该字段(仍随提交传入);适合行内/勾选预填的主键、编号列表
|
||||
- `placements` 含 `batch` 的动作在数据管理表顶常驻,无勾选时禁用(与表删除一致)
|
||||
- 需要确认的副作用操作使用 `confirmation.message`
|
||||
- 短名看不懂或会覆盖数据时,用独立的 `hoverHint`;不要把确认黄条复用成悬停
|
||||
- 模板参数使用 `{{fieldName}}`,且必须在 `inputSchema.properties` 中声明
|
||||
- `entrypoint.args` 项当前只允许 `string`、`number`、`boolean`;不允许对象或 `null`
|
||||
- **不要**在当前模板中宣传文件上传、多选数组、`oneOf`、复杂 nested object 等尚未稳定支持的能力
|
||||
- **不要**在当前模板中宣传文件上传、任意 JSON 多选数组、`oneOf`、复杂 nested object 等尚未稳定支持的能力;Lookup 多选请用 `lookup.multiple`
|
||||
## 风险与入口配置
|
||||
|
||||
- 查询、诊断、版本检查可以放到 `skill-detail` 或 `agent`
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
> **状态:正式契约 v1.0(`schemaVersion` 1)。**
|
||||
> **目标:** 冻结「启用方式 + Snapshot 最小字段 + L2 scope 白名单」,使多数技能(L0)零配置,需要权限的技能(L1/L2)有稳定扩展面。
|
||||
> **非目标:** 宿主实现业务用户/角色库;规定飞书/HTTP/本地表等权限数据源;本期不强制 L3(`visibleWhen` 等)。
|
||||
> **非目标:** 宿主实现业务用户/角色库;规定飞书/HTTP/本地表等权限数据源;本期不强制 L3(行态 `visibleWhen` 等)。
|
||||
> **注意:** L3 预留的行态 `visibleWhen` **不是** Action 参数表单的 `inputSchema.properties.*.visibleWhen`(表单控件显隐,见 [`ACTIONS.md`](ACTIONS.md)「字段条件显隐」)。二者同名不同义,勿混用。
|
||||
> **配套 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(当前身份允许什么)**。
|
||||
@@ -16,7 +17,7 @@
|
||||
| **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` 等 |
|
||||
| **L3** | (预留,本契约不冻结行为) | 如**行态** `visibleWhen`、菜单级藏表、`rows.mode=delegate` 等(≠ Action 表单 `inputSchema.visibleWhen`) |
|
||||
|
||||
**80% 技能停留 L0。** 启用 policy 的技能必须实现约定 CLI,并在写路径自行再鉴权(见 §7)。
|
||||
|
||||
@@ -149,7 +150,7 @@
|
||||
4. `mode=open`:等价「L1 不限制按钮」,可与 L2 行列限制并存。
|
||||
5. **`denylist` 不是 v1 合法值**;出现则 §6 fail-closed。
|
||||
|
||||
`replaces` 接管的默认新增/编辑/删除:视为对应 Action 的 id 是否在 allowlist 中;不在则宿主隐藏该接管按钮。`mode=open` 时不按 id 限制 replaces。
|
||||
`replaces` 接管的默认新增/编辑/删除/导入:视为对应 Action 的 id 是否在 allowlist 中;不在则宿主隐藏该接管按钮(导入按钮是否可用还受 `hostCapabilities` 含 `import` 影响)。`mode=open` 时不按 id 限制 replaces。
|
||||
|
||||
### 2.4 `hostCapabilities`(L1 可选冻)
|
||||
|
||||
@@ -366,7 +367,7 @@ IF mode == open: 候选不变
|
||||
|----|------|
|
||||
| `actions.mode: denylist` | 另开 `schemaVersion` |
|
||||
| `valueFrom: actor.roles` | **明确不做** |
|
||||
| `visibleWhen` / 嵌套 scope / `like` | L3 或永不做 |
|
||||
| `visibleWhen` / 嵌套 scope / `like` | L3 或永不做(此处指**行态**策略字段;Action 表单 `inputSchema.visibleWhen` 见 ACTIONS.md,不属本表) |
|
||||
| `rows.mode: delegate` 宿主实现 | 字段名已占坑 |
|
||||
| 平台全局 Role UI / cron·agent 套用 Snapshot | 另议 |
|
||||
|
||||
|
||||
@@ -17,10 +17,11 @@
|
||||
| 根目录 [`CHANGELOG.md`](../CHANGELOG.md) | 市场更新日志 Tab(版本小节 → `changelog`) |
|
||||
| [`SKILL.md`](../SKILL.md) | LLM / OpenClaw 平台技能入口与触发摘要 |
|
||||
| [`CLI.md`](CLI.md) | 共享 Python、命令行入口、参数与短操作契约 |
|
||||
| [`ACTIONS.md`](ACTIONS.md) | Skill Action、sync/async、任务中心、多入口(可选能力) |
|
||||
| [`ACTIONS.md`](ACTIONS.md) | Skill Action、sync/async、任务中心、多入口、按钮中文名 `label` 参考(按入口定位表挂载;先读 `development/PRODUCT_SURFACES.md`) |
|
||||
| [`SCHEMA.md`](SCHEMA.md) | 本地 SQLite 与数据管理展示契约 |
|
||||
| [`POLICY.md`](POLICY.md) | Data Policy:可选身份授权、Snapshot、行 scope(多数技能 L0 不配) |
|
||||
| [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md) | 数据管理单元格呈现:语义 tone 标色/强调(用途不限;多数技能 P0 不配) |
|
||||
| [`WORK_ITEM_STATUS.md`](WORK_ITEM_STATUS.md) | 业务行生命周期:处理队列的可操作/进行中/终态角色与重做规则(有处理行 Action 时适用) |
|
||||
| [`../development/`](../development/) | 开发、测试、RPA 深度规范(给人 / 编码助手) |
|
||||
|
||||
按需阅读:短操作优先 `CLI.md`;Action / 任务中心读 `ACTIONS.md`;结构化字段读 `SCHEMA.md`;权限 opt-in 读 `POLICY.md`;格子标色读 `CELL_PRESENTATION.md`。
|
||||
按需阅读:短操作优先 `CLI.md`;Action / 任务中心读 `ACTIONS.md`;结构化字段读 `SCHEMA.md`;权限 opt-in 读 `POLICY.md`;格子标色读 `CELL_PRESENTATION.md`;队列处理状态机读 `WORK_ITEM_STATUS.md`。
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# 数据存储与数据管理展示规范
|
||||
|
||||
本文是 skill 本地 SQLite 与匠厂宿主「数据管理」页面展示契约的**权威说明**。
|
||||
|
||||
**产品定位:** 数据管理既是业务数据与过程的可见面,也是与对话**平级**的 **UI 执行面**(表上 Action 走同一 CLI 闭环)。何时挂执行入口见 [`../development/PRODUCT_SURFACES.md`](../development/PRODUCT_SURFACES.md);本文管表结构与展示元数据。
|
||||
|
||||
实现上的 DDL 与默认中文元数据种子见 `scripts/db/display_metadata.py`;`scripts/db/connection.py` 的 `init_db()` 会一并初始化。
|
||||
|
||||
## 职责边界
|
||||
@@ -10,7 +13,7 @@
|
||||
- 定义真实 SQLite 表结构
|
||||
- 使用英文 `snake_case` 表名和字段名
|
||||
- 使用 `_jiangchang_tables` 提供表级展示元数据
|
||||
- 使用 `_jiangchang_columns` 提供字段级展示元数据(含可选 `description` 字段说明)
|
||||
- 使用 `_jiangchang_columns` 提供字段级展示元数据(含可选 `description` 表单说明、可选 `grid_help` 表格说明)
|
||||
- 需要「新增表单预填」时,在物理列上声明 SQLite `DEFAULT`(勿把默认值只写在文案里)
|
||||
- 定义哪些字段可见、可搜索、可创建、可编辑
|
||||
- 定义字段展示类型和枚举选项
|
||||
@@ -22,9 +25,11 @@
|
||||
|
||||
- 读取 `_jiangchang_*` 元数据
|
||||
- 读取 SQLite `PRAGMA table_info`
|
||||
- **数据管理侧栏/表列表**:当库中存在 `_jiangchang_tables` 时,**仅展示**在该表注册且 `visible=1` 的业务表;孤儿物理表(有表、无注册元数据)**不得**出现在侧栏,直接访问 schema API 应返回「表不存在」。极老库无 `_jiangchang_tables` 时,宿主可保留物理表枚举 fallback(兼容 `_schema_meta` 时代)
|
||||
- 根据 schema 渲染数据管理页面
|
||||
- 根据 `editable` 推导字段是否可创建或编辑
|
||||
- 在**新增 / 编辑**表单中,若列有 `_jiangchang_columns.description`,在标签下方展示该说明(灰字;空则不显示)
|
||||
- 在数据管理**表格**中,若表有 `_jiangchang_tables.grid_help` 则在表上方展示可折叠说明;若列有 `_jiangchang_columns.grid_help` 则在列头显示「?」;空/未写则不渲染。**不**把 `description` 当作表格说明
|
||||
- 打开**新增**表单时,对可创建字段用 SQLite 列 `DEFAULT`(`PRAGMA table_info.dflt_value`)预填;无 DEFAULT 则留空
|
||||
- 执行通用查询、新增、编辑、删除、导入、导出
|
||||
- 对字段名、表名、类型和权限进行最终校验
|
||||
@@ -83,7 +88,24 @@ metadata:
|
||||
|
||||
不要把「外部来源只读」错误理解成「技能本地数据库一定只读」。技能通过 `_jiangchang_tables.readonly` 与 `_jiangchang_columns.editable` 声明本地表的实际写权限。
|
||||
|
||||
若创建/更新/删除带有领域副作用(自动建目录、加密、级联清理),不要依赖宿主默认直写:在 `assets/actions.json` 用可选字段 `replaces: create|update|delete` 接管默认按钮,详见 [`ACTIONS.md`](ACTIONS.md)。
|
||||
若创建/更新/删除/导入带有领域副作用(自动建目录、加密、级联清理、归一化字段),不要依赖宿主默认直写:在 `assets/actions.json` 用可选字段 `replaces: create|update|delete|import` 接管默认按钮。其中 **import** 时用户仍使用宿主「导入数据」面板,确认后由技能写库。详见 [`ACTIONS.md`](ACTIONS.md) 与 `assets/examples/replaces-*.action.example.json`。
|
||||
|
||||
## 表与表之间:禁止 SQLite 外键
|
||||
|
||||
业务表之间的「属于哪条父记录 / 引用哪条来源」**只存业务 id**(整数或文本标量),**禁止**在 DDL 中写 SQLite `FOREIGN KEY` / `REFERENCES`,也**不要**指望 `PRAGMA foreign_keys = ON` 替你校验或级联。
|
||||
|
||||
| 要做的事 | 正确做法 | 禁止 |
|
||||
|----------|----------|------|
|
||||
| 子表指向父表 | 子表列如 `case_id INTEGER NOT NULL`;写入前在 repository/service 查父表是否存在 | `FOREIGN KEY (case_id) REFERENCES parent(id)` |
|
||||
| 父表记「最新一版」 | 父表列如 `latest_draft_id INTEGER NOT NULL DEFAULT 0`;由 service 更新指针 | 同上 |
|
||||
| 删除 / 覆盖 / 级联清理 | repository/service 内显式 `DELETE` / `UPDATE`;需要时用 Action `steps` 的 `deleteByFk` | 依赖 DB 级联或宿主 CRUD 被 FK 拦住 |
|
||||
| 数据管理里显示名称 | `_jiangchang_columns.display_type = lookup` + `options_json` 对象(见下文「跨表引用 lookup」) | 把 lookup 当成 DDL 外键 |
|
||||
|
||||
**为什么:** 宿主数据管理对业务表执行通用增删改,会直接写 SQLite。一旦 DDL 带外键,删除、迁移 rename 父表、或父表 id 变更时极易出现「本地数据操作失败」且难排查。关联语义属于技能业务,必须在代码里写清。
|
||||
|
||||
**迁移注意:** 避免「`ALTER TABLE parent RENAME TO parent_old` → 建新 parent → 删 old」这类会隐性改写子表 FK 指向的套路;优先 `ADD COLUMN` + 拷列,或重建子表且**不带** FK。
|
||||
|
||||
`display_metadata_validator` 与 `validate_template_database` 会扫描业务表 DDL;含 `FOREIGN KEY` / `REFERENCES` 时报错(见 `development/CHECKLIST.md` A6-6)。
|
||||
|
||||
## 标准数据同步语义(外源 → 本地库)
|
||||
|
||||
@@ -135,7 +157,8 @@ metadata:
|
||||
|------|------|------|
|
||||
| `table_name` | 是 | 真实英文表名 |
|
||||
| `display_name` | 是 | 用户可见的中文业务名称 |
|
||||
| `description` | 否 | 表说明 |
|
||||
| `description` | 否 | 表说明(表单/备注用途);**不**用于表格上方横幅 |
|
||||
| `grid_help` | 否 | 数据管理表格上方说明;空/未写则不出横幅。与 `description` 独立,禁止互拷(见下节) |
|
||||
| `sort_order` | 否 | 兼容保留字段,不用于替代物理字段顺序 |
|
||||
| `visible` | 是 | `1` 在宿主数据管理中展示 / `0` 隐藏 |
|
||||
| `readonly` | 是 | `1` 整张表只读;`0` 允许宿主根据字段权限执行写操作 |
|
||||
@@ -209,6 +232,43 @@ upsert_table_metadata(
|
||||
)
|
||||
```
|
||||
|
||||
#### 表格说明 `grid_help`(宿主支持)
|
||||
|
||||
技能可给数据管理**表格**写说明,与新增/编辑表单的 `description` **完全独立**:
|
||||
|
||||
| 写在哪 | 宿主做什么 | 空/未写 |
|
||||
|--------|------------|---------|
|
||||
| `_jiangchang_tables.grid_help` | 表格上方可折叠横幅(首行摘要;有后续行才出展开) | 不出横幅 |
|
||||
| `_jiangchang_columns.grid_help` | 列头「?」,点击查看该列说明 | 不出「?」 |
|
||||
|
||||
规则:
|
||||
|
||||
- **opt-in**:大多数表/列不需要写;不写则行为与今日一致
|
||||
- **禁止**把 `description` 拷进 `grid_help`,也禁止把表格规则写进 `description`(`description` 只给表单灰字)
|
||||
- 多行文本时:第一行是摘要,其余行点「展开」后可见
|
||||
- 通过 `upsert_table_metadata(..., grid_help=...)` / `upsert_column_metadata(..., grid_help=...)` 写入;旧库由 `init_display_metadata_tables` 幂等 `ALTER` 补列
|
||||
- 需已支持该字段的匠厂宿主;更旧版本会忽略未知列,不写则无影响
|
||||
|
||||
示例(仅在确有表格规则时写;模板默认业务表不要预填):
|
||||
|
||||
```python
|
||||
upsert_table_metadata(
|
||||
cursor,
|
||||
table_name="timeliness_records",
|
||||
display_name="时效登记",
|
||||
readonly=1,
|
||||
grid_help="超时按环节规则判定。\n交单:预计到港日前 3 天须交单",
|
||||
)
|
||||
upsert_column_metadata(
|
||||
cursor,
|
||||
table_name="timeliness_records",
|
||||
column_name="submit_date",
|
||||
display_name="交单日期",
|
||||
grid_help="预计到港日前 3 天须交单",
|
||||
editable=0,
|
||||
)
|
||||
```
|
||||
|
||||
禁止:
|
||||
|
||||
- 使用中文真实表名
|
||||
@@ -225,9 +285,10 @@ upsert_table_metadata(
|
||||
| `column_name` | 是 | 真实英文字段名 |
|
||||
| `display_name` | 是 | 用户可见的中文字段名(表头与表单标签);保持简短 |
|
||||
| `description` | 否 | 字段说明(给人看的短文案);**宿主在新增/编辑表单标签下方展示**(见下节);不要塞进 `display_name` |
|
||||
| `grid_help` | 否 | 数据管理表头「?」;空/未写则不出。与表单 `description` 独立,禁止互拷 |
|
||||
| `display_order` | 否 | 兼容字段,默认保持 `NULL` |
|
||||
| `visible` | 是 | 是否进入数据管理 schema /「列」面板(`0` = **硬隐藏**,用户无法勾回;默认不展示但可勾回请用表级 `default_visible_columns_json`) |
|
||||
| `searchable` | 是 | 是否参与搜索 |
|
||||
| `searchable` | 是 | 是否参与**列筛选**与**顶栏全局搜索**(`0` = 两者均不参与);详见下节「数据管理:列筛选与全局搜索」 |
|
||||
| `editable` | 是 | `1` 业务字段允许宿主用于新增和编辑;`0` 系统/只读字段 |
|
||||
| `display_type` | 否 | 字段渲染类型;稳定取值见下节「表单与展示:稳定字段类型」 |
|
||||
| `options_json` | 否 | 枚举选项 JSON |
|
||||
@@ -248,7 +309,7 @@ upsert_table_metadata(
|
||||
| `number` | `INTEGER` / `REAL` / `NUMERIC` | 原文 | **数字输入**(`type=number`) | 数字;无 `display_type` 时,物理类型为 INT/REAL/NUMERIC 也应走数字控件 |
|
||||
| `boolean` | `INTEGER` 0/1 或 BOOL | 开关语义展示 | 开关 | `0` / `1` |
|
||||
| (枚举) | 任意 + `options_json` **数组** | 标签映射 | 下拉选择 | 选项 `value` |
|
||||
| `lookup` | 外键式标量 + `options_json` **对象** | 标签映射 | 下拉选择(查关联表) | 关联键值 |
|
||||
| `lookup` | 跨表引用 id;`options_json` **对象**;可选 `multiple` | 单选:标签映射;多选:多标签(如「豆包、元宝」) | 单选下拉;多选见下文 | 单选:业务 id 标量;多选:`TEXT` 逗号分隔 id(非 SQLite 外键) |
|
||||
| (敏感) | 任意;列 `sensitive=1` | 掩码/隐藏策略由宿主定 | 新建可用密码框;编辑策略与既有宿主一致 | 原文(勿日志明文) |
|
||||
| `date` | `TEXT` 推荐 `YYYY-MM-DD`,或可安全归一的值 | 日期 | **日期选择**(日) | `YYYY-MM-DD` |
|
||||
| `datetime_unix_seconds` | `INTEGER` | `YYYY-MM-DD HH:mm:ss` | **日期时间选择**;提交写回 **Unix 秒** | Unix **秒**整数;禁止改成 ISO 字符串 |
|
||||
@@ -260,9 +321,11 @@ upsert_table_metadata(
|
||||
**明确不在本版稳定范围(勿在新技能宣传为已支持):**
|
||||
|
||||
- 文件 / 图片上传
|
||||
- 多选、标签数组、复杂 `oneOf` / nested object 表单
|
||||
- 复杂 `oneOf` / nested object 表单(任意 JSON 数组当业务多选控件)
|
||||
- 把业务可编辑时间存成毫秒、ISO 文本或 SQLite `datetime` 文本(审计字段规范见「标准时间字段」)
|
||||
|
||||
> **Lookup 多选**已纳入本版契约(见下文 `multiple`),与「任意多选/标签数组」不是同一能力。
|
||||
|
||||
`datetime_unix_milliseconds`:仅兼容**列表展示**旧数据;**新技能不要用**;可编辑时间一律 `datetime_unix_seconds`。
|
||||
|
||||
#### 新增/编辑表单:字段说明与默认值(宿主支持)
|
||||
@@ -274,7 +337,9 @@ upsert_table_metadata(
|
||||
| 字段说明 | `_jiangchang_columns.description`(可选) | 新增/编辑表单中,在标签下方以灰字展示;空则不显示 |
|
||||
| 创建默认值 | 物理列 SQLite `DEFAULT`(如 `limit_n INTEGER NOT NULL DEFAULT 0`) | 打开「新增」时,对可创建字段预填该默认值;无 DEFAULT 则留空 |
|
||||
| 编辑已有行 | — | **不**用 DEFAULT 覆盖已有行数据 |
|
||||
| 表格列头 | 只用 `display_name` | 长说明**不**进入列头 |
|
||||
| 表格列头标题 | `display_name` | 短名;不要把长说明塞进列头文字 |
|
||||
| 表格列头「?」 | `_jiangchang_columns.grid_help`(可选) | 有内容才显示「?」;**不**回退 `description` |
|
||||
| 表格上方说明 | `_jiangchang_tables.grid_help`(可选) | 有内容才显示横幅;**不**回退表级 `description` |
|
||||
|
||||
规则与禁止:
|
||||
|
||||
@@ -318,6 +383,76 @@ seed_column_metadata_batch(
|
||||
|
||||
Action 参数表单的 `title` / `description` / `default` / `display_type` 见 [`ACTIONS.md`](ACTIONS.md);勿与本节 CRUD 配置入口混用。
|
||||
|
||||
#### 数据管理:列筛选与全局搜索(宿主支持)
|
||||
|
||||
数据管理表格提供两类与 `searchable` 相关的检索能力,技能通过 `_jiangchang_columns.searchable` 声明是否参与:
|
||||
|
||||
| 能力 | 用户入口 | `searchable = 1` 时 | `searchable = 0` 时 |
|
||||
|------|----------|---------------------|---------------------|
|
||||
| **列筛选** | 表格「筛选」→ 字段 / 操作符 / 值 | 出现在「字段」下拉,可按类型选值与操作符 | 不出现在列筛选字段列表 |
|
||||
| **全局搜索** | 顶栏搜索框 | 参与 OR 模糊匹配(宿主对参与列数有上限,如最多 8 列) | 不参与全局搜索 |
|
||||
|
||||
**存库 vs 展示(核心契约)**
|
||||
|
||||
- **列表**:有 `options_json` 时展示 **label**(如「启用」「停用」)
|
||||
- **筛选 / 查询 / 全局搜索**:比较与匹配 **value**(如 `0`/`1`、`active`、`toutiaohao`)
|
||||
- 宿主负责 label ↔ value 映射;**禁止**为迁就 UI 把存库改成中文枚举,也**禁止**在 `description` 里写「筛选请填 0/1」
|
||||
|
||||
**筛选栏控件 ≠ 新增/编辑表单(勿混用)**
|
||||
|
||||
列筛选「值」与表单使用同一套**类型语义**,但控件形态不同:筛选栏须与「字段」「操作符」同一行工具栏对齐(如统一 `h-9` 下拉),**枚举 / boolean / lookup 在筛选栏一律用下拉 Select**,不用新增/编辑表单里的开关或横向分段单选。表单侧仍按上表「新增/编辑控件」配置。
|
||||
|
||||
| 列类型 | 筛选「值」控件 | 常用操作符(宿主按类型裁剪) | 技能须配置 |
|
||||
|--------|----------------|------------------------------|------------|
|
||||
| 文本 / `url` / `email` | 文本框 | 等于 / 包含 / 开头 / 结尾 / 为空… | 通常 `searchable: 1` |
|
||||
| 数字 | 数字框 | 等于 / 大于 / 小于… | `display_type: number` 或 INT/REAL |
|
||||
| 枚举(`options_json` **数组**) | 下拉(展示 label) | 等于 / 不等于 / 为空 | **`options_json` 必填** + `searchable: 1` |
|
||||
| `boolean` | 下拉(有 `options_json` 用其 label;否则宿主默认可选「是/否」) | 等于 / 不等于 / 为空 | `display_type: boolean`;**推荐同时写 `options_json`** 指定中文 label |
|
||||
| `lookup` 单选 | 下拉 | 等于 / 不等于 / 为空 | `display_type: lookup` + lookup 对象 |
|
||||
| `lookup` + `multiple: true` | 列筛选 UI 演进中;**全局搜索**已支持 | 等于时按逗号分隔 id 的 token 匹配 | 多选存 `TEXT` 逗号分隔 id |
|
||||
| `date` / `datetime_unix_seconds` | 日期 / 日期时间 | 比较类 | 时间列通常 `searchable: 0` |
|
||||
| 敏感列 | **不可筛** | — | 宿主强制排除 |
|
||||
|
||||
**全局搜索扩展(用户可搜中文)**
|
||||
|
||||
宿主在 `searchable = 1` 的列上除原文 `LIKE` 外,还应:
|
||||
|
||||
- 有 `options_json` 枚举:**label 或 value** 均可命中
|
||||
- `boolean` 无 options:认「是/否」等常见别名
|
||||
- `lookup` 多选:按逗号分隔 id 的 **token 边界** 匹配;选项 label 亦可参与
|
||||
- `datetime_unix_seconds`:搜 `YYYY-MM-DD` 形态可匹配当天 Unix 秒区间
|
||||
|
||||
**技能配置建议**
|
||||
|
||||
推荐:
|
||||
|
||||
- 用户常筛的状态列(启用/停用、待生成/已生成、`status` 等)→ `searchable: 1` + `options_json`(value 与 DDL 存库一致)
|
||||
- `boolean` 业务列:`display_type: boolean` **且** `options_json` 写中文 label(示例见下)
|
||||
|
||||
禁止:
|
||||
|
||||
- 仅为筛选把存库 value 改成中文
|
||||
- 在文档或 `description` 要求用户记内部 magic number / 英文 code
|
||||
|
||||
示例(启用开关 + 中文筛选):
|
||||
|
||||
```python
|
||||
{
|
||||
"column_name": "enabled",
|
||||
"display_name": "是否启用",
|
||||
"visible": 1,
|
||||
"searchable": 1,
|
||||
"editable": 1,
|
||||
"display_type": "boolean",
|
||||
"options_json": json.dumps(
|
||||
[{"value": 0, "label": "停用"}, {"value": 1, "label": "启用"}],
|
||||
ensure_ascii=False,
|
||||
),
|
||||
}
|
||||
```
|
||||
|
||||
Action 参数表单**无**列筛选对称能力;筛选语义以本节为准,表单对照见 [`ACTIONS.md`](ACTIONS.md)「表单字段类型对照」。
|
||||
|
||||
## editable 与 createable 的关系
|
||||
|
||||
技能模板只维护 `editable` 这一份字段权限声明:
|
||||
@@ -461,6 +596,19 @@ readonly = 0
|
||||
|
||||
模板**默认不建**此类审计表。过程看 `jiangchang.log`,进度/终态看任务中心,业务结果看业务表。
|
||||
|
||||
### 业务行生命周期(处理队列)
|
||||
|
||||
若技能对某一业务行做「生成 / 触达 / 私信 / 发布」等有副作用处理,状态字段须遵守 **角色模型**(可操作 / 进行中 / 成功·失败·跳过类终态),**不要**把某一技能的状态名当成全平台唯一枚举。默认只处理可操作态;重做靠人工改回可操作。完整规则见 [`WORK_ITEM_STATUS.md`](WORK_ITEM_STATUS.md)。
|
||||
|
||||
建议:
|
||||
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| 状态列 | `options_json` 列出本技能全部业务状态(含进行中与各类终态) |
|
||||
| 失败原因 | 失败类终态应有用户可读的原因字段(勿写密钥) |
|
||||
| 可编辑 | 终态/进行中建议可改回可操作,或提供等价 Action |
|
||||
| `grid_help` | 可一句话说明「仅某某状态可处理」 |
|
||||
|
||||
表级 `readonly = 1` 时,宿主数据管理只读展示;技能 repository 仍可写入。
|
||||
|
||||
### 通用示例:`demo_items`(测试 fixture 参考)
|
||||
@@ -509,36 +657,77 @@ CREATE TABLE demo_items (
|
||||
|
||||
宿主据此渲染选择框;技能侧负责保证 JSON 合法且与物理字段取值一致。
|
||||
|
||||
### 2)表关联 lookup(对象,推荐用于外键式字段)
|
||||
### 2)跨表引用 lookup(对象,用于关联 id 字段)
|
||||
|
||||
当字段取值依赖同库另一张业务表时,`display_type` 设为 `lookup`,`options_json` 写对象(**不是**数组):
|
||||
当字段取值依赖同库另一张业务表时,`display_type` 设为 `lookup`,`options_json` 写对象(**不是**数组)。这是**展示与表单选择**契约,**不是** SQLite 外键。
|
||||
|
||||
**单选(默认,与既有行为一致):**
|
||||
|
||||
```json
|
||||
{
|
||||
"ref_table": "platforms",
|
||||
"value_column": "platform_key",
|
||||
"label_column": "display_name",
|
||||
"extra_column": "category",
|
||||
"filter_column": "enabled",
|
||||
"filter_value": 1
|
||||
}
|
||||
```
|
||||
|
||||
**多选(平行扩展,仅多一个开关):**
|
||||
|
||||
```json
|
||||
{
|
||||
"ref_table": "engines",
|
||||
"value_column": "id",
|
||||
"label_column": "display_name",
|
||||
"filter_column": "enabled",
|
||||
"filter_value": 1,
|
||||
"multiple": true
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|---|---|---|
|
||||
| `ref_table` | 是 | 同库被引用表名(snake_case 标识符) |
|
||||
| `value_column` | 是 | 写入当前字段的值列 |
|
||||
| `label_column` | 是 | 下拉展示名列 |
|
||||
| `value_column` | 是 | 写入当前字段的值列(业务 id / 编号) |
|
||||
| `label_column` | 是 | 选项主文案列(名称) |
|
||||
| `extra_column` | 否 | 第三展示列(不必叫「备注」);有值时拼进括号 |
|
||||
| `filter_column` / `filter_value` | 否 | 等值过滤(如只列出 `enabled=1`) |
|
||||
| `multiple` | 否 | `true` = 多选 Lookup;省略或 `false` = **单选**(默认) |
|
||||
|
||||
宿主在加载表 schema 时查询 `ref_table`,把结果展开成 `{value,label}` 下拉选项;**物理列仍存标量**(如 `platform_key` 文本、`account_id` 整数),不做嵌套子表。
|
||||
宿主在加载表 schema 时查询 `ref_table`,把结果展开成 `{value,label}` 选项列表。
|
||||
|
||||
示例:账号表的平台字段 → 平台信息表;凭据表的账号编号 → 账号信息表。
|
||||
**存库约定:**
|
||||
|
||||
| 模式 | 物理列建议 | 存什么 |
|
||||
|------|------------|--------|
|
||||
| 单选 | 整数或文本标量 | 一个业务 id(如 `2`、`douyin`) |
|
||||
| 多选 | **`TEXT`** | 逗号分隔 id,如 `1,2,5`(与 `$selection.ids` 风格一致);**不要**建联接子表当 UI 多选 |
|
||||
|
||||
**统一展示文案(宿主固定一种,技能只配列):**
|
||||
|
||||
- 单选默认:`{value} · {label}`,例如 `2 · 通用私信-留资`、`douyin · 抖音`
|
||||
- 配置了 `extra_column` 且该行有非空值:`{value} · {label}({extra})`
|
||||
- 未配置 `extra_column`、或额外列为空:不加 `()`
|
||||
- `label` 为空或与 `value` 相同时:只显示 `value`(可再带额外括号)
|
||||
- **多选列表格**:按 id 拆开后逐个映射为 label,再用顿号/逗号拼接(如 `豆包、元宝`);过长可截断并提示「+N」
|
||||
|
||||
**控件呈现(宿主策略,非第二套契约):**
|
||||
|
||||
- 单选:下拉 Select(现状)
|
||||
- 多选且选项较少(建议 ≤6):勾选列表
|
||||
- 多选且选项较多:字段内 Chip + 可搜索下拉(**不是**再开一层业务弹窗)
|
||||
|
||||
Action 弹窗内联 Lookup 与上表 **同构**(字段名一致,含 `multiple`),见 [`ACTIONS.md`](ACTIONS.md)「跨表引用:Lookup」。
|
||||
|
||||
示例:账号表的平台字段 → 平台信息表;凭据表的账号编号 → 账号信息表;私信 Action 选话术 → `message_templates`;抽检 Action 多选引擎 → `engines` + `multiple: true`。
|
||||
|
||||
## 幂等初始化标准
|
||||
|
||||
- `_jiangchang_tables` 和 `_jiangchang_columns` 必须通过 upsert 初始化
|
||||
- 多次执行初始化不能重复记录
|
||||
- 重新初始化可以更新展示名、描述、`visible`、`searchable`、`editable`、`display_type`、`options_json`
|
||||
- 重新初始化可以更新展示名、描述、`grid_help`、`visible`、`searchable`、`editable`、`display_type`、`options_json`
|
||||
- 元数据不能残留旧字段(删除或重命名字段时必须同步清理)
|
||||
- 元数据不能引用不存在的表或字段
|
||||
- 新增字段或表时必须同步增加元数据
|
||||
@@ -582,6 +771,48 @@ seed_column_metadata_batch(
|
||||
|
||||
不要直接修改用户真实数据库作为迁移方式;元数据初始化必须使用真实物理字段列表。
|
||||
|
||||
## 表增删改与退役(技能 schema 迁移)
|
||||
|
||||
技能拥有 SQLite 物理 schema;宿主只渲染**已注册**的元数据。增删改表时技能须在 `init_db()` 中做幂等迁移,并与 `_jiangchang_*` 同步。
|
||||
|
||||
| 操作 | 技能必须做 |
|
||||
|------|------------|
|
||||
| **新增**用户可见表 | `CREATE TABLE` + `upsert_table_metadata` + 列元数据种子 |
|
||||
| **修改**表/列 | 物理迁移(`ALTER` / 重建)+ 更新或 `prune_stale_column_metadata` |
|
||||
| **临时隐藏**(表仍保留) | 元数据 `visible=0`(表与元数据行都保留) |
|
||||
| **永久废弃**用户可见表 | `drop_deprecated_user_table(...)`(见下) |
|
||||
|
||||
### 永久废弃用户可见表
|
||||
|
||||
使用 `scripts/db/display_metadata.py` 的 **`drop_deprecated_user_table`**(幂等):
|
||||
|
||||
1. `DROP TRIGGER`(若有 `{table}_set_updated_at` 等)
|
||||
2. `DROP TABLE IF EXISTS`
|
||||
3. `DELETE` 对应 `_jiangchang_columns` / `_jiangchang_tables` 行
|
||||
|
||||
```python
|
||||
from db.display_metadata import drop_deprecated_user_table
|
||||
|
||||
def init_db() -> None:
|
||||
...
|
||||
drop_deprecated_user_table(cur, "task_logs")
|
||||
init_display_metadata(cur)
|
||||
```
|
||||
|
||||
**反模式(禁止):** 只删 `_jiangchang_*` 元数据、保留物理表。宿主虽可能不展示孤儿表,但库内残留、自检失败,且违背「元数据与物理结构一致」。
|
||||
|
||||
**新技能禁止**创建或写入 `task_logs` 一类通用任务审计表。过程 → `jiangchang.log` / 运行日志;进度/终态 → 任务中心;业务结果 → 业务表(见 [`WORK_ITEM_STATUS.md`](WORK_ITEM_STATUS.md)、[`../development/LOGGING.md`](../development/LOGGING.md))。
|
||||
|
||||
### 旧库迁移测试(废弃表时必做)
|
||||
|
||||
复制 `tests/samples/test_table_deprecation_migration.py.sample`,构造「旧库含废弃表 + 元数据 + 历史行」→ `init_db()` 后断言:
|
||||
|
||||
- `sqlite_master` 无该表
|
||||
- `_jiangchang_*` 无残留行
|
||||
- `iter_user_tables` 不含该表
|
||||
|
||||
卸载技能**不删**用户数据目录;重装会复用旧库,迁移必须在 `init_db()` 中完成(见 [`../development/DATA_PATHS.md`](../development/DATA_PATHS.md))。
|
||||
|
||||
## 中文命名质量
|
||||
|
||||
- 使用普通用户能理解的业务用语:`error_msg` → **错误信息**,`created_at` → **创建时间**。
|
||||
@@ -590,11 +821,12 @@ seed_column_metadata_batch(
|
||||
|
||||
## 元数据同步规则
|
||||
|
||||
1. 新增用户可见业务表 → 同步 `_jiangchang_tables`。
|
||||
1. 新增用户可见业务表 → 同步 `_jiangchang_tables`(`visible=1` 方可在数据管理展示)。
|
||||
2. 新增用户可见字段 → 同步 `_jiangchang_columns`。
|
||||
3. 删除/重命名表或字段 → 同步清理或迁移元数据。
|
||||
3. 删除/重命名表或字段 → 物理迁移 + 同步清理元数据;**废弃表**用 `drop_deprecated_user_table`,禁止只删元数据留物理表。
|
||||
4. 初始化脚本必须幂等。
|
||||
5. 元数据中的表、字段必须在库中真实存在;可见项不得缺少中文 `display_name`。
|
||||
6. `init_db()` 后:`iter_user_tables` 中每个物理业务表都应有对应 `_jiangchang_tables.display_name`,或已被 `drop_deprecated_user_table` 物理删除。
|
||||
|
||||
## 开发自检
|
||||
|
||||
|
||||
157
references/WORK_ITEM_STATUS.md
Normal file
157
references/WORK_ITEM_STATUS.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# 业务行生命周期契约(Work Item Lifecycle)
|
||||
|
||||
> **状态:正式契约 v1.0。**
|
||||
> **目标:** 冻结「对某一行做有副作用处理」时的**状态角色与操作权**规则,使队列型技能(生成、触达、私信、发布等)行为一致、可验收。
|
||||
> **非目标:** 规定某一技能的具体状态码或状态个数;强制所有表都有 status;替代任务中心 / 运行日志;规定 Data Policy 授权。
|
||||
> **适用范围:** 有「处理业务行」类 Action 的技能。纯配置表、目录表、只读展示表标 N/A。
|
||||
|
||||
相关正交轴:[`ACTIONS.md`](ACTIONS.md)(入口与 sync/async)、[`SCHEMA.md`](SCHEMA.md)(表与展示)、[`POLICY.md`](POLICY.md)(谁能点)、任务中心(宿主 Job)。
|
||||
|
||||
---
|
||||
|
||||
## 0. 为什么需要本契约
|
||||
|
||||
若「处理这一行」的按钮对**任意状态**都可点,会出现:
|
||||
|
||||
- 已成功的行被重复执行(重复发信、重复生成、覆盖产物)
|
||||
- 失败行被默认批量静默重试,运营无法区分「未做」与「做过但失败」
|
||||
- 任务中心显示成功,业务表仍停在初始态(结果未回写)
|
||||
|
||||
本契约要求:**默认可自动操作的只有「可操作」角色;其它角色须人工改回可操作后再处理。**
|
||||
|
||||
---
|
||||
|
||||
## 1. 状态角色(通用,不绑死枚举)
|
||||
|
||||
技能自己定义业务状态字符串与中文标签;契约只认**角色**。一个角色可对应**一个或多个**业务状态。
|
||||
|
||||
| 角色 | 含义 | 默认可被处理 Action 捞起? |
|
||||
|------|------|---------------------------|
|
||||
| **operable(可操作)** | 尚未成功处理、允许自动或行操作启动处理 | **是(默认唯一)** |
|
||||
| **in_progress(进行中)** | 已抢占,正在处理 | **否**(防双跑) |
|
||||
| **terminal_success(成功类终态)** | 已按业务定义完成 | **否** |
|
||||
| **terminal_failure(失败类终态)** | 已尝试且失败;通常带失败原因 | **否** |
|
||||
| **terminal_skipped(跳过类终态)** | 人工放弃或不处理 | **否** |
|
||||
|
||||
### 1.1 举例(仅对照,不是规范枚举)
|
||||
|
||||
| 角色 | 技能 A(内容队列)可能叫 | 技能 B(线索触达)可能叫 |
|
||||
|------|--------------------------|--------------------------|
|
||||
| operable | 待生成、新建 | 新建、私信未发 |
|
||||
| in_progress | 生成中 | 触达中 |
|
||||
| terminal_success | 已生成 | 已私信 |
|
||||
| terminal_failure | 失败 | 触达失败 |
|
||||
| terminal_skipped | 已跳过 | 已忽略 |
|
||||
|
||||
技能可以有更多业务状态(审核中、待确认、部分成功等):须在需求里写明各自映射到哪一角色,或声明「不属于处理队列、本契约 N/A」。
|
||||
|
||||
### 1.2 技能必须声明映射
|
||||
|
||||
复制模板后,在 `development/REQUIREMENTS.md`(或等价需求)中写清:
|
||||
|
||||
```text
|
||||
处理队列表:<table>
|
||||
状态字段:<column>
|
||||
可操作:<status1>, <status2>, …
|
||||
进行中:<status…>
|
||||
成功类终态:…
|
||||
失败类终态:…(失败原因字段:…)
|
||||
跳过类终态:…
|
||||
```
|
||||
|
||||
未声明映射的「处理队列」技能,交活 Checklist 不得标 PASS。
|
||||
|
||||
---
|
||||
|
||||
## 2. 硬规则(技能实现必须遵守)
|
||||
|
||||
1. **默认只认 operable**
|
||||
行操作 / 批量 / 定时 / Agent 触发的「处理」类 Action,pick 与 claim **默认**只包含可操作状态。
|
||||
不得把成功、失败、跳过、进行中混进默认捞取集合(除非另做**显式命名**的「重试失败」类 Action,且确认文案写清范围——仍推荐不如人工改回可操作简单)。
|
||||
|
||||
2. **开始即原子抢占 → in_progress**
|
||||
在真正产生副作用前,用「仅当当前为 operable 时 UPDATE 为进行中」的方式抢占。抢占失败 → 跳过或返回明确错误(如「当前不是可操作状态」),**禁止**继续副作用。
|
||||
|
||||
3. **结束必须回写业务表**
|
||||
- 成功 → 对应成功类终态,并写入产物路径/摘要等业务字段(若有)
|
||||
- 失败 → 对应失败类终态,并写入**失败原因**(用户可读;勿塞密钥)
|
||||
- 禁止「任务中心成功、业务行仍停在可操作/进行中」而不回写
|
||||
|
||||
4. **重做靠人工改回 operable**
|
||||
成功 / 失败 / 跳过 / 卡住的进行中,默认不能再点处理。运营在数据管理把状态改回可操作后,方可再次处理。
|
||||
|
||||
5. **任务中心 ≠ 业务状态**
|
||||
Job「进行中 / 成功 / 失败」是宿主任务态。业务「进行中」必须写在业务表。进度 `emit` 不替代状态回写。
|
||||
|
||||
6. **过程与结果分工**(延续既有约定)
|
||||
过程 → 运行日志 / 任务中心 Activity;业务结果 → 业务表。**不要**靠新建 `task_logs` 一类审计表充当状态机。
|
||||
|
||||
---
|
||||
|
||||
## 3. Action 与文案约定
|
||||
|
||||
- 处理类 Action 的 `description` / `confirmation.message` 应写明:**仅处理「可操作」状态**(用本技能用户能懂的词,如「仅待生成」「仅新建」)。
|
||||
- 对非可操作行触发时:返回明确失败码与中文说明,提示「请先改回 xxx」。
|
||||
- 长任务(`async`):除任务中心进度外,业务行应尽快进入进行中,避免界面长时间仍显示可操作导致连点。
|
||||
|
||||
正交提醒:
|
||||
|
||||
| 轴 | 管什么 | 不管什么 |
|
||||
|----|--------|----------|
|
||||
| 本契约 | 什么**状态**能处理 | 谁有权点按钮 |
|
||||
| Data Policy | 谁有权 | 状态机 |
|
||||
| executionProfile | sync/async 通道 | 业务终态 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据管理展示建议
|
||||
|
||||
- 状态列:`options_json` 枚举完整业务状态(含进行中与各类终态)。
|
||||
- 失败原因列:失败类终态可见;成功后宜清空或保留末次原因(技能自定,须一致)。
|
||||
- 终态与进行中的状态字段建议 **可编辑**(或提供「改回可操作」Action),以便人工重做;**不要**只能靠删行重建。
|
||||
- `grid_help` 可一句话写清:「仅某某状态可处理;其它状态请改回后再操作。」
|
||||
|
||||
详见 [`SCHEMA.md`](SCHEMA.md)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 可观测性(与 ACTIONS / 日志约定对齐)
|
||||
|
||||
推荐阶段(名称可自定,须能对应到开始 / 关键节点 / 结束):
|
||||
|
||||
1. 已抢占(业务 → 进行中)
|
||||
2. 处理中关键步骤(可 `emit`)
|
||||
3. 回写成功或失败
|
||||
|
||||
故意失败一条时:业务表应为失败类终态 + 原因;任务中心为失败;日志含 `task_failed` / `error_code=`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 非目标与常见误区
|
||||
|
||||
| 误区 | 正确做法 |
|
||||
|------|----------|
|
||||
| 把「新建/成功/失败/跳过」写成全平台唯一枚举 | 只用角色;各技能自定状态名 |
|
||||
| 默认批量包含失败「方便自动重试」 | 默认不含;重试改回可操作或单独显式 Action |
|
||||
| 只有任务成功、不改业务状态 | 必须回写业务表 |
|
||||
| 用进行中替代任务中心 | 两者都要:业务防双跑,任务轨看进度 |
|
||||
| 所有表都套本契约 | 无「处理行」副作用的表标 N/A |
|
||||
|
||||
---
|
||||
|
||||
## 7. 验收摘要(完整条目见 CHECKLIST)
|
||||
|
||||
有处理队列时至少验证:
|
||||
|
||||
1. 成功类终态再点处理 → 拒绝或跳过,不产生重复副作用
|
||||
2. 可操作行处理中 → 先见进行中,结束后见成功或失败+原因
|
||||
3. 失败行改回可操作后 → 可以再次处理
|
||||
4. 任务中心有过程(长任务)且业务表状态与结果一致
|
||||
|
||||
---
|
||||
|
||||
## 8. 版本
|
||||
|
||||
| 版本 | 说明 |
|
||||
|------|------|
|
||||
| v1.0 | 首版:角色模型 + 硬规则 + 需求映射 + 与 Action/SCHEMA/任务中心边界 |
|
||||
114
release.ps1
114
release.ps1
@@ -17,7 +17,7 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Prefix = "v",
|
||||
[string]$Message = "正式发布",
|
||||
[string]$Message,
|
||||
[switch]$AutoCommit,
|
||||
[switch]$RequireClean,
|
||||
[string]$CommitMessage,
|
||||
@@ -28,6 +28,13 @@ param(
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Default tag message is 「正式发布」 from UTF-8 bytes (not a Chinese literal in
|
||||
# this file). Windows PowerShell 5.1 parses UTF-8 scripts without BOM as the
|
||||
# system ANSI code page, which would turn a source literal into 「姝e紡鍙戝竷」.
|
||||
if (-not $PSBoundParameters.ContainsKey("Message") -or [string]::IsNullOrWhiteSpace($Message)) {
|
||||
$Message = [System.Text.Encoding]::UTF8.GetString([byte[]](0xE6, 0xAD, 0xA3, 0xE5, 0xBC, 0x8F, 0xE5, 0x8F, 0x91, 0xE5, 0xB8, 0x83))
|
||||
}
|
||||
|
||||
function Remove-GitNoise {
|
||||
param([string[]]$Lines)
|
||||
return @($Lines | Where-Object {
|
||||
@@ -35,40 +42,85 @@ function Remove-GitNoise {
|
||||
})
|
||||
}
|
||||
|
||||
function Convert-GitOutputLines {
|
||||
param($Output)
|
||||
return @(
|
||||
@($Output) | ForEach-Object {
|
||||
if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.ToString() }
|
||||
else { "$_" }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
# Call git.exe directly. Never wrap with `cmd /c`: on Chinese Windows, cmd
|
||||
# re-encodes the command line through the OEM code page.
|
||||
# Also never pass Chinese via `git -m`: Windows PowerShell 5.1 encodes native
|
||||
# argv using the console code page, so 「正式发布」 becomes 「姝e紡鍙戝竷」
|
||||
# in the annotated tag. Commit/tag messages go through a UTF-8 file + `-F`.
|
||||
function Invoke-GitRaw {
|
||||
param([Parameter(Mandatory = $true)][string[]]$GitArgs)
|
||||
$prev = $ErrorActionPreference
|
||||
$ErrorActionPreference = "Continue"
|
||||
try {
|
||||
$output = & git @GitArgs 2>&1
|
||||
$code = $LASTEXITCODE
|
||||
} finally {
|
||||
$ErrorActionPreference = $prev
|
||||
}
|
||||
return [pscustomobject]@{
|
||||
ExitCode = $code
|
||||
Lines = @(Remove-GitNoise -Lines @(Convert-GitOutputLines -Output $output))
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Git {
|
||||
param([Parameter(Mandatory = $true)][string]$Args)
|
||||
Write-Host ">> git $Args" -ForegroundColor DarkGray
|
||||
& cmd /c "git $Args 2>&1" | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "git command failed: git $Args"
|
||||
param([Parameter(Mandatory = $true)][string[]]$GitArgs)
|
||||
Write-Host (">> git " + ($GitArgs -join " ")) -ForegroundColor DarkGray
|
||||
$r = Invoke-GitRaw -GitArgs $GitArgs
|
||||
if ($r.ExitCode -ne 0) {
|
||||
throw "git command failed: git $($GitArgs -join ' ')"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-GitWithMessageFile {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string[]]$GitArgsPrefix,
|
||||
[Parameter(Mandatory = $true)][string]$Message
|
||||
)
|
||||
$msgFile = Join-Path ([System.IO.Path]::GetTempPath()) ("jiangchang-git-msg-" + [guid]::NewGuid().ToString("N") + ".txt")
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
|
||||
[System.IO.File]::WriteAllText($msgFile, ($Message.TrimEnd() + "`n"), $utf8NoBom)
|
||||
try {
|
||||
Invoke-Git -GitArgs (@("-c", "i18n.commitEncoding=utf-8") + $GitArgsPrefix + @("-F", $msgFile))
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $msgFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Get-GitOutput {
|
||||
param([Parameter(Mandatory = $true)][string]$Args)
|
||||
$output = & cmd /c "git $Args 2>&1"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "git command failed: git $Args"
|
||||
param([Parameter(Mandatory = $true)][string[]]$GitArgs)
|
||||
$r = Invoke-GitRaw -GitArgs $GitArgs
|
||||
if ($r.ExitCode -ne 0) {
|
||||
throw "git command failed: git $($GitArgs -join ' ')"
|
||||
}
|
||||
return @(Remove-GitNoise -Lines @($output))
|
||||
return @($r.Lines)
|
||||
}
|
||||
|
||||
function Test-Repo {
|
||||
$output = & cmd /c "git rev-parse --is-inside-work-tree 2>&1"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
$r = Invoke-GitRaw -GitArgs @("rev-parse", "--is-inside-work-tree")
|
||||
if ($r.ExitCode -ne 0) {
|
||||
return $false
|
||||
}
|
||||
$clean = Remove-GitNoise -Lines @($output)
|
||||
return (($clean | Select-Object -First 1) -eq "true")
|
||||
return (($r.Lines | Select-Object -First 1) -eq "true")
|
||||
}
|
||||
|
||||
function Get-CurrentBranch {
|
||||
$b = (Get-GitOutput "branch --show-current" | Select-Object -First 1).Trim()
|
||||
$b = (Get-GitOutput -GitArgs @("branch", "--show-current") | Select-Object -First 1).Trim()
|
||||
return $b
|
||||
}
|
||||
|
||||
function Get-StatusPorcelain {
|
||||
$lines = @(Get-GitOutput "status --porcelain")
|
||||
$lines = @(Get-GitOutput -GitArgs @("status", "--porcelain"))
|
||||
return $lines
|
||||
}
|
||||
|
||||
@@ -92,7 +144,7 @@ function Parse-SemVerTag {
|
||||
function Get-NextTag {
|
||||
param([string]$TagPrefix)
|
||||
|
||||
$tags = Get-GitOutput "tag --list"
|
||||
$tags = Get-GitOutput -GitArgs @("tag", "--list")
|
||||
$parsed = @()
|
||||
|
||||
foreach ($t in $tags) {
|
||||
@@ -181,7 +233,7 @@ function Ensure-CleanOrAutoCommit {
|
||||
|
||||
if ($NeedClean) {
|
||||
Write-Host "Working tree is not clean and -RequireClean is enabled." -ForegroundColor Yellow
|
||||
Remove-GitNoise -Lines @(& cmd /c "git status --short 2>&1") | ForEach-Object { Write-Host $_ }
|
||||
(Invoke-GitRaw -GitArgs @("status", "--short")).Lines | ForEach-Object { Write-Host $_ }
|
||||
throw "Abort: dirty working tree."
|
||||
}
|
||||
|
||||
@@ -200,8 +252,8 @@ function Ensure-CleanOrAutoCommit {
|
||||
return
|
||||
}
|
||||
|
||||
Invoke-Git "add -A"
|
||||
Invoke-Git "commit -m `"$commitMsg`""
|
||||
Invoke-Git -GitArgs @("add", "-A")
|
||||
Invoke-GitWithMessageFile -GitArgsPrefix @("commit") -Message $commitMsg
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -223,10 +275,10 @@ try {
|
||||
if ($DryRun) {
|
||||
Write-Host "[DryRun] Would run: git fetch --tags --prune origin" -ForegroundColor Yellow
|
||||
} else {
|
||||
Invoke-Git "fetch --tags --prune origin"
|
||||
Invoke-Git -GitArgs @("fetch", "--tags", "--prune", "origin")
|
||||
}
|
||||
|
||||
$skillRoot = (Get-GitOutput "rev-parse --show-toplevel" | Select-Object -First 1).Trim()
|
||||
$skillRoot = (Get-GitOutput -GitArgs @("rev-parse", "--show-toplevel") | Select-Object -First 1).Trim()
|
||||
if (Test-Path -LiteralPath (Join-Path $skillRoot "SKILL.md")) {
|
||||
Assert-SkillReleasePackagingSources -SkillRoot $skillRoot
|
||||
}
|
||||
@@ -235,8 +287,8 @@ try {
|
||||
|
||||
Ensure-CleanOrAutoCommit -DoAutoCommit:$AutoCommit -NeedClean:$RequireClean -IsDryRun:$DryRun -Msg $CommitMessage
|
||||
|
||||
$null = Remove-GitNoise -Lines @(& cmd /c 'git rev-parse --abbrev-ref --symbolic-full-name "@{u}" 2>&1')
|
||||
$hasUpstream = ($LASTEXITCODE -eq 0)
|
||||
$upstream = Invoke-GitRaw -GitArgs @("rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}")
|
||||
$hasUpstream = ($upstream.ExitCode -eq 0)
|
||||
|
||||
if ($DryRun) {
|
||||
if ($hasUpstream) {
|
||||
@@ -246,29 +298,29 @@ try {
|
||||
}
|
||||
} else {
|
||||
if ($hasUpstream) {
|
||||
Invoke-Git "push"
|
||||
Invoke-Git -GitArgs @("push")
|
||||
} else {
|
||||
Invoke-Git "push -u origin $branch"
|
||||
Invoke-Git -GitArgs @("push", "-u", "origin", $branch)
|
||||
}
|
||||
}
|
||||
|
||||
$nextTag = Get-NextTag -TagPrefix $Prefix
|
||||
Write-Host "Next tag: $nextTag" -ForegroundColor Green
|
||||
|
||||
$existing = @(Get-GitOutput "tag --list `"$nextTag`"")
|
||||
$existing = @(Get-GitOutput -GitArgs @("tag", "--list", $nextTag))
|
||||
if ($existing.Length -gt 0) {
|
||||
throw "Tag already exists: $nextTag"
|
||||
}
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Host "[DryRun] Would run: git tag -a $nextTag -m `"$Message`"" -ForegroundColor Yellow
|
||||
Write-Host "[DryRun] Would run: git tag -a $nextTag -F <utf8-msg-file> ($Message)" -ForegroundColor Yellow
|
||||
Write-Host "[DryRun] Would run: git push origin $nextTag" -ForegroundColor Yellow
|
||||
Write-Host "=== DryRun Complete ===" -ForegroundColor Cyan
|
||||
exit 0
|
||||
}
|
||||
|
||||
Invoke-Git "tag -a $nextTag -m `"$Message`""
|
||||
Invoke-Git "push origin $nextTag"
|
||||
Invoke-GitWithMessageFile -GitArgsPrefix @("tag", "-a", $nextTag) -Message $Message
|
||||
Invoke-Git -GitArgs @("push", "origin", $nextTag)
|
||||
|
||||
Write-Host "Release success: $nextTag" -ForegroundColor Green
|
||||
Write-Host "=== Release Script Done ===" -ForegroundColor Cyan
|
||||
|
||||
@@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS _jiangchang_tables (
|
||||
table_name TEXT PRIMARY KEY,
|
||||
display_name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
grid_help TEXT,
|
||||
sort_order INTEGER,
|
||||
visible INTEGER NOT NULL DEFAULT 1,
|
||||
readonly INTEGER NOT NULL DEFAULT 0,
|
||||
@@ -35,6 +36,7 @@ CREATE TABLE IF NOT EXISTS _jiangchang_columns (
|
||||
column_name TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
grid_help TEXT,
|
||||
display_order INTEGER,
|
||||
visible INTEGER NOT NULL DEFAULT 1,
|
||||
searchable INTEGER NOT NULL DEFAULT 1,
|
||||
@@ -45,22 +47,35 @@ CREATE TABLE IF NOT EXISTS _jiangchang_columns (
|
||||
)
|
||||
"""
|
||||
|
||||
def _pragma_column_names(cursor, table_name: str) -> set[str]:
|
||||
cursor.execute(f"PRAGMA table_info({table_name!r})")
|
||||
return {str(row[1]) for row in cursor.fetchall()}
|
||||
|
||||
|
||||
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()}
|
||||
cols = _pragma_column_names(cursor, "_jiangchang_tables")
|
||||
if "default_sort_json" not in cols:
|
||||
cursor.execute("ALTER TABLE _jiangchang_tables ADD COLUMN default_sort_json TEXT")
|
||||
if "default_visible_columns_json" not in cols:
|
||||
cursor.execute(
|
||||
"ALTER TABLE _jiangchang_tables ADD COLUMN default_visible_columns_json TEXT"
|
||||
)
|
||||
if "grid_help" not in cols:
|
||||
cursor.execute("ALTER TABLE _jiangchang_tables ADD COLUMN grid_help TEXT")
|
||||
|
||||
|
||||
def _ensure_jiangchang_columns_columns(cursor) -> None:
|
||||
cols = _pragma_column_names(cursor, "_jiangchang_columns")
|
||||
if "grid_help" not in cols:
|
||||
cursor.execute("ALTER TABLE _jiangchang_columns ADD COLUMN grid_help TEXT")
|
||||
|
||||
|
||||
def init_display_metadata_tables(cursor) -> None:
|
||||
cursor.execute(_JIANGCHANG_TABLES_DDL)
|
||||
cursor.execute(_JIANGCHANG_COLUMNS_DDL)
|
||||
_ensure_jiangchang_tables_columns(cursor)
|
||||
_ensure_jiangchang_columns_columns(cursor)
|
||||
|
||||
|
||||
def upsert_table_metadata(
|
||||
@@ -69,6 +84,7 @@ def upsert_table_metadata(
|
||||
table_name: str,
|
||||
display_name: str,
|
||||
description: str | None = None,
|
||||
grid_help: str | None = None,
|
||||
visible: int = 1,
|
||||
readonly: int = 0,
|
||||
default_sort_json: str | None = None,
|
||||
@@ -83,17 +99,20 @@ def upsert_table_metadata(
|
||||
default_visible_columns_json: 宿主数据管理默认展示列,JSON 字符串数组,例如
|
||||
'["stat_type","period","stage"]'
|
||||
为空则全显;用户改过「列」后以本机偏好为准(软默认,区别于 visible=0 硬隐藏)。
|
||||
|
||||
grid_help: 数据管理表格上方说明;空则不出横幅。与表单用的 description 独立,禁止互拷。
|
||||
"""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO _jiangchang_tables (
|
||||
table_name, display_name, description, visible, readonly,
|
||||
table_name, display_name, description, grid_help, visible, readonly,
|
||||
default_sort_json, default_visible_columns_json
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(table_name) DO UPDATE SET
|
||||
display_name = excluded.display_name,
|
||||
description = excluded.description,
|
||||
grid_help = excluded.grid_help,
|
||||
visible = excluded.visible,
|
||||
readonly = excluded.readonly,
|
||||
default_sort_json = excluded.default_sort_json,
|
||||
@@ -103,6 +122,7 @@ def upsert_table_metadata(
|
||||
table_name,
|
||||
display_name,
|
||||
description,
|
||||
grid_help,
|
||||
visible,
|
||||
readonly,
|
||||
default_sort_json,
|
||||
@@ -118,6 +138,7 @@ def upsert_column_metadata(
|
||||
column_name: str,
|
||||
display_name: str,
|
||||
description: str | None = None,
|
||||
grid_help: str | None = None,
|
||||
visible: int = 1,
|
||||
searchable: int = 1,
|
||||
editable: int = 1,
|
||||
@@ -125,16 +146,18 @@ def upsert_column_metadata(
|
||||
options_json: str | None = None,
|
||||
) -> None:
|
||||
# display_order 保留为 NULL:宿主按 PRAGMA table_info cid 展示字段,不读 display_order。
|
||||
# grid_help: 表头「?」;空则不出。与表单用的 description 独立,禁止互拷。
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO _jiangchang_columns (
|
||||
table_name, column_name, display_name, description,
|
||||
table_name, column_name, display_name, description, grid_help,
|
||||
display_order, visible, searchable, editable, display_type, options_json
|
||||
)
|
||||
VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(table_name, column_name) DO UPDATE SET
|
||||
display_name = excluded.display_name,
|
||||
description = excluded.description,
|
||||
grid_help = excluded.grid_help,
|
||||
display_order = NULL,
|
||||
visible = excluded.visible,
|
||||
searchable = excluded.searchable,
|
||||
@@ -147,6 +170,7 @@ def upsert_column_metadata(
|
||||
column_name,
|
||||
display_name,
|
||||
description,
|
||||
grid_help,
|
||||
visible,
|
||||
searchable,
|
||||
editable,
|
||||
@@ -171,6 +195,7 @@ def seed_column_metadata_batch(
|
||||
column_name=str(column["column_name"]),
|
||||
display_name=str(column["display_name"]),
|
||||
description=str(column["description"]) if column.get("description") else None,
|
||||
grid_help=str(column["grid_help"]) if column.get("grid_help") else None,
|
||||
visible=int(column.get("visible", 1)),
|
||||
searchable=int(column.get("searchable", 1)),
|
||||
editable=int(column.get("editable", 1)),
|
||||
@@ -252,6 +277,32 @@ def init_display_metadata(cursor) -> None:
|
||||
init_display_metadata_tables(cursor)
|
||||
|
||||
|
||||
def drop_deprecated_user_table(
|
||||
cursor,
|
||||
table_name: str,
|
||||
*,
|
||||
triggers: Iterable[str] | None = None,
|
||||
) -> None:
|
||||
"""永久退役用户可见表:删 trigger、DROP 物理表、清理展示元数据(幂等)。
|
||||
|
||||
用于废弃 task_logs 等旧表。禁止只删元数据而保留物理表。
|
||||
见 references/SCHEMA.md「表增删改与退役」。
|
||||
"""
|
||||
for trigger_name in triggers or ():
|
||||
cursor.execute(f"DROP TRIGGER IF EXISTS {trigger_name}")
|
||||
default_updated_at = f"{table_name}_set_updated_at"
|
||||
cursor.execute(f"DROP TRIGGER IF EXISTS {default_updated_at}")
|
||||
cursor.execute(f"DROP TABLE IF EXISTS {_quote_identifier(table_name)}")
|
||||
cursor.execute(
|
||||
"DELETE FROM _jiangchang_columns WHERE table_name = ?",
|
||||
(table_name,),
|
||||
)
|
||||
cursor.execute(
|
||||
"DELETE FROM _jiangchang_tables WHERE table_name = ?",
|
||||
(table_name,),
|
||||
)
|
||||
|
||||
|
||||
def iter_user_tables(cursor) -> Iterable[str]:
|
||||
cursor.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"
|
||||
|
||||
@@ -30,6 +30,7 @@ _ENGLISH_PLACEHOLDER_NAME_PATTERNS = (
|
||||
)
|
||||
_KEBAB_SLUG = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
||||
_CJK_RE = re.compile(r"[\u4e00-\u9fff]")
|
||||
_FK_DDL_PATTERN = re.compile(r"\bFOREIGN\s+KEY\b|\bREFERENCES\b", re.I)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -218,6 +219,8 @@ def validate_options_json(options_json: str | None) -> tuple[list[str], list[str
|
||||
errors.append(f"options_json.{key} 必须是安全标识符,当前为 {raw!r}")
|
||||
if parsed.get("filter_column") not in (None, "") and "filter_value" not in parsed:
|
||||
warnings.append("options_json lookup 声明了 filter_column 但未提供 filter_value")
|
||||
if "multiple" in parsed and not isinstance(parsed.get("multiple"), bool):
|
||||
errors.append("options_json lookup.multiple 必须是布尔值")
|
||||
return errors, warnings
|
||||
|
||||
if not isinstance(parsed, list):
|
||||
@@ -232,6 +235,50 @@ def validate_options_json(options_json: str | None) -> tuple[list[str], list[str
|
||||
return errors, warnings
|
||||
|
||||
|
||||
def validate_filter_search_metadata(
|
||||
cursor,
|
||||
table_name: str,
|
||||
) -> tuple[list[str], list[str]]:
|
||||
"""对可见但未开启 searchable 的枚举 / boolean 列给出软建议。"""
|
||||
warnings: list[str] = []
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT column_name, visible, searchable, display_type, options_json
|
||||
FROM _jiangchang_columns
|
||||
WHERE table_name = ?
|
||||
""",
|
||||
(table_name,),
|
||||
)
|
||||
for column_name, visible, searchable, display_type, options_json in cursor.fetchall():
|
||||
if visible is not None and int(visible) == 0:
|
||||
continue
|
||||
if searchable is not None and int(searchable) != 0:
|
||||
continue
|
||||
|
||||
display_type_norm = str(display_type or "").strip()
|
||||
is_boolean = display_type_norm == "boolean"
|
||||
|
||||
has_enum = False
|
||||
if options_json is not None and str(options_json).strip():
|
||||
opt_errors, _ = validate_options_json(options_json)
|
||||
if not opt_errors:
|
||||
try:
|
||||
parsed = json.loads(str(options_json))
|
||||
except json.JSONDecodeError:
|
||||
parsed = None
|
||||
has_enum = isinstance(parsed, list) and len(parsed) > 0
|
||||
|
||||
if not has_enum and not is_boolean:
|
||||
continue
|
||||
|
||||
warnings.append(
|
||||
f"{table_name}.{column_name}: 枚举/状态类列建议 searchable=1,"
|
||||
"以便宿主列筛选与按中文 label 全局搜索(见 references/SCHEMA.md)"
|
||||
)
|
||||
|
||||
return [], warnings
|
||||
|
||||
|
||||
def validate_column_permissions(
|
||||
cursor,
|
||||
table_name: str,
|
||||
@@ -412,6 +459,27 @@ def _table_exists(cursor, table_name: str) -> bool:
|
||||
return cursor.fetchone() is not None
|
||||
|
||||
|
||||
def validate_no_sqlite_foreign_keys(cursor) -> tuple[list[str], list[str]]:
|
||||
"""业务表 DDL 不得含 FOREIGN KEY / REFERENCES(关联走 repository/service)。"""
|
||||
errors: list[str] = []
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT name, sql FROM sqlite_master
|
||||
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
||||
"""
|
||||
)
|
||||
for table_name, sql in cursor.fetchall():
|
||||
if table_name in METADATA_TABLES:
|
||||
continue
|
||||
ddl = str(sql or "")
|
||||
if ddl and _FK_DDL_PATTERN.search(ddl):
|
||||
errors.append(
|
||||
f"业务表 {table_name!r} 的 DDL 含 SQLite FOREIGN KEY/REFERENCES;"
|
||||
"请改为业务 id 列 + repository/service 逻辑关联(见 references/SCHEMA.md)"
|
||||
)
|
||||
return errors, []
|
||||
|
||||
|
||||
def validate_display_metadata(conn: sqlite3.Connection, *, check_column_order: bool = True) -> ValidationReport:
|
||||
report = ValidationReport()
|
||||
cur = conn.cursor()
|
||||
@@ -425,6 +493,9 @@ def validate_display_metadata(conn: sqlite3.Connection, *, check_column_order: b
|
||||
if not has_tables_meta or not has_columns_meta:
|
||||
return report
|
||||
|
||||
fk_errors, _ = validate_no_sqlite_foreign_keys(cur)
|
||||
report.errors.extend(fk_errors)
|
||||
|
||||
for table_name in iter_user_tables(cur):
|
||||
cur.execute(
|
||||
"SELECT display_name FROM _jiangchang_tables WHERE table_name = ?",
|
||||
@@ -484,6 +555,9 @@ def validate_display_metadata(conn: sqlite3.Connection, *, check_column_order: b
|
||||
report.errors.extend(perm_errors)
|
||||
report.warnings.extend(perm_warnings)
|
||||
|
||||
_, filter_warnings = validate_filter_search_metadata(cur, table_name)
|
||||
report.warnings.extend(filter_warnings)
|
||||
|
||||
cur.execute("SELECT table_name, column_name FROM _jiangchang_columns")
|
||||
for table_name, column_name in cur.fetchall():
|
||||
if table_name in METADATA_TABLES:
|
||||
@@ -514,5 +588,5 @@ def validate_display_metadata(conn: sqlite3.Connection, *, check_column_order: b
|
||||
|
||||
|
||||
def validate_template_database(conn: sqlite3.Connection) -> ValidationReport:
|
||||
"""模板自检入口:校验展示元数据完整性。模板默认无业务表。"""
|
||||
"""模板自检入口:校验展示元数据完整性,并扫描业务表 DDL 是否含 SQLite 外键。"""
|
||||
return validate_display_metadata(conn)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
- 目录:仅一层 `html/*.html`(非递归)
|
||||
- 重新生成 Action id 必须为 `render-html`(宿主硬编码)
|
||||
- 页面布局底线见 `references/ACTIONS.md`「宿主预览布局」;模板壳 `assets/html/example-report.html`
|
||||
- 成功后建议 `steps: refreshCatalog` 以重扫侧栏
|
||||
- 本模块只提供路径/模板/原子写;业务 KPI/表格由各技能自行填充
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。"""
|
||||
|
||||
SKILL_SLUG = "your-skill-slug"
|
||||
SKILL_VERSION = "1.0.81"
|
||||
SKILL_VERSION = "1.0.102"
|
||||
LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug"
|
||||
PLATFORM_KIT_MIN_VERSION = "1.2.11"
|
||||
|
||||
66
tests/samples/test_table_deprecation_migration.py.sample
Normal file
66
tests/samples/test_table_deprecation_migration.py.sample
Normal file
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""旧库废弃表迁移样例(复制到 tests/ 并按技能改写)。
|
||||
|
||||
验收:给定旧库含废弃物理表 + 元数据 + 历史行 → init_db() 后表与元数据均清除。
|
||||
见 references/SCHEMA.md「表增删改与退役」。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from _support import IsolatedDataRoot
|
||||
|
||||
|
||||
class TestTableDeprecationMigration(unittest.TestCase):
|
||||
def test_init_db_drops_legacy_task_logs(self) -> None:
|
||||
with IsolatedDataRoot():
|
||||
from db.connection import get_conn, init_db
|
||||
from db.display_metadata import iter_user_tables
|
||||
|
||||
# 1) 模拟升级前的旧库
|
||||
init_db()
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE task_logs (
|
||||
id INTEGER PRIMARY KEY,
|
||||
task_type TEXT,
|
||||
status TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO _jiangchang_tables
|
||||
(table_name, display_name, visible, readonly)
|
||||
VALUES ('task_logs', '任务日志', 1, 0)
|
||||
"""
|
||||
)
|
||||
cur.execute("INSERT INTO task_logs (task_type, status) VALUES ('run', 'ok')")
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# 2) 新代码 init_db() 应幂等清除(在 connection.init_db 中调用 drop_deprecated_user_table)
|
||||
init_db()
|
||||
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='task_logs'"
|
||||
)
|
||||
self.assertIsNone(cur.fetchone())
|
||||
cur.execute(
|
||||
"SELECT COUNT(*) FROM _jiangchang_tables WHERE table_name='task_logs'"
|
||||
)
|
||||
self.assertEqual(int(cur.fetchone()[0]), 0)
|
||||
self.assertNotIn("task_logs", list(iter_user_tables(cur)))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -74,8 +74,10 @@ def _validate_bind_tables(action_id: str, bind: object) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if not isinstance(bind, dict):
|
||||
return [f"{action_id}: bind must be object"]
|
||||
if set(bind.keys()) - {"tables"}:
|
||||
errors.append(f"{action_id}: bind only allows tables, got {sorted(bind.keys())}")
|
||||
allowed = {"tables", "inputMapping", "columnMapping", "labelColumn"}
|
||||
unknown = set(bind.keys()) - allowed
|
||||
if unknown:
|
||||
errors.append(f"{action_id}: bind only allows {sorted(allowed)}, got {sorted(bind.keys())}")
|
||||
tables = bind.get("tables")
|
||||
if not isinstance(tables, list) or len(tables) < 1:
|
||||
errors.append(f"{action_id}: bind.tables must be non-empty array")
|
||||
@@ -90,6 +92,12 @@ def _validate_bind_tables(action_id: str, bind: object) -> list[str]:
|
||||
if table in seen:
|
||||
errors.append(f"{action_id}: bind.tables duplicate {table!r}")
|
||||
seen.add(table)
|
||||
label_column = bind.get("labelColumn")
|
||||
if label_column is not None:
|
||||
if not isinstance(label_column, str) or not label_column.strip():
|
||||
errors.append(f"{action_id}: bind.labelColumn must be non-empty string")
|
||||
elif not _TABLE_NAME_PATTERN.fullmatch(label_column):
|
||||
errors.append(f"{action_id}: bind.labelColumn {label_column!r} must be snake_case")
|
||||
return errors
|
||||
|
||||
|
||||
@@ -188,7 +196,7 @@ class TestActionsManifest(unittest.TestCase):
|
||||
if replaces is not None:
|
||||
self.assertIn(
|
||||
replaces,
|
||||
{"create", "update", "delete"},
|
||||
{"create", "update", "delete", "import"},
|
||||
msg=f"{action_id} invalid replaces {replaces!r}",
|
||||
)
|
||||
bind = action.get("bind") or {}
|
||||
@@ -236,7 +244,7 @@ class TestActionsManifest(unittest.TestCase):
|
||||
self.assertIn("interactionByPlacement", props)
|
||||
by_placement = props["interactionByPlacement"]
|
||||
self.assertIs(by_placement.get("additionalProperties"), False)
|
||||
for key in ("toolbar", "row", "batch", "cron", "agent", "skill-detail", "create", "update", "delete"):
|
||||
for key in ("toolbar", "row", "batch", "cron", "agent", "skill-detail", "create", "update", "delete", "import"):
|
||||
self.assertIn(key, by_placement.get("properties") or {})
|
||||
self.assertEqual(
|
||||
set((by_placement["properties"][key].get("enum") or [])),
|
||||
@@ -246,9 +254,57 @@ class TestActionsManifest(unittest.TestCase):
|
||||
self.assertEqual(props["replaces"].get("$ref"), "#/$defs/mutationKind")
|
||||
self.assertEqual(
|
||||
set(schema["$defs"]["mutationKind"].get("enum") or []),
|
||||
{"create", "update", "delete"},
|
||||
{"create", "update", "delete", "import"},
|
||||
)
|
||||
|
||||
def test_schema_defines_hover_hint(self) -> None:
|
||||
schema = _load_actions_schema()
|
||||
action_def = schema["$defs"]["action"]
|
||||
props = action_def["properties"]
|
||||
self.assertIn("hoverHint", props)
|
||||
self.assertNotIn("hoverHint", action_def.get("required") or [])
|
||||
self.assertEqual(props["hoverHint"].get("type"), "string")
|
||||
|
||||
def test_schema_defines_visible_when_on_input_property(self) -> None:
|
||||
schema = _load_actions_schema()
|
||||
prop_def = schema["$defs"]["inputProperty"]
|
||||
props = prop_def["properties"]
|
||||
self.assertIn("visibleWhen", props)
|
||||
self.assertNotIn("visibleWhen", prop_def.get("required") or [])
|
||||
# Must live on input property, not action top-level (≠ Policy L3).
|
||||
action_props = schema["$defs"]["action"]["properties"]
|
||||
self.assertNotIn("visibleWhen", action_props)
|
||||
|
||||
def test_default_manifest_omits_visible_when(self) -> None:
|
||||
for action in _load_actions_manifest()["actions"]:
|
||||
properties = ((action.get("inputSchema") or {}).get("properties")) or {}
|
||||
for name, prop in properties.items():
|
||||
self.assertNotIn(
|
||||
"visibleWhen",
|
||||
prop if isinstance(prop, dict) else {},
|
||||
msg=f"{action['id']}.{name} default template should omit visibleWhen",
|
||||
)
|
||||
|
||||
def test_visible_when_example_exists_and_uses_field_rules(self) -> None:
|
||||
path = os.path.join(
|
||||
get_skill_root(), "assets", "examples", "visible-when-action.example.json"
|
||||
)
|
||||
self.assertTrue(os.path.isfile(path), msg="visible-when example must exist")
|
||||
with open(path, encoding="utf-8") as f:
|
||||
example = json.load(f)
|
||||
props = example["actions"][0]["inputSchema"]["properties"]
|
||||
self.assertIn("visibleWhen", props["keyword"])
|
||||
self.assertIn("visibleWhen", props["authorUrl"])
|
||||
self.assertNotIn("visibleWhen", props["collectMode"])
|
||||
|
||||
def test_default_manifest_omits_hover_hint(self) -> None:
|
||||
for action in _load_actions_manifest()["actions"]:
|
||||
self.assertNotIn(
|
||||
"hoverHint",
|
||||
action,
|
||||
msg=f"{action['id']} default template action should omit hoverHint",
|
||||
)
|
||||
|
||||
def test_schema_defines_host_effects_and_steps(self) -> None:
|
||||
schema = _load_actions_schema()
|
||||
action_def = schema["$defs"]["action"]
|
||||
@@ -384,6 +440,36 @@ class TestActionsManifest(unittest.TestCase):
|
||||
with self.assertRaises(ValidationError):
|
||||
jsonschema.validate(instance=manifest, schema=schema)
|
||||
|
||||
def test_schema_allows_bind_label_column(self) -> None:
|
||||
try:
|
||||
import jsonschema
|
||||
except ImportError:
|
||||
self.fail("jsonschema is required")
|
||||
schema = _load_actions_schema()
|
||||
bind_props = schema["$defs"]["bind"]["properties"]
|
||||
self.assertIn("labelColumn", bind_props)
|
||||
manifest = {
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actions": [
|
||||
{
|
||||
"id": "probe-label-column",
|
||||
"label": "处理勾选",
|
||||
"description": "验证 bind.labelColumn 可通过 Schema",
|
||||
"placements": ["toolbar", "batch"],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "confirm",
|
||||
"bind": {
|
||||
"tables": ["demo_items"],
|
||||
"inputMapping": {"ids": "$selection.ids"},
|
||||
"labelColumn": "display_name",
|
||||
},
|
||||
"entrypoint": {"type": "cli", "command": "health", "args": []},
|
||||
}
|
||||
],
|
||||
}
|
||||
jsonschema.validate(instance=manifest, schema=schema)
|
||||
|
||||
def test_no_execution_profile_placement_coupling_in_tests(self) -> None:
|
||||
"""守护:本文件不得引入入口位置与 sync/async 的错误硬耦合断言。"""
|
||||
path = os.path.join(get_skill_root(), "tests", "test_actions_manifest.py")
|
||||
@@ -537,11 +623,16 @@ class TestActionsManifest(unittest.TestCase):
|
||||
"POLICY.md",
|
||||
"hostEffects",
|
||||
"steps",
|
||||
"hoverHint",
|
||||
"visibleWhen",
|
||||
"字段条件显隐",
|
||||
"reloadTable",
|
||||
"afterSuccess",
|
||||
"afterFailure",
|
||||
"cellPresentation",
|
||||
"CELL_PRESENTATION.md",
|
||||
"labelColumn",
|
||||
"已选择",
|
||||
):
|
||||
self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}")
|
||||
self.assertNotIn(
|
||||
|
||||
@@ -57,6 +57,7 @@ POLICY_IDS = (
|
||||
"POLICY-SKILL-ACTION-002",
|
||||
"POLICY-SKILL-ACTION-003",
|
||||
"POLICY-SKILL-ACTION-004",
|
||||
"POLICY-TUTORIAL-001",
|
||||
"POLICY-ARCH-CORE-001",
|
||||
"POLICY-DATA-SYNC-001",
|
||||
)
|
||||
|
||||
@@ -225,6 +225,37 @@ class TestDisplayMetadata(unittest.TestCase):
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_validate_rejects_sqlite_foreign_keys_on_business_tables(self) -> None:
|
||||
from db.display_metadata import (
|
||||
_JIANGCHANG_COLUMNS_DDL,
|
||||
_JIANGCHANG_TABLES_DDL,
|
||||
)
|
||||
from db.display_metadata_validator import validate_display_metadata
|
||||
|
||||
conn = sqlite3.connect(":memory:")
|
||||
try:
|
||||
conn.execute(_JIANGCHANG_TABLES_DDL)
|
||||
conn.execute(_JIANGCHANG_COLUMNS_DDL)
|
||||
conn.execute("CREATE TABLE parent_rows (id INTEGER PRIMARY KEY, title TEXT)")
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE child_rows (
|
||||
id INTEGER PRIMARY KEY,
|
||||
parent_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (parent_id) REFERENCES parent_rows(id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
report = validate_display_metadata(conn)
|
||||
self.assertFalse(report.ok)
|
||||
self.assertTrue(
|
||||
any("FOREIGN KEY" in err or "REFERENCES" in err for err in report.errors),
|
||||
msg=f"expected FK error, got: {report.errors}",
|
||||
)
|
||||
self.assertTrue(any("child_rows" in err for err in report.errors))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_validate_missing_both_metadata_tables(self) -> None:
|
||||
conn = sqlite3.connect(":memory:")
|
||||
try:
|
||||
@@ -522,6 +553,41 @@ metadata:
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_enum_column_without_searchable_warns(self) -> None:
|
||||
conn = sqlite3.connect(":memory:")
|
||||
try:
|
||||
conn.executescript(
|
||||
"""
|
||||
CREATE TABLE filter_demo (id INTEGER PRIMARY KEY, enabled INTEGER NOT NULL DEFAULT 1);
|
||||
"""
|
||||
)
|
||||
from db.display_metadata import (
|
||||
init_display_metadata_tables,
|
||||
upsert_column_metadata,
|
||||
upsert_table_metadata,
|
||||
)
|
||||
from db.display_metadata_validator import validate_filter_search_metadata
|
||||
|
||||
cur = conn.cursor()
|
||||
init_display_metadata_tables(cur)
|
||||
upsert_table_metadata(cur, table_name="filter_demo", display_name="筛选演示", readonly=0)
|
||||
upsert_column_metadata(
|
||||
cur,
|
||||
table_name="filter_demo",
|
||||
column_name="enabled",
|
||||
display_name="是否启用",
|
||||
visible=1,
|
||||
searchable=0,
|
||||
editable=1,
|
||||
display_type="boolean",
|
||||
options_json='[{"value":0,"label":"停用"},{"value":1,"label":"启用"}]',
|
||||
)
|
||||
conn.commit()
|
||||
_, warnings = validate_filter_search_metadata(cur, "filter_demo")
|
||||
self.assertTrue(any("searchable=1" in item for item in warnings))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_schema_md_documents_readonly_and_seed_helpers(self) -> None:
|
||||
path = os.path.join(get_skill_root(), "references", "SCHEMA.md")
|
||||
with open(path, encoding="utf-8") as f:
|
||||
@@ -578,6 +644,167 @@ metadata:
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_grid_help_upsert_and_migrate_from_legacy_schema(self) -> None:
|
||||
conn = sqlite3.connect(":memory:")
|
||||
try:
|
||||
from db.display_metadata import (
|
||||
init_display_metadata_tables,
|
||||
upsert_column_metadata,
|
||||
upsert_table_metadata,
|
||||
)
|
||||
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE _jiangchang_tables (
|
||||
table_name TEXT PRIMARY KEY,
|
||||
display_name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
sort_order INTEGER,
|
||||
visible INTEGER NOT NULL DEFAULT 1,
|
||||
readonly INTEGER NOT NULL DEFAULT 0,
|
||||
default_sort_json TEXT,
|
||||
default_visible_columns_json TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE _jiangchang_columns (
|
||||
table_name TEXT NOT NULL,
|
||||
column_name TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
display_order INTEGER,
|
||||
visible INTEGER NOT NULL DEFAULT 1,
|
||||
searchable INTEGER NOT NULL DEFAULT 1,
|
||||
editable INTEGER NOT NULL DEFAULT 1,
|
||||
display_type TEXT,
|
||||
options_json TEXT,
|
||||
PRIMARY KEY (table_name, column_name)
|
||||
)
|
||||
"""
|
||||
)
|
||||
init_display_metadata_tables(cur)
|
||||
cur.execute("PRAGMA table_info('_jiangchang_tables')")
|
||||
self.assertIn("grid_help", {str(row[1]) for row in cur.fetchall()})
|
||||
cur.execute("PRAGMA table_info('_jiangchang_columns')")
|
||||
self.assertIn("grid_help", {str(row[1]) for row in cur.fetchall()})
|
||||
|
||||
upsert_table_metadata(
|
||||
cur,
|
||||
table_name="demo_items",
|
||||
display_name="示例记录",
|
||||
description="表单备注,不应出现在表格横幅",
|
||||
grid_help="表格横幅说明",
|
||||
)
|
||||
upsert_column_metadata(
|
||||
cur,
|
||||
table_name="demo_items",
|
||||
column_name="title",
|
||||
display_name="标题",
|
||||
description="填标题",
|
||||
grid_help="列头问号说明",
|
||||
)
|
||||
upsert_table_metadata(
|
||||
cur,
|
||||
table_name="empty_help",
|
||||
display_name="无表格说明",
|
||||
)
|
||||
conn.commit()
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT description, grid_help
|
||||
FROM _jiangchang_tables
|
||||
WHERE table_name = 'demo_items'
|
||||
"""
|
||||
)
|
||||
self.assertEqual(cur.fetchone(), ("表单备注,不应出现在表格横幅", "表格横幅说明"))
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT description, grid_help
|
||||
FROM _jiangchang_columns
|
||||
WHERE table_name = 'demo_items' AND column_name = 'title'
|
||||
"""
|
||||
)
|
||||
self.assertEqual(cur.fetchone(), ("填标题", "列头问号说明"))
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT grid_help FROM _jiangchang_tables
|
||||
WHERE table_name = 'empty_help'
|
||||
"""
|
||||
)
|
||||
self.assertTrue(not cur.fetchone()[0])
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_drop_deprecated_user_table_removes_physical_and_metadata(self) -> None:
|
||||
conn = sqlite3.connect(":memory:")
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE _jiangchang_tables (
|
||||
table_name TEXT PRIMARY KEY,
|
||||
display_name TEXT,
|
||||
description TEXT,
|
||||
sort_order INTEGER,
|
||||
visible INTEGER,
|
||||
readonly INTEGER
|
||||
)
|
||||
"""
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE _jiangchang_columns (
|
||||
table_name TEXT,
|
||||
column_name TEXT,
|
||||
display_name TEXT,
|
||||
description TEXT,
|
||||
display_order INTEGER,
|
||||
visible INTEGER,
|
||||
searchable INTEGER,
|
||||
editable INTEGER,
|
||||
display_type TEXT,
|
||||
options_json TEXT,
|
||||
PRIMARY KEY (table_name, column_name)
|
||||
)
|
||||
"""
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE task_logs (
|
||||
id INTEGER PRIMARY KEY,
|
||||
status TEXT,
|
||||
created_at INTEGER,
|
||||
updated_at INTEGER
|
||||
)
|
||||
"""
|
||||
)
|
||||
cur.execute(
|
||||
"INSERT INTO _jiangchang_tables VALUES ('task_logs', '任务日志', NULL, 1, 1, 0)"
|
||||
)
|
||||
cur.execute(
|
||||
"INSERT INTO _jiangchang_columns VALUES "
|
||||
"('task_logs', 'id', '编号', NULL, 1, 1, 1, 0, 'text', NULL)"
|
||||
)
|
||||
cur.execute("INSERT INTO task_logs (status) VALUES ('success')")
|
||||
|
||||
from db.display_metadata import drop_deprecated_user_table, iter_user_tables
|
||||
|
||||
drop_deprecated_user_table(cur, "task_logs")
|
||||
cur.execute(
|
||||
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='task_logs'"
|
||||
)
|
||||
self.assertIsNone(cur.fetchone())
|
||||
cur.execute(
|
||||
"SELECT COUNT(*) FROM _jiangchang_tables WHERE table_name='task_logs'"
|
||||
)
|
||||
self.assertEqual(int(cur.fetchone()[0]), 0)
|
||||
self.assertNotIn("task_logs", list(iter_user_tables(cur)))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -181,6 +181,17 @@ class TestDocsStandards(unittest.TestCase):
|
||||
self.assertIn("CHANGELOG.md", text)
|
||||
self.assertIn("demo_video_url", text)
|
||||
self.assertIn("tutorial_md", text)
|
||||
self.assertIn("TUTORIAL_SURFACES.md", text)
|
||||
|
||||
def test_tutorial_surfaces_contract_doc_and_checklist(self) -> None:
|
||||
surfaces = self._read("development/TUTORIAL_SURFACES.md")
|
||||
self.assertIn("说明", surfaces)
|
||||
self.assertIn("教程", surfaces)
|
||||
self.assertIn("skill-detail", surfaces)
|
||||
checklist = self._read("development/CHECKLIST.md")
|
||||
self.assertIn("A2-7", checklist)
|
||||
self.assertIn("A2-8", checklist)
|
||||
self.assertIn("TUTORIAL_SURFACES.md", checklist)
|
||||
|
||||
def test_naming_md_exists_and_covers_standards(self) -> None:
|
||||
text = self._read("development/NAMING.md")
|
||||
@@ -301,6 +312,7 @@ class TestDocsStandards(unittest.TestCase):
|
||||
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, "references", "WORK_ITEM_STATUS.md")))
|
||||
self.assertTrue(
|
||||
os.path.isfile(os.path.join(root, "assets", "schemas", "skill-actions.schema.json"))
|
||||
)
|
||||
@@ -341,11 +353,27 @@ class TestDocsStandards(unittest.TestCase):
|
||||
"表单与展示:稳定字段类型(宿主支持,须与 Action 对称)",
|
||||
"datetime_unix_seconds",
|
||||
"display_type: \"number\"",
|
||||
"extra_column",
|
||||
"{value} · {label}",
|
||||
"DEFAULT",
|
||||
"description",
|
||||
"文件 / 图片上传",
|
||||
"default_visible_columns_json",
|
||||
"默认展示列",
|
||||
"grid_help",
|
||||
"表格说明 `grid_help`",
|
||||
"表与表之间:禁止 SQLite 外键",
|
||||
"禁止 SQLite 外键",
|
||||
"跨表引用 lookup",
|
||||
"\"multiple\": true",
|
||||
"Lookup 多选",
|
||||
"数据管理:列筛选与全局搜索",
|
||||
"列筛选与全局搜索",
|
||||
"searchable: 1",
|
||||
"筛选栏控件",
|
||||
"表增删改与退役",
|
||||
"drop_deprecated_user_table",
|
||||
"孤儿物理表",
|
||||
):
|
||||
self.assertIn(marker, text, msg=f"SCHEMA.md missing {marker!r}")
|
||||
|
||||
@@ -354,13 +382,44 @@ class TestDocsStandards(unittest.TestCase):
|
||||
for marker in (
|
||||
"inputSchema 与参数表单",
|
||||
"表单字段类型对照(须与 CRUD 对称)",
|
||||
"跨表引用:Lookup",
|
||||
"display_type: \"lookup\"",
|
||||
"extra_column",
|
||||
"multiple",
|
||||
"数据管理新增/编辑",
|
||||
"_jiangchang_columns.description",
|
||||
"datetime_unix_seconds",
|
||||
"SCHEMA.md",
|
||||
"字段条件显隐",
|
||||
"visibleWhen",
|
||||
"grid_help",
|
||||
"列筛选",
|
||||
):
|
||||
self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}")
|
||||
|
||||
def test_actions_md_disambiguates_visible_when_from_policy(self) -> None:
|
||||
actions = self._read("references/ACTIONS.md")
|
||||
policy = self._read("references/POLICY.md")
|
||||
self.assertIn("字段条件显隐", actions)
|
||||
self.assertIn("inputSchema.properties", actions)
|
||||
self.assertTrue(
|
||||
"POLICY.md" in actions and "L3" in actions,
|
||||
msg="ACTIONS.md must point readers away from Policy L3 visibleWhen",
|
||||
)
|
||||
self.assertTrue(
|
||||
"inputSchema" in policy and "ACTIONS.md" in policy,
|
||||
msg="POLICY.md must disambiguate L3 visibleWhen from Action form visibleWhen",
|
||||
)
|
||||
|
||||
def test_visible_when_example_is_listed(self) -> None:
|
||||
text = self._read("assets/examples/README.md")
|
||||
self.assertIn("visible-when-action.example.json", text)
|
||||
checklist = self._read("development/CHECKLIST.md")
|
||||
self.assertIn("A4-7", checklist)
|
||||
self.assertIn("visibleWhen", checklist)
|
||||
runtime = self._read("development/SKILL_ACTION_RUNTIME.md")
|
||||
self.assertIn("visibleWhen", runtime)
|
||||
|
||||
def test_guarded_docs_do_not_reference_amazon_implementation(self) -> None:
|
||||
forbidden = (
|
||||
"download-settlement-report-amazon",
|
||||
@@ -457,6 +516,7 @@ class TestDocsStandards(unittest.TestCase):
|
||||
text = self._read("references/ACTIONS.md")
|
||||
self.assertIn("最小组合", text)
|
||||
self.assertIn("confirmation.message", text)
|
||||
self.assertIn("hoverHint", text)
|
||||
self.assertIn("用户意图路由表", text)
|
||||
|
||||
def test_actions_md_states_profile_not_entry_for_task_center(self) -> None:
|
||||
|
||||
@@ -61,6 +61,15 @@ class TestHtmlPageService(unittest.TestCase):
|
||||
with self.assertRaises(ValueError):
|
||||
write_html_page("note.txt", "<html></html>")
|
||||
|
||||
def test_example_report_shell_embedded_preview_layout(self) -> None:
|
||||
"""守护 example-report.html:数据管理 iframe 预览布局基线不退化。"""
|
||||
template = load_html_template("assets/html/example-report.html")
|
||||
self.assertIn("overflow-x: hidden", template)
|
||||
self.assertIn("min-width: 0", template)
|
||||
self.assertIn("table-layout: fixed", template)
|
||||
self.assertIn("overflow-wrap: anywhere", template)
|
||||
self.assertNotIn("max-width:", template)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
198
tests/test_tutorial_surfaces.py
Normal file
198
tests/test_tutorial_surfaces.py
Normal file
@@ -0,0 +1,198 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""用户市场文档契约:README≠教程、TUTORIAL 三入口结构、与 actions.json 对齐。
|
||||
|
||||
契约全文:development/TUTORIAL_SURFACES.md
|
||||
- 模板占位 slug(your-skill-slug):只检查骨架与契约文档存在
|
||||
- 业务技能:按 actions.json placements 硬校验教程章节与 README 反模式
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import unittest
|
||||
|
||||
from _support import get_skill_root
|
||||
from util.constants import SKILL_SLUG
|
||||
|
||||
TEMPLATE_PLACEHOLDER_SLUG = "your-skill-slug"
|
||||
|
||||
_SECTION_DATA_MGMT = re.compile(r"(?m)^##\s*.*数据管理")
|
||||
_SECTION_DIALOG = re.compile(r"(?m)^##\s*.*(对话任务|对话)")
|
||||
_SECTION_CRON = re.compile(r"(?m)^##\s*.*定时任务")
|
||||
_SECTION_COMPARE = re.compile(r"(?m)^##\s*.*(对照表|主能力)")
|
||||
_SECTION_STUCK = re.compile(r"(?m)^##\s*.*(常见卡住|卡住怎么办)")
|
||||
_SECTION_WHAT = re.compile(r"(?m)^##\s*.*(本技能做什么|做什么)")
|
||||
_NO_CRON_HINT = re.compile(r"不需要定时|未提供定时|不提供定时|不适用.*定时|定时.*不适用")
|
||||
|
||||
# README 反模式:分步操作 / Agent 话术专章(说明 Tab 禁止)
|
||||
_README_HOWTO_AGENT = re.compile(r"(?m)^##\s*.*告诉\s*Agent")
|
||||
_README_ENTRY_CHAPTER = re.compile(r"(?m)^##\s*入口[一二三]")
|
||||
_README_NUMBERED_DM_PATH = re.compile(
|
||||
r"(?m)^\d+[\.、]\s*.*(数据管理|选品任务|表顶).{0,40}(点击|点\s*「)"
|
||||
)
|
||||
|
||||
|
||||
def _read(rel: str) -> str:
|
||||
path = os.path.join(get_skill_root(), rel)
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def _load_actions() -> list[dict]:
|
||||
path = os.path.join(get_skill_root(), "assets", "actions.json")
|
||||
if not os.path.isfile(path):
|
||||
return []
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return list(data.get("actions") or [])
|
||||
|
||||
|
||||
def _placement_sets(actions: list[dict]) -> tuple[bool, bool, bool]:
|
||||
has_dm = False
|
||||
has_agent = False
|
||||
has_cron = False
|
||||
dm_tokens = frozenset({"toolbar", "row", "batch"})
|
||||
for action in actions:
|
||||
placements = set(action.get("placements") or [])
|
||||
if placements & dm_tokens:
|
||||
has_dm = True
|
||||
if "agent" in placements and str(action.get("id") or "") not in (
|
||||
"health",
|
||||
"version",
|
||||
"config-path",
|
||||
):
|
||||
has_agent = True
|
||||
if "cron" in placements:
|
||||
has_cron = True
|
||||
return has_dm, has_agent, has_cron
|
||||
|
||||
|
||||
class TestTutorialSurfacesContract(unittest.TestCase):
|
||||
def test_tutorial_surfaces_doc_exists(self) -> None:
|
||||
path = os.path.join(get_skill_root(), "development", "TUTORIAL_SURFACES.md")
|
||||
self.assertTrue(
|
||||
os.path.isfile(path),
|
||||
msg="development/TUTORIAL_SURFACES.md must exist (user market doc contract)",
|
||||
)
|
||||
text = _read("development/TUTORIAL_SURFACES.md")
|
||||
for marker in (
|
||||
"README.md",
|
||||
"TUTORIAL.md",
|
||||
"数据管理",
|
||||
"对话",
|
||||
"定时",
|
||||
"skill-detail",
|
||||
"对照表",
|
||||
):
|
||||
self.assertIn(marker, text, msg=f"TUTORIAL_SURFACES.md missing {marker!r}")
|
||||
|
||||
def test_development_md_points_to_tutorial_surfaces(self) -> None:
|
||||
text = _read("development/DEVELOPMENT.md")
|
||||
self.assertIn(
|
||||
"TUTORIAL_SURFACES.md",
|
||||
text,
|
||||
msg="DEVELOPMENT.md must reference TUTORIAL_SURFACES.md for market docs",
|
||||
)
|
||||
|
||||
def test_template_or_business_tutorial_has_required_skeleton(self) -> None:
|
||||
tutorial = _read("TUTORIAL.md")
|
||||
self.assertRegex(
|
||||
tutorial,
|
||||
_SECTION_WHAT,
|
||||
msg="TUTORIAL.md must have a '本技能做什么' (or equivalent) section",
|
||||
)
|
||||
self.assertRegex(
|
||||
tutorial,
|
||||
_SECTION_DATA_MGMT,
|
||||
msg="TUTORIAL.md must have a 数据管理 section (or 不适用 placeholder)",
|
||||
)
|
||||
self.assertRegex(
|
||||
tutorial,
|
||||
_SECTION_DIALOG,
|
||||
msg="TUTORIAL.md must have a 对话 section (or 不适用 placeholder)",
|
||||
)
|
||||
self.assertRegex(
|
||||
tutorial,
|
||||
_SECTION_CRON,
|
||||
msg="TUTORIAL.md must have a 定时任务 section (teach or N/A)",
|
||||
)
|
||||
self.assertRegex(
|
||||
tutorial,
|
||||
_SECTION_COMPARE,
|
||||
msg="TUTORIAL.md must have a 主能力对照表 section",
|
||||
)
|
||||
self.assertRegex(
|
||||
tutorial,
|
||||
_SECTION_STUCK,
|
||||
msg="TUTORIAL.md must have a 常见卡住 section",
|
||||
)
|
||||
# 禁止旧「对话单线」作为唯一主结构标题
|
||||
self.assertNotRegex(
|
||||
tutorial,
|
||||
r"(?m)^##\s*第一步:打开技能并说清目标\s*$",
|
||||
msg="TUTORIAL.md must not use legacy dialogue-only first step as sole structure",
|
||||
)
|
||||
|
||||
def test_readme_has_no_legacy_agent_howto_chapter(self) -> None:
|
||||
readme = _read("README.md")
|
||||
self.assertIsNone(
|
||||
_README_HOWTO_AGENT.search(readme),
|
||||
msg="README.md must not have '告诉 Agent' howto chapter (belongs in TUTORIAL)",
|
||||
)
|
||||
self.assertIsNone(
|
||||
_README_ENTRY_CHAPTER.search(readme),
|
||||
msg="README.md must not have 入口一/二/三 chapters (belongs in TUTORIAL)",
|
||||
)
|
||||
|
||||
def test_business_skill_tutorial_aligns_with_actions(self) -> None:
|
||||
if SKILL_SLUG == TEMPLATE_PLACEHOLDER_SLUG:
|
||||
self.skipTest("template placeholder slug: skip actions alignment hard checks")
|
||||
|
||||
actions = _load_actions()
|
||||
has_dm, has_agent, has_cron = _placement_sets(actions)
|
||||
tutorial = _read("TUTORIAL.md")
|
||||
readme = _read("README.md")
|
||||
|
||||
if has_dm:
|
||||
self.assertRegex(
|
||||
tutorial,
|
||||
_SECTION_DATA_MGMT,
|
||||
msg="business skill with data-management placements must document 数据管理 in TUTORIAL",
|
||||
)
|
||||
# 准备类若挂了 toolbar,教程不应把 skill-detail 当唯一主路径
|
||||
self.assertNotRegex(
|
||||
tutorial,
|
||||
r"(?m)^\d+[\.、]\s*.*技能详情.{0,30}点击",
|
||||
msg="TUTORIAL must not use 技能详情点击 as primary numbered path when DM exists",
|
||||
)
|
||||
|
||||
if has_agent:
|
||||
self.assertRegex(
|
||||
tutorial,
|
||||
_SECTION_DIALOG,
|
||||
msg="business skill with agent placements must document 对话 in TUTORIAL",
|
||||
)
|
||||
|
||||
if has_cron:
|
||||
self.assertRegex(
|
||||
tutorial,
|
||||
_SECTION_CRON,
|
||||
msg="with cron placements, TUTORIAL must have 定时任务 section",
|
||||
)
|
||||
else:
|
||||
self.assertRegex(
|
||||
tutorial,
|
||||
_NO_CRON_HINT,
|
||||
msg="without cron placements, TUTORIAL must declare 不需要/未提供定时",
|
||||
)
|
||||
|
||||
# README 不应出现「数据管理 → … → 点击」式编号步骤
|
||||
self.assertIsNone(
|
||||
_README_NUMBERED_DM_PATH.search(readme),
|
||||
msg="README.md must not contain numbered data-management click steps",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user