Compare commits

...

7 Commits

Author SHA1 Message Date
295b2356ac docs: deposit selection summary and bind.labelColumn contract
All checks were successful
技能自动化发布 / release (push) Successful in 5s
2026-09-02 18:37:33 +08:00
7257d09813 Release v1.0.101: Action file/directory pathRoot contract
All checks were successful
技能自动化发布 / release (push) Successful in 6s
2026-08-31 19:14:01 +08:00
9d33c5ea2c docs: sediment TUTORIAL_SURFACES contract for README vs tutorial and three entry surfaces
All checks were successful
技能自动化发布 / release (push) Successful in 5s
2026-08-31 14:18:42 +08:00
b5d120469b Release skill-template v1.0.99: table registry contract and drop_deprecated_user_table
All checks were successful
技能自动化发布 / release (push) Successful in 7s
2026-08-25 13:40:08 +08:00
d35d04f779 Release v1.0.98: data management filter and global search contract
All checks were successful
技能自动化发布 / release (push) Successful in 5s
2026-08-23 17:10:52 +08:00
cbebd19ab3 docs(html): 沉淀数据管理 iframe 预览布局契约
All checks were successful
技能自动化发布 / release (push) Successful in 4s
2026-08-22 16:35:05 +08:00
05764cac15 docs: add Action label naming guidance in ACTIONS.md (v1.0.96)
All checks were successful
技能自动化发布 / release (push) Successful in 5s
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 11:52:41 +08:00
35 changed files with 1407 additions and 122 deletions

View File

@@ -9,6 +9,48 @@
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
## 1.0.102
- 沉淀数据管理 Action「已选择」摘要契约:confirm/form 弹层由宿主默认展示勾选对象(#id + 可读标签);技能零改动即可用
- 新增可选 `bind.labelColumn`(仅影响展示,不改提交 payload,不决定业务跳过逻辑)
- 更新 `skill-actions.schema.json`、`ACTIONS.md`、`SKILL_ACTION_RUNTIME.md`;新增 `selection-label-column.action.example.json`
## 1.0.101
- 沉淀 Action 表单本地路径契约:`file` / `directory` 可选 **`pathRoot`**(相对技能数据根的起始目录;省略则宿主默认打开本技能 `input/`)
- 更新 `skill-actions.schema.json`、`ACTIONS.md`、`DATA_PATHS.md`;新增 `assets/examples/path-root-file-action.example.json`
- 与宿主导入面板默认 `input/` 划清边界;禁止技能写死用户 ID 或绝对盘符
## 1.0.100
- 沉淀用户市场文档契约 `development/TUTORIAL_SURFACES.md`:**说明 Tab ≠ 教程 Tab**;教程按数据管理 ∥ 对话、定时可选三入口结构
- 重写根 `README.md` / `TUTORIAL.md` 模板骨架;CHECKLIST 增 A2-7~A2-11;`actions.json` 准备类优先 toolbar、skill-detail 不可作教程主路径
- 新增 `tests/test_tutorial_surfaces.py`(POLICY-TUTORIAL-001);REQUIREMENTS §0.1 增教程章节列与用户文档对齐要求
## 1.0.99
- 沉淀数据管理**表注册门禁**契约:存在 `_jiangchang_tables` 时,宿主仅展示注册且 `visible=1` 的表;孤儿物理表不展示(匠厂宿主 ≥6.8.25)
- 新增「表增删改与退役」:`drop_deprecated_user_table()` 标准迁移;禁止只删元数据留物理表;新技能禁止 `task_logs`
- `LOGGING.md` 可观测性三分法;`DATA_PATHS.md` 卸载不删用户数据;Checklist A6-13 / A6-14;`tests/samples/test_table_deprecation_migration.py.sample`
## 1.0.98
- 沉淀数据管理**列筛选与全局搜索**契约:`searchable` 同时控制列筛选字段列表与顶栏全局搜索;存库 value / 展示 label 分工;筛选栏枚举/boolean/lookup 用下拉 Select(与表单开关/分段单选区分)
- 全局搜索:options label 映射、boolean 别名、lookup 多选 token、`datetime_unix_seconds` 日期区间
- `ACTIONS.md` 表单对照表补充「列筛选」脚注;`CHECKLIST` A6-11 / A6-12;`display_metadata_validator` 对未开 searchable 的枚举/boolean 列软 warning
## 1.0.97
- 沉淀数据管理 HTML 预览**布局契约**:`references/ACTIONS.md` 新增「宿主预览布局」(流体铺满 iframe、禁横向滚动、表格断行等)
- 升级 `assets/html/example-report.html` 为通用布局基线;`test_html_page_service` 守护不退化
- `DEVELOPMENT.md` / `CHECKLIST`(S0-12、A6-10)/ `TESTING.md` 交叉引用
## 1.0.96
- 在 `references/ACTIONS.md` 沉淀数据管理技能按钮中文名(`label`)参考表:同类通用动作用同一动词,不是白名单
- 打开类改为「打开目录 / 打开浏览器」,一点即开、成功不弹「执行成功」;示例不再使用「打开文件位置」
- 交活 Checklist A7-7;POLICY_MATRIX 软规则:不扫描 label 关键字
## 1.0.95
- 正式沉淀「业务行生命周期」通用契约(`references/WORK_ITEM_STATUS.md`):用可操作/进行中/成功·失败·跳过类终态等**角色**约束处理队列,不绑死某一技能的状态名

10
DEMO.md
View File

@@ -15,9 +15,13 @@ demo_video_url: ""
## 建议观看顺序
1. 如何发起一次典型任务
2. 执行过程中会看到什么提示 / 进度
3. 成功后的结果长什么样
建议与 `TUTORIAL.md` 三入口结构对齐(有则拍、无则跳过):
1. 数据管理:准备数据 → 主流程按钮 → 结果表/报告
2. 对话任务(若有):一句自然语言触发同一能力
3. 定时任务(若有):到点自动跑;若本技能无定时,演示中可说明「不需要定时」
成功后的结果长什么样(表、任务中心、HTML 等)
## 没有视频时

View File

@@ -1,12 +1,13 @@
---
description: "用用户能理解的方式说明这个技能能做什么、适合什么场景,以及如何开始使用。"
description: "用用户能理解的方式说明这个技能能做什么、适合什么场景,以及边界与结果形态。"
---
> **模板说明(复制后删除本段)**
> 本文是技能市场详情与普通用户说明,**不是** Agent 参考索引、CLI 契约或开发文档。
> frontmatter 的 `description` 必须是用户能看懂的一句话能力描述;**不得**写成 CLI、schema、references、development、契约、数据结构、开发规范或 Agent 参考索引。
> 正文只回答用户关心的问题,**不要**把 Playwright、DOM、Python、RPA、references、development、schema 等作为面向用户的主说明内容;若业务上必须出现技术词,须用用户能理解的方式解释,且不能喧宾夺主。
> **复制后验收章节(标题可微调,建议保留):**「能帮你做什么」「适合什么场景」「使用前需要准备什么」「你可以这样告诉 Agent」「执行后你会得到什么」「注意事项」「常见问题」。
> 本文是技能市场 **「说明」** Tab(`readme_md`),**不是**使用教程。
> frontmatter 的 `description` 必须是用户能看懂的一句话能力描述;**不得**写成 CLI、schema、references、development、契约或「如何开始使用」类教程承诺。
> **禁止**写入:分步操作(1/2/3)、Agent 示例话术块、「入口一/入口二」专章、逐步排障。这些一律写在根目录 `TUTORIAL.md`。
> 正文只回答:是什么、能做什么、适合谁、准备什么、得到什么、注意事项。完整契约见 `development/TUTORIAL_SURFACES.md` §1。
> **复制后验收章节(标题可微调):**「能帮你做什么」「适合什么场景」「使用前需要准备什么」「怎么使用」「执行后你会得到什么」「注意事项」「常见问题」。
# 【技能名称】
@@ -17,7 +18,7 @@ description: "用用户能理解的方式说明这个技能能做什么、适合
## 这个技能能帮你做什么
用 3 到 6 条业务能力说明技能价值,例如:
用 3 到 6 条业务能力说明技能价值(**不要**写成操作步骤),例如:
- 【自动整理/查询/生成/同步/核对/提交】某类业务数据
- 帮你减少重复操作,降低人工录入或核对成本
@@ -38,42 +39,33 @@ description: "用用户能理解的方式说明这个技能能做什么、适合
## 使用前需要准备什么
说明用户在使用前要准备的东西,例如:
说明**前置条件**(装什么、要不要账号、要不要外站登录),**不要**写「去哪张表点哪个按钮」:
- **网页自动化类:** 首次运行可由技能自动登记平台账号(浏览器会话由系统隔离管理)。也可在「账号管理」里指定登录标识。登记账号 ≠ 已登录目标站
- **账号在哪里看:** 已登记账号可在「数据管理 / 账号管理」中查看;每个账号使用独立浏览器会话,批量或多账号任务不会共用登录状态
- **是否要登录目标站:** 按本技能说明(有的平台必须登录,有的公开页可不登录);与「是否有账号记录」不是一回事
- **网页自动化类:** 可能需要在账号管理中登记平台账号;登记账号 ≠ 已登录目标站(细节见使用教程)
- 相关业务权限(查询、导出、提交等)已具备
- 需要处理的文件、时间范围、订单号、客户名称、项目名称等信息
- 如果涉及验证码或人工确认,请保证能及时在弹出的浏览器窗口中操作
- 需要处理的文件、时间范围、单号等信息(若适用)
- 若涉及验证码或人工确认,使用时请能及时在弹出窗口中操作
---
## 你可以这样告诉 Agent
## 怎么使用
用自然语言示例展示用户可以怎么发起任务,例如:
本技能支持哪些宿主入口,用**一两句话**概括即可,例如:
> “帮我查询今天的【业务对象】,整理成表格。”
>
> “帮我核对这些【数据/单据】是否有异常,并告诉我问题在哪里。”
>
> “帮我把【时间范围】内的【业务数据】导出并生成处理报告。”
>
> “帮我根据这个文件里的内容,逐条完成【业务动作】。”
- 支持 **数据管理**(点表操作)与 **对话任务**(自然语言);是否提供定时任务按本技能说明。
> 注意:示例应是自然语言,不要写命令行命令。
**逐步操作、示例话术与排障,请查看技能市场「使用教程」Tab。**
(完整写法见 `development/TUTORIAL_SURFACES.md`。)
---
## 执行后你会得到什么
说明用户会看到什么结果,例如:
说明用户会看到什么**结果形态**(不要教如何点出来),例如:
- 任务是否成功完成
- 处理了多少条数据
- 哪些内容成功、哪些失败
- 失败原因和建议处理方式
- 生成的文件、表格、报告或记录
- 处理了多少条数据;哪些成功、哪些失败
- 生成的文件、表格、报告或业务表记录
---
@@ -90,18 +82,16 @@ description: "用用户能理解的方式说明这个技能能做什么、适合
## 常见问题
### 为什么任务没有查到数据?
> FAQ 写**概念与边界**;需要「点哪里」的排障写到「使用教程」。
> 可能是时间范围、关键词、账号权限或系统筛选条件不正确。请先确认输入信息是否准确。
### 这个技能适合我吗?
### 为什么任务执行到一半需要我操作?
> 若你的场景与上文「适合什么场景」一致,可以安装后按「使用教程」完成第一次操作。
> 可能遇到了登录、验证码、权限确认、二次审批或系统异常。请根据 Agent 的提示完成必要操作。
### 我不会操作,从哪里开始?
### 结果文件在哪里查看?
> 请根据 Agent 返回的结果说明查看生成文件、任务记录或处理报告。
> 请打开技能市场 **「使用教程」** Tab,按「入口一:数据管理」或「入口二:对话任务」任选一种跟做。
### 已经登记的账号在哪里查看?
> 请到匠厂的「数据管理 / 账号管理」查看账号记录和状态。若任务仍提示账号异常,请把账号编号、任务时间和界面提示原文提供给支持人员;不要发送密码、Cookie 或验证码。
> 请到匠厂的「数据管理 / 账号管理」查看账号记录和状态。不要发送密码、Cookie 或验证码。

View File

@@ -1,7 +1,7 @@
---
name: 技能开发模板(通用业务版)
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
version: 1.0.95
version: 1.0.102
author: 深圳匠厂科技有限公司
metadata:
openclaw:
@@ -109,10 +109,11 @@ python {baseDir}/scripts/main.py init-db
**回答「怎么用 / 能做什么」时的要求:**
1. 「能做什么 / 场景 / FAQ」以 `README.md` 为主;「第一次怎么用完」以 `TUTORIAL.md` 为主。
1. 「能做什么 / 场景 / 概念类 FAQ」以 `README.md`(说明 Tab)为主;「点哪里 / 说什么 / 排障」以 `TUTORIAL.md`(教程 Tab)为主。说明 Tab **不应**含分步操作;若 README 仍有旧式步骤,以教程为准并提示开发者按 `development/TUTORIAL_SURFACES.md` 修正。
2. **禁止**在回答中粘贴 `python …/scripts/main.py …` 或其它命令行(除非用户明确索要命令,或自称技术人员/开发者)。
3. **禁止**为答用法而去 `dir` / `ls` / 列技能目录,或默认打开 `references/`、`development/`;仅当用户明确问命令、参数、集成或排错时再读 `references/`。
4. **不要**向普通用户暴露 Playwright、DOM、Python、RPA、库表名、`error_code` 等实现细节,除非用户明确询问技术实现。
5. 用户问「怎么用」时:**优先读 `TUTORIAL.md`**;不要只根据 README 臆造按钮路径。
**文档读取边界:**
@@ -125,13 +126,13 @@ python {baseDir}/scripts/main.py init-db
| 文档 | 读者 | 用途 |
|------|------|------|
| 根目录 `README.md` | 普通用户 | 市场 **说明** Tab(`readme_md`) |
| 根目录 `TUTORIAL.md` | 普通用户 | 市场 **教程** Tab(`tutorial_md`) |
| 根目录 `README.md` | 普通用户 | 市场 **说明** Tab(`readme_md`):能力/场景/边界;**非教程** |
| 根目录 `TUTORIAL.md` | 普通用户 | 市场 **教程** Tab(`tutorial_md`):分入口操作;结构见 `development/TUTORIAL_SURFACES.md` |
| 根目录 `DEMO.md` | 普通用户 | 市场 **演示** Tab(`demo_video_url` + `demo_md`) |
| 根目录 `CHANGELOG.md` | 普通用户 | 市场 **更新日志** Tab(按版本小节 → `changelog`) |
| `SKILL.md`(本文) | LLM / OpenClaw 平台 | 技能入口、触发与运行契约摘要 |
| `references/` | Agent 编排 / 调用 | CLI、Action、schema 契约(Agent 主读) |
| `development/` | 开发者 / AI 编程代理 | 需求、开发教程、测试、深度技术规范;**产品入口**见 `development/PRODUCT_SURFACES.md` |
| `development/` | 开发者 / AI 编程代理 | 需求、开发教程、测试、深度技术规范;**产品入口**见 `development/PRODUCT_SURFACES.md`;**用户文档契约**见 `development/TUTORIAL_SURFACES.md` |
## 目录约定

View File

@@ -1,12 +1,34 @@
> **模板说明(复制后删除本段)**
> 本文对应技能市场 **「使用教程」** Tab(发布时同步为 `tutorial_md`)。
> 面向普通用户写**可跟着做的步骤**,不要写 CLI、目录结构、release、开发规范。
> 与根 `README.md`(技能说明)分工:README 讲「是什么 / 能做什么」;本文讲「第一次怎么用完」。
> **复制后验收章节(标题可微调):**「开始前」「第一步…」「第二步…」「常见卡住怎么办」。
> 与根 `README.md`(技能说明)分工:README 讲「是什么 / 能做什么 / 边界」;本文讲「从哪个入口怎么用完」。
> **完整契约:** `development/TUTORIAL_SURFACES.md`(三入口结构、说明≠教程、skill-detail 不可作主路径)。
> **复制后验收章节(标题可微调,语义不可缺):**「本技能做什么」「开始前」「入口一:数据管理」「入口二:对话任务」「入口三:定时任务」「主能力对照表」「常见卡住怎么办」。
> 若本技能 §0.1 未挂数据管理 / 对话 / 定时,对应入口节改为「不适用」并写一句理由(与 REQUIREMENTS 一致)。
# 【技能名称】使用教程
# 【技能名称】· 使用教程
用最短路径带用户完成一次成功使用。步骤要具体,可用「打开匠厂 → …」这类操作语言。
本技能有两种**平行**的使用方式(按本技能入口定位填写;未挂的入口删掉或写不适用):
- **数据管理**:在左侧表上点工具栏 / 行内按钮
- **对话任务**:用自然语言让 Agent 完成同一能力
两种方式触发同一套业务能力,结果一致,选你习惯的即可。
定时任务是否提供:见下方「入口三」(没有则写「本技能不需要定时」+ 理由)。
---
## 本技能做什么
用业务语言画清闭环(**不要**写库表英文名或命令行),例如:
```text
【输入/准备】 → 【核心动作】 → 【结果可见处】 → 【可选报告/导出】
```
- **输入**:……
- **输出**:……
- **说明**:边界(教学/正式、是否接外站等)
---
@@ -14,43 +36,105 @@
确认:
1. 已安装并登录匠厂客户端
2. 已在技能市场安装本技能(或更新到最新版)
3. 【网页自动化类技能】首次运行一般会自动登记本平台账号;也可在「账号管理」中指定登录标识。浏览器会话由系统隔离管理——**不要**用本机日常浏览器用户目录去跑真实站
4. 【按平台】若本技能说明「需要先登录目标站」,请在弹出的有头浏览器中按提示登录或扫码;「登记账号」不等于「已登录目标站」。若说明「可不登录也能用」,仍须有可用的账号会话目录
5. 【网页自动化类技能】已登记账号可在「数据管理 / 账号管理」查看;每个账号都有独立浏览器会话
6. 【如需】需要的文件 / 时间范围 / 单号等已准备好
1. 已安装并登录匠厂客户端
2. 已在技能市场安装本技能(建议更新到最新版)
3. 【网页自动化类】账号登记 / 目标站登录要求(按本技能;登记 ≠ 已登录)
4. 【如需】文件 / 时间范围 / 单号等已准备好
5. 【如需】首次须先完成「载入样例 / 导入 / 同步」等准备动作(见入口一或入口二)
---
## 第一步:打开技能并说清目标
## 入口一:数据管理
在匠厂里新建任务或打开对话,用自然语言说明要做什么,例如:
> 若本技能无业务表、无 toolbar/row/batch:整节改为「不适用」+ 理由。
> “帮我【完成某件业务事】,范围是【时间 / 对象 / 条件】。”
适合:对照数据、喜欢点界面、机房/运营用户。
建议一次只交代一件事;范围说清楚,减少来回确认。
### 左侧各表是干什么的
| 表名(中文) | 作用 |
|--------------|------|
| 【表 A】 | …… |
| 【表 B】 | …… |
### 首次准备(若需要)
1. 进入 **数据管理** → **【主表】**
2. 点击表顶 **「【准备类按钮 label】」**(若为空表可点,写明是否须选中行)
3. 成功后应看到:……
> **注意:** 准备/载入/导入类按钮须挂在数据管理 **toolbar**(用户看得见)。不要把「技能详情页点按钮」写成主路径——详情页常只显示四 Tab,不一定渲染 Action 按钮。
### 主流程
1. 进入 **【主表】**
2. 【是否选中行】
3. 点击 **「【主能力 label】」**
4. 到 **【结果表 / 任务中心 / 报告】** 核对结果
### 行级 / 批量(若有)
| 你想做什么 | 操作 |
|------------|------|
| …… | **【表】** 行内 → **「【label】」** |
| …… | 勾选多行 → **「【label】」** |
---
## 第二步:按提示补充信息
## 入口二:对话任务
如果 Agent 问你缺什么(账号、文件、筛选条件),按提示补充即可。
> 若本技能不挂 `agent`:整节改为「不适用」+ 理由。
- 有文件时:按界面提示选择或拖入
- 有账号时:确认已在对应平台登录,且具备操作权限
适合:临时提问、要解释、不想切换表。
### 什么时候用对话
- ……
- 与入口一效果相同;习惯看表时优先用数据管理
### 示例话术
| 你想做什么 | 可以这样说 |
|------------|------------|
| …… | 「……」 |
| …… | 「……」 |
### Agent 可能追问什么
- 缺参数时:……
- 失败时:可改用数据管理点按钮
---
## 第三步:等待结果并核对
## 入口三:定时任务
任务结束后,请核对:
> **有 `cron`:** 写如何新建定时、选哪个 Action、默认参数、预期结果。
> **无 `cron`:** 用下面「不需要」模板,删掉「如何配置」小节。
- 是否成功完成
- 处理了多少条 / 哪些失败
- 生成的文件、表格或记录是否在预期位置
### 本技能不需要定时时(示例文案,按需保留/改写)
若失败,先看失败原因说明,再按下一节排查。
本技能**未提供定时任务入口**,原因:【一次性 / 须人指定对象 / 须当场确认 / ……】。
请使用 **数据管理** 或 **对话任务**。
### 本技能需要定时时(示例结构)
1. 打开侧栏 **定时任务** → 新建
2. 选择本技能 → Action **「【label】」**
3. 填写参数 → 保存;可「立即运行」验证
4. 到点后在【结果表 / 任务中心】查看
---
## 主能力对照表
同一能力,两种主入口(按本技能实际填写;只挂一端的注明):
| 想做什么 | 数据管理 | 对话任务 |
|----------|----------|----------|
| …… | **【表】** → **「【label】」** | 「……」 |
| …… | …… | …… |
按钮名必须与 `assets/actions.json` 的 `label` 一致。
---
@@ -58,10 +142,10 @@
| 现象 | 你可以先试 |
|------|------------|
| 提示未登录 / 无权限 | 确认匠厂已登录;业务账号是否在线、权限是否足够 |
| 提示账号不存在 / 被占用 | 到「数据管理 / 账号管理」确认账号记录和状态,稍后再试;不要重复创建账号 |
| 提示缺少必要信息 | 把时间范围、单号、文件等说完整后再试一次 |
| 一直没有结果 | 到任务中心查看进度或失败原因;必要时重新发起 |
| 结果不对 | 用更明确的条件再说一次,或缩小范围重试 |
| 主表是空的 | 先做「首次准备」或对话载入/导入 |
| 按钮点不了 | 是否须先选中行;是否装错版本 |
| 一直没有结果 | 看任务中心 / 运行日志;async 任务看进度 |
| 【网页自动化】未登录 / 无权限 | 确认账号在线与目标站登录状态 |
| 结果不对 | 缩小范围重试,或改用数据管理核对表数据 |
仍无法解决时,把「你说了什么 + 界面提示原文」发给支持人员,便于排查。
仍无法解决时,把「你用的入口 + 操作步骤 + 界面提示原文」发给支持人员。

View File

@@ -1,12 +1,13 @@
# assets
- `actions.json`:Skill Action manifest 最小示例(按入口定位表决定是否保留/扩展;见 `development/PRODUCT_SURFACES.md`)。每个 action **必须**显式声明 `executionProfile`(`sync` \| `async`)。默认仅 `health` / `version` / `config-path`,不放 toolbar,故无 `bind` 示例。
- `html/example-report.html`:HTML 报告页最小壳(`{{TITLE}}` / `{{GENERATED_AT}}` / `{{BODY}}`);渲染后须写入数据目录 `html/`,见 `references/ACTIONS.md`「HTML 页面报告」。
- `examples/`:CLI 成功输出形状示例、以及 `render-html.action.example.json`(虚构路径与数据)。
- **业务技能注意:** 若主路径在数据管理,准备/载入/导入类 Action 应挂 `toolbar` + `bind.tables`(用户看得见),**不要**只挂 `skill-detail` 却在教程里写成「技能详情点按钮」。见 `development/TUTORIAL_SURFACES.md` §5、`references/ACTIONS.md`「用户可见性」。
- `html/example-report.html`:HTML 报告页最小壳(`{{TITLE}}` / `{{GENERATED_AT}}` / `{{BODY}}`);含数据管理 iframe 预览布局基线(流体铺满、禁横向滚动);渲染后须写入数据目录 `html/`,见 `references/ACTIONS.md`「HTML 页面报告」。
- `examples/`:CLI 成功输出形状示例、Action 形状参考(含 `path-root-file-action.example.json`、`selection-label-column.action.example.json`、`render-html.action.example.json` 等;虚构路径与数据)。
- `schemas/`:轻量 JSON Schema(`skill-actions.schema.json` 为**新技能严格规范**)。
`skill-actions.schema.json` 使用 `additionalProperties: false`。正式支持 `bind.tables`(toolbar Action 必填)与可选 `bind.inputMapping`;placements 含 `toolbar` / `row` / `batch` / `cron` / `agent` / `skill-detail`(对齐匠厂宿主)。可选 `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`**(选择摘要标签列);placements 含 `toolbar` / `row` / `batch` / `cron` / `agent` / `skill-detail`(对齐匠厂宿主)。数据管理 confirm/form 弹层默认展示「已选择」摘要(宿主能力,技能可不改)。可选 `hoverHint` 为数据管理工具栏悬停(省略不显示,不回退)。`concurrency`/`locks` 仍为预留。`placements` 与 `executionProfile` 正交——进任务中心只看 `async`,与入口无关。表级 Action 与 Agent 契约见 [`references/ACTIONS.md`](../references/ACTIONS.md);开发深度规范见 [`development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md)。
- 用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.md)
- 用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.md);写法契约见 [`development/TUTORIAL_SURFACES.md`](../development/TUTORIAL_SURFACES.md)
- Agent 调用/编排参考见 [`references/`](../references/)(含 [`ACTIONS.md`](../references/ACTIONS.md))
- 开发规范见 [`development/`](../development/)

View 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" }]
}
]
}

View 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"]
}
}

View File

@@ -11,17 +11,31 @@
--muted: #64748b;
--card: #ffffff;
--line: #e2e8f0;
--page-pad-x: clamp(14px, 2.5vw, 24px);
--page-pad-y: clamp(12px, 2vw, 20px);
}
* { box-sizing: border-box; }
html { overflow-y: auto; }
body {
html {
width: 100%;
height: 100%;
margin: 0;
overflow-x: hidden;
overflow-y: auto;
}
body {
width: 100%;
min-height: 100%;
margin: 0;
overflow-x: hidden;
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
}
header {
padding: 20px 24px 8px;
padding: var(--page-pad-y) var(--page-pad-x) 8px;
background: var(--card);
border-bottom: 1px solid var(--line);
}
header h1 {
margin: 0 0 6px;
@@ -34,7 +48,23 @@
font-size: 13px;
}
main {
padding: 8px 24px 28px;
width: 100%;
min-width: 0;
padding: var(--page-pad-y) var(--page-pad-x) 28px;
}
.section {
margin-bottom: 18px;
}
.section h2 {
margin: 0 0 8px;
font-size: 13px;
font-weight: 650;
color: var(--muted);
}
.meta {
margin: 0 0 10px;
color: var(--muted);
font-size: 12px;
}
.card {
background: var(--card);
@@ -44,6 +74,35 @@
line-height: 1.6;
font-size: 14px;
}
table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
background: var(--card);
border: 1px solid var(--line);
border-radius: 8px;
overflow: hidden;
font-size: 13px;
}
th, td {
padding: 8px 10px;
border-bottom: 1px solid var(--line);
text-align: left;
vertical-align: top;
word-break: break-word;
overflow-wrap: anywhere;
}
th {
background: #f8fafc;
font-weight: 600;
color: var(--muted);
font-size: 12px;
}
tr:last-child td { border-bottom: none; }
/*
宿主数据管理在 iframe 内预览本页:应横向铺满可用宽度,仅纵向滚动。
复杂看板可在 {{BODY}} 内使用 .section / table;布局底线见 references/ACTIONS.md「宿主预览布局」。
*/
</style>
</head>
<body>

View File

@@ -121,13 +121,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 +301,15 @@
"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 等),再不行只显示编号。"
}
},
"description": "表级绑定;toolbar 必填。可选 inputMapping / columnMapping。concurrency/locks 勿写入。"
"description": "表级绑定;toolbar 必填。可选 inputMapping / columnMapping / labelColumn。concurrency/locks 勿写入。"
},
"hostEffectId": {
"type": "string",

View File

@@ -60,6 +60,7 @@ checklist_version: 1.0
- [ ] S0-9 **入口定位表**(见 `PRODUCT_SURFACES.md` / `REQUIREMENTS` §0.1):已填写 / 未填写
- [ ] S0-10 本技能宣称的主用户路径涉及:对话 / 数据管理(执行或仅展示) / 定时(可多选;与定位表一致)
- [ ] S0-11 **业务行生命周期**(见 `references/WORK_ITEM_STATUS.md` / `REQUIREMENTS` §0.3):有处理队列且已声明状态→角色映射 / 不适用
- [ ] S0-12 **HTML 页面报告**(见 `references/ACTIONS.md`「HTML 页面报告」):有侧栏可预览的 `html/*.html` + `render-html` / 不适用
---
@@ -76,10 +77,15 @@ checklist_version: 1.0
- [ ] A2-1 **P0** 市场「说明」能让非开发者理解:做什么、给谁用、开始前准备什么
- [ ] A2-2 **P0** 「说明/教程」不以开发目录、内部实现细节作为用户主路径
- [ ] A2-3 **P1** 「教程」可按真实顺序跟做(准备 → 步骤 → 失败怎么办)
- [ ] A2-3 **P1** 「教程」可按真实顺序跟做(准备 → 分入口步骤 → 失败怎么办);结构见 `TUTORIAL_SURFACES.md`
- [ ] A2-4 **P1** 「演示」结构完整(有演示信息位;无视频则标明暂无,不假装有)
- [ ] A2-5 **P0** 「更新日志」能对应到当前交付版本
- [ ] A2-6 **P0** 平台/Agent 侧说明中,常见用户意图的触发方式与真实能力一致(短操作 vs 长任务不混淆)
- [ ] A2-7 **P0** 「说明」Tab(`README.md`)**不含**分步操作(1/2/3)、Agent 话术块、「入口一/二」专章;操作指引指向「使用教程」
- [ ] A2-8 **P0** 「教程」Tab 覆盖 `REQUIREMENTS` §0.1 / `actions.json` 中所有「挂」的入口(数据管理 / 对话;结构见 `TUTORIAL_SURFACES.md`)
- [ ] A2-9 **P0** 无 `cron` 时教程有「不需要定时」+ 理由;有 `cron` 时教程写清如何配置
- [ ] A2-10 **P1** 教程主能力对照表与 `actions.json` 的 `label` 一致
- [ ] A2-11 **P1** 若有数据管理主路径:准备/载入/导入类能力在对应表 **toolbar** 可见;教程**不以** skill-detail 为唯一主路径
---
@@ -134,6 +140,11 @@ checklist_version: 1.0
- [ ] A6-7 **P0** 有「处理业务行」类 Action 时(见 `WORK_ITEM_STATUS.md`):默认只处理**可操作**角色状态;成功/失败/跳过/进行中再点处理应拒绝或跳过且不产生重复副作用;重做须人工改回可操作。无处理队列 → **N/A**
- [ ] A6-8 **P0** 同上适用时:开始处理后业务表进入**进行中**;成功回写成功类终态;失败回写失败类终态且**失败原因**可读(任务成功但业务行仍停在可操作/进行中 → **FAIL**)
- [ ] A6-9 **P1** 同上适用时:`REQUIREMENTS` §0.3(或等价)已填写本技能状态→角色映射;处理类 Action 确认文案写清仅可操作态
- [ ] A6-10 **P1** 有 HTML 侧栏预览时(S0-12):预览区横向铺满、无横向滚动条;重新生成后内容与页头时间更新;布局符合 `references/ACTIONS.md`「宿主预览布局」。无 HTML 报告 → **N/A**
- [ ] A6-11 **P1** 业务**状态 / 启用类**列若用户需要筛表或顶栏搜中文:`searchable: 1` 且 `options_json`(或 `boolean` + `options_json`)的 **value 与存库一致**;仅 `searchable: 0` 则宿主不提供列筛与全局搜索。见 `references/SCHEMA.md`「列筛选与全局搜索」
- [ ] A6-12 **P2** `display_type: boolean` 的展示列推荐同时配置 `options_json` 中文 label(如 停用/启用),避免仅靠宿主默认「是/否」;无 boolean 列 → **N/A**
- [ ] A6-13 **P0** 新技能**不得**创建 `task_logs`;过程/进度/业务结果走运行日志、任务中心、业务表(见 `LOGGING.md`、`SCHEMA.md`)
- [ ] A6-14 **P0** 废弃用户可见表时:`drop_deprecated_user_table` 物理删表 + 清元数据;**禁止**只删元数据留物理表;有迁移时须提供旧库回归测试(见 `SCHEMA.md`、`tests/samples/test_table_deprecation_migration.py.sample`)
---
@@ -145,6 +156,7 @@ checklist_version: 1.0
- [ ] A7-4 **P1** 关键步骤有据可查(能对应到开始/关键节点/结束;RPA 宜有多个 `stage=`)
- [ ] A7-5 **P1** 若宣称支持录屏或截图存证:打开开关后关键步骤或失败能留下可查看产物;宣称支持却完全无产物 → **FAIL**
- [ ] A7-6 **P2** 用户可见文案偏业务语言,而非大段开发黑话
- [ ] A7-7 **P2** 数据管理技能按钮(toolbar / row / batch)中文名对照 `references/ACTIONS.md`「Action 按钮中文名」;不与宿主「查看 / 编辑 / 删除」抢名;打开类成功不弹成功条。行业专有按钮名可自创
---

View File

@@ -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. 相关文档

View File

@@ -352,9 +352,9 @@ git remote -v
| `DEMO.md` | 演示 | `demo_md` + frontmatter `demo_video_url` |
| `CHANGELOG.md` | 更新日志 | 匹配当前 tag 版本的 `##` 小节 → `changelog` |
**`README.md`**:能力与场景说明。含 frontmatter `description`;写能做什么、场景、准备、如何告诉 Agent、结果、注意事项、FAQ。不要写开发教程、目录规范、release、requirements。
**`README.md`(说明 Tab)**:能力、场景、前置条件、结果形态、注意事项、概念类 FAQ。含 frontmatter `description`(**不要**写「如何开始使用」)。**禁止**分步操作、Agent 话术块、「入口一/二」专章、逐步排障——这些属教程。完整契约见 [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md) §1。
**`TUTORIAL.md`**:第一次怎么用完的逐步操作。写「开始前 → 步骤 → 卡住怎么办」;不要写 CLI / 目录 / 开发规范。
**`TUTORIAL.md`(教程 Tab)**:按 [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md) 写三入口结构(数据管理 ∥ 对话;定时有则教、无则 N/A)+ 主能力对照表 + 常见卡住。步骤必须与 `REQUIREMENTS` §0.1、`actions.json` 及宿主真实 UI 对齐;**禁止**把仅 `skill-detail` 的 Action 写成用户主路径。不要写 CLI / 目录 / 开发规范。
**`DEMO.md`**:演示视频与补充说明。frontmatter 必须含 `demo_video_url`(无视频时用 `""`);正文可选。
@@ -590,7 +590,7 @@ cmd_stats() # 只读 sync:JSON,无 job_context
- 批量数量用 **`pickCount` / `--pick N` 参数**(任意正整数),不写死条数。
- **顺序执行、不并发**同一浏览器 Profile / account lease。
- **同步数据**与**宿主表导出**职责分离:同步不写 CSV/Excel;表导出交给数据管理。
- **HTML 页面报告**(可选):需要侧栏预览整页时,写入 `html/` + Action `render-html`;契约见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「HTML 页面报告」、[`DATA_PATHS.md`](DATA_PATHS.md);薄写入 helper 见 `scripts/service/html_page_service.py`。
- **HTML 页面报告**(可选):需要侧栏预览整页时,写入 `html/` + Action `render-html`;契约见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「HTML 页面报告」(含**宿主预览布局**底线)、[`DATA_PATHS.md`](DATA_PATHS.md);薄写入 helper 见 `scripts/service/html_page_service.py`。建议从 `assets/html/example-report.html` 复制壳;`{{BODY}}` 内数据须转义;样式升级后提醒用户重新生成。
- 详情见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md);契约样例见 `tests/samples/test_sync_contract.py.sample`、`tests/samples/test_action_core_contract.py.sample`。
### 队列 + pick(可选)
@@ -837,7 +837,7 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
- 长任务(async)是否有可见进度,能否取消或等待说明
- 网页 RPA 是否有头运行;需登录/验证码时是否停下等人,而不是静默失败
- 数据管理:表名/字段中文、按钮文案是否清楚;结果是否写回用户看得见的地方
- 市场说明 / 教程是否与真实行为一致
- 市场说明 / 教程是否与真实行为一致;**说明 Tab 不含分步操作**,教程覆盖已挂入口(见 [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md))
| 宿主入口(侧栏 / 界面) | 技能侧如何挂上 | 建议验收什么 |
|-------------------------|----------------|--------------|

View File

@@ -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(用户可见动作与存证)

View File

@@ -41,6 +41,7 @@
| POLICY-SKILL-ACTION-002 | 每个 Action 必须显式声明 `executionProfile`,且只能是 `sync` 或 `async` | development/SKILL_ACTION_RUNTIME.md;references/ACTIONS.md;assets/schemas/skill-actions.schema.json | hard | schema required + manifest 扫描 | `tests/test_development_policy_guard.py::TestPolicySkillAction002`;`tests/test_actions_manifest.py` |
| POLICY-SKILL-ACTION-003 | toolbar Action 必须声明合法非空 `bind.tables`(snake_case、唯一) | references/ACTIONS.md;assets/schemas/skill-actions.schema.json | hard | schema if/then + manifest 扫描 | `tests/test_development_policy_guard.py::TestPolicySkillAction003`;`tests/test_actions_manifest.py` |
| POLICY-SKILL-ACTION-004 | `placements` 与 `executionProfile` 正交;文档不得把数据管理入口与异步执行方式硬绑定;Schema 须接受 toolbar/row/batch/cron/agent/skill-detail × sync/async 全部组合 | development/SKILL_ACTION_RUNTIME.md;references/ACTIONS.md;assets/schemas/skill-actions.schema.json | hard | Schema 6×2 矩阵校验 + 文档禁止短语扫描 | `tests/test_actions_manifest.py::test_schema_allows_all_placement_execution_profile_combinations`;`tests/test_development_policy_guard.py::TestPolicySkillAction004` |
| POLICY-TUTORIAL-001 | 存在 `development/TUTORIAL_SURFACES.md`;`TUTORIAL.md` 须含数据管理/对话/定时/对照表骨架;`README.md` 禁止「告诉 Agent」专章与入口专章;业务技能教程与 `actions.json` placements 对齐(无 cron 须声明不需要定时) | development/TUTORIAL_SURFACES.md;development/CHECKLIST.md A2-7~A2-11 | hard | 文档存在性 + 章节/反模式扫描 + placements 对齐 | `tests/test_tutorial_surfaces.py`;`tests/test_docs_standards.py::test_tutorial_surfaces_contract_doc_and_checklist` |
| POLICY-ARCH-CORE-001 | 同一业务能力的 Action、CLI、Agent、cron、数据管理入口复用同一业务内核(文档要求已声明) | development/DEVELOPMENT.md;development/SKILL_ACTION_RUNTIME.md | hard(文档标记) / soft(语义) | 文档关键表述存在性;真实复用靠 sample/review | `tests/test_development_policy_guard.py::TestPolicyArchCore001`;`tests/samples/test_action_core_contract.py.sample` |
| POLICY-DATA-SYNC-001 | 同步型业务须遵循幂等、事务、完整性与空结果保护(文档已声明) | references/SCHEMA.md;development/SKILL_ACTION_RUNTIME.md | hard(文档标记) / soft(语义) | 文档关键表述存在性;完整语义靠 sample/review | `tests/test_development_policy_guard.py::TestPolicyDataSync001`;`tests/samples/test_sync_contract.py.sample` |
@@ -67,6 +68,7 @@
| `SKILL.md` / `constants.SKILL_SLUG` 一致性 | development/DEVELOPMENT.md §16 | 已有 `tests/test_skill_metadata.py` |
| platform_kit_min_version >= 1.2.11 | development/RUNTIME.md | 已有 `tests/test_platform_import.py` |
| 宿主按 `bind.tables` 过滤 toolbar 按钮 | references/ACTIONS.md | 待宿主升级后完整生效;模板侧已强制显式绑定 |
| 数据管理技能按钮中文名(`label`)对照 ACTIONS.md 建议表;打开类成功静默 | references/ACTIONS.md「Action 按钮中文名」 | 参考非白名单;行业词允许自创;勿做 label 关键字扫描 |
| Data Policy 未启用(L0)时宿主不得拉 Snapshot / 不得改按钮与行列行为 | references/POLICY.md | soft;宿主实现契约 |
| Data Policy 启用后 fail-closed(禁止回退 L0 全开);写路径 domain 必须再鉴权 | references/POLICY.md §6–§7 | soft;`POLICY-SKILL-AUTHZ-*` 硬门禁可后续加 |
| `actions.mode` 仅 allowlist/open;`hostCapabilities` 省略=不限制;scope `valueFrom` 仅 `actor.userId` | references/POLICY.md §0.1 | soft;Schema 已约束 |

View File

@@ -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)

View File

@@ -10,6 +10,7 @@
|------|--------|------|
| 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 |
@@ -27,6 +28,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 等,可扩展)

View File

@@ -33,19 +33,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 协作与授权边界(涉及外部系统时必填)

View File

@@ -9,7 +9,7 @@
| 文档 | 职责 |
|------|------|
| 本文 | sync/async、任务中心、Agent 禁令、单内核编排、队列 pick、同步 vs 导出 |
| [`../references/ACTIONS.md`](../references/ACTIONS.md) | `actions.json` 字段、placements、`bind.tables`、inputSchema、跨表 Lookup、字段条件显隐 `visibleWhen` |
| [`../references/ACTIONS.md`](../references/ACTIONS.md) | `actions.json` 字段、placements、`bind.tables`、inputSchema、跨表 Lookup、字段条件显隐 `visibleWhen`、本地路径 `file`/`directory` + 可选 **`pathRoot`**、**按钮中文名 `label` 参考** |
| [`LOGGING.md`](LOGGING.md) | `job_context` / `emit` / `finish` / Run Journal |
| [`DATA_PATHS.md`](DATA_PATHS.md) | 下载/导入等落盘路径 |
@@ -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):选择摘要可读标签列;省略 = 宿主常见列名启发。可选 **`cellPresentation`**(见 `references/CELL_PRESENTATION.md`):单元格语义标色;省略 = P0。
4. **长耗时建议 async** — 开浏览器、RPA、HTTP 大文件下载等建议 `executionProfile: "async"`,进**任务中心**;这是对**方法**的选择,**不**限制可以出现在哪些 placement / 入口。
5. **Agent:短 CLI / 长 Action** — 短查询可用共享 Python bash CLI;长任务 / RPA **禁止** `exec` + `process poll`,必须 `run_skill_action`。
6. **进度走 Journal** — `emit` / `video.add_step` → Run Journal;UI / Skill Run Card / 任务中心展示;**不靠 stdout 流**。

View File

@@ -35,6 +35,7 @@
- **发布打包守护**:[`tests/test_release_packaging_constraints.py`](../tests/test_release_packaging_constraints.py) ——`scripts/**/*.py` 单文件 < 1000 行、文本文件 UTF-8 without BOM。
- **开发规范守护(hard policy)**:[`tests/test_development_policy_guard.py`](../tests/test_development_policy_guard.py) ——规则索引见 [`development/POLICY_MATRIX.md`](POLICY_MATRIX.md),只执行可机器检测的 hard 规则。
- **slug 语义**:[`tests/test_slug_naming.py`](../tests/test_slug_naming.py) ——verb-noun-platform 命名规范校验。
- **HTML 报告壳**:[`tests/test_html_page_service.py`](../tests/test_html_page_service.py) 守护 `assets/html/example-report.html` 的数据管理 iframe 预览布局基线;业务技能若有自定义 `assets/html/*.html`,建议在自有仓库加同类静态断言(见 `references/ACTIONS.md`「宿主预览布局」)。
**原则:`tests/test_*.py` 不允许隐形访问外网、不允许拉起真实浏览器、不允许读写开发者机器的真实数据根**。如果需要仿真服务器,也应仅在 integration(并由档位变量放行)。

View File

@@ -0,0 +1,206 @@
# 用户市场文档契约(说明 Tab / 教程 Tab)
本文给**技能开发者**与 **AI 编程助手**读:规定根目录 `README.md`(技能说明)与 `TUTORIAL.md`(使用教程)**写什么、不写什么**,以及如何与宿主三入口、入口定位表、`actions.json` 对齐。
**单一真相链:**
```text
REQUIREMENTS.md §0.1 入口定位表
↓
assets/actions.json(placements / label / bind)
↓
TUTORIAL.md(用户可读、分入口操作)
README.md(能力与边界;不写步骤)
```
产品入口「挂什么」见 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md)。本文只管**用户市场文档怎么投影**。
---
## 0. 与四 Tab 的关系
| 文件 | 市场 Tab | 本文角色 |
|------|----------|----------|
| `README.md` | **说明** | §1:是什么 / 给谁 / 边界 |
| `TUTORIAL.md` | **教程** | §2~§6:怎么用(分入口) |
| `DEMO.md` | 演示 | 观看顺序可对齐三入口,不另造标准 |
| `CHANGELOG.md` | 更新日志 | 与文档契约无关 |
**铁律:** 说明就是说明,教程就是教程。**禁止**在 README 里写分步操作、Agent 话术块、排障「点哪里」;这些一律进 `TUTORIAL.md`。
---
## 1. README.md(说明 Tab)
### 1.1 必须回答
| 问题 | 写法 |
|------|------|
| 这是什么 | 标题 + 一两句价值 |
| 能做什么 | 3~6 条能力 bullet(**无步骤编号**) |
| 适合谁 / 场景 | 业务语言 |
| 使用前准备 | 安装、账号、权限等**前置条件**;不写「去哪张表点哪」 |
| 执行后得到什么 | 结果形态(表、报告、文件),**不教怎么生成** |
| 注意事项 | 边界、风险、人工复核 |
| 怎么使用(可选,短) | **一两句**:支持哪些入口 + **「步骤见使用教程 Tab」** |
### 1.2 禁止写入 README
- `1.` `2.` `3.` 操作步骤(如「选品任务 → 点某某按钮」)
- 「你可以这样告诉 Agent」整节或大块 `>` 示例话术
- 「入口一 / 入口二」专章结构
- 逐步排障(「先点 A 再点 B」)
- frontmatter `description` 不要写「以及如何开始使用」类教程承诺
### 1.3 FAQ 边界
- README FAQ:**概念类**(和真实系统区别、从哪开始学、是否支持定时)→ 可指向「使用教程」Tab
- **操作类**排障 → 只写在 `TUTORIAL.md`「常见卡住怎么办」
### 1.4 建议章节(标题可微调)
```text
# 【技能中文名】
## 这个技能能帮你做什么
## 适合什么场景
## 使用前需要准备什么
## 怎么使用 ← 仅短摘要 + 指向教程
## 执行后你会得到什么
## 注意事项
## 常见问题 ← 概念类
```
---
## 2. TUTORIAL.md(教程 Tab)必选结构
复制业务技能后,**验收章节(标题可微调,语义不可缺)**:
```text
# 【技能中文名】· 使用教程
(开篇:数据管理与对话平行;定时有/无一句)
## 本技能做什么 ← 业务数据流(非 CLI/SQL)
## 开始前
## 入口一:数据管理 ← §0.1 有 toolbar/row/batch/replaces 则必填
## 入口二:对话任务 ← §0.1 有 agent 则必填
## 入口三:定时任务 ← 有 cron:怎么用;无 cron:不需要 + 理由
## 主能力对照表 ← 双挂能力必填
## 常见卡住怎么办
```
### 2.1 与入口定位表的关系
| §0.1 / actions | TUTORIAL 要求 |
|----------------|---------------|
| 任一能力挂数据管理(toolbar/row/batch/replaces) | **必须**有「入口一:数据管理」专章 |
| 任一能力挂 `agent` | **必须**有「入口二:对话任务」专章(**禁止**用「或者你也可以说…」替代专章) |
| 任一能力挂 `cron` | **必须**有「入口三」且写清如何建定时、选哪个 Action |
| **无** `cron` | **必须**写「本技能不需要定时」+ 一句理由(与 §0.1 一致) |
| 某能力只挂一端 | 对照表注明「仅数据管理」或「仅对话」 |
### 2.2 平行关系(数据管理 ∥ 对话)
- 两者是**平行主入口**,不是「对话主、点表辅」或反过来
- 禁止全文只有「第一步:打开对话说清目标」且无数据管理专章
- 允许用户只用其中一种;教程须写清「两种效果一致,选你习惯的」
---
## 3. 入口一:数据管理(怎么写)
适合写:
1. 左侧有哪些业务表、各干什么(业务语言)
2. 首次准备(载入/导入/新建)——**按钮必须出现在用户能看到的地方**(见 §5)
3. 主流程:进哪张表 → 是否要选中行 → 点哪个 `label` → 去哪看结果
4. 行级 / 批量操作(若有 `row` / `batch`)
按钮中文名 = `assets/actions.json` 的 `label`,不要另造别名。
---
## 4. 入口二:对话任务(怎么写)
适合写:
1. 什么时候用对话(与数据管理互补)
2. 示例话术表(与主能力一一对应)
3. Agent 可能追问什么、用户怎么答
**不要**把对话写成唯一路径;也不要把话术塞进 README。
---
## 5. 准确性铁律(用户看得见才写)
教程步骤必须对应 **宿主真实 UI**,不是只对应 manifest 声明。
| 规则 | 说明 |
|------|------|
| **placements 对齐** | 教程写的入口 ∈ `actions.json` 已挂 placement |
| **skill-detail 不可作主路径** | 声明了 `skill-detail` **不等于**详情页一定渲染出按钮;当前宿主常只显示四 Tab。**禁止**把「技能详情 → 点某某」写成用户主路径 |
| **准备/载入/导入类** | 优先挂 **数据管理 `toolbar`** + `bind.tables`(空表也可点的能力尤其如此);可保留 `agent`;`skill-detail` 仅作可选兼容 |
| **写入后刷新** | 建议 `steps: [{ "when": "afterSuccess", "do": "refreshCatalog" }]`,避免用户点完表不刷新 |
| **对照表** | 「想做什么 \| 数据管理 \| 对话」覆盖双挂主能力;`label` 与界面一致 |
字段细节见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「用户可见性与教程准确性」。
---
## 6. 入口三:定时任务(可选)
### 6.1 何时挂 `cron`(教程教「怎么用定时」)
- 参数相对固定、按节奏重复(同步、巡检、消费队列)
- 无人值守仍安全、可幂等或可失败重试
- 与数据管理是「白天点选 + 夜里自动啃」组合
教程写:侧栏定时任务 → 新建 → 选本技能 → 选 Action → 填参 → 保存/立即运行。
### 6.2 何时不挂(教程写「不需要」)
- 一次性 / 课堂演示,对象当场指定
- 强不可逆、须当场确认
- 每次对象不同、须人指定链接或范围
- 主路径依赖验证码 / 人工介入
决策三问(与 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md) §2.3 一致):
1. 明天人不盯着、同样参数再跑一遍,业务上合理吗?
2. 失败后自动重跑可接受吗?
3. 有没有须人当场确认或动态指定的输入?
①是 且 ②可接受 且 ③否 → 可挂 cron;否则不挂,§0.1 写理由,教程声明 N/A。
---
## 7. 复制后自检(发布前)
- [ ] README 无分步操作、无 Agent 话术块
- [ ] TUTORIAL 有三入口结构(定时有或 N/A)
- [ ] 对照表与 `actions.json` `label` 一致
- [ ] 准备类按钮在数据管理表顶可见(若技能有数据管理主路径)
- [ ] 教程未把 skill-detail 当唯一载入/主流程入口
- [ ] §0.1「挂」的入口在 TUTORIAL 有专章;「不挂」有理由且教程一致
交活清单见 [`CHECKLIST.md`](CHECKLIST.md) A2-7~A2-11。
---
## 8. 参考样例
已验收参考(业务技能,非模板正文):`client-gdcm/review-sku-selection-default`
(说明 Tab 瘦身 + 教程三入口 + 「载入课堂样例」挂 `selection_cases` toolbar。)
复制模板后**不要**照搬选品表名与样例编码;只复用**章节结构与职责划分**。
---
## 9. 相关文档
- 宿主入口何时挂:[`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md)
- 入口定位表:[`REQUIREMENTS.md`](REQUIREMENTS.md) §0.1
- Action placements / 可见性:[`../references/ACTIONS.md`](../references/ACTIONS.md)
- 市场四 Tab 同步:[`DEVELOPMENT.md`](DEVELOPMENT.md) §6 附近「用户市场四 Tab」
- 交活验收:[`CHECKLIST.md`](CHECKLIST.md) §2

View File

@@ -107,10 +107,10 @@ 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`**(行内/批量预填与选择摘要标签,见下节) |
| **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`;可选 `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` 混淆 |
@@ -132,15 +132,25 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
规则:
- `bind` 为 object,`additionalProperties: false`;当前允许 `tables`(必填键当存在 bind 时)与可选 `inputMapping`。
- `bind` 为 object,`additionalProperties: false`;当前允许 `tables`(必填键当存在 bind 时)与可选 `inputMapping`、`columnMapping`、**`labelColumn`**。
- `tables` 非空数组、元素唯一、英文 **snake_case**、不得为空字符串。
- **`placements` 含 `toolbar` 时必须显式声明非空 `bind.tables`**(模板 Schema `if/then` + 自动测试强制)。
- `row` / `batch` **建议**同样声明 `bind.tables`,避免在无关表上露出按钮;未声明时宿主可能按兼容逻辑在更多表上展示。
- 表名须真实存在于技能 SQLite 与 `_jiangchang_tables`。
- 希望出现在多张表时,显式列出所有表;**不要**依赖「无 bind 则全表展示」——那是宿主对**旧技能**的兼容,不是新技能标准。
- **`inputMapping`(可选)**:把行/勾选映射到 `inputSchema` 字段,例如 `"leadId": "$row.$pk"`、`"ids": "$selection.ids"`。适合 `row` / `batch`。宿主仍会提交这些字段;可用 `inputSchema.properties.*.readOnly: true` 在表单中只读展示。
- **`columnMapping`(可选)**:把 inputSchema 属性名映射到物理列名,供表单复用列展示名 / lookup。
- **`labelColumn`(可选)**:物理列名,供数据管理 Action 弹层「已选择」摘要展示 `#id + 标签`。**仅影响展示,不改变 CLI/提交 payload,也不决定业务是否跳过某些状态。** 省略时宿主用常见列名启发(如 `title` / `name` / `nickname` 等);再没有则只显示编号。列名不常见、或启发会选错列时再显式声明。
- `concurrency` / `locks` 仍未稳定,**不要**写入新技能 manifest。
### 数据管理:选择摘要(宿主默认能力)
当用户在数据管理**勾选行**后打开 `confirm` / `form` 弹层时,宿主会展示通用 **「已选择 · N 条」** 摘要(`#主键` + 可读标签;超过约 3 条可展开查看全部)。这是**宿主 UX**,对所有技能默认生效:
- **技能零改动即可受益**;不要在 `confirmation.message` 里手写「已选 id 列表」,也不要为了面板去拼业务状态跳过文案。
- 摘要与执行解耦:勾选了谁由 UI 说明;**实际处理哪些行、是否跳过某些状态,仍由技能 CLI / domain service 决定**。
- 需要稳定标签列时,在 `bind` 上声明可选 `labelColumn`(见上)。示例:[`assets/examples/selection-label-column.action.example.json`](../assets/examples/selection-label-column.action.example.json)。
## `replaces`(可选:接管默认新增 / 编辑 / 删除 / 导入)
有领域副作用的表(归一化字段、建目录、加密、级联清理)不要依赖宿主直写 SQLite。此时可为该表声明 `replaces`,**用户仍点宿主默认按钮**,由技能 CLI 完成写入。
@@ -329,16 +339,16 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
| 值 | 数据管理体验 | 典型场景 |
|----|--------------|----------|
| `direct` | **一点即跑**,无弹窗 | 打开文件位置、无参只读、行内主键已映射 |
| `confirm` | 轻量确认弹窗(无参数区;文案优先 `confirmation.message`) | 打开浏览器、有副作用但无可编辑入参 |
| `form` | 完整参数表单(现状默认路径) | 导入、查询条件、需用户改写的字段 |
| `direct` | **一点即跑**,无弹窗 | 打开目录、打开浏览器、无参只读、行内主键已映射 |
| `confirm` | 轻量确认弹窗(无参数区;文案优先 `confirmation.message`;有勾选时宿主展示「已选择」摘要) | 处理待发送、删除前确认、有副作用但无可编辑入参 |
| `form` | 完整参数表单(有勾选时同样展示「已选择」摘要) | 导入、查询条件、需用户改写的字段 |
| `auto` / 省略 | 宿主推断:有**可编辑**参数 → `form`;仅有 `confirmation` → `confirm`;否则 → `direct` | 旧技能兼容;新技能建议显式写出 |
```json
{
"id": "reveal-profile",
"label": "打开文件位置",
"description": "在资源管理器中打开 Profile 目录。",
"label": "打开目录",
"description": "打开该账号的浏览器 Profile 目录。",
"placements": ["row"],
"executionProfile": "sync",
"interaction": "direct",
@@ -365,10 +375,10 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
{
"id": "open-browser",
"label": "打开浏览器",
"description": "用该账号持久化 Profile 打开系统浏览器,仅供肉眼核对。",
"placements": ["row"],
"executionProfile": "async",
"interaction": "confirm",
"confirmation": { "message": "将用该账号 Profile 打开浏览器,仅供查看。" },
"executionProfile": "sync",
"interaction": "direct",
"bind": {
"tables": ["accounts"],
"inputMapping": { "accountId": "$row.$pk" }
@@ -377,7 +387,7 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
"type": "object",
"required": ["accountId"],
"properties": {
"accountId": { "type": "number", "readOnly": true }
"accountId": { "type": "number", "title": "账号 ID", "readOnly": true }
}
},
"entrypoint": {
@@ -396,6 +406,39 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
- **cron 触发 / agent**:忽略 `interaction`(无人值守 / 模型填参);`confirmation` 对 agent 仍作 `requiresConfirmation` 元数据。
- 不要用 `interaction: "direct"` 掩盖需要用户填写的可编辑字段——宿主在 `auto` 下会因可编辑参数回落到 `form`;显式 `direct` + 可编辑字段时参数取 default/预填,作者须自觉。
## Action 按钮中文名(`label`,参考)
`label` 是数据管理 **toolbar / row / batch** 以及技能详情直调上用户看见的短按钮名。本节是**参考契约,不是白名单**:行业专有动作(私信、审单、报关登记等)按业务起名。同类通用动作尽量用同一动词,避免每个技能一套叫法。
**写法:** 短中文,动词或「动词 + 对象」(约 2~8 字)。同一能力用**一个** Action 换 `placements`,不要为入口复制同义按钮。
以后增补:在下表加行,并在模板 `CHANGELOG.md` 记一笔。废弃说法挪到「不要叫」,不要直接删行。
### 宿主已有,技能不要再做一套同义按钮
数据管理宿主已提供:**查看 / 编辑 / 删除 / 新增 / 导入 / 导出 / 刷新**。
技能要用 CLI 接管增删改导入时用 `replaces`(见上文),**不要**再把同义按钮挂到 `placements: ["row"]` 或 `toolbar`(例如「编辑平台」「删除 Key」与底下宿主「编辑」「删除」叠两套)。用户仍点宿主那几个字。
### 建议动词(有则用之)
| 类型 | 建议 `label` | 不要叫 | 说明 |
|------|----------------|--------|------|
| 基础设施 | 运行检查、查看版本、查看配置位置、初始化数据 | 健康检查、版本信息、配置路径、初始化数据库 | 多挂 `skill-detail` |
| 打开 | 打开浏览器、打开目录、打开文件 | 打开文件位置、在资源管理器中打开、用浏览器打开 | 见下节静默成功 |
| 改库 | 新增…、编辑…、删除…、启用/停用、设置默认…、加入待…、重置为待… | 表记录用「添加」;无必要拆成「启用」「停用」两个按钮 | 对象名可带,如「新增账号」 |
| 外源入库 | 同步数据、全量同步、采集…、扫描… | 把宿主「刷新」叫成同步;把导出 CSV 叫成同步 | 默认用「同步数据」;全量才写「全量同步」 |
| 队列与探测 | 处理待…、测试 API、测试网页、抽检本条、抽检所选、批量抽检 | 行 / 勾选 / 工具栏三个入口都叫「抽检」 | 行=本条,勾选=所选,不管勾选的批量=批量 |
| 生成发布 | 生成 HTML、生成…、发布到… | 用「导出」代替宿主表导出 | 固定 id `render-html` 的 label 用「生成 HTML」 |
### 打开类:一点即开,成功不提示
打开目录 / 打开浏览器 / 打开文件:`interaction: "direct"`,`executionProfile` 通常 `"sync"`。
- **成功**:不要输出用户可见成功文案。若打印 JSON,令 `"message": ""`(宿主据此不弹「执行成功」)。操作系统已经弹出目录或浏览器,就是反馈。
- **失败**:仍 `ERROR:<CODE>:`(可加 `HINT:`)。
- **不要**用宿主「查看」表示打开目录或浏览器——「查看」是只读详情。
## `hoverHint`(工具栏悬停,可选)
与 `label` / `description` / `confirmation.message` **独立**。只决定数据管理**顶栏技能按钮**鼠标悬停时显示什么。
@@ -584,6 +627,7 @@ python {skill_root}/scripts/main.py <command> <args...>
- 使用稳定的退出码
- 成功时输出可解析的结构化结果
- **例外(打开类)**:打开目录 / 打开浏览器 / 打开文件成功时不要输出用户可见成功文案;若打印 JSON,令 `"message": ""`。失败仍走下一款错误约定
- 失败时输出稳定错误信息
- 对可预期错误提供稳定错误码
- 对用户可处理的问题提供 `HINT`
@@ -610,6 +654,18 @@ HINT: <next step>
| `agent` | Agent 可直接调用(`run_skill_action`) |
| `cron` | 定时任务「技能直调」 |
### 用户可见性与教程准确性
`placements` 决定 Action **可以**出现在哪;教程只能写用户在宿主里**实际能点到**的路径。完整用户文档契约见 [`../development/TUTORIAL_SURFACES.md`](../development/TUTORIAL_SURFACES.md)。
| 规则 | 说明 |
|------|------|
| **skill-detail ≠ 一定可见** | 声明 `skill-detail` 不保证详情页渲染出按钮;当前宿主详情常只有说明/教程/演示/更新日志四 Tab。**禁止**把「技能详情 → 点某某」写成用户主路径或教程唯一入口 |
| **准备 / 载入 / 导入 / 重置** | 若技能以数据管理为主路径,此类能力优先 `toolbar` + 非空 `bind.tables`(空表也可点的准备动作尤其如此);可同时挂 `agent`;`skill-detail` 仅作可选兼容 |
| **写入后刷新** | 会写入业务表的 Action 建议 `steps: [{ "when": "afterSuccess", "do": "refreshCatalog" }]`,避免用户点完表不更新 |
| **教程按钮名** | 必须等于本文件 `label`;对照表与数据管理顶栏/行内文案一致 |
| **README vs 教程** | 说明 Tab 不写分步;操作步骤只写 `TUTORIAL.md` |
### 行级入口 `row`(宿主呈现契约)
- **技能决定有什么**:`placements` 含 `"row"`(且通过 `bind.tables` / policy 等过滤)的 Action,构成该表行的技能菜单项集合;配什么就出现什么。
@@ -655,7 +711,7 @@ HINT: <next step>
| 数据同步(外源 → 本地库) | sync 或 async | toolbar + cron + agent + skill-detail(按需) | 可选 |
| 单行重试 / 行内处理 | sync 或 async | row(可加 agent) | 可选 |
| 批量处理勾选行 | sync 或 async | batch(可加 toolbar/cron) | 可选 |
| 导入 / 重置 | sync 或 async | skill-detail, agent | 可选 |
| 导入 / 重置 | sync 或 async | **优先** toolbar(须 `bind.tables`)+ agent;skill-detail 仅可选 | 可选 |
| 单条副作用 RPA | async(建议) | agent, skill-detail | **建议** |
| 批量顺序 pick RPA | async(建议) | toolbar, cron, agent | **建议** |
## 同步数据 vs 导出当前表 vs 特殊业务报告 vs HTML 页面
@@ -706,7 +762,25 @@ HINT: <next step>
| `steps` | `afterSuccess` → **`refreshCatalog`**(重扫 `htmlPages`;不要只 `reloadTable`) |
| `entrypoint` | CLI 子命令(如 `render-html`)调用同一写入内核 |
完整 JSON 形状见 [`../assets/examples/render-html.action.example.json`](../assets/examples/render-html.action.example.json)。最小页面壳见 [`../assets/html/example-report.html`](../assets/html/example-report.html)。
完整 JSON 形状见 [`../assets/examples/render-html.action.example.json`](../assets/examples/render-html.action.example.json)。最小页面壳见 [`../assets/html/example-report.html`](../assets/html/example-report.html)(已含数据管理 iframe 预览的布局基线;复制后可按需扩展 `{{BODY}}` 内结构)。
### 宿主预览布局
数据管理在 **iframe** 内预览 `html/*.html`;宿主 iframe 已占满右侧预览区,**技能页不应再人为收窄正文**(尤其数据管理窗口通常较窄)。
| 级别 | 约定 |
|------|------|
| **必须** | 页面横向**铺满** iframe 可用宽度:`main { width: 100%; min-width: 0; }` |
| **必须** | **禁止** `main`(或等价正文容器)使用 `max-width: Npx` 且**无** `margin: 0 auto`(宽屏会左对齐留白;窄宿主窗浪费横向空间) |
| **必须** | `html` / `body` 设 `overflow-x: hidden`;滚动以**纵向**为主,避免横向滚动条 |
| **必须** | 写入 `{{BODY}}` 的业务数据经 `html.escape`(或等价)转义,防 XSS |
| **应当** | `header` 与 `main` 水平内边距一致(如共用 CSS 变量或 `clamp()`) |
| **应当** | 多列表格:`width: 100%` + 单元格 `word-break` / `overflow-wrap`,长文本在格内换行 |
| **应当** | KPI / 卡片网格用 `repeat(auto-fit, minmax(…, 1fr))` 等流体列,勿死写固定列数 |
| **禁止** | 在 `<td>` 上用 `max-width` 限制「说明列」等业务列(易导致列宽异常或横向溢出) |
| **禁止** | 依赖外链 CSS/JS 或 `networkidle` 式加载(iframe `sandbox` 仅 `allow-scripts`;页面应自包含) |
长文阅读型报告若确需版心,可对整个 `main` 使用 `max-width` **且** `margin: 0 auto`,并令 `header` 与 `main` 同宽;**数据看板类**默认不设 `max-width`。
### 实现要点
@@ -719,6 +793,7 @@ HINT: <next step>
### 验收(复制后自测)
- [ ] `html/` 下出现 `.html`,数据管理侧栏能看到报告项,预览正常
- [ ] 预览区**横向铺满**、**无横向滚动条**;拖窄侧栏后内容自适应(见上节「宿主预览布局」)
- [ ] 表顶栏(若声明 toolbar)或报告右键「重新生成」能跑通;成功后预览刷新
- [ ] 未登录/无权限用户被拒绝,且有可读错误
- [ ] 成功路径带 `refreshCatalog`(或等价让侧栏/预览看到更新)
@@ -813,14 +888,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 上传。)
日期时间约定:
@@ -828,11 +907,71 @@ 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`**
- `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}}`)

View File

@@ -17,7 +17,7 @@
| 根目录 [`CHANGELOG.md`](../CHANGELOG.md) | 市场更新日志 Tab(版本小节 → `changelog`) |
| [`SKILL.md`](../SKILL.md) | LLM / OpenClaw 平台技能入口与触发摘要 |
| [`CLI.md`](CLI.md) | 共享 Python、命令行入口、参数与短操作契约 |
| [`ACTIONS.md`](ACTIONS.md) | Skill Action、sync/async、任务中心、多入口(按入口定位表挂载;先读 `development/PRODUCT_SURFACES.md`) |
| [`ACTIONS.md`](ACTIONS.md) | Skill Action、sync/async、任务中心、多入口、按钮中文名 `label` 参考(按入口定位表挂载;先读 `development/PRODUCT_SURFACES.md`) |
| [`SCHEMA.md`](SCHEMA.md) | 本地 SQLite 与数据管理展示契约 |
| [`POLICY.md`](POLICY.md) | Data Policy:可选身份授权、Snapshot、行 scope(多数技能 L0 不配) |
| [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md) | 数据管理单元格呈现:语义 tone 标色/强调(用途不限;多数技能 P0 不配) |

View File

@@ -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` 物理删除。
## 开发自检

View File

@@ -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"

View File

@@ -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:

View File

@@ -4,6 +4,7 @@
- 目录:仅一层 `html/*.html`(非递归)
- 重新生成 Action id 必须为 `render-html`(宿主硬编码)
- 页面布局底线见 `references/ACTIONS.md`「宿主预览布局」;模板壳 `assets/html/example-report.html`
- 成功后建议 `steps: refreshCatalog` 以重扫侧栏
- 本模块只提供路径/模板/原子写;业务 KPI/表格由各技能自行填充
"""

View File

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

View 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()

View File

@@ -74,8 +74,10 @@ def _validate_bind_tables(action_id: str, bind: object) -> list[str]:
errors: list[str] = []
if not isinstance(bind, dict):
return [f"{action_id}: bind must be object"]
if set(bind.keys()) - {"tables"}:
errors.append(f"{action_id}: bind only allows tables, got {sorted(bind.keys())}")
allowed = {"tables", "inputMapping", "columnMapping", "labelColumn"}
unknown = set(bind.keys()) - allowed
if unknown:
errors.append(f"{action_id}: bind only allows {sorted(allowed)}, got {sorted(bind.keys())}")
tables = bind.get("tables")
if not isinstance(tables, list) or len(tables) < 1:
errors.append(f"{action_id}: bind.tables must be non-empty array")
@@ -90,6 +92,12 @@ def _validate_bind_tables(action_id: str, bind: object) -> list[str]:
if table in seen:
errors.append(f"{action_id}: bind.tables duplicate {table!r}")
seen.add(table)
label_column = bind.get("labelColumn")
if label_column is not None:
if not isinstance(label_column, str) or not label_column.strip():
errors.append(f"{action_id}: bind.labelColumn must be non-empty string")
elif not _TABLE_NAME_PATTERN.fullmatch(label_column):
errors.append(f"{action_id}: bind.labelColumn {label_column!r} must be snake_case")
return errors
@@ -432,6 +440,36 @@ class TestActionsManifest(unittest.TestCase):
with self.assertRaises(ValidationError):
jsonschema.validate(instance=manifest, schema=schema)
def test_schema_allows_bind_label_column(self) -> None:
try:
import jsonschema
except ImportError:
self.fail("jsonschema is required")
schema = _load_actions_schema()
bind_props = schema["$defs"]["bind"]["properties"]
self.assertIn("labelColumn", bind_props)
manifest = {
"schemaVersion": 1,
"skill": "your-skill-slug",
"actions": [
{
"id": "probe-label-column",
"label": "处理勾选",
"description": "验证 bind.labelColumn 可通过 Schema",
"placements": ["toolbar", "batch"],
"executionProfile": "sync",
"interaction": "confirm",
"bind": {
"tables": ["demo_items"],
"inputMapping": {"ids": "$selection.ids"},
"labelColumn": "display_name",
},
"entrypoint": {"type": "cli", "command": "health", "args": []},
}
],
}
jsonschema.validate(instance=manifest, schema=schema)
def test_no_execution_profile_placement_coupling_in_tests(self) -> None:
"""守护:本文件不得引入入口位置与 sync/async 的错误硬耦合断言。"""
path = os.path.join(get_skill_root(), "tests", "test_actions_manifest.py")
@@ -593,6 +631,8 @@ class TestActionsManifest(unittest.TestCase):
"afterFailure",
"cellPresentation",
"CELL_PRESENTATION.md",
"labelColumn",
"已选择",
):
self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}")
self.assertNotIn(

View File

@@ -57,6 +57,7 @@ POLICY_IDS = (
"POLICY-SKILL-ACTION-002",
"POLICY-SKILL-ACTION-003",
"POLICY-SKILL-ACTION-004",
"POLICY-TUTORIAL-001",
"POLICY-ARCH-CORE-001",
"POLICY-DATA-SYNC-001",
)

View File

@@ -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()

View File

@@ -181,6 +181,17 @@ class TestDocsStandards(unittest.TestCase):
self.assertIn("CHANGELOG.md", text)
self.assertIn("demo_video_url", text)
self.assertIn("tutorial_md", text)
self.assertIn("TUTORIAL_SURFACES.md", text)
def test_tutorial_surfaces_contract_doc_and_checklist(self) -> None:
surfaces = self._read("development/TUTORIAL_SURFACES.md")
self.assertIn("说明", surfaces)
self.assertIn("教程", surfaces)
self.assertIn("skill-detail", surfaces)
checklist = self._read("development/CHECKLIST.md")
self.assertIn("A2-7", checklist)
self.assertIn("A2-8", checklist)
self.assertIn("TUTORIAL_SURFACES.md", checklist)
def test_naming_md_exists_and_covers_standards(self) -> None:
text = self._read("development/NAMING.md")
@@ -356,6 +367,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 +393,7 @@ class TestDocsStandards(unittest.TestCase):
"字段条件显隐",
"visibleWhen",
"grid_help",
"列筛选",
):
self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}")

View File

@@ -61,6 +61,15 @@ class TestHtmlPageService(unittest.TestCase):
with self.assertRaises(ValueError):
write_html_page("note.txt", "<html></html>")
def test_example_report_shell_embedded_preview_layout(self) -> None:
"""守护 example-report.html:数据管理 iframe 预览布局基线不退化。"""
template = load_html_template("assets/html/example-report.html")
self.assertIn("overflow-x: hidden", template)
self.assertIn("min-width: 0", template)
self.assertIn("table-layout: fixed", template)
self.assertIn("overflow-wrap: anywhere", template)
self.assertNotIn("max-width:", template)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,198 @@
# -*- coding: utf-8 -*-
"""用户市场文档契约:README≠教程、TUTORIAL 三入口结构、与 actions.json 对齐。
契约全文:development/TUTORIAL_SURFACES.md
- 模板占位 slug(your-skill-slug):只检查骨架与契约文档存在
- 业务技能:按 actions.json placements 硬校验教程章节与 README 反模式
"""
from __future__ import annotations
import json
import os
import re
import unittest
from _support import get_skill_root
from util.constants import SKILL_SLUG
TEMPLATE_PLACEHOLDER_SLUG = "your-skill-slug"
_SECTION_DATA_MGMT = re.compile(r"(?m)^##\s*.*数据管理")
_SECTION_DIALOG = re.compile(r"(?m)^##\s*.*(对话任务|对话)")
_SECTION_CRON = re.compile(r"(?m)^##\s*.*定时任务")
_SECTION_COMPARE = re.compile(r"(?m)^##\s*.*(对照表|主能力)")
_SECTION_STUCK = re.compile(r"(?m)^##\s*.*(常见卡住|卡住怎么办)")
_SECTION_WHAT = re.compile(r"(?m)^##\s*.*(本技能做什么|做什么)")
_NO_CRON_HINT = re.compile(r"不需要定时|未提供定时|不提供定时|不适用.*定时|定时.*不适用")
# README 反模式:分步操作 / Agent 话术专章(说明 Tab 禁止)
_README_HOWTO_AGENT = re.compile(r"(?m)^##\s*.*告诉\s*Agent")
_README_ENTRY_CHAPTER = re.compile(r"(?m)^##\s*入口[一二三]")
_README_NUMBERED_DM_PATH = re.compile(
r"(?m)^\d+[\.、]\s*.*(数据管理|选品任务|表顶).{0,40}(点击|点\s*「)"
)
def _read(rel: str) -> str:
path = os.path.join(get_skill_root(), rel)
with open(path, encoding="utf-8") as f:
return f.read()
def _load_actions() -> list[dict]:
path = os.path.join(get_skill_root(), "assets", "actions.json")
if not os.path.isfile(path):
return []
with open(path, encoding="utf-8") as f:
data = json.load(f)
return list(data.get("actions") or [])
def _placement_sets(actions: list[dict]) -> tuple[bool, bool, bool]:
has_dm = False
has_agent = False
has_cron = False
dm_tokens = frozenset({"toolbar", "row", "batch"})
for action in actions:
placements = set(action.get("placements") or [])
if placements & dm_tokens:
has_dm = True
if "agent" in placements and str(action.get("id") or "") not in (
"health",
"version",
"config-path",
):
has_agent = True
if "cron" in placements:
has_cron = True
return has_dm, has_agent, has_cron
class TestTutorialSurfacesContract(unittest.TestCase):
def test_tutorial_surfaces_doc_exists(self) -> None:
path = os.path.join(get_skill_root(), "development", "TUTORIAL_SURFACES.md")
self.assertTrue(
os.path.isfile(path),
msg="development/TUTORIAL_SURFACES.md must exist (user market doc contract)",
)
text = _read("development/TUTORIAL_SURFACES.md")
for marker in (
"README.md",
"TUTORIAL.md",
"数据管理",
"对话",
"定时",
"skill-detail",
"对照表",
):
self.assertIn(marker, text, msg=f"TUTORIAL_SURFACES.md missing {marker!r}")
def test_development_md_points_to_tutorial_surfaces(self) -> None:
text = _read("development/DEVELOPMENT.md")
self.assertIn(
"TUTORIAL_SURFACES.md",
text,
msg="DEVELOPMENT.md must reference TUTORIAL_SURFACES.md for market docs",
)
def test_template_or_business_tutorial_has_required_skeleton(self) -> None:
tutorial = _read("TUTORIAL.md")
self.assertRegex(
tutorial,
_SECTION_WHAT,
msg="TUTORIAL.md must have a '本技能做什么' (or equivalent) section",
)
self.assertRegex(
tutorial,
_SECTION_DATA_MGMT,
msg="TUTORIAL.md must have a 数据管理 section (or 不适用 placeholder)",
)
self.assertRegex(
tutorial,
_SECTION_DIALOG,
msg="TUTORIAL.md must have a 对话 section (or 不适用 placeholder)",
)
self.assertRegex(
tutorial,
_SECTION_CRON,
msg="TUTORIAL.md must have a 定时任务 section (teach or N/A)",
)
self.assertRegex(
tutorial,
_SECTION_COMPARE,
msg="TUTORIAL.md must have a 主能力对照表 section",
)
self.assertRegex(
tutorial,
_SECTION_STUCK,
msg="TUTORIAL.md must have a 常见卡住 section",
)
# 禁止旧「对话单线」作为唯一主结构标题
self.assertNotRegex(
tutorial,
r"(?m)^##\s*第一步:打开技能并说清目标\s*$",
msg="TUTORIAL.md must not use legacy dialogue-only first step as sole structure",
)
def test_readme_has_no_legacy_agent_howto_chapter(self) -> None:
readme = _read("README.md")
self.assertIsNone(
_README_HOWTO_AGENT.search(readme),
msg="README.md must not have '告诉 Agent' howto chapter (belongs in TUTORIAL)",
)
self.assertIsNone(
_README_ENTRY_CHAPTER.search(readme),
msg="README.md must not have 入口一/二/三 chapters (belongs in TUTORIAL)",
)
def test_business_skill_tutorial_aligns_with_actions(self) -> None:
if SKILL_SLUG == TEMPLATE_PLACEHOLDER_SLUG:
self.skipTest("template placeholder slug: skip actions alignment hard checks")
actions = _load_actions()
has_dm, has_agent, has_cron = _placement_sets(actions)
tutorial = _read("TUTORIAL.md")
readme = _read("README.md")
if has_dm:
self.assertRegex(
tutorial,
_SECTION_DATA_MGMT,
msg="business skill with data-management placements must document 数据管理 in TUTORIAL",
)
# 准备类若挂了 toolbar,教程不应把 skill-detail 当唯一主路径
self.assertNotRegex(
tutorial,
r"(?m)^\d+[\.、]\s*.*技能详情.{0,30}点击",
msg="TUTORIAL must not use 技能详情点击 as primary numbered path when DM exists",
)
if has_agent:
self.assertRegex(
tutorial,
_SECTION_DIALOG,
msg="business skill with agent placements must document 对话 in TUTORIAL",
)
if has_cron:
self.assertRegex(
tutorial,
_SECTION_CRON,
msg="with cron placements, TUTORIAL must have 定时任务 section",
)
else:
self.assertRegex(
tutorial,
_NO_CRON_HINT,
msg="without cron placements, TUTORIAL must declare 不需要/未提供定时",
)
# README 不应出现「数据管理 → … → 点击」式编号步骤
self.assertIsNone(
_README_NUMBERED_DM_PATH.search(readme),
msg="README.md must not contain numbered data-management click steps",
)
if __name__ == "__main__":
unittest.main()