Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| db2ed089a3 | |||
| c43c26ad15 | |||
| fe4209b37f | |||
| 3bebd6468f | |||
| e7fa3c79b5 | |||
| 0ce0c3e852 | |||
| 201d4d1367 | |||
| 295b2356ac | |||
| 7257d09813 | |||
| 9d33c5ea2c | |||
| b5d120469b | |||
| d35d04f779 |
64
CHANGELOG.md
64
CHANGELOG.md
@@ -9,6 +9,70 @@
|
||||
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
|
||||
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
|
||||
|
||||
## 1.0.108
|
||||
|
||||
- 沉淀数据管理「用户可见文案」契约:弹窗 description / 确认黄条 / 参数说明 / 任务失败 message 须全中文产品用语
|
||||
- 明确禁止把字段名、CLI 子命令、async/pending、Agent 禁令写进用户可见文案;Agent 路由仍放 SKILL.md
|
||||
- 修正 `selection-mode-optional` 示例为口语文案;更新 ACTIONS / RUNTIME / CHECKLIST A4-10
|
||||
|
||||
## 1.0.107
|
||||
|
||||
- 沉淀数据管理 `bind.selectionMode` 契约(`required` / `none` / `optional`):同一按钮可「有勾选走 ids、无勾选走 pickCount」
|
||||
- 明确省略时的宿主兼容推断、空选数量字段名约定,以及勿再拆同义双按钮
|
||||
- 更新 ACTIONS.md / Schema / SKILL_ACTION_RUNTIME / CHECKLIST A4-9;新增 `selection-mode-optional.action.example.json`
|
||||
|
||||
## 1.0.106
|
||||
|
||||
- 文档契约:补充可选 `metadata.openclaw.brands`(OEM 市场编码 → `skill_market_relations`)
|
||||
- 明确未写则仅挂 `jiangchang`;与 `developer_ids` 分工;勿用 Gitea 组织名推断市场
|
||||
- 更新 `SKILL.md`、`DEVELOPMENT.md` §6、`development/README.md`、`REQUIREMENTS.md` 与发布检查清单
|
||||
|
||||
## 1.0.105
|
||||
|
||||
- 沉淀数据管理 form 与「已选择」摘要对齐契约:映射 `readOnly` 字段默认不占参数区(仍提交),与 confirm 信息架构一致
|
||||
- 新增可选 `revealInForm` 逃生阀;明确禁止用 `visibleWhen` 藏映射主键
|
||||
- 更新 ACTIONS.md / Schema / SKILL_ACTION_RUNTIME / CHECKLIST A4-8
|
||||
|
||||
## 1.0.104
|
||||
|
||||
- 新增发布契约 POLICY-RELEASE-001:`release.ps1` 打 tag 前强制校验 Next tag = `SKILL.md` version = `SKILL_VERSION`,避免市场一直显示「更新」
|
||||
- 日常测试增加 `SKILL.md` 与 `SKILL_VERSION` 对齐;更新 DEVELOPMENT / POLICY_MATRIX / CHECKLIST A9-4
|
||||
|
||||
## 1.0.103
|
||||
|
||||
- 用户市场文档:说明/教程正文禁止与宿主页头、Tab 重复的文首一级标题;主分节统一用 `##`
|
||||
- 更新 `TUTORIAL.md` / `README.md` 模板骨架、`TUTORIAL_SURFACES.md` §0.1、CHECKLIST A2-12 与契约测试
|
||||
|
||||
## 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、禁横向滚动、表格断行等)
|
||||
|
||||
10
DEMO.md
10
DEMO.md
@@ -15,9 +15,13 @@ demo_video_url: ""
|
||||
|
||||
## 建议观看顺序
|
||||
|
||||
1. 如何发起一次典型任务
|
||||
2. 执行过程中会看到什么提示 / 进度
|
||||
3. 成功后的结果长什么样
|
||||
建议与 `TUTORIAL.md` 三入口结构对齐(有则拍、无则跳过):
|
||||
|
||||
1. 数据管理:准备数据 → 主流程按钮 → 结果表/报告
|
||||
2. 对话任务(若有):一句自然语言触发同一能力
|
||||
3. 定时任务(若有):到点自动跑;若本技能无定时,演示中可说明「不需要定时」
|
||||
|
||||
成功后的结果长什么样(表、任务中心、HTML 等)
|
||||
|
||||
## 没有视频时
|
||||
|
||||
|
||||
63
README.md
63
README.md
@@ -1,14 +1,14 @@
|
||||
---
|
||||
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 +17,7 @@ description: "用用户能理解的方式说明这个技能能做什么、适合
|
||||
|
||||
## 这个技能能帮你做什么
|
||||
|
||||
用 3 到 6 条业务能力说明技能价值,例如:
|
||||
用 3 到 6 条业务能力说明技能价值(**不要**写成操作步骤),例如:
|
||||
|
||||
- 【自动整理/查询/生成/同步/核对/提交】某类业务数据
|
||||
- 帮你减少重复操作,降低人工录入或核对成本
|
||||
@@ -38,42 +38,33 @@ description: "用用户能理解的方式说明这个技能能做什么、适合
|
||||
|
||||
## 使用前需要准备什么
|
||||
|
||||
说明用户在使用前要准备的东西,例如:
|
||||
说明**前置条件**(装什么、要不要账号、要不要外站登录),**不要**写「去哪张表点哪个按钮」:
|
||||
|
||||
- **网页自动化类:** 首次运行可由技能自动登记平台账号(浏览器会话由系统隔离管理)。也可在「账号管理」里指定登录标识。登记账号 ≠ 已登录目标站
|
||||
- **账号在哪里看:** 已登记账号可在「数据管理 / 账号管理」中查看;每个账号使用独立浏览器会话,批量或多账号任务不会共用登录状态
|
||||
- **是否要登录目标站:** 按本技能说明(有的平台必须登录,有的公开页可不登录);与「是否有账号记录」不是一回事
|
||||
- **网页自动化类:** 可能需要在账号管理中登记平台账号;登记账号 ≠ 已登录目标站(细节见使用教程)
|
||||
- 相关业务权限(查询、导出、提交等)已具备
|
||||
- 需要处理的文件、时间范围、订单号、客户名称、项目名称等信息
|
||||
- 如果涉及验证码或人工确认,请保证能及时在弹出的浏览器窗口中操作
|
||||
- 需要处理的文件、时间范围、单号等信息(若适用)
|
||||
- 若涉及验证码或人工确认,使用时请能及时在弹出窗口中操作
|
||||
|
||||
---
|
||||
|
||||
## 你可以这样告诉 Agent
|
||||
## 怎么使用
|
||||
|
||||
用自然语言示例展示用户可以怎么发起任务,例如:
|
||||
本技能支持哪些宿主入口,用**一两句话**概括即可,例如:
|
||||
|
||||
> “帮我查询今天的【业务对象】,整理成表格。”
|
||||
>
|
||||
> “帮我核对这些【数据/单据】是否有异常,并告诉我问题在哪里。”
|
||||
>
|
||||
> “帮我把【时间范围】内的【业务数据】导出并生成处理报告。”
|
||||
>
|
||||
> “帮我根据这个文件里的内容,逐条完成【业务动作】。”
|
||||
- 支持 **数据管理**(点表操作)与 **对话任务**(自然语言);是否提供定时任务按本技能说明。
|
||||
|
||||
> 注意:示例应是自然语言,不要写命令行命令。
|
||||
**逐步操作、示例话术与排障,请查看技能市场「使用教程」Tab。**
|
||||
(完整写法见 `development/TUTORIAL_SURFACES.md`。)
|
||||
|
||||
---
|
||||
|
||||
## 执行后你会得到什么
|
||||
|
||||
说明用户会看到什么结果,例如:
|
||||
说明用户会看到什么**结果形态**(不要教如何点出来),例如:
|
||||
|
||||
- 任务是否成功完成
|
||||
- 处理了多少条数据
|
||||
- 哪些内容成功、哪些失败
|
||||
- 失败原因和建议处理方式
|
||||
- 生成的文件、表格、报告或记录
|
||||
- 处理了多少条数据;哪些成功、哪些失败
|
||||
- 生成的文件、表格、报告或业务表记录
|
||||
|
||||
---
|
||||
|
||||
@@ -90,18 +81,16 @@ description: "用用户能理解的方式说明这个技能能做什么、适合
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 为什么任务没有查到数据?
|
||||
> FAQ 写**概念与边界**;需要「点哪里」的排障写到「使用教程」。
|
||||
|
||||
> 可能是时间范围、关键词、账号权限或系统筛选条件不正确。请先确认输入信息是否准确。
|
||||
### 这个技能适合我吗?
|
||||
|
||||
### 为什么任务执行到一半需要我操作?
|
||||
> 若你的场景与上文「适合什么场景」一致,可以安装后按「使用教程」完成第一次操作。
|
||||
|
||||
> 可能遇到了登录、验证码、权限确认、二次审批或系统异常。请根据 Agent 的提示完成必要操作。
|
||||
### 我不会操作,从哪里开始?
|
||||
|
||||
### 结果文件在哪里查看?
|
||||
|
||||
> 请根据 Agent 返回的结果说明查看生成文件、任务记录或处理报告。
|
||||
> 请打开技能市场 **「使用教程」** Tab,按「入口一:数据管理」或「入口二:对话任务」任选一种跟做。
|
||||
|
||||
### 已经登记的账号在哪里查看?
|
||||
|
||||
> 请到匠厂的「数据管理 / 账号管理」查看账号记录和状态。若任务仍提示账号异常,请把账号编号、任务时间和界面提示原文提供给支持人员;不要发送密码、Cookie 或验证码。
|
||||
> 请到匠厂的「数据管理 / 账号管理」查看账号记录和状态。不要发送密码、Cookie 或验证码。
|
||||
|
||||
22
SKILL.md
22
SKILL.md
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: 技能开发模板(通用业务版)
|
||||
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
|
||||
version: 1.0.97
|
||||
version: 1.0.108
|
||||
author: 深圳匠厂科技有限公司
|
||||
metadata:
|
||||
openclaw:
|
||||
@@ -11,6 +11,9 @@ metadata:
|
||||
category: "通用"
|
||||
developer_ids:
|
||||
- 12345 # 占位,请换成你的匠厂用户 ID
|
||||
# brands: # 可选;未写则发布后仅挂匠厂市场(jiangchang)
|
||||
# - jiangchang # 匠厂主站
|
||||
# - huashang # 华商等 OEM:按目标市场填写,可多选
|
||||
allowed-tools:
|
||||
- bash
|
||||
---
|
||||
@@ -35,6 +38,8 @@ allowed-tools:
|
||||
- **数据管理**:既是 **UI 执行面**(点按钮走与对话相同的闭环),也是数据与过程可见;不是「只读库」。
|
||||
- **`placements`**:只决定 Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail)。其中 `row` 表示**行级入口**(宿主可呈现为操作列、行右键、行内「⋯」等;技能配什么就显示什么,无独立菜单 DSL)。数据管理技能按钮顺序见 `actions` **数组相对顺序**(详见 `references/ACTIONS.md`),无独立 sortOrder。
|
||||
- **`bind.tables`**:只决定数据管理出现在哪些表;含 `toolbar` 时**必须**显式非空绑定。
|
||||
- **`bind.selectionMode`(可选)**:表勾选如何驱动 Action(`required` / `none` / `optional`);省略由宿主推断。同一能力勿拆两个同义顶栏按钮,见 `references/ACTIONS.md`。
|
||||
- **用户可见文案**:数据管理弹窗 / 任务中心文案须全中文产品用语;勿把 `pickCount`、`async`、`pending`、CLI 子命令写进弹窗(见 `references/ACTIONS.md`「用户可见文案」)。
|
||||
- **`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`。
|
||||
@@ -109,10 +114,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 臆造按钮路径。
|
||||
|
||||
**文档读取边界:**
|
||||
|
||||
@@ -125,13 +131,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/PRODUCT_SURFACES.md` |
|
||||
| `development/` | 开发者 / AI 编程代理 | 需求、开发教程、测试、深度技术规范;**产品入口**见 `development/PRODUCT_SURFACES.md`;**用户文档契约**见 `development/TUTORIAL_SURFACES.md` |
|
||||
|
||||
## 目录约定
|
||||
|
||||
@@ -166,3 +172,9 @@ python {baseDir}/scripts/main.py init-db
|
||||
- 开发期技能常为不公开:不加本人 ID,开发者自己也无法在市场安装自测。取 ID 与写入步骤见 `development/DEVELOPMENT.md` §6。
|
||||
- 当 `access_scope = 0`(不公开)时,平台会把 `developer_ids` 中的用户自动补写到 `skill_user_access`。
|
||||
- `developer_ids` 建议写为正整数数组;第一个 ID 会作为主开发者同步到 `skills.developer_id`。
|
||||
- `metadata.openclaw.brands`(**可选**):OEM 技能市场编码列表,写入平台 `skill_market_relations.market_code`。兼容别名 `oem_clients` / `client_keys`。
|
||||
- **未写或为空**:发布时仅幂等补默认 `jiangchang`(匠厂主站),兼容老技能。
|
||||
- **显式列表**(如 `jiangchang`、`huashang`):发布时幂等补写对应市场(**只补不删**);客户端靠 Header `X-Market-Code` 过滤目录。
|
||||
- **不是** Gitea 组织名推断:仓库在 `client-huashang` 下 ≠ 自动出现在华商市场,必须以 `brands` 声明。
|
||||
- 与 `developer_ids` 分工:`brands` 管「进哪个 OEM 市场目录」;`developer_ids` 管「不公开时谁能看见/安装」。
|
||||
- 完整步骤与验收见 `development/DEVELOPMENT.md` §6「关于 brands」。
|
||||
|
||||
143
TUTORIAL.md
143
TUTORIAL.md
@@ -1,12 +1,33 @@
|
||||
> **模板说明(复制后删除本段)**
|
||||
> 本文对应技能市场 **「使用教程」** Tab(发布时同步为 `tutorial_md`)。
|
||||
> 面向普通用户写**可跟着做的步骤**,不要写 CLI、目录结构、release、开发规范。
|
||||
> 与根 `README.md`(技能说明)分工:README 讲「是什么 / 能做什么」;本文讲「第一次怎么用完」。
|
||||
> **复制后验收章节(标题可微调):**「开始前」「第一步…」「第二步…」「常见卡住怎么办」。
|
||||
> 与根 `README.md`(技能说明)分工:README 讲「是什么 / 能做什么 / 边界」;本文讲「从哪个入口怎么用完」。
|
||||
> **完整契约:** `development/TUTORIAL_SURFACES.md`(三入口结构、说明≠教程、skill-detail 不可作主路径)。
|
||||
> **复制后验收章节(标题可微调,语义不可缺):**「本技能做什么」「开始前」「入口一:数据管理」「入口二:对话任务」「入口三:定时任务」「主能力对照表」「常见卡住怎么办」。
|
||||
> 若本技能 §0.1 未挂数据管理 / 对话 / 定时,对应入口节改为「不适用」并写一句理由(与 REQUIREMENTS 一致)。
|
||||
> **标题层级:** 宿主页头已有技能名、Tab 已有「使用教程」——**禁止**文首再写 `# 技能名 · 使用教程`;正文主分节用 `##`,步骤用 `###`。
|
||||
|
||||
# 【技能名称】使用教程
|
||||
本技能有两种**平行**的使用方式(按本技能入口定位填写;未挂的入口删掉或写不适用):
|
||||
|
||||
用最短路径带用户完成一次成功使用。步骤要具体,可用「打开匠厂 → …」这类操作语言。
|
||||
- **数据管理**:在左侧表上点工具栏 / 行内按钮
|
||||
- **对话任务**:用自然语言让 Agent 完成同一能力
|
||||
|
||||
两种方式触发同一套业务能力,结果一致,选你习惯的即可。
|
||||
定时任务是否提供:见下方「入口三」(没有则写「本技能不需要定时」+ 理由)。
|
||||
|
||||
---
|
||||
|
||||
## 本技能做什么
|
||||
|
||||
用业务语言画清闭环(**不要**写库表英文名或命令行),例如:
|
||||
|
||||
```text
|
||||
【输入/准备】 → 【核心动作】 → 【结果可见处】 → 【可选报告/导出】
|
||||
```
|
||||
|
||||
- **输入**:……
|
||||
- **输出**:……
|
||||
- **说明**:边界(教学/正式、是否接外站等)
|
||||
|
||||
---
|
||||
|
||||
@@ -14,43 +35,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 +141,10 @@
|
||||
|
||||
| 现象 | 你可以先试 |
|
||||
|------|------------|
|
||||
| 提示未登录 / 无权限 | 确认匠厂已登录;业务账号是否在线、权限是否足够 |
|
||||
| 提示账号不存在 / 被占用 | 到「数据管理 / 账号管理」确认账号记录和状态,稍后再试;不要重复创建账号 |
|
||||
| 提示缺少必要信息 | 把时间范围、单号、文件等说完整后再试一次 |
|
||||
| 一直没有结果 | 到任务中心查看进度或失败原因;必要时重新发起 |
|
||||
| 结果不对 | 用更明确的条件再说一次,或缩小范围重试 |
|
||||
| 主表是空的 | 先做「首次准备」或对话载入/导入 |
|
||||
| 按钮点不了 | 是否须先选中行;是否装错版本 |
|
||||
| 一直没有结果 | 看任务中心 / 运行日志;async 任务看进度 |
|
||||
| 【网页自动化】未登录 / 无权限 | 确认账号在线与目标站登录状态 |
|
||||
| 结果不对 | 缩小范围重试,或改用数据管理核对表数据 |
|
||||
|
||||
仍无法解决时,把「你说了什么 + 界面提示原文」发给支持人员,便于排查。
|
||||
仍无法解决时,把「你用的入口 + 操作步骤 + 界面提示原文」发给支持人员。
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# assets
|
||||
|
||||
- `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 成功输出形状示例、以及 `render-html.action.example.json`(虚构路径与数据)。
|
||||
- `examples/`:CLI 成功输出形状示例、Action 形状参考(含 `path-root-file-action.example.json`、`selection-label-column.action.example.json`、`selection-mode-optional.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`(对齐匠厂宿主)。可选 `hoverHint` 为数据管理工具栏悬停(省略不显示,不回退)。`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`**(选择摘要标签列)/ **`selectionMode`**(`required` \| `none` \| `optional`:表勾选如何驱动 Action;省略则宿主兼容推断)。placements 含 `toolbar` / `row` / `batch` / `cron` / `agent` / `skill-detail`(对齐匠厂宿主)。数据管理 confirm/form 弹层在会应用勾选时展示「已选择」摘要;**form + 有摘要时映射 `readOnly` 默认不占参数区(仍提交)**,可选 **`revealInForm`** 强制显示。弹窗 `description` / 确认黄条 / 参数说明须**用户可见中文**(勿写 `pickCount`/`async`/`pending`/CLI 子命令;见 ACTIONS.md「用户可见文案」)。可选 `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/)
|
||||
|
||||
@@ -6,4 +6,6 @@
|
||||
- `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)。
|
||||
- `selection-label-column.action.example.json`:选择摘要 `bind.labelColumn` 形状参考(见 `references/ACTIONS.md`「选择摘要」)。
|
||||
- `selection-mode-optional.action.example.json`:`bind.selectionMode: "optional"` 智能勾选/按数量合并形状参考;用户可见文案为口语中文(见 `references/ACTIONS.md`「勾选驱动模式」「用户可见文案」)。
|
||||
- `replaces-create.action.example.json` / `replaces-update.action.example.json` / `replaces-delete.action.example.json` / `replaces-import.action.example.json`:数据管理默认按钮接管(`replaces`)形状参考;**import 时 UI 仍用宿主「导入数据」面板**,确认后调技能写库(见 `references/ACTIONS.md`「`replaces`」)。
|
||||
|
||||
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" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
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"]
|
||||
}
|
||||
}
|
||||
46
assets/examples/selection-mode-optional.action.example.json
Normal file
46
assets/examples/selection-mode-optional.action.example.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"id": "process-queue",
|
||||
"label": "处理队列",
|
||||
"description": "从业务队列处理待处理记录。有勾选则处理勾选的;没有勾选则按下方数量从队列领取。已成功或失败的会自动跳过。进度可在任务中心查看。",
|
||||
"placements": ["toolbar", "batch", "cron", "agent"],
|
||||
"executionProfile": "async",
|
||||
"interaction": "form",
|
||||
"confirmation": {
|
||||
"message": "将按规则处理记录(有勾选则处理勾选的;未勾选则按数量从队列领取),是否继续?"
|
||||
},
|
||||
"bind": {
|
||||
"tables": ["business_items"],
|
||||
"selectionMode": "optional",
|
||||
"labelColumn": "display_name",
|
||||
"inputMapping": {
|
||||
"itemIds": "$selection.ids"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["itemIds", "pickCount"],
|
||||
"properties": {
|
||||
"itemIds": {
|
||||
"type": "string",
|
||||
"title": "编号列表",
|
||||
"description": "有勾选时自动填入,一般无需修改",
|
||||
"readOnly": true
|
||||
},
|
||||
"pickCount": {
|
||||
"type": "number",
|
||||
"title": "处理数量",
|
||||
"description": "没有勾选时:从待处理队列里领取的条数",
|
||||
"default": 1,
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "process",
|
||||
"args": ["--ids", "{{itemIds}}", "--pick", "{{pickCount}}"]
|
||||
},
|
||||
"hostEffects": {
|
||||
"onSuccess": ["reloadTable", "clearSelection"]
|
||||
}
|
||||
}
|
||||
@@ -110,7 +110,11 @@
|
||||
"sensitive": { "type": "boolean" },
|
||||
"readOnly": {
|
||||
"type": "boolean",
|
||||
"description": "宿主数据管理表单只读展示该字段(仍会随提交传入 entrypoint);用于行内/勾选预填的主键等"
|
||||
"description": "只读。有「已选择」且字段来自 inputMapping(或约定 id 字段)时,宿主 form 默认不渲染该控件,预设值仍提交;无摘要时只读展示。勿用 visibleWhen 代替(visibleWhen 隐藏不提交)。"
|
||||
},
|
||||
"revealInForm": {
|
||||
"type": "boolean",
|
||||
"description": "可选逃生阀。true 时即使弹层有「已选择」摘要,也仍渲染该 readOnly 字段。不改变提交 payload。省略=映射只读字段在有摘要时默认折叠。"
|
||||
},
|
||||
"column": {
|
||||
"type": "string",
|
||||
@@ -121,13 +125,18 @@
|
||||
"display_type": {
|
||||
"type": "string",
|
||||
"enum": ["textarea", "date", "datetime_unix_seconds", "url", "email", "lookup", "directory", "file"],
|
||||
"description": "可选。表单控件提示;与 format 二选一即可。lookup=关联下拉(须配 lookup 对象或绑到已有 lookup 列)。file/directory=本地路径选择。须与 CRUD display_type 语义对齐(见 ACTIONS.md / SCHEMA.md)。"
|
||||
"description": "可选。表单控件提示;与 format 二选一即可。lookup=关联下拉(须配 lookup 对象或绑到已有 lookup 列)。file/directory=本地路径选择(可选 pathRoot 指定起始目录,见 ACTIONS.md)。须与 CRUD display_type 语义对齐。"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"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」。"
|
||||
@@ -296,9 +305,20 @@
|
||||
"pattern": "^[a-z][a-z0-9_]*$"
|
||||
},
|
||||
"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 等),再不行只显示编号。"
|
||||
},
|
||||
"selectionMode": {
|
||||
"type": "string",
|
||||
"enum": ["required", "none", "optional"],
|
||||
"description": "可选。数据管理勾选如何驱动本 Action:required=必须有勾选(无勾选禁用);none=忽略表勾选(不出现假「已选择」);optional=有勾选走 $selection.ids,无勾选走 pickCount/maxLeads 等空选数量字段。省略时宿主推断:有 $selection.ids 或 batch → required,否则 → none。与 placements/executionProfile 正交。"
|
||||
}
|
||||
},
|
||||
"description": "表级绑定;toolbar 必填。可选 inputMapping / columnMapping。concurrency/locks 勿写入。"
|
||||
"description": "表级绑定;toolbar 必填。可选 inputMapping / columnMapping / labelColumn / selectionMode。concurrency/locks 勿写入。"
|
||||
},
|
||||
"hostEffectId": {
|
||||
"type": "string",
|
||||
|
||||
@@ -77,10 +77,16 @@ 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 为唯一主路径
|
||||
- [ ] A2-12 **P0** 说明/教程正文**不以** `# 技能名` 或 `# 技能名 · 使用教程` 开篇(宿主页头/Tab 已有);主分节用 `##`,不要把原 `##` 升成 `#`(见 `TUTORIAL_SURFACES.md` §0.1)
|
||||
|
||||
---
|
||||
|
||||
@@ -104,7 +110,9 @@ checklist_version: 1.0
|
||||
- [ ] A4-5 **P1** 已对外暴露的动作参数覆盖主路径关键输入(无“命令行能做、动作入口不能做”的关键缺口)
|
||||
- [ ] A4-6 **P0** 若 Action 需引用同库另一张业务表(话术/平台/账号等):参数为 Lookup 下拉(内联 `lookup` 或绑列),**不是**手填内部编号;可选「自动」可留空提交
|
||||
- [ ] A4-7 **P1** 若 Action 有互斥采集/操作模式(如「按关键词 / 按主页」):用 `inputSchema` 字段级 **`visibleWhen`** 联动显隐,勿只靠 `description` 灰字说明「仅某某时填写」(见 `references/ACTIONS.md`;勿与 Policy L3 行态 `visibleWhen` 混淆)
|
||||
|
||||
- [ ] A4-8 **P1** 行内/勾选打开的 `form`:有「已选择」时参数区不应再重复映射主键(`inputMapping` + `readOnly`);需要强制露出时用 `revealInForm`,**禁止**用 `visibleWhen` 藏主键(会不提交)
|
||||
- [ ] A4-9 **P1** 若同一能力既支持「勾选处理」又支持「按数量从队列领取」:用一个 Action + `bind.selectionMode: "optional"`(勿拆两个同义顶栏按钮);无勾选可点、有勾选显示「已选择」、空选数量字段用 `pickCount` 等约定名(见 `references/ACTIONS.md`)
|
||||
- [ ] A4-10 **P0** 数据管理弹窗与任务失败文案:全中文产品用语;禁止在 `description` / `confirmation.message` / 参数 `description` / 用户向 `message` 写英文字段名、CLI 子命令、`async`/`pending`/`status=` 等(Agent 路由放 `SKILL.md`;见 `references/ACTIONS.md`「用户可见文案」)
|
||||
---
|
||||
|
||||
## 5. 账号与浏览器会话(网页 RPA 适用;其他形态标 N/A 或改测等价项)
|
||||
@@ -136,6 +144,10 @@ checklist_version: 1.0
|
||||
- [ ] 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`)
|
||||
|
||||
---
|
||||
|
||||
@@ -146,7 +158,7 @@ checklist_version: 1.0
|
||||
- [ ] A7-3 **P0** 故意失败一条时,文件日志先有 `task_failed`/`warning`,再有 `ERROR:CODE` 或任务中心失败态
|
||||
- [ ] A7-4 **P1** 关键步骤有据可查(能对应到开始/关键节点/结束;RPA 宜有多个 `stage=`)
|
||||
- [ ] A7-5 **P1** 若宣称支持录屏或截图存证:打开开关后关键步骤或失败能留下可查看产物;宣称支持却完全无产物 → **FAIL**
|
||||
- [ ] A7-6 **P2** 用户可见文案偏业务语言,而非大段开发黑话
|
||||
- [ ] A7-6 **P2** 用户可见文案偏业务语言,而非大段开发黑话(数据管理弹窗 / 任务失败见 **A4-10**,更高优先级)
|
||||
- [ ] A7-7 **P2** 数据管理技能按钮(toolbar / row / batch)中文名对照 `references/ACTIONS.md`「Action 按钮中文名」;不与宿主「查看 / 编辑 / 删除」抢名;打开类成功不弹成功条。行业专有按钮名可自创
|
||||
|
||||
---
|
||||
@@ -177,7 +189,7 @@ checklist_version: 1.0
|
||||
- [ ] A9-1 **P0** 仓库默认测试套件通过(执行技能约定的一键测试命令,如 `python tests/run_tests.py -v`)
|
||||
- [ ] A9-2 **P0** 不以「仅模拟档通过」作为交活依据(有真实档要求时必须真实跑通或书面阶段性豁免)
|
||||
- [ ] A9-3 **P0** **NEED_MANUAL** 入口定位表中定为「挂」的宿主入口(详情/Agent/工具栏/定时等)按声明抽测可通过;未在定位表中规划且未声明的入口可标 N/A(须引用定位表理由),**禁止**用「未声明」掩盖「从未做入口定位」
|
||||
- [ ] A9-4 **P1** 对外版本一致:详情、version、更新日志、发布标签能对上
|
||||
- [ ] A9-4 **P0** 对外版本一致(POLICY-RELEASE-001):`SKILL.md` version = `SKILL_VERSION` = 发布 git tag(无前缀);`release.ps1` 打 tag 前会硬校验,漏改不得发布;CHANGELOG 有对应小节
|
||||
- [ ] A9-5 **P1** 开发者按平台规则能安装并测到该技能(若平台有开发者可见性要求)
|
||||
|
||||
---
|
||||
|
||||
@@ -67,7 +67,8 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
||||
|
||||
宿主约定(与本模板对齐):
|
||||
|
||||
- 工具栏「导入」**UI 一律**打开宿主「导入数据」面板(默认从 `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 页面报告」)
|
||||
|
||||
@@ -176,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. 相关文档
|
||||
|
||||
@@ -335,7 +335,7 @@ git remote -v
|
||||
|
||||
应该重点写:
|
||||
|
||||
- YAML frontmatter:`name`、`description`、`version`、`metadata.openclaw`(含 `slug`、`platform_kit_min_version`、`developer_ids` 等)
|
||||
- YAML frontmatter:`name`、`description`、`version`、`metadata.openclaw`(含 `slug`、`platform_kit_min_version`、`developer_ids`、可选 `brands` 等)
|
||||
- 何时触发本技能、CLI 入口摘要、运行契约要点
|
||||
- 指向 `references/`(Agent 调用)与 `development/`(开发规范)的分工说明
|
||||
|
||||
@@ -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) §0.1 / §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`(无视频时用 `""`);正文可选。
|
||||
|
||||
@@ -407,6 +407,51 @@ metadata:
|
||||
- 一期只做“补授权”,不会因为你 later 修改数组而自动撤销旧授权
|
||||
- **首次正式 release 前**就应配好;配错或漏配时,CI 可能成功,但你在技能市场仍找不到技能
|
||||
|
||||
### 关于 `metadata.openclaw.brands`(OEM 市场,按需)
|
||||
|
||||
#### 目的
|
||||
|
||||
匠厂与华商、Lili、GDCM 等 OEM 客户端共用同一技能库,但技能市场目录按 **`market_code`** 过滤(客户端 Header `X-Market-Code`;缺省为 `jiangchang`)。
|
||||
|
||||
发布接口 `POST /api/skill/update` 会把 `metadata.openclaw.brands` 幂等写入 `skill_market_relations`(**只补不删**)。**未声明 `brands` 时仅补默认 `jiangchang`**,因此只写在 Gitea 的 `client-huashang` 组织下、却不写 `brands: [huashang]`,华商市场仍然搜不到该技能。
|
||||
|
||||
`brands` 与 `developer_ids` 正交:
|
||||
|
||||
| 字段 | 管什么 |
|
||||
|------|--------|
|
||||
| `brands` | 技能出现在哪些 OEM 市场目录 |
|
||||
| `developer_ids` | 技能不公开(`access_scope = 0`)时,哪些用户仍可见并可安装 |
|
||||
|
||||
#### 何时填写
|
||||
|
||||
- **仅匠厂验收 / 通用技能**:可省略(默认匠厂)。
|
||||
- **须在华商或其他 OEM 客户端出现**:必须显式列出对应编码(通常同时保留 `jiangchang` 以便主站也能看到)。
|
||||
|
||||
常见编码(小写机器码,非中文展示名):`jiangchang`、`huashang`、`lili`、`gdcm` 等。非法编码会导致发布接口 400,CI 失败。
|
||||
|
||||
#### 在 `SKILL.md` 中声明
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
openclaw:
|
||||
slug: your-skill-slug
|
||||
category: 通用
|
||||
developer_ids:
|
||||
- 12345
|
||||
brands:
|
||||
- jiangchang
|
||||
- huashang
|
||||
```
|
||||
|
||||
兼容别名(平台按优先级取第一个出现的键):`brands` → `oem_clients` → `client_keys`。模板与文档统一推荐写 **`brands`**。
|
||||
|
||||
约定:
|
||||
|
||||
- **可选字段**:未写或空列表 → 行为与历史一致,只挂 `jiangchang`
|
||||
- **一期只补不删**:从 `brands` 去掉某市场后,不会自动删除库中已有关联;若需下架请走运营/后台或后续平台能力
|
||||
- **不要**指望 Gitea 组织名、目录名 `client-*` 自动映射市场
|
||||
- 发布后在对应 OEM 客户端(须带正确 `X-Market-Code`)搜索验收;也可查库 `skill_market_relations`
|
||||
|
||||
## 7. 文档目录分工
|
||||
|
||||
当前规范 skill 的文档按读者拆分:
|
||||
@@ -720,9 +765,15 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
|
||||
|
||||
在执行 `release.ps1` 之前,必须先在本地确认 `python tests/run_tests.py -v` 通过。测试不通过不得发起正式发布。
|
||||
|
||||
**发布前必须**将 `SKILL.md` 的 `version:` 与 `scripts/util/constants.py` 的 `SKILL_VERSION` 调整为将打出的版本(例如下一 patch),再执行 `release.ps1`。`release.ps1` **不会**自动改这两个字段;漏改会造成 git tag 与技能版本号不一致。
|
||||
**POLICY-RELEASE-001(硬门禁)**:发布前必须将 `SKILL.md` 的 `version:`、`scripts/util/constants.py` 的 `SKILL_VERSION`、以及 `CHANGELOG.md` 对应小节,调整为**即将打出的**版本(与 `.\release.ps1 -DryRun` 打印的 `Next tag` 去掉 `v` 前缀一致)。`release.ps1` **不会**自动改这些字段;若三者与 Next tag 不一致,脚本在跑测试/打 tag **之前**直接失败,避免市场「最新版本」(git tag) 与安装包「当前版本」(本地 `SKILL.md`) 长期错位、技能市场一直显示「更新」。
|
||||
|
||||
`release.ps1` 会在 **auto commit 之前**、推送 tag 之前,对**当前工作区**自动执行默认测试(`python tests/run_tests.py -v`);失败则中止 release,不会留下失败状态的自动提交。仅紧急排障可使用 `-SkipTests` 跳过(会打印黄色警告)。`-DryRun` 预览发布时**仍会实际运行**默认测试,便于在不打 tag 的情况下验证门禁。
|
||||
建议流程:
|
||||
|
||||
1. `.\release.ps1 -DryRun` 查看 `Next tag`(例如 `v1.0.104`)
|
||||
2. 将 `SKILL.md` / `SKILL_VERSION` / `CHANGELOG` 改为 `1.0.104`
|
||||
3. 再执行正式 `.\release.ps1`(可用 `-AutoCommit`)
|
||||
|
||||
`release.ps1` 会在 **auto commit 之前**、推送 tag 之前,对**当前工作区**自动执行默认测试(`python tests/run_tests.py -v`);失败则中止 release,不会留下失败状态的自动提交。仅紧急排障可使用 `-SkipTests` 跳过(会打印黄色警告)。`-DryRun` 预览发布时**仍会实际运行**版本对齐门禁与默认测试,便于在不打 tag 的情况下验证。
|
||||
|
||||
新增或调整开发规范时,须同步 [`development/POLICY_MATRIX.md`](POLICY_MATRIX.md) 与 [`tests/test_development_policy_guard.py`](../tests/test_development_policy_guard.py)(或注明由既有测试覆盖)。
|
||||
|
||||
@@ -837,7 +888,7 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
|
||||
- 长任务(async)是否有可见进度,能否取消或等待说明
|
||||
- 网页 RPA 是否有头运行;需登录/验证码时是否停下等人,而不是静默失败
|
||||
- 数据管理:表名/字段中文、按钮文案是否清楚;结果是否写回用户看得见的地方
|
||||
- 市场说明 / 教程是否与真实行为一致
|
||||
- 市场说明 / 教程是否与真实行为一致;**说明 Tab 不含分步操作**,教程覆盖已挂入口(见 [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md))
|
||||
|
||||
| 宿主入口(侧栏 / 界面) | 技能侧如何挂上 | 建议验收什么 |
|
||||
|-------------------------|----------------|--------------|
|
||||
@@ -878,6 +929,7 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
|
||||
- [ ] 目录名、`SKILL.md` slug、`constants.SKILL_SLUG` 三者一致
|
||||
- [ ] `SKILL.md` 中 slug、名称、描述都已替换
|
||||
- [ ] `SKILL.md` 的 `developer_ids` 已换成**本人**匠厂用户 ID(设置 → 用户信息 → 复制),不是模板示例 ID
|
||||
- [ ] 若须在华商等 OEM 市场出现:`SKILL.md` 的 `brands` 已声明(如 `jiangchang` + `huashang`);未写则仅匠厂;不要用 Gitea 组织名代替
|
||||
- [ ] `scripts/util/constants.py` 已修改
|
||||
- [ ] `../references/CLI.md` 示例命令已改成真实命令
|
||||
- [ ] `service` 下的核心业务文件(如 `task_service.py`)已按领域改名并实现
|
||||
|
||||
@@ -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(用户可见动作与存证)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
| POLICY-RPA-006 | 浏览器 RPA 必须保留 account-manager 生命周期标准;所有样例账号选择路径都须返回非空 `profile_dir + lease_token`,指定数字账号 ID 不得退化为无租约 `account get` | development/ACCOUNT_MANAGER.md;development/ADAPTER.md §兄弟依赖;development/RPA.md §0.2 | hard | 检查标准文档关键标记;扫描真实/仿真 `account_client.py` 的指定 ID 带租约实现并禁止空租约赋值 | `tests/test_development_policy_guard.py::TestPolicyRpa006` |
|
||||
| POLICY-PACKAGING-001 | `scripts/**/*.py` 单文件 < 1000 行 | development/DEVELOPMENT.md §3.4 发布打包约束;development/RUNTIME.md §发布打包约束 | hard | 行数统计 | **已有测试覆盖**:`tests/test_release_packaging_constraints.py::test_scripts_py_files_under_pyarmor_line_limit` |
|
||||
| POLICY-PACKAGING-002 | 文本文件 UTF-8 without BOM,无 `U+FEFF` | development/DEVELOPMENT.md §3.4;development/RUNTIME.md §编码与输出 | hard | BOM / 解码检查 | **已有测试覆盖**:`tests/test_release_packaging_constraints.py::test_text_files_are_utf8_without_bom` |
|
||||
| POLICY-RELEASE-001 | 发布 tag(去掉前缀)必须等于 `SKILL.md` 的 `version:` 且等于 `scripts/util/constants.py` 的 `SKILL_VERSION`;`release.ps1` 打 tag 前硬失败,不自动改写字段 | development/DEVELOPMENT.md §15 发布;宿主市场「最新版本」来自 tag、「当前版本」来自包内 `SKILL.md` | hard | `release.ps1` 门禁 + 日常版本字段对齐测试 | `release.ps1`(`Assert-SkillReleaseVersionAlignment`);`tests/test_development_policy_guard.py::TestPolicyRelease001`;`tests/test_skill_metadata.py::test_skill_version_alignment` |
|
||||
| POLICY-DOCS-001 | 本矩阵存在且包含上述全部 `policy_id` | 本次规范化约定 | hard | 解析 `development/POLICY_MATRIX.md` | `tests/test_development_policy_guard.py::TestPolicyDocs001` |
|
||||
| POLICY-LOGGING-001 | CLI 入口必须调用 `setup_skill_logging` | development/LOGGING.md;development/RUNTIME.md | hard | 扫描 `scripts/cli/app.py` 含 `setup_skill_logging` 与 logger info | `tests/test_development_policy_guard.py::TestPolicyLogging001` |
|
||||
| POLICY-LOGGING-002 | service 主任务入口必须使用 `get_skill_logger` | development/LOGGING.md;development/DEVELOPMENT.md | hard | 扫描 `scripts/service/task_service.py` | `tests/test_development_policy_guard.py::TestPolicyLogging002` |
|
||||
@@ -41,6 +42,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」专章与入口专章;说明/教程正文禁止与页头重复的 `#` H1(主分节用 `##`);业务技能教程与 `actions.json` placements 对齐(无 cron 须声明不需要定时) | development/TUTORIAL_SURFACES.md;development/CHECKLIST.md A2-7~A2-12 | 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 +69,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 | 待宿主升级后完整生效;模板侧已强制显式绑定 |
|
||||
| form +「已选择」时映射 `readOnly` 默认不占参数区(仍提交);`revealInForm` 逃生阀 | references/ACTIONS.md「选择摘要」 | soft(宿主 UX);文档与 schema 已声明;勿用 `visibleWhen` 代替 |
|
||||
| 数据管理技能按钮中文名(`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-*` 硬门禁可后续加 |
|
||||
|
||||
@@ -164,9 +164,25 @@
|
||||
|
||||
---
|
||||
|
||||
## 7. 相关文档
|
||||
## 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)
|
||||
|
||||
@@ -10,10 +10,12 @@
|
||||
|------|--------|------|
|
||||
| 1 | **拿到仓库**:① 项目经理在 [自建 Gitea](https://git.jc2009.com/)(`git.jc2009.com`,非 Gitea 官网)开仓并授权后 `git clone`;或 ② 本地已有 `skill-template` 时用 scaffold / 复制出新目录(遵守 Git 红线) | [`DEVELOPMENT.md`](DEVELOPMENT.md) §1、§4 |
|
||||
| 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」 |
|
||||
| 6b | **按需配置 `brands`(OEM 市场)**:须在华商等 OEM 客户端出现时声明 `brands`(如 `jiangchang` + `huashang`);未写则仅挂匠厂。Gitea 组织名不会自动映射市场 | [`DEVELOPMENT.md`](DEVELOPMENT.md) §6「关于 brands」 |
|
||||
| 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) |
|
||||
@@ -27,6 +29,7 @@
|
||||
开始定制 skill 前,建议按以下顺序阅读:
|
||||
|
||||
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 等,可扩展)
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
- 中文 description(一句话):
|
||||
- account-manager platform_key(如有):
|
||||
- 命名形态:标准型 / scope 型
|
||||
- **目标 OEM 市场 `brands`(可选)**:未写则发布后仅挂匠厂(`jiangchang`)。须在华商等 OEM 客户端可见时填写,例如 `jiangchang`、`huashang`(见 `DEVELOPMENT.md` §6「关于 brands」)。**不要**用 Gitea 组织名代替本字段。
|
||||
- **网页 RPA(如有)— 登录策略(三选一,须写明):**
|
||||
- `required`:目标站必须登录才能闭环(用户须完成/保持站点登录;技能每轮主路径须有登录门禁 + 验证码/人工等待)
|
||||
- `optional`:登录可提升能力,但不登录也能完成主路径(**仅当**公开页确可闭环时选用;会话依赖型采集/评论/私信**禁止**默认抄成 optional)
|
||||
@@ -33,19 +34,26 @@
|
||||
|
||||
## 0.1 宿主入口定位(必填)
|
||||
|
||||
先读 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md)。对话任务 / 数据管理 / 定时任务在宿主上**平级**;挂哪些由本技能业务与 UX 定,模板不规定「必须三入口都挂满」。
|
||||
先读 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md)。对话任务 / 数据管理 / 定时任务在宿主上**平级**;挂哪些由本技能业务与 UX 定,模板不规定「必须三入口都挂满」。用户文档如何写见 [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md)。
|
||||
|
||||
对每个主能力填一行(可增行;「不挂」必须写理由):
|
||||
|
||||
| 主能力(Action id / CLI) | 对话 | 数据管理 | 定时 | 不挂理由(若不挂) |
|
||||
|---------------------------|------|----------|------|--------------------|
|
||||
| (例)`<action-or-cli>` | 挂 / 不挂 | 不挂 / toolbar / row / batch / replaces / 仅展示表 | 挂 / 不挂 | |
|
||||
| 主能力(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 协作与授权边界(涉及外部系统时必填)
|
||||
|
||||
@@ -244,7 +252,9 @@
|
||||
- `tests/test_display_metadata.py` 通过
|
||||
- 真实联调样例仍放 `tests/integration/`,默认套件不包含真实外联(与业务 `.env` 默认 `real_rpa` 不冲突)
|
||||
- `SKILL.md` 的 `developer_ids` 已配置为开发者本人匠厂用户 ID(开发期不公开时,否则本人无法在市场安装自测;取 ID 步骤见 `development/DEVELOPMENT.md` §6)
|
||||
- 若目标含华商等 OEM 客户端:`SKILL.md` 的 `brands` 已声明对应 `market_code`(通常含 `jiangchang` + 目标 OEM);未写则仅匠厂市场可见
|
||||
- 发布后 Gitea 工作流成功;匠厂技能市场对开发者账号可见最新版本并可安装
|
||||
- 若声明了 OEM `brands`:在对应 OEM 客户端(`X-Market-Code` 正确)也能检索到该技能
|
||||
- 安装后按本技能入口定位表与 `actions.json` 声明完成宿主多入口验收(见 `development/DEVELOPMENT.md` §15、`PRODUCT_SURFACES.md`);**须看用户体验**(进度可见、失败可读、RPA 有头、文案与真实行为一致等),后台通了不算完
|
||||
- 已人工核验授权与技术路线:无破解、逆向、私有协议、验证码绕过、Hook / 注入、自动换号或其他规避平台限制的实现
|
||||
- RPA 真实主路径已验证状态等待、`1~3` 秒正常操作节奏、有限重试、账号 / 设备串行、人工介入、结果核验和结束存证
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
| 文档 | 职责 |
|
||||
|------|------|
|
||||
| 本文 | sync/async、任务中心、Agent 禁令、单内核编排、队列 pick、同步 vs 导出 |
|
||||
| [`../references/ACTIONS.md`](../references/ACTIONS.md) | `actions.json` 字段、placements、`bind.tables`、inputSchema、跨表 Lookup、字段条件显隐 `visibleWhen`、**按钮中文名 `label` 参考** |
|
||||
| [`../references/ACTIONS.md`](../references/ACTIONS.md) | `actions.json` 字段、placements、`bind.tables`、`bind.selectionMode`、**用户可见文案**、inputSchema、跨表 Lookup、字段条件显隐 `visibleWhen`、本地路径 `file`/`directory` + 可选 **`pathRoot`**、选择摘要「已选择」与映射 **`readOnly` / `revealInForm`**、**按钮中文名 `label` 参考** |
|
||||
| [`LOGGING.md`](LOGGING.md) | `job_context` / `emit` / `finish` / Run Journal |
|
||||
| [`DATA_PATHS.md`](DATA_PATHS.md) | 下载/导入等落盘路径 |
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
1. **宿主管调度,技能管业务** — 不自行实现任务中心 / Job 轮询 / 后台守护进程。
|
||||
2. **单业务内核、多入口** — 数据管理、定时任务、Agent、技能详情与直接 CLI 复用同一 domain service;禁止按入口复制业务逻辑。
|
||||
3. **五职责正交** — `placements` / `bind.tables` / `executionProfile` / `entrypoint` / domain service 各管一事(见 ACTIONS.md)。可选第六轴 **policy**(见 `references/POLICY.md`):未启用 = L0 与今日一致。可选 **`hostEffects` / `steps`**(见 ACTIONS.md):数据管理交互前后的 UI/级联编排;省略 = 不额外改 UI(匠厂宿主 ≥2.8.5)。可选 **`hoverHint`**(见 ACTIONS.md):数据管理工具栏悬停;省略 = 不显示,不回退说明或确认黄条。可选 **`visibleWhen`**(见 ACTIONS.md):Action 参数表单字段条件显隐;省略 = 字段始终平铺;**勿与** Policy L3 行态 `visibleWhen` 混淆。可选 **`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):选择摘要可读标签列;省略 = 宿主常见列名启发。可选 **`bind.selectionMode`**(见 ACTIONS.md):表勾选如何驱动 Action(`required` / `none` / `optional`);省略 = 宿主按 `$selection.ids` / `batch` 推断。可选 **`revealInForm`**(见 ACTIONS.md):有「已选择」时仍强制显示某 `readOnly` 字段;省略 = 映射只读默认不占 form 参数区(仍提交)。可选 **`cellPresentation`**(见 `references/CELL_PRESENTATION.md`):单元格语义标色;省略 = P0。**用户可见文案**(见 ACTIONS.md「用户可见文案」):弹窗 `description` / 确认黄条 / 参数说明 / 任务失败 `message` 须全中文产品用语;Agent 路由勿塞进弹窗。
|
||||
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 流**。
|
||||
|
||||
222
development/TUTORIAL_SURFACES.md
Normal file
222
development/TUTORIAL_SURFACES.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# 用户市场文档契约(说明 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`。
|
||||
|
||||
### 0.1 标题层级(说明 / 教程共用)
|
||||
|
||||
宿主详情页**已经**展示技能名;「说明 / 使用教程」Tab(及数据管理右侧教程抽屉)**已经**标明文档类型。因此:
|
||||
|
||||
| 层级 | 用途 |
|
||||
|------|------|
|
||||
| `#` | **正文不要用**(会与页头/Tab 标题重复抢视觉) |
|
||||
| `##` | 主分节(能帮你做什么、入口一:数据管理…) |
|
||||
| `###` | 分节内步骤 |
|
||||
|
||||
**禁止:**
|
||||
|
||||
- `README.md` 文首 `# 【技能名】` / `# 审计疑点辅助` 这类与页头重复的 H1
|
||||
- `TUTORIAL.md` 文首 `# 【技能名】· 使用教程` 这类与 Tab/抽屉标题重复的 H1
|
||||
|
||||
去掉重复 H1 后,**不要**把原来的 `##` 全部升成 `#`。
|
||||
|
||||
---
|
||||
|
||||
## 1. README.md(说明 Tab)
|
||||
|
||||
### 1.1 必须回答
|
||||
|
||||
| 问题 | 写法 |
|
||||
|------|------|
|
||||
| 这是什么 | 开篇一两句价值(**普通段落或引用**,不要再写技能名 H1) |
|
||||
| 能做什么 | 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
|
||||
@@ -107,13 +107,14 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
|
||||
| entrypoint | `type = cli`、`command`、`args`(**必填** JSON 数组,项为 `string` / `number` / `boolean`) |
|
||||
| placements | `toolbar`、`row`、`batch`、`skill-detail`、`agent`、`cron`;仅 `replaces` 时可为空数组 |
|
||||
| **replaces** | 可选 `"create"` \| `"update"` \| `"delete"` \| `"import"` — 接管默认新增/编辑/删除/导入(须 `bind.tables`) |
|
||||
| **bind** | `{ "tables": ["business_table"] }`;可选 `inputMapping`(行内/批量预填,见下节) |
|
||||
| **bind** | `{ "tables": ["business_table"] }`;可选 `inputMapping`、`columnMapping`、**`labelColumn`**、**`selectionMode`**(行内/批量预填、选择摘要标签、勾选驱动模式,见下节) |
|
||||
| **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` / `lookup`);可选 **`visibleWhen`**(字段条件显隐,见专节) |
|
||||
| inputSchema | 根 `type: object`;properties 使用 `string` / `number` / `boolean`;支持 `enum`、`default`、`required`、`sensitive`、`readOnly`、可选 **`revealInForm`**;可选 `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` 混淆 |
|
||||
| **visibleWhen** | 可选。写在 **property** 上(非 action 顶层):依赖同表单其它字段值决定是否展示;隐藏字段不提交、不校验 `required`。**勿与** [`POLICY.md`](POLICY.md) L3 行态 `visibleWhen` 混淆;也**勿**用它藏「已选择」已表达的映射主键(见「选择摘要」) |
|
||||
| **revealInForm** | 可选。写在 **property** 上:有「已选择」时仍强制画出该 `readOnly` 字段;省略 = 映射只读字段默认不占参数区(仍提交) |
|
||||
| **executionProfile** | `"sync"` \| `"async"`(**必须显式写出**,禁止依赖缺省) |
|
||||
| **interaction** | `"auto"` \| `"direct"` \| `"confirm"` \| `"form"`(可选;省略≈`auto`) |
|
||||
| **interactionByPlacement** | 可选;按 placement 覆盖为 `direct`/`confirm`/`form`(不可写 `auto`) |
|
||||
@@ -132,15 +133,102 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
|
||||
|
||||
规则:
|
||||
|
||||
- `bind` 为 object,`additionalProperties: false`;当前允许 `tables`(必填键当存在 bind 时)与可选 `inputMapping`。
|
||||
- `bind` 为 object,`additionalProperties: false`;当前允许 `tables`(必填键当存在 bind 时)与可选 `inputMapping`、`columnMapping`、**`labelColumn`**、**`selectionMode`**。
|
||||
- `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` 在表单中只读展示。
|
||||
- **`inputMapping`(可选)**:把行/勾选映射到 `inputSchema` 字段,例如 `"leadId": "$row.$pk"`、`"ids": "$selection.ids"`。适合 `row` / `batch`。宿主仍会提交这些字段。映射字段建议标 `readOnly: true`:无「已选择」时只读展示;**有「已选择」时默认不占参数区**(仍提交,见下节)。不要用 `visibleWhen` 去「藏」映射主键。
|
||||
- **`columnMapping`(可选)**:把 inputSchema 属性名映射到物理列名,供表单复用列展示名 / lookup。
|
||||
- **`labelColumn`(可选)**:物理列名,供数据管理 Action 弹层「已选择」摘要展示 `#id + 标签`。**仅影响展示,不改变 CLI/提交 payload,也不决定业务是否跳过某些状态。** 省略时宿主用常见列名启发(如 `title` / `name` / `nickname` 等);再没有则只显示编号。列名不常见、或启发会选错列时再显式声明。
|
||||
- **`selectionMode`(可选)**:数据管理**表勾选**如何驱动本 Action(与 `placements` / `executionProfile` 正交)。见下节「勾选驱动模式」。
|
||||
- `concurrency` / `locks` 仍未稳定,**不要**写入新技能 manifest。
|
||||
|
||||
### 数据管理:勾选驱动模式 `bind.selectionMode`
|
||||
|
||||
同一业务能力应尽量用**一个** Action(多个 `placements`),避免「勾选下载」与「按数量处理队列」拆成两个同义顶栏按钮、却让用户误以为勾选生效。
|
||||
|
||||
| 值 | 含义 | 典型场景 |
|
||||
|----|------|----------|
|
||||
| `required` | 必须有表勾选;无勾选时宿主**禁用**按钮;弹层可显示「已选择」 | 经典批量:仅处理勾选行 |
|
||||
| `none` | **忽略**表勾选;即使勾选了也不显示「已选择」、也不把 `$selection.ids` 带进提交 | 经典工具栏「按数量从队列领取」 |
|
||||
| `optional` | **智能合并**:有勾选 → 走 `$selection.ids`(显示「已选择」);无勾选 → 走空选数量字段(如 `pickCount`) | 统一「视频下载 / 处理队列」一类按钮 |
|
||||
| **省略** | 宿主兼容推断:`inputMapping` 含 `$selection.ids` 或 `placements` 含 `batch` → `required`;否则 → `none` | 旧技能零改动 |
|
||||
|
||||
**`optional` 写法要点:**
|
||||
|
||||
1. `bind.inputMapping` 声明 `"itemIds": "$selection.ids"`(字段名自定),并 `selectionMode: "optional"`。
|
||||
2. `inputSchema` 同时声明勾选字段与空选数量字段;两者都可写进 `required`——宿主只校验**当前分支**。
|
||||
3. 空选数量字段名约定(宿主按字段名识别,保守列表):`pickCount` / `maxLeads` / `maxCount` / `takeCount` / `pick`。
|
||||
4. `entrypoint.args` 可同时写 `--ids {{itemIds}}` 与 `--pick {{pickCount}}`;缺值的 `--flag` + 空串由宿主压缩掉,不会留下脏 argv。
|
||||
5. 有勾选时隐藏数量字段;无勾选时隐藏 `$selection.ids` 映射字段(`$row.*` 行上下文不受影响)。
|
||||
6. **不要**为同一能力再挂第二个 toolbar 按钮。
|
||||
|
||||
示例:[`assets/examples/selection-mode-optional.action.example.json`](../assets/examples/selection-mode-optional.action.example.json)。
|
||||
|
||||
弹窗上的 `description` / 参数说明须用**普通用户中文**(见下节「用户可见文案」);`pickCount` 等只作 **property 名 / CLI 参数名**,不要写进用户可读句子。
|
||||
|
||||
### 用户可见文案(双受众,必读)
|
||||
|
||||
宿主会把下列字段**原样展示给普通用户**(数据管理弹窗、任务中心时间线、表说明等):
|
||||
|
||||
| 字段 | 出现位置 |
|
||||
|------|----------|
|
||||
| Action `description` | 弹窗顶部说明 |
|
||||
| `confirmation.message` | 确认/表单黄条 |
|
||||
| `inputSchema` 各字段 `title` / `description` | 参数区标签与帮助 |
|
||||
| CLI / `finish` 返回的用户向 `message` | 任务中心、失败摘要 |
|
||||
| 表/列 `display_name`、表 `description` | 数据管理侧栏与表头帮助 |
|
||||
|
||||
**总原则:** 以上文案 = **产品用语、全中文**。Agent 路由禁令、实现细节、契约自述放在 **`SKILL.md` / `references/`**,**不要**塞进弹窗 `description`。
|
||||
|
||||
**允许写:** 待下载、任务中心、打开浏览器、有勾选 / 没有勾选、下载数量、按钮中文名(如「导入待下载链接」)。
|
||||
|
||||
**禁止写进用户文案(常见反例):**
|
||||
|
||||
| 类型 | 反例 |
|
||||
|------|------|
|
||||
| 字段名 / CLI 子命令 | `pickCount`、`itemIds`、`import-urls`、`enqueue` |
|
||||
| 英文状态 / 条件式 | `pending`、`status=pending`、`failed` |
|
||||
| 执行剖面黑话 | `async`、`sync`、单独甩 `Job`(可说「任务中心」) |
|
||||
| Agent 禁令 | 「不是明确 URL 入口」「不要先 download-enqueue…」 |
|
||||
| 契约自述 | 「示例:bind.selectionMode=optional」 |
|
||||
|
||||
**对照:**
|
||||
|
||||
| 差(开发者备注) | 好(普通用户) |
|
||||
|------------------|----------------|
|
||||
| 按 `pickCount` 从 `pending` 领取(async) | 没有勾选则按下方数量从待下载队列领取;进度可在任务中心查看 |
|
||||
| 请先 import-urls | 请先在数据管理录入链接,或使用「导入待下载链接」 |
|
||||
|
||||
**边界:**
|
||||
|
||||
- `inputSchema` **属性名**、`entrypoint` 参数、库表状态枚举值可以仍是英文;变的是 **title / description / message**。
|
||||
- `label` 仍遵循上文「Action 按钮中文名」;本节约束的是说明与帮助文字。
|
||||
- 与 `bind.selectionMode` 正交:行为对了,文案仍须对人说人话。
|
||||
|
||||
### 数据管理:选择摘要(宿主默认能力)
|
||||
|
||||
当用户在数据管理**勾选行**(或行内打开带行上下文的动作)后打开 `confirm` / `form` 弹层时,若当前 Action 的 `selectionMode` 会**应用**表勾选(`required`,或 `optional` 且已勾选),宿主会展示通用 **「已选择 · N 条」** 摘要(`#主键` + 可读标签;超过约 3 条可展开查看全部)。这是**宿主 UX**,对适用技能默认生效:
|
||||
|
||||
- **技能零改动即可受益**(省略 `selectionMode` 时的 `required` 推断路径);不要在 `confirmation.message` 里手写「已选 id 列表」,也不要为了面板去拼业务状态跳过文案。
|
||||
- 摘要与执行解耦:勾选了谁由 UI 说明;**实际处理哪些行、是否跳过某些状态,仍由技能 CLI / domain service 决定**。
|
||||
- `selectionMode: "none"` 时即使表上有勾选,也**不**展示假「已选择」、不把勾选 id 预填进本 Action。
|
||||
- 需要稳定标签列时,在 `bind` 上声明可选 `labelColumn`(见上)。示例:[`assets/examples/selection-label-column.action.example.json`](../assets/examples/selection-label-column.action.example.json)。
|
||||
|
||||
**与 `confirm` / `form` 的信息架构对齐(映射只读字段):**
|
||||
|
||||
| 交互 | 参数区 | 映射主键(`inputMapping` + `readOnly`) |
|
||||
|------|--------|----------------------------------------|
|
||||
| `confirm` | 整区不画 | 不画;预设值仍提交 |
|
||||
| `form`(有「已选择」) | 只画用户要填/要改的字段 | **默认不画**;预设值仍提交 |
|
||||
| `form`(无「已选择」) | 正常画字段 | `readOnly` **只读展示**(如工具栏靠 lookup 手选作业) |
|
||||
|
||||
- 需要用户在有「已选择」时仍看见某只读字段:该 property 加 **`revealInForm: true`**(逃生阀;不改变提交)。
|
||||
- **禁止**用 `visibleWhen` 藏映射主键:`visibleWhen` 隐藏的字段**不进提交**、不校验 `required`,会弄断 `--id` / `{{job_id}}`。
|
||||
- 非映射的只读说明字段(未出现在 `inputMapping`)在有摘要时**仍会显示**;仅映射上下文字段被默认折叠。
|
||||
|
||||
## `replaces`(可选:接管默认新增 / 编辑 / 删除 / 导入)
|
||||
|
||||
有领域副作用的表(归一化字段、建目录、加密、级联清理)不要依赖宿主直写 SQLite。此时可为该表声明 `replaces`,**用户仍点宿主默认按钮**,由技能 CLI 完成写入。
|
||||
@@ -330,8 +418,8 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
|
||||
| 值 | 数据管理体验 | 典型场景 |
|
||||
|----|--------------|----------|
|
||||
| `direct` | **一点即跑**,无弹窗 | 打开目录、打开浏览器、无参只读、行内主键已映射 |
|
||||
| `confirm` | 轻量确认弹窗(无参数区;文案优先 `confirmation.message`) | 处理待发送、删除前确认、有副作用但无可编辑入参 |
|
||||
| `form` | 完整参数表单(现状默认路径) | 导入、查询条件、需用户改写的字段 |
|
||||
| `confirm` | 轻量确认弹窗(无参数区;文案优先 `confirmation.message`;有勾选时宿主展示「已选择」摘要;映射主键仍提交) | 处理待发送、删除前确认、有副作用但无可编辑入参 |
|
||||
| `form` | 完整参数表单(有勾选时同样展示「已选择」摘要;**映射 `readOnly` 默认不占参数区**,见「选择摘要」) | 导入、选文件、查询条件、需用户改写的字段 |
|
||||
| `auto` / 省略 | 宿主推断:有**可编辑**参数 → `form`;仅有 `confirmation` → `confirm`;否则 → `direct` | 旧技能兼容;新技能建议显式写出 |
|
||||
|
||||
```json
|
||||
@@ -639,11 +727,23 @@ HINT: <next step>
|
||||
|----|------|
|
||||
| `toolbar` | 数据管理表顶栏技能按钮(须配合 `bind.tables`) |
|
||||
| `row` | 数据管理**行级入口**(建议 `bind.tables` + `inputMapping`;表需有主键)。技能声明后,由宿主呈现为操作列链接、**行右键菜单**和/或行内「⋯」等壳;**不**新增 `row-context-menu` 等 placement |
|
||||
| `batch` | 数据管理批量操作(需勾选行;无勾选时宿主禁用按钮) |
|
||||
| `batch` | 数据管理批量操作入口。默认(`selectionMode` 省略或 `required`)需勾选行,无勾选时宿主禁用按钮;`optional` / `none` 时无勾选也可点(见 `bind.selectionMode`) |
|
||||
| `skill-detail` | 技能详情页直调(与安装/四 Tab 同属技能市场详情;已声明则须自测) |
|
||||
| `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,构成该表行的技能菜单项集合;配什么就出现什么。
|
||||
@@ -689,7 +789,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 页面
|
||||
@@ -866,14 +966,18 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件
|
||||
| 日期时间 | `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 专用表单;`visibleWhen` 多条件 AND/OR 树、跨 Action 依赖、CRUD 新增/编辑对称显隐。
|
||||
(**Lookup 多选**已支持:同一 `lookup` + `multiple: true`,见上文;不是任意标签数组。)
|
||||
**本版不做(勿宣传):** 浏览器内文件上传控件、复杂 `oneOf` / nested object 专用表单;`visibleWhen` 多条件 AND/OR 树、跨 Action 依赖、CRUD 新增/编辑对称显隐;Action 参数表单的列筛选对称。
|
||||
(**Lookup 多选**已支持:同一 `lookup` + `multiple: true`,见上文;不是任意标签数组。)
|
||||
(**本地路径选择**已支持:`file` / `directory` + 可选 `pathRoot`,见下节;提交值仍是本机绝对路径字符串,不是 multipart 上传。)
|
||||
|
||||
日期时间约定:
|
||||
|
||||
@@ -881,16 +985,77 @@ 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` | **`lookup`**
|
||||
- `enum` / `default` / `required` / `sensitive` / `readOnly` / 可选 `revealInForm`
|
||||
- `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`
|
||||
- **选择摘要下的映射只读折叠**:`form` +「已选择」时,`inputMapping` 映射且 `readOnly` 的字段默认不渲染(仍提交);`revealInForm: true` 可强制显示;**勿**用 `visibleWhen` 代替
|
||||
|
||||
### 字段条件显隐(`visibleWhen`)
|
||||
|
||||
@@ -999,6 +1164,8 @@ CRUD 侧宿主义务与禁止事项见 [`SCHEMA.md`](SCHEMA.md)「表单与展
|
||||
|
||||
若还需要默认篇数,可在同一 `properties` 里为 `pickCount` 增加 `"default": 1`、`"minimum": 1`。
|
||||
|
||||
与 **`bind.selectionMode: "optional"`** 联用时:无勾选弹层展示 `pickCount`;有勾选则隐藏 `pickCount`、提交 `$selection.ids`。不要再拆第二个「只 pick / 只勾选」toolbar 按钮。见上文「勾选驱动模式」。
|
||||
|
||||
约定:
|
||||
|
||||
- `boolean` 在宿主中渲染为开关
|
||||
@@ -1008,8 +1175,8 @@ CRUD 侧宿主义务与禁止事项见 [`SCHEMA.md`](SCHEMA.md)「表单与展
|
||||
- `display_type`/`format: url` / `email` 为薄层输入类型(校验从宽,勿当强校验网关)
|
||||
- `required` 字段在提交前校验
|
||||
- `sensitive` 字段不得出现在低信任入口
|
||||
- `readOnly: true` 时数据管理表单只读展示该字段(仍随提交传入);适合行内/勾选预填的主键、编号列表
|
||||
- `placements` 含 `batch` 的动作在数据管理表顶常驻,无勾选时禁用(与表删除一致)
|
||||
- `readOnly: true`:数据管理表单中标记为只读;**有「已选择」且该字段来自 `inputMapping`(或无 mapping 时的约定 id 字段)时,宿主默认不渲染该控件,预设值仍随提交传入**(与 `confirm` 无参数区对齐信息架构)。无「已选择」时仍只读展示。需要强制露出时加 `revealInForm: true`。**不要**用 `visibleWhen` 藏映射主键(会不提交)。
|
||||
- `placements` 含 `batch` 的动作:默认(`selectionMode` 为 `required` 或省略推断为 required)无勾选时禁用;`selectionMode` 为 `optional` / `none` 时无勾选也可点(见「勾选驱动模式」)
|
||||
- 需要确认的副作用操作使用 `confirmation.message`
|
||||
- 短名看不懂或会覆盖数据时,用独立的 `hoverHint`;不要把确认黄条复用成悬停
|
||||
- 模板参数使用 `{{fieldName}}`,且必须在 `inputSchema.properties` 中声明
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
- 读取 `_jiangchang_*` 元数据
|
||||
- 读取 SQLite `PRAGMA table_info`
|
||||
- **数据管理侧栏/表列表**:当库中存在 `_jiangchang_tables` 时,**仅展示**在该表注册且 `visible=1` 的业务表;孤儿物理表(有表、无注册元数据)**不得**出现在侧栏,直接访问 schema API 应返回「表不存在」。极老库无 `_jiangchang_tables` 时,宿主可保留物理表枚举 fallback(兼容 `_schema_meta` 时代)
|
||||
- 根据 schema 渲染数据管理页面
|
||||
- 根据 `editable` 推导字段是否可创建或编辑
|
||||
- 在**新增 / 编辑**表单中,若列有 `_jiangchang_columns.description`,在标签下方展示该说明(灰字;空则不显示)
|
||||
@@ -287,7 +288,7 @@ upsert_column_metadata(
|
||||
| `grid_help` | 否 | 数据管理表头「?」;空/未写则不出。与表单 `description` 独立,禁止互拷 |
|
||||
| `display_order` | 否 | 兼容字段,默认保持 `NULL` |
|
||||
| `visible` | 是 | 是否进入数据管理 schema /「列」面板(`0` = **硬隐藏**,用户无法勾回;默认不展示但可勾回请用表级 `default_visible_columns_json`) |
|
||||
| `searchable` | 是 | 是否参与搜索 |
|
||||
| `searchable` | 是 | 是否参与**列筛选**与**顶栏全局搜索**(`0` = 两者均不参与);详见下节「数据管理:列筛选与全局搜索」 |
|
||||
| `editable` | 是 | `1` 业务字段允许宿主用于新增和编辑;`0` 系统/只读字段 |
|
||||
| `display_type` | 否 | 字段渲染类型;稳定取值见下节「表单与展示:稳定字段类型」 |
|
||||
| `options_json` | 否 | 枚举选项 JSON |
|
||||
@@ -382,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` 这一份字段权限声明:
|
||||
@@ -700,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` → **创建时间**。
|
||||
@@ -708,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` 物理删除。
|
||||
|
||||
## 开发自检
|
||||
|
||||
|
||||
108
release.ps1
108
release.ps1
@@ -7,6 +7,10 @@
|
||||
Encryption, ZIP packaging, and marketplace sync are done by CI
|
||||
(.github/workflows/release_skill.yaml → reusable-release-skill.yaml@main).
|
||||
|
||||
POLICY-RELEASE-001: before tagging, requires SKILL.md version and
|
||||
scripts/util/constants.py SKILL_VERSION to equal the next tag (without prefix).
|
||||
release.ps1 does not auto-edit those fields.
|
||||
|
||||
Requires: git, PowerShell 5+
|
||||
|
||||
After fetch, runs default tests against the current working tree (before auto-commit):
|
||||
@@ -162,6 +166,93 @@ function Get-NextTag {
|
||||
return "$TagPrefix$($latest.Major).$($latest.Minor).$([int]$latest.Patch + 1)"
|
||||
}
|
||||
|
||||
function Read-SkillMdVersion {
|
||||
param([Parameter(Mandatory = $true)][string]$SkillRoot)
|
||||
|
||||
$path = Join-Path $SkillRoot "SKILL.md"
|
||||
if (-not (Test-Path -LiteralPath $path)) {
|
||||
return $null
|
||||
}
|
||||
$text = Get-Content -LiteralPath $path -Raw -Encoding UTF8 -ErrorAction SilentlyContinue
|
||||
if ([string]::IsNullOrWhiteSpace($text)) {
|
||||
return $null
|
||||
}
|
||||
$m = [regex]::Match($text, '(?m)^version:\s*["'']?([0-9]+\.[0-9]+\.[0-9]+)["'']?\s*$')
|
||||
if (-not $m.Success) {
|
||||
return $null
|
||||
}
|
||||
return $m.Groups[1].Value.Trim()
|
||||
}
|
||||
|
||||
function Read-SkillConstantsVersion {
|
||||
param([Parameter(Mandatory = $true)][string]$SkillRoot)
|
||||
|
||||
$path = Join-Path $SkillRoot "scripts\util\constants.py"
|
||||
if (-not (Test-Path -LiteralPath $path)) {
|
||||
return $null
|
||||
}
|
||||
$text = Get-Content -LiteralPath $path -Raw -Encoding UTF8 -ErrorAction SilentlyContinue
|
||||
if ([string]::IsNullOrWhiteSpace($text)) {
|
||||
return $null
|
||||
}
|
||||
$m = [regex]::Match($text, '(?m)^SKILL_VERSION\s*=\s*["'']([0-9]+\.[0-9]+\.[0-9]+)["'']')
|
||||
if (-not $m.Success) {
|
||||
return $null
|
||||
}
|
||||
return $m.Groups[1].Value.Trim()
|
||||
}
|
||||
|
||||
# POLICY-RELEASE-001: marketplace latest (git tag) must equal package manifest versions.
|
||||
function Assert-SkillReleaseVersionAlignment {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$SkillRoot,
|
||||
[Parameter(Mandatory = $true)][string]$NextTag,
|
||||
[Parameter(Mandatory = $true)][string]$TagPrefix
|
||||
)
|
||||
|
||||
$skillMdPath = Join-Path $SkillRoot "SKILL.md"
|
||||
if (-not (Test-Path -LiteralPath $skillMdPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
$expected = $NextTag
|
||||
if ($TagPrefix -and $expected.StartsWith($TagPrefix)) {
|
||||
$expected = $expected.Substring($TagPrefix.Length)
|
||||
}
|
||||
$expected = $expected.Trim()
|
||||
if ($expected -notmatch '^\d+\.\d+\.\d+$') {
|
||||
throw "Release check failed (POLICY-RELEASE-001): cannot derive semver from Next tag '$NextTag'."
|
||||
}
|
||||
|
||||
$mdVersion = Read-SkillMdVersion -SkillRoot $SkillRoot
|
||||
$constVersion = Read-SkillConstantsVersion -SkillRoot $SkillRoot
|
||||
|
||||
$problems = @()
|
||||
if ([string]::IsNullOrWhiteSpace($mdVersion)) {
|
||||
$problems += "SKILL.md missing top-level version: X.Y.Z"
|
||||
}
|
||||
elseif ($mdVersion -ne $expected) {
|
||||
$problems += "SKILL.md version=$mdVersion (want $expected)"
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($constVersion)) {
|
||||
$problems += "scripts/util/constants.py missing SKILL_VERSION = `"X.Y.Z`""
|
||||
}
|
||||
elseif ($constVersion -ne $expected) {
|
||||
$problems += "SKILL_VERSION=$constVersion (want $expected)"
|
||||
}
|
||||
|
||||
if ($problems.Count -gt 0) {
|
||||
throw (
|
||||
"Release aborted (POLICY-RELEASE-001): Next tag=$NextTag requires manifest version $expected, but " +
|
||||
($problems -join "; ") +
|
||||
". Bump SKILL.md version and scripts/util/constants.py SKILL_VERSION to $expected (and CHANGELOG), then re-run release.ps1. " +
|
||||
"release.ps1 does not auto-edit these fields."
|
||||
)
|
||||
}
|
||||
|
||||
Write-Host "Version alignment OK: tag=$NextTag == SKILL.md=$mdVersion == SKILL_VERSION=$constVersion" -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
function Assert-SkillReleasePackagingSources {
|
||||
param([Parameter(Mandatory = $true)][string]$SkillRoot)
|
||||
|
||||
@@ -283,6 +374,15 @@ try {
|
||||
Assert-SkillReleasePackagingSources -SkillRoot $skillRoot
|
||||
}
|
||||
|
||||
$nextTag = Get-NextTag -TagPrefix $Prefix
|
||||
Write-Host "Next tag: $nextTag" -ForegroundColor Green
|
||||
Assert-SkillReleaseVersionAlignment -SkillRoot $skillRoot -NextTag $nextTag -TagPrefix $Prefix
|
||||
|
||||
$existing = @(Get-GitOutput -GitArgs @("tag", "--list", $nextTag))
|
||||
if ($existing.Length -gt 0) {
|
||||
throw "Tag already exists: $nextTag"
|
||||
}
|
||||
|
||||
Invoke-DefaultTests -SkillRoot $skillRoot -Skip:$SkipTests
|
||||
|
||||
Ensure-CleanOrAutoCommit -DoAutoCommit:$AutoCommit -NeedClean:$RequireClean -IsDryRun:$DryRun -Msg $CommitMessage
|
||||
@@ -304,14 +404,6 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
$nextTag = Get-NextTag -TagPrefix $Prefix
|
||||
Write-Host "Next tag: $nextTag" -ForegroundColor Green
|
||||
|
||||
$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 -F <utf8-msg-file> ($Message)" -ForegroundColor Yellow
|
||||
Write-Host "[DryRun] Would run: git push origin $nextTag" -ForegroundColor Yellow
|
||||
|
||||
@@ -277,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"
|
||||
|
||||
@@ -235,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,
|
||||
@@ -511,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:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。"""
|
||||
|
||||
SKILL_SLUG = "your-skill-slug"
|
||||
SKILL_VERSION = "1.0.97"
|
||||
SKILL_VERSION = "1.0.108"
|
||||
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", "selectionMode"}
|
||||
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,17 @@ 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")
|
||||
selection_mode = bind.get("selectionMode")
|
||||
if selection_mode is not None and selection_mode not in ("required", "none", "optional"):
|
||||
errors.append(
|
||||
f"{action_id}: bind.selectionMode must be required|none|optional, got {selection_mode!r}"
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
@@ -432,6 +445,136 @@ 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_schema_allows_bind_selection_mode(self) -> None:
|
||||
try:
|
||||
import jsonschema
|
||||
except ImportError:
|
||||
self.fail("jsonschema is required")
|
||||
schema = _load_actions_schema()
|
||||
bind_props = schema["$defs"]["bind"]["properties"]
|
||||
self.assertIn("selectionMode", bind_props)
|
||||
self.assertEqual(
|
||||
bind_props["selectionMode"].get("enum"),
|
||||
["required", "none", "optional"],
|
||||
)
|
||||
manifest = {
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actions": [
|
||||
{
|
||||
"id": "probe-selection-mode",
|
||||
"label": "处理队列",
|
||||
"description": "验证 bind.selectionMode 可通过 Schema",
|
||||
"placements": ["toolbar", "batch"],
|
||||
"executionProfile": "async",
|
||||
"interaction": "form",
|
||||
"bind": {
|
||||
"tables": ["demo_items"],
|
||||
"selectionMode": "optional",
|
||||
"inputMapping": {"itemIds": "$selection.ids"},
|
||||
"labelColumn": "display_name",
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["itemIds", "pickCount"],
|
||||
"properties": {
|
||||
"itemIds": {"type": "string", "readOnly": True},
|
||||
"pickCount": {"type": "number", "default": 1, "minimum": 1},
|
||||
},
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "process",
|
||||
"args": ["--ids", "{{itemIds}}", "--pick", "{{pickCount}}"],
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
jsonschema.validate(instance=manifest, schema=schema)
|
||||
|
||||
def test_action_examples_user_facing_copy_avoids_jargon(self) -> None:
|
||||
"""示例 Action 的用户可见字符串不得含常见开发黑话(属性名本身除外)。"""
|
||||
examples_dir = os.path.join(get_skill_root(), "assets", "examples")
|
||||
# 故意拆开,避免本测试源码触发自身扫描。
|
||||
forbidden = (
|
||||
"pickCount",
|
||||
"itemIds",
|
||||
"status=",
|
||||
"import-urls",
|
||||
"download-enqueue",
|
||||
"bind.selectionMode",
|
||||
"selectionMode=",
|
||||
)
|
||||
# async/pending 作为整词出现在 description 才禁止;executionProfile 值除外。
|
||||
word_forbidden = re.compile(r"\b(async|pending|sync)\b", re.I)
|
||||
|
||||
for name in os.listdir(examples_dir):
|
||||
if not name.endswith(".action.example.json"):
|
||||
continue
|
||||
path = os.path.join(examples_dir, name)
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
actions = data["actions"] if isinstance(data, dict) and "actions" in data else [data]
|
||||
for action in actions:
|
||||
if not isinstance(action, dict):
|
||||
continue
|
||||
texts: list[str] = []
|
||||
for key in ("description",):
|
||||
if action.get(key):
|
||||
texts.append(str(action[key]))
|
||||
conf = action.get("confirmation") or {}
|
||||
if isinstance(conf, dict) and conf.get("message"):
|
||||
texts.append(str(conf["message"]))
|
||||
props = ((action.get("inputSchema") or {}).get("properties")) or {}
|
||||
for prop in props.values():
|
||||
if not isinstance(prop, dict):
|
||||
continue
|
||||
for key in ("title", "description"):
|
||||
if prop.get(key):
|
||||
texts.append(str(prop[key]))
|
||||
blob = "\n".join(texts)
|
||||
for token in forbidden:
|
||||
self.assertNotIn(
|
||||
token,
|
||||
blob,
|
||||
msg=f"{name} user-facing copy must not contain {token!r}",
|
||||
)
|
||||
match = word_forbidden.search(blob)
|
||||
if match is not None:
|
||||
self.fail(
|
||||
f"{name} user-facing copy must not contain {match.group(0)!r}"
|
||||
)
|
||||
|
||||
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")
|
||||
@@ -593,6 +736,14 @@ class TestActionsManifest(unittest.TestCase):
|
||||
"afterFailure",
|
||||
"cellPresentation",
|
||||
"CELL_PRESENTATION.md",
|
||||
"labelColumn",
|
||||
"selectionMode",
|
||||
"勾选驱动",
|
||||
"用户可见文案",
|
||||
"双受众",
|
||||
"已选择",
|
||||
"revealInForm",
|
||||
"映射只读",
|
||||
):
|
||||
self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}")
|
||||
self.assertNotIn(
|
||||
|
||||
@@ -14,7 +14,7 @@ from jiangchang_skill_core import config
|
||||
|
||||
# scripts/ 已由 _support 注入 sys.path
|
||||
from cli.app import main
|
||||
from util.constants import SKILL_SLUG
|
||||
from util.constants import SKILL_SLUG, SKILL_VERSION
|
||||
|
||||
|
||||
class TestCliSmoke(unittest.TestCase):
|
||||
@@ -57,6 +57,7 @@ class TestCliSmoke(unittest.TestCase):
|
||||
self.assertIn("version", payload)
|
||||
self.assertIn("skill", payload)
|
||||
self.assertEqual(payload["skill"], SKILL_SLUG)
|
||||
self.assertEqual(payload["version"], SKILL_VERSION)
|
||||
|
||||
def test_init_db_creates_database_and_is_idempotent(self) -> None:
|
||||
with IsolatedDataRoot(user_id="_cli_init_db"):
|
||||
|
||||
@@ -40,6 +40,7 @@ POLICY_IDS = (
|
||||
"POLICY-RPA-006",
|
||||
"POLICY-PACKAGING-001",
|
||||
"POLICY-PACKAGING-002",
|
||||
"POLICY-RELEASE-001",
|
||||
"POLICY-DOCS-001",
|
||||
"POLICY-LOGGING-001",
|
||||
"POLICY-LOGGING-002",
|
||||
@@ -57,6 +58,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",
|
||||
)
|
||||
@@ -1529,6 +1531,44 @@ class TestPolicyDataSync001(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
class TestPolicyRelease001(unittest.TestCase):
|
||||
def test_release_ps1_enforces_version_alignment(self) -> None:
|
||||
skill_root = get_skill_root()
|
||||
path = os.path.join(skill_root, "release.ps1")
|
||||
self.assertTrue(
|
||||
os.path.isfile(path),
|
||||
msg=_policy_msg("POLICY-RELEASE-001", "release.ps1", "file missing"),
|
||||
)
|
||||
text = _read_text(path)
|
||||
self.assertIn(
|
||||
"Assert-SkillReleaseVersionAlignment",
|
||||
text,
|
||||
msg=_policy_msg(
|
||||
"POLICY-RELEASE-001",
|
||||
"release.ps1",
|
||||
"must define Assert-SkillReleaseVersionAlignment",
|
||||
),
|
||||
)
|
||||
self.assertIn(
|
||||
"POLICY-RELEASE-001",
|
||||
text,
|
||||
msg=_policy_msg(
|
||||
"POLICY-RELEASE-001",
|
||||
"release.ps1",
|
||||
"must reference POLICY-RELEASE-001",
|
||||
),
|
||||
)
|
||||
self.assertRegex(
|
||||
text,
|
||||
r"Assert-SkillReleaseVersionAlignment\s+-SkillRoot",
|
||||
msg=_policy_msg(
|
||||
"POLICY-RELEASE-001",
|
||||
"release.ps1",
|
||||
"must invoke Assert-SkillReleaseVersionAlignment before tagging",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class TestPolicyDocs001(unittest.TestCase):
|
||||
def test_policy_matrix_exists_and_lists_policy_ids(self) -> None:
|
||||
skill_root = get_skill_root()
|
||||
|
||||
@@ -553,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:
|
||||
@@ -703,6 +738,73 @@ metadata:
|
||||
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,18 @@ 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("A2-12", checklist)
|
||||
self.assertIn("TUTORIAL_SURFACES.md", checklist)
|
||||
|
||||
def test_naming_md_exists_and_covers_standards(self) -> None:
|
||||
text = self._read("development/NAMING.md")
|
||||
@@ -356,6 +368,13 @@ class TestDocsStandards(unittest.TestCase):
|
||||
"跨表引用 lookup",
|
||||
"\"multiple\": true",
|
||||
"Lookup 多选",
|
||||
"数据管理:列筛选与全局搜索",
|
||||
"列筛选与全局搜索",
|
||||
"searchable: 1",
|
||||
"筛选栏控件",
|
||||
"表增删改与退役",
|
||||
"drop_deprecated_user_table",
|
||||
"孤儿物理表",
|
||||
):
|
||||
self.assertIn(marker, text, msg=f"SCHEMA.md missing {marker!r}")
|
||||
|
||||
@@ -375,6 +394,7 @@ class TestDocsStandards(unittest.TestCase):
|
||||
"字段条件显隐",
|
||||
"visibleWhen",
|
||||
"grid_help",
|
||||
"列筛选",
|
||||
):
|
||||
self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}")
|
||||
|
||||
@@ -395,11 +415,24 @@ class TestDocsStandards(unittest.TestCase):
|
||||
def test_visible_when_example_is_listed(self) -> None:
|
||||
text = self._read("assets/examples/README.md")
|
||||
self.assertIn("visible-when-action.example.json", text)
|
||||
self.assertIn("selection-mode-optional.action.example.json", text)
|
||||
checklist = self._read("development/CHECKLIST.md")
|
||||
self.assertIn("A4-7", checklist)
|
||||
self.assertIn("visibleWhen", checklist)
|
||||
self.assertIn("A4-8", checklist)
|
||||
self.assertIn("revealInForm", checklist)
|
||||
self.assertIn("A4-9", checklist)
|
||||
self.assertIn("selectionMode", checklist)
|
||||
self.assertIn("A4-10", checklist)
|
||||
self.assertIn("用户可见文案", checklist)
|
||||
runtime = self._read("development/SKILL_ACTION_RUNTIME.md")
|
||||
self.assertIn("visibleWhen", runtime)
|
||||
self.assertIn("revealInForm", runtime)
|
||||
self.assertIn("selectionMode", runtime)
|
||||
self.assertIn("用户可见文案", runtime)
|
||||
actions = self._read("references/ACTIONS.md")
|
||||
self.assertIn("用户可见文案", actions)
|
||||
self.assertIn("双受众", actions)
|
||||
|
||||
def test_guarded_docs_do_not_reference_amazon_implementation(self) -> None:
|
||||
forbidden = (
|
||||
|
||||
@@ -50,6 +50,26 @@ class TestSkillMetadata(unittest.TestCase):
|
||||
self.assertEqual(SKILL_SLUG, skill_dir)
|
||||
self.assertEqual(slug_md, skill_dir)
|
||||
|
||||
def test_skill_version_alignment(self) -> None:
|
||||
"""SKILL.md version 必须与 constants.SKILL_VERSION 一致(POLICY-RELEASE-001 日常侧)。"""
|
||||
root = get_skill_root()
|
||||
md_path = os.path.join(root, "SKILL.md")
|
||||
with open(md_path, encoding="utf-8") as f:
|
||||
md = f.read()
|
||||
match = re.search(r"(?m)^version:\s*[\"']?([0-9]+\.[0-9]+\.[0-9]+)[\"']?\s*$", md)
|
||||
self.assertIsNotNone(match, "SKILL.md must declare top-level semver version")
|
||||
assert match is not None
|
||||
md_version = match.group(1)
|
||||
|
||||
from util.constants import SKILL_VERSION
|
||||
|
||||
self.assertEqual(
|
||||
md_version,
|
||||
SKILL_VERSION,
|
||||
"SKILL.md version 必须与 constants.SKILL_VERSION 一致(发布前再与 git tag 对齐)",
|
||||
)
|
||||
self.assertRegex(SKILL_VERSION, r"^\d+\.\d+\.\d+$")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
221
tests/test_tutorial_surfaces.py
Normal file
221
tests/test_tutorial_surfaces.py
Normal file
@@ -0,0 +1,221 @@
|
||||
# -*- 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_market_docs_forbid_redundant_page_h1(self) -> None:
|
||||
"""宿主已有技能名与 Tab 标题:说明/教程正文禁止再用 # 一级标题。"""
|
||||
|
||||
def strip_fences(src: str) -> str:
|
||||
return re.sub(r"```.*?```", "", src, flags=re.S)
|
||||
|
||||
for rel in ("README.md", "TUTORIAL.md"):
|
||||
body = strip_fences(_read(rel))
|
||||
# YAML frontmatter 里不应出现 ATX H1;正文亦禁止 #
|
||||
self.assertIsNone(
|
||||
re.search(r"(?m)^#\s+\S", body),
|
||||
msg=(
|
||||
f"{rel} must not use ATX H1 (# …); host already shows skill name / tab. "
|
||||
"Use ## for sections (see TUTORIAL_SURFACES.md §0.1)."
|
||||
),
|
||||
)
|
||||
self.assertIsNone(
|
||||
re.search(r"(?m)^#\s+.·\s*使用教程\s*$", body),
|
||||
msg=f"{rel} must not start sections with '# … · 使用教程'",
|
||||
)
|
||||
|
||||
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