Compare commits

...

18 Commits

Author SHA1 Message Date
0ce0c3e852 Enforce POLICY-RELEASE-001: align git tag with SKILL.md and SKILL_VERSION
All checks were successful
技能自动化发布 / release (push) Successful in 6s
2026-09-03 13:51:00 +08:00
201d4d1367 Release v1.0.103: forbid redundant market-doc H1
All checks were successful
技能自动化发布 / release (push) Successful in 6s
2026-09-03 10:32:41 +08:00
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
0a6b0171d8 docs: settle work item lifecycle contract (v1.0.95)
All checks were successful
技能自动化发布 / release (push) Successful in 6s
2026-08-21 16:30:34 +08:00
43486afec4 docs: dedupe changelog after lookup.multiple contract (v1.0.94)
All checks were successful
技能自动化发布 / release (push) Successful in 5s
2026-08-21 16:20:53 +08:00
225d5c0777 docs: add lookup.multiple multi-select contract (v1.0.93)
All checks were successful
技能自动化发布 / release (push) Successful in 5s
2026-08-21 16:19:13 +08:00
fbf326d525 Document replaces:import and add CRUD/import action examples
All checks were successful
技能自动化发布 / release (push) Successful in 7s
2026-08-21 11:57:38 +08:00
28c3c8d8ab Release v1.0.91: document no SQLite FK between business tables
All checks were successful
技能自动化发布 / release (push) Successful in 4s
2026-08-19 16:03:42 +08:00
571c135114 Release v1.0.90: document grid_help data-management contract
All checks were successful
技能自动化发布 / release (push) Successful in 7s
2026-08-18 11:00:20 +08:00
d840745758 chore: add MIT license, SECURITY.md, and placeholder developer_ids
All checks were successful
技能自动化发布 / release (push) Successful in 5s
2026-08-18 10:44:56 +08:00
cf3f0ae990 fix: default git tag message from UTF-8 bytes for Windows PowerShell 5.1
All checks were successful
技能自动化发布 / release (push) Successful in 5s
2026-08-18 10:19:47 +08:00
a11cdf4e7e fix: write git tag/commit messages via UTF-8 -F file
All checks were successful
技能自动化发布 / release (push) Successful in 5s
2026-08-18 10:16:33 +08:00
49 changed files with 2625 additions and 212 deletions

View File

@@ -9,10 +9,111 @@
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
## 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、禁横向滚动、表格断行等)
- 升级 `assets/html/example-report.html` 为通用布局基线;`test_html_page_service` 守护不退化
- `DEVELOPMENT.md` / `CHECKLIST`(S0-12、A6-10)/ `TESTING.md` 交叉引用
## 1.0.96
- 在 `references/ACTIONS.md` 沉淀数据管理技能按钮中文名(`label`)参考表:同类通用动作用同一动词,不是白名单
- 打开类改为「打开目录 / 打开浏览器」,一点即开、成功不弹「执行成功」;示例不再使用「打开文件位置」
- 交活 Checklist A7-7;POLICY_MATRIX 软规则:不扫描 label 关键字
## 1.0.95
- 正式沉淀「业务行生命周期」通用契约(`references/WORK_ITEM_STATUS.md`):用可操作/进行中/成功·失败·跳过类终态等**角色**约束处理队列,不绑死某一技能的状态名
- 默认只处理可操作态;开始抢占进行中;结束回写成功或失败(失败带原因);重做靠人工改回可操作
- REQUIREMENTS §0.3 状态→角色映射;CHECKLIST S0-11 / A6-7~A6-9;SKILL / ACTIONS / SCHEMA / references 索引交叉引用
## 1.0.94
- 修正 1.0.93 更新日志重复小节
## 1.0.93
- 沉淀 Lookup **多选**契约:同一 `lookup` / `options_json` 对象上加 `multiple: true`(平行扩展单选,不另造 display_type)
- 多选存 `TEXT` 逗号分隔 id;列表展示映射为多个 label;控件由宿主按选项数量选择(短列表勾选 / 长列表 Chip+下拉)
- 更新 SCHEMA / ACTIONS / skill-actions.schema / lookup 示例与 options_json 校验
## 1.0.92
- 沉淀数据管理 `replaces: import`:导入 UI 统一宿主面板,确认后由技能写库
- ACTIONS / schema / 守护测试对齐 create·update·delete·import;补充可抄示例 `replaces-*.action.example.json`
- 修正旧文档「宿主导入仍直写」的过时说明
## 1.0.91
- 沉淀「表与表之间禁止 SQLite 外键」契约:只存业务 id,关联与级联在 repository/service;界面用 `lookup`
- `display_metadata_validator` 扫描业务表 DDL,含 `FOREIGN KEY` / `REFERENCES` 时报错
- 交活 Checklist 新增 A6-6;`lookup` 文档用语改为「跨表引用 id」,避免与 SQLite 外键混淆
## 1.0.90
- 沉淀数据管理表格说明契约:`_jiangchang_tables.grid_help` / `_jiangchang_columns.grid_help`
- 有内容才显示表上方说明和列头「?」;不写则与今日一致
- 与新增/编辑表单的 `description` 独立,禁止互拷(见 `references/SCHEMA.md`)
## 1.0.89
- 仓库增加 MIT 许可证,版权归属深圳匠厂科技有限公司
- 增加 SECURITY.md:未公开漏洞请通过匠厂官网联系,不要发在公开 Issue
- 模板 developer_ids 改为占位 12345,复制后请换成自己的匠厂用户 ID
## 1.0.88
- `release.ps1` 默认 tag 说明改为 UTF-8 字节写入,不再依赖脚本源码里的中文默认参数(Windows PowerShell 5.1 会把无 BOM 的 UTF-8 按系统代码页解析)
- 不改写已发布 tag(v1.0.86 / v1.0.87 说明仍可能乱码)
## 1.0.87
- `release.ps1` 打 annotated tag / 自动 commit 改为 UTF-8 消息文件 + `git -F`,避免 `-m` 走控制台代码页
- 默认说明仍可能乱码(见 1.0.88)
## 1.0.86
- 修复 `release.ps1` 在中文 Windows 下经 `cmd /c` 打 annotated tag 时,默认说明「正式发布」被 OEM 代码页转成乱码的问题
- 改为直接调用 `git.exe` 并按参数数组传 `-m`,commit / tag 中文说明保持 UTF-8;不改写历史 tag
- 去掉 `release.ps1` 里通过 `cmd /c` 调 git 的写法;中文 tag 说明在部分 Windows 控制台代码页下仍可能乱码,见 1.0.87
## 1.0.85

10
DEMO.md
View File

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

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 深圳匠厂科技有限公司
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -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 或验证码。

20
SECURITY.md Normal file
View File

@@ -0,0 +1,20 @@
# 安全漏洞披露
本仓库是匠厂技能开发模板。请把**未公开的安全漏洞**发给深圳匠厂科技有限公司,不要在公开 Issue、PR 或讨论区里贴完整复现步骤、PoC 或利用细节。
## 怎么报告
请通过匠厂官网联系渠道提交:[https://www.jc2009.com/](https://www.jc2009.com/)
建议在联系时注明:
- 标题或开头带 `[Security]`
- 影响的仓库 / 版本(例如 `skill-template`、tag `v1.0.x`)
- 问题类型、影响范围、复现步骤
- 若已有修复思路,可一并说明
我们会确认收悉,并在有结论后告知是否修复、是否公开披露。
## 不在本页范围
本页只说明**如何向我们报告漏洞**。技能运行时的权限、RPA 合规、加密发布等产品安全设计,见 `development/` 与 `references/`,不要写进本文件。

View File

@@ -1,7 +1,7 @@
---
name: 技能开发模板(通用业务版)
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
version: 1.0.86
version: 1.0.104
author: 深圳匠厂科技有限公司
metadata:
openclaw:
@@ -10,8 +10,7 @@ metadata:
emoji: "📦"
category: "通用"
developer_ids:
- 10032
- 12428
- 12345 # 占位,请换成你的匠厂用户 ID
allowed-tools:
- bash
---
@@ -39,6 +38,7 @@ allowed-tools:
- **`executionProfile`**:只决定执行方式——`sync` 当场返回结果,`async` 进**任务中心**。与 placements / 入口**无关**。心智上多数为 sync,仅长任务 / RPA 标 async;宿主省略该字段时按 sync。每个 Action **仍必须**显式写 `sync` 或 `async`。
- **`policy`(可选)**:需要按用户/角色控制数据管理按钮或行列时,在 `actions.json` 顶层启用 Data Policy,由技能 `policy` CLI 输出 Snapshot;**未启用 = 与今日一致(L0)**。详见 `references/POLICY.md`。用户/角色数据源(飞书/库/API)属技能内部,宿主不托管。
- **`cellPresentation`(可选)**:需要数据管理「某格特殊标色/强调」时启用;技能按当前数据写入呈现(语义 tone),宿主映射主题样式;**未启用 = P0 无标色**。用途不限(超时、预警、驳回等)。详见 `references/CELL_PRESENTATION.md`。
- **业务行生命周期(有处理队列时)**:对某一行做生成/触达/私信等处理时,默认只操作「可操作」角色状态;开始抢占「进行中」;结束回写成功或失败(失败带原因);重做靠人工改回可操作。状态名由本技能自定。详见 `references/WORK_ITEM_STATUS.md`。
- **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;文件型特殊报告才另做 `export-*`(落 `output/`);**HTML 整页报告**写 `html/` 并用固定 Action id `render-html`(见 `references/ACTIONS.md`)。
- **准备 vs 执行(适用则拆)**:若技能存在「先写入/准备本地数据」与「再对外产生副作用」两段,Agent 必须能分开触发;禁止默认绑成不可拆的一步。一键无准备态的技能可跳过本条。
@@ -71,8 +71,9 @@ allowed-tools:
4. **入口按定位表挂载**:复制业务技能后须在 `development/REQUIREMENTS.md` 填写入口定位表(见 `development/PRODUCT_SURFACES.md`)。定位需要数据管理 / 定时 / Agent 结构化调用时,必须提供 `actions.json` 并声明对应 `placements`;定位确认无需任何宿主结构化入口时可不提供。有长任务 / RPA 时**必须**提供至少一条 `executionProfile: "async"` 且 `placements` 含 `agent` 的 Action。
5. **一个业务能力一个 Action**,用多个 `placements` 挂入口;底层仍是同一 `scripts/main.py` CLI。
6. 是否进任务中心**只**看该 Action 的 `executionProfile`,与数据管理 / 定时 / Agent 入口无关。
7. **处理业务行**时遵守状态角色契约(可操作 / 进行中 / 终态);禁止对已成功/失败/跳过默认重复处理。详见 `references/WORK_ITEM_STATUS.md`。
Agent 执行契约细节:`references/CLI.md`、`references/ACTIONS.md`、`references/SCHEMA.md`、按需 `references/POLICY.md`(不要把 `development/` 当 Agent 主读路径)。
Agent 执行契约细节:`references/CLI.md`、`references/ACTIONS.md`、`references/SCHEMA.md`、按需 `references/POLICY.md` / `references/WORK_ITEM_STATUS.md`(不要把 `development/` 当 Agent 主读路径)。
## 共享 Python(必读)
@@ -108,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 臆造按钮路径。
**文档读取边界:**
@@ -124,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,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 任务看进度 |
| 【网页自动化】未登录 / 无权限 | 确认账号在线与目标站登录状态 |
| 结果不对 | 缩小范围重试,或改用数据管理核对表数据 |
仍无法解决时,把「你说了什么 + 界面提示原文」发给支持人员,便于排查。
仍无法解决时,把「你用的入口 + 操作步骤 + 界面提示原文」发给支持人员。

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

@@ -4,5 +4,6 @@
- `skill-policy-snapshot.*.example.json`:Data Policy Snapshot 形状参考(见 `references/POLICY.md`)。
- `skill-cell-presentation.example.json`:数据管理单元格呈现文档形状参考(见 `references/CELL_PRESENTATION.md`)。
- `render-html.action.example.json`:HTML 报告「重新生成」Action 形状参考(固定 id `render-html`;见 `references/ACTIONS.md`)。
- `lookup-action.example.json`:Action 内联 Lookup 下拉形状参考(跨表引用勿填编号;见 `references/ACTIONS.md`「跨表引用:Lookup」)。
- `lookup-action.example.json`:Action 内联 Lookup 形状参考(单选 + `multiple: true` 多选并列;跨表引用勿填编号;见 `references/ACTIONS.md`「跨表引用:Lookup」)。
- `visible-when-action.example.json`:Action 参数字段条件显隐 `visibleWhen` 形状参考(互斥模式联动;见 `references/ACTIONS.md`「字段条件显隐」;≠ Policy L3)。
- `replaces-create.action.example.json` / `replaces-update.action.example.json` / `replaces-delete.action.example.json` / `replaces-import.action.example.json`:数据管理默认按钮接管(`replaces`)形状参考;**import 时 UI 仍用宿主「导入数据」面板**,确认后调技能写库(见 `references/ACTIONS.md`「`replaces`」)。

View File

@@ -1,12 +1,12 @@
{
"_comment": "形状参考:Action 参数引用同库另一张表时用 Lookup 下拉,勿让用户填内部编号。完整契约见 references/ACTIONS.md「跨表引用:Lookup」与 references/SCHEMA.md。",
"_comment": "形状参考:Action 参数引用同库另一张表时用 Lookup。单选与多选同一 lookup 对象,多选只加 multiple:true。完整契约见 references/ACTIONS.md「跨表引用:Lookup」与 references/SCHEMA.md。",
"schemaVersion": 1,
"skill": "your-skill-slug",
"actions": [
{
"id": "send-with-template",
"label": "按模板发送(示例)",
"description": "演示内联 lookup:从 message_templates 选模板;-- 表示自动挑选。",
"description": "演示内联 lookup 单选:从 message_templates 选模板;-- 表示自动挑选。",
"placements": ["toolbar"],
"executionProfile": "async",
"interaction": "form",
@@ -43,6 +43,41 @@
}
}
}
},
{
"id": "probe-with-engines",
"label": "按引擎抽检(多选示例)",
"description": "演示 lookup.multiple:从 engines 多选,提交为逗号分隔 id。",
"placements": ["toolbar"],
"executionProfile": "async",
"interaction": "form",
"bind": {
"tables": ["queries"]
},
"entrypoint": {
"type": "cli",
"command": "query",
"args": ["batch-probe", "--engine-ids", "{{engineIds}}"]
},
"inputSchema": {
"type": "object",
"properties": {
"engineIds": {
"type": "string",
"title": "将使用的引擎",
"description": "可多选;提交值为逗号分隔编号,如 1,2。",
"display_type": "lookup",
"lookup": {
"ref_table": "engines",
"value_column": "id",
"label_column": "display_name",
"filter_column": "enabled",
"filter_value": 1,
"multiple": true
}
}
}
}
}
]
}

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,28 @@
{
"id": "item-create",
"label": "新增记录",
"description": "新增业务记录并执行领域副作用(走技能 CLI,非宿主直写库)。",
"replaces": "create",
"placements": [],
"executionProfile": "sync",
"interaction": "form",
"bind": {
"tables": ["items"]
},
"hostEffects": {
"onSuccess": ["reloadTable"]
},
"inputSchema": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string", "title": "名称" },
"note": { "type": "string", "title": "备注" }
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["create", "--name", "{{name}}", "--note", "{{note}}"]
}
}

View File

@@ -0,0 +1,33 @@
{
"id": "item-delete",
"label": "删除记录",
"description": "删除业务记录并清理关联副作用。",
"replaces": "delete",
"placements": [],
"executionProfile": "sync",
"interaction": "confirm",
"confirmation": {
"message": "确定删除该记录?关联数据将一并清理。"
},
"bind": {
"tables": ["items"],
"inputMapping": {
"id": "$row.$pk"
}
},
"hostEffects": {
"onSuccess": ["reloadTable", "clearSelection"]
},
"inputSchema": {
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "number", "title": "编号", "readOnly": true }
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["delete", "--id", "{{id}}"]
}
}

View File

@@ -0,0 +1,38 @@
{
"id": "item-import",
"label": "导入记录",
"description": "由宿主「导入数据」面板调用:解析 Excel/CSV 并写入业务表(含领域校验与派生字段)。",
"replaces": "import",
"placements": [],
"executionProfile": "sync",
"interaction": "form",
"bind": {
"tables": ["items"]
},
"hostEffects": {
"onSuccess": ["reloadTable"]
},
"inputSchema": {
"type": "object",
"required": ["filePath"],
"properties": {
"filePath": {
"type": "string",
"title": "导入文件",
"description": "由宿主导入面板传入的 Excel/CSV 路径。",
"format": "file"
},
"onError": {
"type": "string",
"title": "导入规则(出错时)",
"enum": ["跳过该行,继续导入", "立即停止,不再导入后续行"],
"default": "跳过该行,继续导入"
}
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["import", "--file", "{{filePath}}", "--on-error", "{{onError}}"]
}
}

View File

@@ -0,0 +1,34 @@
{
"id": "item-update",
"label": "编辑记录",
"description": "编辑业务记录并重算派生字段。",
"replaces": "update",
"placements": [],
"executionProfile": "sync",
"interaction": "form",
"bind": {
"tables": ["items"],
"inputMapping": {
"id": "$row.$pk",
"name": "$row.name",
"note": "$row.note"
}
},
"hostEffects": {
"onSuccess": ["reloadTable"]
},
"inputSchema": {
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "number", "title": "编号", "readOnly": true },
"name": { "type": "string", "title": "名称" },
"note": { "type": "string", "title": "备注" }
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["update", "--id", "{{id}}", "--name", "{{name}}", "--note", "{{note}}"]
}
}

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

@@ -1,4 +1,4 @@
{
"version": "1.0.0",
"version": "1.0.92",
"skill": "your-skill-slug"
}

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

@@ -84,8 +84,8 @@
},
"mutationKind": {
"type": "string",
"enum": ["create", "update", "delete"],
"description": "可选。接管数据管理默认按钮:create=新增、update=编辑、delete=删除。须配合非空 bind.tables;无配置时宿主仍直写 SQLite。"
"enum": ["create", "update", "delete", "import"],
"description": "可选。接管数据管理默认按钮:create=新增、update=编辑、delete=删除、import=导入。须配合非空 bind.tables;无配置时宿主仍直写 SQLite。import 时 UI 仍用宿主「导入数据」面板,确认后调用本 Action 写库。"
},
"scalarArg": {
"type": ["string", "number", "boolean"]
@@ -120,14 +120,19 @@
},
"display_type": {
"type": "string",
"enum": ["textarea", "date", "datetime_unix_seconds", "url", "email", "lookup"],
"description": "可选。表单控件提示;与 format 二选一即可。lookup=关联下拉(须配 lookup 对象或绑到已有 lookup 列)。须与 CRUD display_type 语义对齐(见 ACTIONS.md / SCHEMA.md)。"
"enum": ["textarea", "date", "datetime_unix_seconds", "url", "email", "lookup", "directory", "file"],
"description": "可选。表单控件提示;与 format 二选一即可。lookup=关联下拉(须配 lookup 对象或绑到已有 lookup 列)。file/directory=本地路径选择(可选 pathRoot 指定起始目录,见 ACTIONS.md)。须与 CRUD display_type 语义对齐。"
},
"format": {
"type": "string",
"enum": ["textarea", "date", "datetime_unix_seconds", "url", "email", "lookup"],
"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」。"
@@ -214,9 +219,13 @@
"filter_value": {
"type": ["string", "number", "boolean", "null"],
"description": "可选。与 filter_column 配对的等值"
},
"multiple": {
"type": "boolean",
"description": "可选。true=多选 Lookup(存/提交 TEXT 逗号分隔 id);省略或 false=单选(默认)。与 CRUD options_json lookup 同构。"
}
},
"description": "同库 FK 式 Lookup。展示:value · label;有 extra 时 value · label(extra)。宿主亦接受 camelCase 键名。"
"description": "同库 FK 式 Lookup。展示:value · label;有 extra 时 value · label(extra)。multiple 时多选。宿主亦接受 camelCase 键名。"
},
"inputSchema": {
"type": "object",
@@ -292,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",
@@ -420,7 +435,8 @@
"skill-detail": { "type": "string", "enum": ["direct", "confirm", "form"] },
"create": { "type": "string", "enum": ["direct", "confirm", "form"] },
"update": { "type": "string", "enum": ["direct", "confirm", "form"] },
"delete": { "type": "string", "enum": ["direct", "confirm", "form"] }
"delete": { "type": "string", "enum": ["direct", "confirm", "form"] },
"import": { "type": "string", "enum": ["direct", "confirm", "form"] }
},
"description": "按 placement / replaces 覆盖 interaction(只能写 concrete 模式,不能写 auto)。"
},

View File

@@ -59,6 +59,8 @@ checklist_version: 1.0
- [ ] S0-8 是否包含提交、付款、删除、审批、发布、发送等不可逆副作用:是 / 否
- [ ] S0-9 **入口定位表**(见 `PRODUCT_SURFACES.md` / `REQUIREMENTS` §0.1):已填写 / 未填写
- [ ] S0-10 本技能宣称的主用户路径涉及:对话 / 数据管理(执行或仅展示) / 定时(可多选;与定位表一致)
- [ ] S0-11 **业务行生命周期**(见 `references/WORK_ITEM_STATUS.md` / `REQUIREMENTS` §0.3):有处理队列且已声明状态→角色映射 / 不适用
- [ ] S0-12 **HTML 页面报告**(见 `references/ACTIONS.md`「HTML 页面报告」):有侧栏可预览的 `html/*.html` + `render-html` / 不适用
---
@@ -75,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)
---
@@ -129,6 +137,15 @@ checklist_version: 1.0
- [ ] A6-3 **P1** 重复执行同一业务键时,结果符合宣称(更新/去重/追加),无莫名脏数据或静默丢数据
- [ ] A6-4 **P1** 「零结果成功」与「失败」可区分
- [ ] A6-5 **P1** 若产出文件:落在约定数据位置且可找到,不丢在不明临时路径
- [ ] A6-6 **P0** 有业务表时:`sqlite_master` 中业务表 DDL **不得**含 SQLite `FOREIGN KEY` / `REFERENCES`;跨表只存业务 id,关联与级联在 repository/service;界面名称用 `lookup`(见 `references/SCHEMA.md`);可跑 `validate_template_database` / `init-db` 后自检
- [ ] A6-7 **P0** 有「处理业务行」类 Action 时(见 `WORK_ITEM_STATUS.md`):默认只处理**可操作**角色状态;成功/失败/跳过/进行中再点处理应拒绝或跳过且不产生重复副作用;重做须人工改回可操作。无处理队列 → **N/A**
- [ ] A6-8 **P0** 同上适用时:开始处理后业务表进入**进行中**;成功回写成功类终态;失败回写失败类终态且**失败原因**可读(任务成功但业务行仍停在可操作/进行中 → **FAIL**)
- [ ] A6-9 **P1** 同上适用时:`REQUIREMENTS` §0.3(或等价)已填写本技能状态→角色映射;处理类 Action 确认文案写清仅可操作态
- [ ] A6-10 **P1** 有 HTML 侧栏预览时(S0-12):预览区横向铺满、无横向滚动条;重新生成后内容与页头时间更新;布局符合 `references/ACTIONS.md`「宿主预览布局」。无 HTML 报告 → **N/A**
- [ ] A6-11 **P1** 业务**状态 / 启用类**列若用户需要筛表或顶栏搜中文:`searchable: 1` 且 `options_json`(或 `boolean` + `options_json`)的 **value 与存库一致**;仅 `searchable: 0` 则宿主不提供列筛与全局搜索。见 `references/SCHEMA.md`「列筛选与全局搜索」
- [ ] A6-12 **P2** `display_type: boolean` 的展示列推荐同时配置 `options_json` 中文 label(如 停用/启用),避免仅靠宿主默认「是/否」;无 boolean 列 → **N/A**
- [ ] A6-13 **P0** 新技能**不得**创建 `task_logs`;过程/进度/业务结果走运行日志、任务中心、业务表(见 `LOGGING.md`、`SCHEMA.md`)
- [ ] A6-14 **P0** 废弃用户可见表时:`drop_deprecated_user_table` 物理删表 + 清元数据;**禁止**只删元数据留物理表;有迁移时须提供旧库回归测试(见 `SCHEMA.md`、`tests/samples/test_table_deprecation_migration.py.sample`)
---
@@ -140,6 +157,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 按钮中文名」;不与宿主「查看 / 编辑 / 删除」抢名;打开类成功不弹成功条。行业专有按钮名可自创
---
@@ -169,7 +187,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** 开发者按平台规则能安装并测到该技能(若平台有开发者可见性要求)
---

View File

@@ -67,7 +67,9 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
宿主约定(与本模板对齐):
- 工具栏「导入」默认从 `input/` 取文件;「导出」默认写到 `output/`
- 工具栏「导入」**UI 一律**打开宿主「导入数据」面板(默认从本技能 `input/` 选文件);「导出」默认写到 `output/`
- **Action 表单**里 `display_type`/`format` 为 `file` 或 `directory` 时:宿主原生对话框默认打开**当前技能** `{skill_data}/input`;若字段声明可选 **`pathRoot`**(相对技能数据根,如 `input/库存数据`),则打开该子目录。禁止在技能里写用户 ID 或绝对盘符。契约见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「本地路径选择与 pathRoot」
- 若表声明了 `replaces: import`,确认导入后由技能 CLI 写库(见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「`replaces`」);仅有 `replaces: create` 时宿主可按行调 create(兼容)
- 数据管理扫描 `{skill_data}/html/*.html`(一层、非递归);`<title>` → 侧栏显示名;「重新生成」调用 Action id **`render-html`**(见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「HTML 页面报告」)
---
@@ -175,6 +177,10 @@ DOWNLOAD_OUTPUT_DIR=./outputs/videos
- `tests/test_runtime_paths.py` 覆盖 `resolve_data_path` / `resolve_input_path`。
- `POLICY-DATA-PATH-001`:禁止 `.env.example` 中 `./` 路径;禁止业务代码 `abspath(config.get(...))`。
### 卸载技能与数据保留
宿主**卸载技能只删除安装目录**(如 `.openclaw/skills/{slug}`),**不会**删除 `{JIANGCHANG_DATA_ROOT}/{user_id}/{skill_slug}/` 下的 SQLite、输入输出与日志。重装会复用同一用户库;schema 迁移须在 `init_db()` 中幂等完成(见 [`../references/SCHEMA.md`](../references/SCHEMA.md)「表增删改与退役」)。
---
## 9. 相关文档

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) §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`(无视频时用 `""`);正文可选。
@@ -527,6 +527,7 @@ assets/actions.json → placements / bind / executionProfile / entry
- `db/connection.py`
只做连接和建表(模板默认只建 `_jiangchang_*` 元数据表;业务表按需 CREATE)
**业务表之间禁止 SQLite `FOREIGN KEY` / `REFERENCES`**:只存业务 id,存在性 / 级联 / 删除在 repository/service 处理(见 [`references/SCHEMA.md`](../references/SCHEMA.md)「表与表之间:禁止 SQLite 外键」)
- `db/<domain>_repository.py`
只做业务表增删查改(有业务表才建;模板不再默认提供任务审计表)
@@ -589,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(可选)
@@ -719,9 +720,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)(或注明由既有测试覆盖)。
@@ -836,7 +843,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

@@ -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 | 待宿主升级后完整生效;模板侧已强制显式绑定 |
| 数据管理技能按钮中文名(`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

@@ -56,7 +56,7 @@
数据管理有两层,**同等重要**:
1. **执行面**:工具栏 / 行级 / 批量 / `replaces` 接管 CRUD —— 与对话一样触发同一 CLI,走完整业务闭环(传统用户可不聊天)。
1. **执行面**:工具栏 / 行级 / 批量 / `replaces` 接管新增·编辑·删除·**导入** —— 与对话一样触发同一 CLI,走完整业务闭环(传统用户可不聊天)。导入按钮的 UX 始终是宿主「导入数据」面板;有 `replaces:import` 时确认后由技能写库。
2. **运营面**:业务表、状态流向、配置、input/output、日志、HTML —— 看清数据从哪来、到哪去、卡在哪。
**适合:**
@@ -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 协作与授权边界(涉及外部系统时必填)
@@ -85,6 +92,20 @@
- 需要保存哪些日志、截图、视频和业务结果:
- 定位失败时如何保存 DOM/UIA/XML、候选数量和环境版本:
## 0.3 业务行生命周期(有「处理某一行」类能力时必填)
若本技能存在对业务表某一行做生成、触达、私信、发布等**有副作用**的处理,须遵守 [`../references/WORK_ITEM_STATUS.md`](../references/WORK_ITEM_STATUS.md),并在此声明**本技能状态 → 角色**映射(状态名自定,角色固定):
| 角色 | 本技能状态值(可多个) | 说明 |
|------|------------------------|------|
| 可操作(operable) | (例)新建 / 待生成 / … | 默认唯一可被处理 Action 捞起 |
| 进行中(in_progress) | (例)生成中 / 触达中 / … | 抢占后、结束前 |
| 成功类终态 | (例)已生成 / 已私信 / … | 默认不可再处理 |
| 失败类终态 | (例)失败 / 触达失败 / … | 默认不可再处理;失败原因字段: |
| 跳过类终态 | (例)已跳过 / … | 默认不可再处理 |
无此类处理队列时整节写「不适用」即可。
## 1. 文档目标
说明这份需求文档的作用,以及它解决什么问题。

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,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

View File

@@ -29,6 +29,7 @@
3. 有可感知副作用时:提供 `confirmation.message`
4. 需要用户/Agent 入参时:简单 `inputSchema` + entrypoint 中的 `{{fieldName}}`
5. `SKILL.md`「用户意图路由表」与 `CLI.md`「Action 强制黑名单」**出现同一 action id / 命令**
6. 若处理的是**业务队列表中的一行**:遵守 [`WORK_ITEM_STATUS.md`](WORK_ITEM_STATUS.md)——默认只捞**可操作**角色;开始抢占为**进行中**;结束回写成功/失败(失败带原因);确认文案写清仅可操作态可处理
默认模板 `assets/actions.json` 仅含诊断类 sync Action,**不要**为通过本 checklist 而塞假业务 Action;业务技能按需添加。完整字段见下文 Phase 1。
@@ -83,7 +84,7 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
| 字段 / 层 | 只决定什么 | 不决定什么 |
|-----------|------------|------------|
| **`placements`** | Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail;`row`=行级入口,壳由宿主呈现) | 不决定 sync/async,不决定点了是否弹窗,不决定行级用操作列还是右键,不决定谁能点 |
| **`replaces`** | **可选**:接管数据管理默认「新增 / 编辑 / 删除」 | 不强制;无配置时宿主仍直写 SQLite |
| **`replaces`** | **可选**:接管数据管理默认「新增 / 编辑 / 删除 / **导入**」 | 不强制;无配置时宿主仍直写 SQLite;**import 时 UI 仍用宿主「导入数据」面板** |
| **`bind.tables`** | 数据管理里出现在**哪些业务表** | 不决定业务实现 |
| **`executionProfile`** | 宿主是**同步等待**还是**异步 Job** | 不限制可展示入口,不决定是否弹窗 |
| **`interaction`** | 可点击入口(数据管理 toolbar/row/batch、默认 CRUD 接管、未来 skill-detail)**点了怎么交互** | 不决定 sync/async;**cron 触发与 agent 忽略** |
@@ -105,11 +106,11 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
| action | `id`、`label`、`description`、`placements`、`entrypoint`、**`executionProfile`(必填)**;可选 `hoverHint`、`inputSchema`、`confirmation`、**`interaction`**、**`interactionByPlacement`**、**`bind`**、**`replaces`**、**`hostEffects`**、**`steps`**、**`authz`**(仅文档提示) |
| entrypoint | `type = cli`、`command`、`args`(**必填** JSON 数组,项为 `string` / `number` / `boolean`) |
| placements | `toolbar`、`row`、`batch`、`skill-detail`、`agent`、`cron`;仅 `replaces` 时可为空数组 |
| **replaces** | 可选 `"create"` \| `"update"` \| `"delete"` — 接管默认新增/编辑/删除(须 `bind.tables`) |
| **bind** | `{ "tables": ["business_table"] }`;可选 `inputMapping`(行内/批量预填,见下节) |
| **replaces** | 可选 `"create"` \| `"update"` \| `"delete"` \| `"import"` — 接管默认新增/编辑/删除/导入(须 `bind.tables`) |
| **bind** | `{ "tables": ["business_table"] }`;可选 `inputMapping`、`columnMapping`、**`labelColumn`**(行内/批量预填与选择摘要标签,见下节) |
| **policy** | 可选;`enabled` + `entrypoint`(推荐 `policy` CLI);见 POLICY.md。未启用 = L0 |
| **cellPresentation** | 可选;`{ "enabled": true }` 时宿主应加载单元格呈现;见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)。未启用 = P0 |
| inputSchema | 根 `type: object`;properties 使用 `string` / `number` / `boolean`;支持 `enum`、`default`、`required`、`sensitive`、`readOnly`;可选 `display_type` / `format`(`textarea` / `date` / `datetime_unix_seconds` / `url` / `email` / `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` 混淆 |
@@ -131,41 +132,185 @@ 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。
## `replaces`(可选:接管默认新增 / 编辑 / 删除)
### 数据管理:选择摘要(宿主默认能力)
有领域副作用的表(建目录、加密、级联清理)不要依赖宿主直写 SQLite。此时可为该表声明:
当用户在数据管理**勾选行**后打开 `confirm` / `form` 弹层时,宿主会展示通用 **「已选择 · N 条」** 摘要(`#主键` + 可读标签;超过约 3 条可展开查看全部)。这是**宿主 UX**,对所有技能默认生效:
- **技能零改动即可受益**;不要在 `confirmation.message` 里手写「已选 id 列表」,也不要为了面板去拼业务状态跳过文案。
- 摘要与执行解耦:勾选了谁由 UI 说明;**实际处理哪些行、是否跳过某些状态,仍由技能 CLI / domain service 决定**。
- 需要稳定标签列时,在 `bind` 上声明可选 `labelColumn`(见上)。示例:[`assets/examples/selection-label-column.action.example.json`](../assets/examples/selection-label-column.action.example.json)。
## `replaces`(可选:接管默认新增 / 编辑 / 删除 / 导入)
有领域副作用的表(归一化字段、建目录、加密、级联清理)不要依赖宿主直写 SQLite。此时可为该表声明 `replaces`,**用户仍点宿主默认按钮**,由技能 CLI 完成写入。
### 怎么选
| 场景 | 建议 |
|------|------|
| 纯表字段、无副作用 | 不配 `replaces`,宿主直写即可 |
| 单条新增/编辑/删除有副作用 | `replaces: create` / `update` / `delete` |
| 批量导入也有副作用(推荐) | 另配 `replaces: import`(与 create 可同表并存) |
| 只有 create、暂无 import | 宿主「导入」面板确认后会**逐行调 create**(兼容);正式产品应补 `import` |
### 导入分流(必读)
点工具栏「导入」时:
1. **UI 一律**打开宿主「导入数据」面板(下载模板 → 选文件 → 映射 → 出错规则 → 预览)。
2. **写库**按表能力分流:
- 有 `replaces: import` → 把选中文件路径 + 出错规则交给该 Action(技能解析并写入)
- 仅有 `replaces: create` → 宿主按行调用 create(兼容)
- 都没有 → 宿主直写 SQLite
**不要**再做一张技能自己的「开始执行」导入表单来冒充导入体验。`placements` 对 import 保持 `[]`;`description` 写明「由宿主导入面板调用」。
可抄文件见 [`assets/examples/`](../assets/examples/):`replaces-*.action.example.json`。
### 示例:create
```json
{
"id": "account-add-web",
"label": "新增账号",
"description": "登记网页账号并初始化 profile 目录。",
"id": "item-create",
"label": "新增记录",
"description": "新增业务记录并执行领域副作用(走技能 CLI,非宿主直写库)。",
"replaces": "create",
"placements": [],
"executionProfile": "sync",
"interaction": "form",
"bind": { "tables": ["accounts"] },
"bind": { "tables": ["items"] },
"hostEffects": { "onSuccess": ["reloadTable"] },
"inputSchema": {
"type": "object",
"required": ["platform_key", "account_label"],
"required": ["name"],
"properties": {
"platform_key": { "type": "string", "title": "平台" },
"account_label": { "type": "string", "title": "账号名称" }
"name": { "type": "string", "title": "名称" },
"note": { "type": "string", "title": "备注" }
}
},
"entrypoint": {
"type": "cli",
"command": "account",
"args": ["add-web", "--platform", "{{platform_key}}", "--label", "{{account_label}}"]
"command": "item",
"args": ["create", "--name", "{{name}}", "--note", "{{note}}"]
}
}
```
### 示例:update
```json
{
"id": "item-update",
"label": "编辑记录",
"description": "编辑业务记录并重算派生字段。",
"replaces": "update",
"placements": [],
"executionProfile": "sync",
"interaction": "form",
"bind": {
"tables": ["items"],
"inputMapping": {
"id": "$row.$pk",
"name": "$row.name",
"note": "$row.note"
}
},
"hostEffects": { "onSuccess": ["reloadTable"] },
"inputSchema": {
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "number", "title": "编号", "readOnly": true },
"name": { "type": "string", "title": "名称" },
"note": { "type": "string", "title": "备注" }
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["update", "--id", "{{id}}", "--name", "{{name}}", "--note", "{{note}}"]
}
}
```
### 示例:delete
```json
{
"id": "item-delete",
"label": "删除记录",
"description": "删除业务记录并清理关联副作用。",
"replaces": "delete",
"placements": [],
"executionProfile": "sync",
"interaction": "confirm",
"confirmation": { "message": "确定删除该记录?关联数据将一并清理。" },
"bind": {
"tables": ["items"],
"inputMapping": { "id": "$row.$pk" }
},
"hostEffects": { "onSuccess": ["reloadTable", "clearSelection"] },
"inputSchema": {
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "number", "title": "编号", "readOnly": true }
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["delete", "--id", "{{id}}"]
}
}
```
### 示例:import
```json
{
"id": "item-import",
"label": "导入记录",
"description": "由宿主「导入数据」面板调用:解析 Excel/CSV 并写入业务表(含领域校验与派生字段)。",
"replaces": "import",
"placements": [],
"executionProfile": "sync",
"interaction": "form",
"bind": { "tables": ["items"] },
"hostEffects": { "onSuccess": ["reloadTable"] },
"inputSchema": {
"type": "object",
"required": ["filePath"],
"properties": {
"filePath": {
"type": "string",
"title": "导入文件",
"description": "由宿主导入面板传入的 Excel/CSV 路径。",
"format": "file"
},
"onError": {
"type": "string",
"title": "导入规则(出错时)",
"enum": ["跳过该行,继续导入", "立即停止,不再导入后续行"],
"default": "跳过该行,继续导入"
}
}
},
"entrypoint": {
"type": "cli",
"command": "item",
"args": ["import", "--file", "{{filePath}}", "--on-error", "{{onError}}"]
}
}
```
@@ -173,12 +318,13 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
规则:
- **全可选**:无 `replaces` 时,宿主默认按钮仍直写库,行为与以前一致。
- 取值:`create`(新增)/ `update`(编辑)/ `delete`(删除)。
- 取值:`create`(新增)/ `update`(编辑)/ `delete`(删除)/ `import`(导入写库)。
- **必须**非空 `bind.tables`;同一技能同一表同一 `replaces` 至多一个 Action。
- 与 `placements` 正交:可只接管默认按钮(`placements: []`),不必再挂 toolbar「第二个新增」。
- 点默认按钮时走该 Action 的 `interaction`(create/update 常用 `form`;delete 常用 `confirm`)。
- 与 `placements` 正交:可只接管默认按钮(`placements: []`),不必再挂 toolbar「第二个新增/导入」。
- 点默认「新增/编辑/删除」时走该 Action 的 `interaction`(create/update 常用 `form`;delete 常用 `confirm`)。
- 点默认「导入」时**固定**打开宿主导入面板;确认后若有 `replaces:import` 则调用该 Action(`interaction` 不决定是否再弹技能表单)。
- `update` / `delete` 可用 `inputMapping`(`$row.$pk`、`$selection.ids`)预填主键。
- 宿主导入仍直写:有 `replaces:create` 的表慎用宿主导入,或另做 import Action。
- `import` 约定:`filePath`(或 `file`)+ 可选 `onError`(`skip`/`abort` 或上例中文枚举);技能自行解析文件与表头。
- 整表 `_jiangchang_tables.readonly=1` 时仍隐藏写按钮(优先于 `replaces`)。
- **列元数据复用(lookup)**:Action 表单(含 toolbar/batch 弹窗与 create/update 抽屉)可将 `inputSchema` 字段对齐业务列,复用中文名与 lookup 下拉:
- 优先让属性名 = 物理列名(如 `platform_key`);
@@ -193,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",
@@ -229,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" }
@@ -241,7 +387,7 @@ Action 是否提供、挂哪些 `placements`,由本技能 **入口定位表**
"type": "object",
"required": ["accountId"],
"properties": {
"accountId": { "type": "number", "readOnly": true }
"accountId": { "type": "number", "title": "账号 ID", "readOnly": true }
}
},
"entrypoint": {
@@ -260,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` **独立**。只决定数据管理**顶栏技能按钮**鼠标悬停时显示什么。
@@ -448,6 +627,7 @@ python {skill_root}/scripts/main.py <command> <args...>
- 使用稳定的退出码
- 成功时输出可解析的结构化结果
- **例外(打开类)**:打开目录 / 打开浏览器 / 打开文件成功时不要输出用户可见成功文案;若打印 JSON,令 `"message": ""`。失败仍走下一款错误约定
- 失败时输出稳定错误信息
- 对可预期错误提供稳定错误码
- 对用户可处理的问题提供 `HINT`
@@ -474,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,构成该表行的技能菜单项集合;配什么就出现什么。
@@ -519,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 页面
@@ -570,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`。
### 实现要点
@@ -583,6 +793,7 @@ HINT: <next step>
### 验收(复制后自测)
- [ ] `html/` 下出现 `.html`,数据管理侧栏能看到报告项,预览正常
- [ ] 预览区**横向铺满**、**无横向滚动条**;拖窄侧栏后内容自适应(见上节「宿主预览布局」)
- [ ] 表顶栏(若声明 toolbar)或报告右键「重新生成」能跑通;成功后预览刷新
- [ ] 未登录/无权限用户被拒绝,且有可读错误
- [ ] 成功路径带 `refreshCatalog`(或等价让侧栏/预览看到更新)
@@ -598,7 +809,7 @@ HINT: <next step>
| 当前表已有同含义列,且列上已是 CRUD lookup | `column` / `bind.columnMapping`,或属性名 = 列名 | toolbar/batch 弹窗与 create/update 抽屉都会复用该列已解析的选项 |
| 当前表没有该列(如在「潜客」表上选「话术模板」) | `display_type: "lookup"` + 字段内联 `lookup` 对象 | 打开表单时宿主按当前技能库解析选项 |
内联 `lookup` 与列元数据 `options_json` **同构**(见 [`SCHEMA.md`](SCHEMA.md)「表关联 lookup」):
内联 `lookup` 与列元数据 `options_json` **同构**(见 [`SCHEMA.md`](SCHEMA.md)「跨表引用 lookup」):
```json
{
@@ -615,6 +826,20 @@ HINT: <next step>
"filter_column": "is_active",
"filter_value": 1
}
},
"engineIds": {
"type": "string",
"title": "监测引擎",
"description": "可多选;提交为逗号分隔编号。",
"display_type": "lookup",
"lookup": {
"ref_table": "engines",
"value_column": "id",
"label_column": "display_name",
"filter_column": "enabled",
"filter_value": 1,
"multiple": true
}
}
}
```
@@ -622,20 +847,26 @@ HINT: <next step>
| 字段 | 必填 | 说明 |
|------|------|------|
| `ref_table` | 是 | 同库被引用表 |
| `value_column` | 是 | 提交给 CLI 的值(编号 / 外键) |
| `value_column` | 是 | 提交给 CLI 的值(业务 id / 编号) |
| `label_column` | 是 | 主文案列 |
| `extra_column` | 否 | 第三展示列(任意列名,不限「备注」) |
| `filter_column` / `filter_value` | 否 | 等值过滤 |
| `multiple` | 否 | `true` = 多选;省略/`false` = 单选(默认) |
**下拉展示(宿主统一,技能只配列):** `{value} · {label}`;有非空 `extra_column` 时为 `{value} · {label}({extra})`。例:`2 · 通用私信-留资`、`2 · 通用私信-留资(dm)`。
**下拉/选项展示(宿主统一,技能只配列):** 单选为 `{value} · {label}`;有非空 `extra_column` 时为 `{value} · {label}({extra})`。例:`2 · 通用私信-留资`、`2 · 通用私信-留资(dm)`。
**可选「自动」:** 字段不要放进 `required`;不要用 `minimum: 1` 卡死留空。宿主会省略空的可选数字参数,避免「提交的数据无效」。
**多选存值与模板:** Action / CRUD 存 **`TEXT` 逗号分隔 id**(如 `1,2,5`);`{{engineIds}}` 原样替换进 CLI。多选字段在 `inputSchema` 里用 `type: "string"`(逗号串),不要用尚未稳定的 JSON array 当业务控件。
**多选控件(宿主策略):** 选项少(建议 ≤6)用勾选列表;选项多用字段内 Chip + 可搜索下拉(不是再开业务弹窗)。
**可选「自动」(单选常见):** 字段不要放进 `required`;不要用 `minimum: 1` 卡死留空。宿主会省略空的可选数字参数,避免「提交的数据无效」。
**反模式(勿做):**
- 标题写成「某某编号」,说明里教用户去左侧表抄 id
- 用静态 `enum` 罗列会增长/变更的业务行(会过期)
- 在技能里自己拼假 label 列代替宿主统一格式(难维护)
- 另造 `lookup_multi` / 第二套 display_type(应只加 `multiple: true`)
形状参考:[`assets/examples/lookup-action.example.json`](../assets/examples/lookup-action.example.json)。
@@ -651,18 +882,24 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件
| 数字 | `type: number` | `display_type: number` 或 INT/REAL/NUMERIC |
| 开关 | `type: boolean` | `display_type: boolean` |
| 枚举下拉 | `enum: [...]` | `options_json` 数组 |
| 关联下拉(Lookup) | 内联 `display_type: lookup` + `lookup`,或绑列 `column` / `columnMapping` | `display_type: lookup` + `options_json` 对象 |
| 关联下拉(Lookup) | 内联 `display_type: lookup` + `lookup`(可选 `multiple`),或绑列 | 同左;多选存 `TEXT` 逗号分隔 id |
| 密码 | `sensitive: true` | 列 `sensitive` |
| 日期(日) | `display_type` 或 `format`: `date` | `display_type: date` → 存 `YYYY-MM-DD` |
| 日期时间 | `display_type` 或 `format`: `datetime_unix_seconds` | 同左;**存/传 Unix 秒** |
| URL(薄层) | `display_type` 或 `format`: `url` | `display_type: url` |
| Email(薄层) | `display_type` 或 `format`: `email` | `display_type: email` |
| **本地文件路径** | `display_type`/`format`: **`file`**;可选 **`pathRoot`** | CRUD 一般不用;见下节 |
| **本地目录路径** | `display_type`/`format`: **`directory`**;可选 **`pathRoot`** | CRUD 一般不用;见下节 |
| 说明灰字 | `description` | `_jiangchang_columns.description` |
| 表格横幅 / 列头「?」 | (无;Action 不用) | `_jiangchang_tables.grid_help` / `_jiangchang_columns.grid_help` |
| 默认值 | `default` | SQLite 列 `DEFAULT` |
| 只读 | `readOnly: true` | 列 `editable: 0` / 宿主只读规则 |
| **条件显隐** | **`visibleWhen`**(见下节) | **本版 CRUD 无对称**;仅 Action / 定时任务参数表单 |
| **列筛选「值」** | **无对称**(Action 无表格筛选) | 见 [`SCHEMA.md`](SCHEMA.md)「数据管理:列筛选与全局搜索」:枚举 / boolean / lookup 在筛选栏用**下拉 Select**(非开关、非分段单选);存库仍用 value,展示与搜索认 label |
**本版不做(勿宣传):** 文件上传、多选/标签数组、复杂 `oneOf` / nested object 专用表单;`visibleWhen` 多条件 AND/OR 树、跨 Action 依赖、CRUD 新增/编辑对称显隐。
**本版不做(勿宣传):** 浏览器内文件上传控件、复杂 `oneOf` / nested object 专用表单;`visibleWhen` 多条件 AND/OR 树、跨 Action 依赖、CRUD 新增/编辑对称显隐;Action 参数表单的列筛选对称。
(**Lookup 多选**已支持:同一 `lookup` + `multiple: true`,见上文;不是任意标签数组。)
(**本地路径选择**已支持:`file` / `directory` + 可选 `pathRoot`,见下节;提交值仍是本机绝对路径字符串,不是 multipart 上传。)
日期时间约定:
@@ -670,12 +907,72 @@ 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`**
- 字段内联 `lookup`(与 CRUD `options_json` lookup 对象同构;可选 `extra_column`)
- `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 校验失败
@@ -727,6 +1024,8 @@ Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件
| Skill Action 弹窗 | `inputSchema.title` | `inputSchema.description` | `inputSchema.default` | `type` + `display_type`/`format` |
| 数据管理新增/编辑 | `_jiangchang_columns.display_name` | `_jiangchang_columns.description` | 物理列 SQLite `DEFAULT` | `display_type` + 物理类型推断 |
数据管理**表格**上方说明与列头「?」用 `_jiangchang_tables.grid_help` / `_jiangchang_columns.grid_help`(空则不渲染),**不要**用 `description`。见 [`SCHEMA.md`](SCHEMA.md)「表格说明 `grid_help`」。
CRUD 侧宿主义务与禁止事项见 [`SCHEMA.md`](SCHEMA.md)「表单与展示:稳定字段类型」「新增/编辑表单:字段说明与默认值」。同一业务含义两边文案与控件应对齐,但分别配置。
示例:
@@ -777,13 +1076,15 @@ CRUD 侧宿主义务与禁止事项见 [`SCHEMA.md`](SCHEMA.md)「表单与展
"pickCount": {
"type": "number",
"title": "处理数量",
"description": "从队列选取 pending 记录条数",
"default": 1,
"minimum": 1
"description": "从队列选取「可操作」状态记录的条数(具体状态名由本技能定义,见 WORK_ITEM_STATUS)"
}
}
```
默认**不要**把失败/成功/跳过混进该 pick;重做靠人工改回可操作态。详见 [`WORK_ITEM_STATUS.md`](WORK_ITEM_STATUS.md)。
若还需要默认篇数,可在同一 `properties` 里为 `pickCount` 增加 `"default": 1`、`"minimum": 1`。
约定:
- `boolean` 在宿主中渲染为开关
@@ -799,7 +1100,7 @@ CRUD 侧宿主义务与禁止事项见 [`SCHEMA.md`](SCHEMA.md)「表单与展
- 短名看不懂或会覆盖数据时,用独立的 `hoverHint`;不要把确认黄条复用成悬停
- 模板参数使用 `{{fieldName}}`,且必须在 `inputSchema.properties` 中声明
- `entrypoint.args` 项当前只允许 `string`、`number`、`boolean`;不允许对象或 `null`
- **不要**在当前模板中宣传文件上传、多选数组、`oneOf`、复杂 nested object 等尚未稳定支持的能力
- **不要**在当前模板中宣传文件上传、任意 JSON 多选数组、`oneOf`、复杂 nested object 等尚未稳定支持的能力;Lookup 多选请用 `lookup.multiple`
## 风险与入口配置
- 查询、诊断、版本检查可以放到 `skill-detail` 或 `agent`

View File

@@ -150,7 +150,7 @@
4. `mode=open`:等价「L1 不限制按钮」,可与 L2 行列限制并存。
5. **`denylist` 不是 v1 合法值**;出现则 §6 fail-closed。
`replaces` 接管的默认新增/编辑/删除:视为对应 Action 的 id 是否在 allowlist 中;不在则宿主隐藏该接管按钮。`mode=open` 时不按 id 限制 replaces。
`replaces` 接管的默认新增/编辑/删除/导入:视为对应 Action 的 id 是否在 allowlist 中;不在则宿主隐藏该接管按钮(导入按钮是否可用还受 `hostCapabilities` 含 `import` 影响)。`mode=open` 时不按 id 限制 replaces。
### 2.4 `hostCapabilities`(L1 可选冻)

View File

@@ -17,10 +17,11 @@
| 根目录 [`CHANGELOG.md`](../CHANGELOG.md) | 市场更新日志 Tab(版本小节 → `changelog`) |
| [`SKILL.md`](../SKILL.md) | LLM / OpenClaw 平台技能入口与触发摘要 |
| [`CLI.md`](CLI.md) | 共享 Python、命令行入口、参数与短操作契约 |
| [`ACTIONS.md`](ACTIONS.md) | Skill Action、sync/async、任务中心、多入口(按入口定位表挂载;先读 `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 不配) |
| [`WORK_ITEM_STATUS.md`](WORK_ITEM_STATUS.md) | 业务行生命周期:处理队列的可操作/进行中/终态角色与重做规则(有处理行 Action 时适用) |
| [`../development/`](../development/) | 开发、测试、RPA 深度规范(给人 / 编码助手) |
按需阅读:短操作优先 `CLI.md`;Action / 任务中心读 `ACTIONS.md`;结构化字段读 `SCHEMA.md`;权限 opt-in 读 `POLICY.md`;格子标色读 `CELL_PRESENTATION.md`。
按需阅读:短操作优先 `CLI.md`;Action / 任务中心读 `ACTIONS.md`;结构化字段读 `SCHEMA.md`;权限 opt-in 读 `POLICY.md`;格子标色读 `CELL_PRESENTATION.md`;队列处理状态机读 `WORK_ITEM_STATUS.md`。

View File

@@ -13,7 +13,7 @@
- 定义真实 SQLite 表结构
- 使用英文 `snake_case` 表名和字段名
- 使用 `_jiangchang_tables` 提供表级展示元数据
- 使用 `_jiangchang_columns` 提供字段级展示元数据(含可选 `description` 字段说明)
- 使用 `_jiangchang_columns` 提供字段级展示元数据(含可选 `description` 表单说明、可选 `grid_help` 表格说明)
- 需要「新增表单预填」时,在物理列上声明 SQLite `DEFAULT`(勿把默认值只写在文案里)
- 定义哪些字段可见、可搜索、可创建、可编辑
- 定义字段展示类型和枚举选项
@@ -25,9 +25,11 @@
- 读取 `_jiangchang_*` 元数据
- 读取 SQLite `PRAGMA table_info`
- **数据管理侧栏/表列表**:当库中存在 `_jiangchang_tables` 时,**仅展示**在该表注册且 `visible=1` 的业务表;孤儿物理表(有表、无注册元数据)**不得**出现在侧栏,直接访问 schema API 应返回「表不存在」。极老库无 `_jiangchang_tables` 时,宿主可保留物理表枚举 fallback(兼容 `_schema_meta` 时代)
- 根据 schema 渲染数据管理页面
- 根据 `editable` 推导字段是否可创建或编辑
- 在**新增 / 编辑**表单中,若列有 `_jiangchang_columns.description`,在标签下方展示该说明(灰字;空则不显示)
- 在数据管理**表格**中,若表有 `_jiangchang_tables.grid_help` 则在表上方展示可折叠说明;若列有 `_jiangchang_columns.grid_help` 则在列头显示「?」;空/未写则不渲染。**不**把 `description` 当作表格说明
- 打开**新增**表单时,对可创建字段用 SQLite 列 `DEFAULT`(`PRAGMA table_info.dflt_value`)预填;无 DEFAULT 则留空
- 执行通用查询、新增、编辑、删除、导入、导出
- 对字段名、表名、类型和权限进行最终校验
@@ -86,7 +88,24 @@ metadata:
不要把「外部来源只读」错误理解成「技能本地数据库一定只读」。技能通过 `_jiangchang_tables.readonly` 与 `_jiangchang_columns.editable` 声明本地表的实际写权限。
若创建/更新/删除带有领域副作用(自动建目录、加密、级联清理),不要依赖宿主默认直写:在 `assets/actions.json` 用可选字段 `replaces: create|update|delete` 接管默认按钮,详见 [`ACTIONS.md`](ACTIONS.md)。
若创建/更新/删除/导入带有领域副作用(自动建目录、加密、级联清理、归一化字段),不要依赖宿主默认直写:在 `assets/actions.json` 用可选字段 `replaces: create|update|delete|import` 接管默认按钮。其中 **import** 时用户仍使用宿主「导入数据」面板,确认后由技能写库。详见 [`ACTIONS.md`](ACTIONS.md) 与 `assets/examples/replaces-*.action.example.json`。
## 表与表之间:禁止 SQLite 外键
业务表之间的「属于哪条父记录 / 引用哪条来源」**只存业务 id**(整数或文本标量),**禁止**在 DDL 中写 SQLite `FOREIGN KEY` / `REFERENCES`,也**不要**指望 `PRAGMA foreign_keys = ON` 替你校验或级联。
| 要做的事 | 正确做法 | 禁止 |
|----------|----------|------|
| 子表指向父表 | 子表列如 `case_id INTEGER NOT NULL`;写入前在 repository/service 查父表是否存在 | `FOREIGN KEY (case_id) REFERENCES parent(id)` |
| 父表记「最新一版」 | 父表列如 `latest_draft_id INTEGER NOT NULL DEFAULT 0`;由 service 更新指针 | 同上 |
| 删除 / 覆盖 / 级联清理 | repository/service 内显式 `DELETE` / `UPDATE`;需要时用 Action `steps` 的 `deleteByFk` | 依赖 DB 级联或宿主 CRUD 被 FK 拦住 |
| 数据管理里显示名称 | `_jiangchang_columns.display_type = lookup` + `options_json` 对象(见下文「跨表引用 lookup」) | 把 lookup 当成 DDL 外键 |
**为什么:** 宿主数据管理对业务表执行通用增删改,会直接写 SQLite。一旦 DDL 带外键,删除、迁移 rename 父表、或父表 id 变更时极易出现「本地数据操作失败」且难排查。关联语义属于技能业务,必须在代码里写清。
**迁移注意:** 避免「`ALTER TABLE parent RENAME TO parent_old` → 建新 parent → 删 old」这类会隐性改写子表 FK 指向的套路;优先 `ADD COLUMN` + 拷列,或重建子表且**不带** FK。
`display_metadata_validator` 与 `validate_template_database` 会扫描业务表 DDL;含 `FOREIGN KEY` / `REFERENCES` 时报错(见 `development/CHECKLIST.md` A6-6)。
## 标准数据同步语义(外源 → 本地库)
@@ -138,7 +157,8 @@ metadata:
|------|------|------|
| `table_name` | 是 | 真实英文表名 |
| `display_name` | 是 | 用户可见的中文业务名称 |
| `description` | 否 | 表说明 |
| `description` | 否 | 表说明(表单/备注用途);**不**用于表格上方横幅 |
| `grid_help` | 否 | 数据管理表格上方说明;空/未写则不出横幅。与 `description` 独立,禁止互拷(见下节) |
| `sort_order` | 否 | 兼容保留字段,不用于替代物理字段顺序 |
| `visible` | 是 | `1` 在宿主数据管理中展示 / `0` 隐藏 |
| `readonly` | 是 | `1` 整张表只读;`0` 允许宿主根据字段权限执行写操作 |
@@ -212,6 +232,43 @@ upsert_table_metadata(
)
```
#### 表格说明 `grid_help`(宿主支持)
技能可给数据管理**表格**写说明,与新增/编辑表单的 `description` **完全独立**:
| 写在哪 | 宿主做什么 | 空/未写 |
|--------|------------|---------|
| `_jiangchang_tables.grid_help` | 表格上方可折叠横幅(首行摘要;有后续行才出展开) | 不出横幅 |
| `_jiangchang_columns.grid_help` | 列头「?」,点击查看该列说明 | 不出「?」 |
规则:
- **opt-in**:大多数表/列不需要写;不写则行为与今日一致
- **禁止**把 `description` 拷进 `grid_help`,也禁止把表格规则写进 `description`(`description` 只给表单灰字)
- 多行文本时:第一行是摘要,其余行点「展开」后可见
- 通过 `upsert_table_metadata(..., grid_help=...)` / `upsert_column_metadata(..., grid_help=...)` 写入;旧库由 `init_display_metadata_tables` 幂等 `ALTER` 补列
- 需已支持该字段的匠厂宿主;更旧版本会忽略未知列,不写则无影响
示例(仅在确有表格规则时写;模板默认业务表不要预填):
```python
upsert_table_metadata(
cursor,
table_name="timeliness_records",
display_name="时效登记",
readonly=1,
grid_help="超时按环节规则判定。\n交单:预计到港日前 3 天须交单",
)
upsert_column_metadata(
cursor,
table_name="timeliness_records",
column_name="submit_date",
display_name="交单日期",
grid_help="预计到港日前 3 天须交单",
editable=0,
)
```
禁止:
- 使用中文真实表名
@@ -228,9 +285,10 @@ upsert_table_metadata(
| `column_name` | 是 | 真实英文字段名 |
| `display_name` | 是 | 用户可见的中文字段名(表头与表单标签);保持简短 |
| `description` | 否 | 字段说明(给人看的短文案);**宿主在新增/编辑表单标签下方展示**(见下节);不要塞进 `display_name` |
| `grid_help` | 否 | 数据管理表头「?」;空/未写则不出。与表单 `description` 独立,禁止互拷 |
| `display_order` | 否 | 兼容字段,默认保持 `NULL` |
| `visible` | 是 | 是否进入数据管理 schema /「列」面板(`0` = **硬隐藏**,用户无法勾回;默认不展示但可勾回请用表级 `default_visible_columns_json`) |
| `searchable` | 是 | 是否参与搜索 |
| `searchable` | 是 | 是否参与**列筛选**与**顶栏全局搜索**(`0` = 两者均不参与);详见下节「数据管理:列筛选与全局搜索」 |
| `editable` | 是 | `1` 业务字段允许宿主用于新增和编辑;`0` 系统/只读字段 |
| `display_type` | 否 | 字段渲染类型;稳定取值见下节「表单与展示:稳定字段类型」 |
| `options_json` | 否 | 枚举选项 JSON |
@@ -251,7 +309,7 @@ upsert_table_metadata(
| `number` | `INTEGER` / `REAL` / `NUMERIC` | 原文 | **数字输入**(`type=number`) | 数字;无 `display_type` 时,物理类型为 INT/REAL/NUMERIC 也应走数字控件 |
| `boolean` | `INTEGER` 0/1 或 BOOL | 开关语义展示 | 开关 | `0` / `1` |
| (枚举) | 任意 + `options_json` **数组** | 标签映射 | 下拉选择 | 选项 `value` |
| `lookup` | 外键式标量 + `options_json` **对象** | 标签映射 | 下拉选择(查关联表) | 关联键值 |
| `lookup` | 跨表引用 id;`options_json` **对象**;可选 `multiple` | 单选:标签映射;多选:多标签(如「豆包、元宝」) | 单选下拉;多选见下文 | 单选:业务 id 标量;多选:`TEXT` 逗号分隔 id(非 SQLite 外键) |
| (敏感) | 任意;列 `sensitive=1` | 掩码/隐藏策略由宿主定 | 新建可用密码框;编辑策略与既有宿主一致 | 原文(勿日志明文) |
| `date` | `TEXT` 推荐 `YYYY-MM-DD`,或可安全归一的值 | 日期 | **日期选择**(日) | `YYYY-MM-DD` |
| `datetime_unix_seconds` | `INTEGER` | `YYYY-MM-DD HH:mm:ss` | **日期时间选择**;提交写回 **Unix 秒** | Unix **秒**整数;禁止改成 ISO 字符串 |
@@ -263,9 +321,11 @@ upsert_table_metadata(
**明确不在本版稳定范围(勿在新技能宣传为已支持):**
- 文件 / 图片上传
- 多选、标签数组、复杂 `oneOf` / nested object 表单
- 复杂 `oneOf` / nested object 表单(任意 JSON 数组当业务多选控件)
- 把业务可编辑时间存成毫秒、ISO 文本或 SQLite `datetime` 文本(审计字段规范见「标准时间字段」)
> **Lookup 多选**已纳入本版契约(见下文 `multiple`),与「任意多选/标签数组」不是同一能力。
`datetime_unix_milliseconds`:仅兼容**列表展示**旧数据;**新技能不要用**;可编辑时间一律 `datetime_unix_seconds`。
#### 新增/编辑表单:字段说明与默认值(宿主支持)
@@ -277,7 +337,9 @@ upsert_table_metadata(
| 字段说明 | `_jiangchang_columns.description`(可选) | 新增/编辑表单中,在标签下方以灰字展示;空则不显示 |
| 创建默认值 | 物理列 SQLite `DEFAULT`(如 `limit_n INTEGER NOT NULL DEFAULT 0`) | 打开「新增」时,对可创建字段预填该默认值;无 DEFAULT 则留空 |
| 编辑已有行 | — | **不**用 DEFAULT 覆盖已有行数据 |
| 表格列头 | 只用 `display_name` | 长说明**不**进入列头 |
| 表格列头标题 | `display_name` | 短名;不要把长说明塞进列头文字 |
| 表格列头「?」 | `_jiangchang_columns.grid_help`(可选) | 有内容才显示「?」;**不**回退 `description` |
| 表格上方说明 | `_jiangchang_tables.grid_help`(可选) | 有内容才显示横幅;**不**回退表级 `description` |
规则与禁止:
@@ -321,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` 这一份字段权限声明:
@@ -464,6 +596,19 @@ readonly = 0
模板**默认不建**此类审计表。过程看 `jiangchang.log`,进度/终态看任务中心,业务结果看业务表。
### 业务行生命周期(处理队列)
若技能对某一业务行做「生成 / 触达 / 私信 / 发布」等有副作用处理,状态字段须遵守 **角色模型**(可操作 / 进行中 / 成功·失败·跳过类终态),**不要**把某一技能的状态名当成全平台唯一枚举。默认只处理可操作态;重做靠人工改回可操作。完整规则见 [`WORK_ITEM_STATUS.md`](WORK_ITEM_STATUS.md)。
建议:
| 项 | 说明 |
|----|------|
| 状态列 | `options_json` 列出本技能全部业务状态(含进行中与各类终态) |
| 失败原因 | 失败类终态应有用户可读的原因字段(勿写密钥) |
| 可编辑 | 终态/进行中建议可改回可操作,或提供等价 Action |
| `grid_help` | 可一句话说明「仅某某状态可处理」 |
表级 `readonly = 1` 时,宿主数据管理只读展示;技能 repository 仍可写入。
### 通用示例:`demo_items`(测试 fixture 参考)
@@ -512,9 +657,11 @@ CREATE TABLE demo_items (
宿主据此渲染选择框;技能侧负责保证 JSON 合法且与物理字段取值一致。
### 2)表关联 lookup(对象,推荐用于外键式字段)
### 2)跨表引用 lookup(对象,用于关联 id 字段)
当字段取值依赖同库另一张业务表时,`display_type` 设为 `lookup`,`options_json` 写对象(**不是**数组):
当字段取值依赖同库另一张业务表时,`display_type` 设为 `lookup`,`options_json` 写对象(**不是**数组)。这是**展示与表单选择**契约,**不是** SQLite 外键。
**单选(默认,与既有行为一致):**
```json
{
@@ -527,32 +674,60 @@ CREATE TABLE demo_items (
}
```
**多选(平行扩展,仅多一个开关):**
```json
{
"ref_table": "engines",
"value_column": "id",
"label_column": "display_name",
"filter_column": "enabled",
"filter_value": 1,
"multiple": true
}
```
| 字段 | 必填 | 说明 |
|---|---|---|
| `ref_table` | 是 | 同库被引用表名(snake_case 标识符) |
| `value_column` | 是 | 写入当前字段的值列(编号 / 外键) |
| `label_column` | 是 | 下拉主文案列(名称) |
| `value_column` | 是 | 写入当前字段的值列(业务 id / 编号) |
| `label_column` | 是 | 选项主文案列(名称) |
| `extra_column` | 否 | 第三展示列(不必叫「备注」);有值时拼进括号 |
| `filter_column` / `filter_value` | 否 | 等值过滤(如只列出 `enabled=1`) |
| `multiple` | 否 | `true` = 多选 Lookup;省略或 `false` = **单选**(默认) |
宿主在加载表 schema 时查询 `ref_table`,把结果展开成 `{value,label}` 下拉选项;**物理列仍存标量**(如 `platform_key` 文本、`account_id` 整数),不做嵌套子表。
宿主在加载表 schema 时查询 `ref_table`,把结果展开成 `{value,label}` 选项列表。
**存库约定:**
| 模式 | 物理列建议 | 存什么 |
|------|------------|--------|
| 单选 | 整数或文本标量 | 一个业务 id(如 `2`、`douyin`) |
| 多选 | **`TEXT`** | 逗号分隔 id,如 `1,2,5`(与 `$selection.ids` 风格一致);**不要**建联接子表当 UI 多选 |
**统一展示文案(宿主固定一种,技能只配列):**
- 默认:`{value} · {label}`,例如 `2 · 通用私信-留资`、`douyin · 抖音`
- 单选默认:`{value} · {label}`,例如 `2 · 通用私信-留资`、`douyin · 抖音`
- 配置了 `extra_column` 且该行有非空值:`{value} · {label}({extra})`
- 未配置 `extra_column`、或额外列为空:不加 `()`
- `label` 为空或与 `value` 相同时:只显示 `value`(可再带额外括号)
- **多选列表格**:按 id 拆开后逐个映射为 label,再用顿号/逗号拼接(如 `豆包、元宝`);过长可截断并提示「+N」
Action 弹窗内联 Lookup 与上表 **同构**(字段名一致),见 [`ACTIONS.md`](ACTIONS.md)「跨表引用:Lookup」。
**控件呈现(宿主策略,非第二套契约):**
示例:账号表的平台字段 → 平台信息表;凭据表的账号编号 → 账号信息表;私信 Action 选话术 → `message_templates`。
- 单选:下拉 Select(现状)
- 多选且选项较少(建议 ≤6):勾选列表
- 多选且选项较多:字段内 Chip + 可搜索下拉(**不是**再开一层业务弹窗)
Action 弹窗内联 Lookup 与上表 **同构**(字段名一致,含 `multiple`),见 [`ACTIONS.md`](ACTIONS.md)「跨表引用:Lookup」。
示例:账号表的平台字段 → 平台信息表;凭据表的账号编号 → 账号信息表;私信 Action 选话术 → `message_templates`;抽检 Action 多选引擎 → `engines` + `multiple: true`。
## 幂等初始化标准
- `_jiangchang_tables` 和 `_jiangchang_columns` 必须通过 upsert 初始化
- 多次执行初始化不能重复记录
- 重新初始化可以更新展示名、描述、`visible`、`searchable`、`editable`、`display_type`、`options_json`
- 重新初始化可以更新展示名、描述、`grid_help`、`visible`、`searchable`、`editable`、`display_type`、`options_json`
- 元数据不能残留旧字段(删除或重命名字段时必须同步清理)
- 元数据不能引用不存在的表或字段
- 新增字段或表时必须同步增加元数据
@@ -596,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` → **创建时间**。
@@ -604,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

@@ -0,0 +1,157 @@
# 业务行生命周期契约(Work Item Lifecycle)
> **状态:正式契约 v1.0。**
> **目标:** 冻结「对某一行做有副作用处理」时的**状态角色与操作权**规则,使队列型技能(生成、触达、私信、发布等)行为一致、可验收。
> **非目标:** 规定某一技能的具体状态码或状态个数;强制所有表都有 status;替代任务中心 / 运行日志;规定 Data Policy 授权。
> **适用范围:** 有「处理业务行」类 Action 的技能。纯配置表、目录表、只读展示表标 N/A。
相关正交轴:[`ACTIONS.md`](ACTIONS.md)(入口与 sync/async)、[`SCHEMA.md`](SCHEMA.md)(表与展示)、[`POLICY.md`](POLICY.md)(谁能点)、任务中心(宿主 Job)。
---
## 0. 为什么需要本契约
若「处理这一行」的按钮对**任意状态**都可点,会出现:
- 已成功的行被重复执行(重复发信、重复生成、覆盖产物)
- 失败行被默认批量静默重试,运营无法区分「未做」与「做过但失败」
- 任务中心显示成功,业务表仍停在初始态(结果未回写)
本契约要求:**默认可自动操作的只有「可操作」角色;其它角色须人工改回可操作后再处理。**
---
## 1. 状态角色(通用,不绑死枚举)
技能自己定义业务状态字符串与中文标签;契约只认**角色**。一个角色可对应**一个或多个**业务状态。
| 角色 | 含义 | 默认可被处理 Action 捞起? |
|------|------|---------------------------|
| **operable(可操作)** | 尚未成功处理、允许自动或行操作启动处理 | **是(默认唯一)** |
| **in_progress(进行中)** | 已抢占,正在处理 | **否**(防双跑) |
| **terminal_success(成功类终态)** | 已按业务定义完成 | **否** |
| **terminal_failure(失败类终态)** | 已尝试且失败;通常带失败原因 | **否** |
| **terminal_skipped(跳过类终态)** | 人工放弃或不处理 | **否** |
### 1.1 举例(仅对照,不是规范枚举)
| 角色 | 技能 A(内容队列)可能叫 | 技能 B(线索触达)可能叫 |
|------|--------------------------|--------------------------|
| operable | 待生成、新建 | 新建、私信未发 |
| in_progress | 生成中 | 触达中 |
| terminal_success | 已生成 | 已私信 |
| terminal_failure | 失败 | 触达失败 |
| terminal_skipped | 已跳过 | 已忽略 |
技能可以有更多业务状态(审核中、待确认、部分成功等):须在需求里写明各自映射到哪一角色,或声明「不属于处理队列、本契约 N/A」。
### 1.2 技能必须声明映射
复制模板后,在 `development/REQUIREMENTS.md`(或等价需求)中写清:
```text
处理队列表:<table>
状态字段:<column>
可操作:<status1>, <status2>, …
进行中:<status…>
成功类终态:…
失败类终态:…(失败原因字段:…)
跳过类终态:…
```
未声明映射的「处理队列」技能,交活 Checklist 不得标 PASS。
---
## 2. 硬规则(技能实现必须遵守)
1. **默认只认 operable**
行操作 / 批量 / 定时 / Agent 触发的「处理」类 Action,pick 与 claim **默认**只包含可操作状态。
不得把成功、失败、跳过、进行中混进默认捞取集合(除非另做**显式命名**的「重试失败」类 Action,且确认文案写清范围——仍推荐不如人工改回可操作简单)。
2. **开始即原子抢占 → in_progress**
在真正产生副作用前,用「仅当当前为 operable 时 UPDATE 为进行中」的方式抢占。抢占失败 → 跳过或返回明确错误(如「当前不是可操作状态」),**禁止**继续副作用。
3. **结束必须回写业务表**
- 成功 → 对应成功类终态,并写入产物路径/摘要等业务字段(若有)
- 失败 → 对应失败类终态,并写入**失败原因**(用户可读;勿塞密钥)
- 禁止「任务中心成功、业务行仍停在可操作/进行中」而不回写
4. **重做靠人工改回 operable**
成功 / 失败 / 跳过 / 卡住的进行中,默认不能再点处理。运营在数据管理把状态改回可操作后,方可再次处理。
5. **任务中心 ≠ 业务状态**
Job「进行中 / 成功 / 失败」是宿主任务态。业务「进行中」必须写在业务表。进度 `emit` 不替代状态回写。
6. **过程与结果分工**(延续既有约定)
过程 → 运行日志 / 任务中心 Activity;业务结果 → 业务表。**不要**靠新建 `task_logs` 一类审计表充当状态机。
---
## 3. Action 与文案约定
- 处理类 Action 的 `description` / `confirmation.message` 应写明:**仅处理「可操作」状态**(用本技能用户能懂的词,如「仅待生成」「仅新建」)。
- 对非可操作行触发时:返回明确失败码与中文说明,提示「请先改回 xxx」。
- 长任务(`async`):除任务中心进度外,业务行应尽快进入进行中,避免界面长时间仍显示可操作导致连点。
正交提醒:
| 轴 | 管什么 | 不管什么 |
|----|--------|----------|
| 本契约 | 什么**状态**能处理 | 谁有权点按钮 |
| Data Policy | 谁有权 | 状态机 |
| executionProfile | sync/async 通道 | 业务终态 |
---
## 4. 数据管理展示建议
- 状态列:`options_json` 枚举完整业务状态(含进行中与各类终态)。
- 失败原因列:失败类终态可见;成功后宜清空或保留末次原因(技能自定,须一致)。
- 终态与进行中的状态字段建议 **可编辑**(或提供「改回可操作」Action),以便人工重做;**不要**只能靠删行重建。
- `grid_help` 可一句话写清:「仅某某状态可处理;其它状态请改回后再操作。」
详见 [`SCHEMA.md`](SCHEMA.md)。
---
## 5. 可观测性(与 ACTIONS / 日志约定对齐)
推荐阶段(名称可自定,须能对应到开始 / 关键节点 / 结束):
1. 已抢占(业务 → 进行中)
2. 处理中关键步骤(可 `emit`)
3. 回写成功或失败
故意失败一条时:业务表应为失败类终态 + 原因;任务中心为失败;日志含 `task_failed` / `error_code=`。
---
## 6. 非目标与常见误区
| 误区 | 正确做法 |
|------|----------|
| 把「新建/成功/失败/跳过」写成全平台唯一枚举 | 只用角色;各技能自定状态名 |
| 默认批量包含失败「方便自动重试」 | 默认不含;重试改回可操作或单独显式 Action |
| 只有任务成功、不改业务状态 | 必须回写业务表 |
| 用进行中替代任务中心 | 两者都要:业务防双跑,任务轨看进度 |
| 所有表都套本契约 | 无「处理行」副作用的表标 N/A |
---
## 7. 验收摘要(完整条目见 CHECKLIST)
有处理队列时至少验证:
1. 成功类终态再点处理 → 拒绝或跳过,不产生重复副作用
2. 可操作行处理中 → 先见进行中,结束后见成功或失败+原因
3. 失败行改回可操作后 → 可以再次处理
4. 任务中心有过程(长任务)且业务表状态与结果一致
---
## 8. 版本
| 版本 | 说明 |
|------|------|
| v1.0 | 首版:角色模型 + 硬规则 + 需求映射 + 与 Action/SCHEMA/任务中心边界 |

View File

@@ -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):
@@ -17,7 +21,7 @@
[CmdletBinding()]
param(
[string]$Prefix = "v",
[string]$Message = "正式发布",
[string]$Message,
[switch]$AutoCommit,
[switch]$RequireClean,
[string]$CommitMessage,
@@ -28,6 +32,13 @@ param(
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# Default tag message is 「正式发布」 from UTF-8 bytes (not a Chinese literal in
# this file). Windows PowerShell 5.1 parses UTF-8 scripts without BOM as the
# system ANSI code page, which would turn a source literal into 「姝e紡鍙戝竷」.
if (-not $PSBoundParameters.ContainsKey("Message") -or [string]::IsNullOrWhiteSpace($Message)) {
$Message = [System.Text.Encoding]::UTF8.GetString([byte[]](0xE6, 0xAD, 0xA3, 0xE5, 0xBC, 0x8F, 0xE5, 0x8F, 0x91, 0xE5, 0xB8, 0x83))
}
function Remove-GitNoise {
param([string[]]$Lines)
return @($Lines | Where-Object {
@@ -45,9 +56,11 @@ function Convert-GitOutputLines {
)
}
# Call git.exe directly. Do not wrap with `cmd /c` — on Chinese Windows, cmd
# re-encodes -m / -F arguments through the OEM code page and garbles UTF-8
# annotated tag / commit messages (e.g. 「正式发布」 → 「姝e紡鍙戝竷」).
# Call git.exe directly. Never wrap with `cmd /c`: on Chinese Windows, cmd
# re-encodes the command line through the OEM code page.
# Also never pass Chinese via `git -m`: Windows PowerShell 5.1 encodes native
# argv using the console code page, so 「正式发布」 becomes 「姝e紡鍙戝竷」
# in the annotated tag. Commit/tag messages go through a UTF-8 file + `-F`.
function Invoke-GitRaw {
param([Parameter(Mandatory = $true)][string[]]$GitArgs)
$prev = $ErrorActionPreference
@@ -73,6 +86,21 @@ function Invoke-Git {
}
}
function Invoke-GitWithMessageFile {
param(
[Parameter(Mandatory = $true)][string[]]$GitArgsPrefix,
[Parameter(Mandatory = $true)][string]$Message
)
$msgFile = Join-Path ([System.IO.Path]::GetTempPath()) ("jiangchang-git-msg-" + [guid]::NewGuid().ToString("N") + ".txt")
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText($msgFile, ($Message.TrimEnd() + "`n"), $utf8NoBom)
try {
Invoke-Git -GitArgs (@("-c", "i18n.commitEncoding=utf-8") + $GitArgsPrefix + @("-F", $msgFile))
} finally {
Remove-Item -LiteralPath $msgFile -Force -ErrorAction SilentlyContinue
}
}
function Get-GitOutput {
param([Parameter(Mandatory = $true)][string[]]$GitArgs)
$r = Invoke-GitRaw -GitArgs $GitArgs
@@ -138,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)
@@ -229,7 +344,7 @@ function Ensure-CleanOrAutoCommit {
}
Invoke-Git -GitArgs @("add", "-A")
Invoke-Git -GitArgs @("commit", "-m", $commitMsg)
Invoke-GitWithMessageFile -GitArgsPrefix @("commit") -Message $commitMsg
}
try {
@@ -259,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
@@ -280,22 +404,14 @@ 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 -m `"$Message`"" -ForegroundColor Yellow
Write-Host "[DryRun] Would run: git tag -a $nextTag -F <utf8-msg-file> ($Message)" -ForegroundColor Yellow
Write-Host "[DryRun] Would run: git push origin $nextTag" -ForegroundColor Yellow
Write-Host "=== DryRun Complete ===" -ForegroundColor Cyan
exit 0
}
Invoke-Git -GitArgs @("tag", "-a", $nextTag, "-m", $Message)
Invoke-GitWithMessageFile -GitArgsPrefix @("tag", "-a", $nextTag) -Message $Message
Invoke-Git -GitArgs @("push", "origin", $nextTag)
Write-Host "Release success: $nextTag" -ForegroundColor Green

View File

@@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS _jiangchang_tables (
table_name TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
description TEXT,
grid_help TEXT,
sort_order INTEGER,
visible INTEGER NOT NULL DEFAULT 1,
readonly INTEGER NOT NULL DEFAULT 0,
@@ -35,6 +36,7 @@ CREATE TABLE IF NOT EXISTS _jiangchang_columns (
column_name TEXT NOT NULL,
display_name TEXT NOT NULL,
description TEXT,
grid_help TEXT,
display_order INTEGER,
visible INTEGER NOT NULL DEFAULT 1,
searchable INTEGER NOT NULL DEFAULT 1,
@@ -45,22 +47,35 @@ CREATE TABLE IF NOT EXISTS _jiangchang_columns (
)
"""
def _pragma_column_names(cursor, table_name: str) -> set[str]:
cursor.execute(f"PRAGMA table_info({table_name!r})")
return {str(row[1]) for row in cursor.fetchall()}
def _ensure_jiangchang_tables_columns(cursor) -> None:
"""旧库补列:CREATE IF NOT EXISTS 不会给已有表加新字段。"""
cursor.execute("PRAGMA table_info('_jiangchang_tables')")
cols = {str(row[1]) for row in cursor.fetchall()}
cols = _pragma_column_names(cursor, "_jiangchang_tables")
if "default_sort_json" not in cols:
cursor.execute("ALTER TABLE _jiangchang_tables ADD COLUMN default_sort_json TEXT")
if "default_visible_columns_json" not in cols:
cursor.execute(
"ALTER TABLE _jiangchang_tables ADD COLUMN default_visible_columns_json TEXT"
)
if "grid_help" not in cols:
cursor.execute("ALTER TABLE _jiangchang_tables ADD COLUMN grid_help TEXT")
def _ensure_jiangchang_columns_columns(cursor) -> None:
cols = _pragma_column_names(cursor, "_jiangchang_columns")
if "grid_help" not in cols:
cursor.execute("ALTER TABLE _jiangchang_columns ADD COLUMN grid_help TEXT")
def init_display_metadata_tables(cursor) -> None:
cursor.execute(_JIANGCHANG_TABLES_DDL)
cursor.execute(_JIANGCHANG_COLUMNS_DDL)
_ensure_jiangchang_tables_columns(cursor)
_ensure_jiangchang_columns_columns(cursor)
def upsert_table_metadata(
@@ -69,6 +84,7 @@ def upsert_table_metadata(
table_name: str,
display_name: str,
description: str | None = None,
grid_help: str | None = None,
visible: int = 1,
readonly: int = 0,
default_sort_json: str | None = None,
@@ -83,17 +99,20 @@ def upsert_table_metadata(
default_visible_columns_json: 宿主数据管理默认展示列,JSON 字符串数组,例如
'["stat_type","period","stage"]'
为空则全显;用户改过「列」后以本机偏好为准(软默认,区别于 visible=0 硬隐藏)。
grid_help: 数据管理表格上方说明;空则不出横幅。与表单用的 description 独立,禁止互拷。
"""
cursor.execute(
"""
INSERT INTO _jiangchang_tables (
table_name, display_name, description, visible, readonly,
table_name, display_name, description, grid_help, visible, readonly,
default_sort_json, default_visible_columns_json
)
VALUES (?, ?, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(table_name) DO UPDATE SET
display_name = excluded.display_name,
description = excluded.description,
grid_help = excluded.grid_help,
visible = excluded.visible,
readonly = excluded.readonly,
default_sort_json = excluded.default_sort_json,
@@ -103,6 +122,7 @@ def upsert_table_metadata(
table_name,
display_name,
description,
grid_help,
visible,
readonly,
default_sort_json,
@@ -118,6 +138,7 @@ def upsert_column_metadata(
column_name: str,
display_name: str,
description: str | None = None,
grid_help: str | None = None,
visible: int = 1,
searchable: int = 1,
editable: int = 1,
@@ -125,16 +146,18 @@ def upsert_column_metadata(
options_json: str | None = None,
) -> None:
# display_order 保留为 NULL:宿主按 PRAGMA table_info cid 展示字段,不读 display_order。
# grid_help: 表头「?」;空则不出。与表单用的 description 独立,禁止互拷。
cursor.execute(
"""
INSERT INTO _jiangchang_columns (
table_name, column_name, display_name, description,
table_name, column_name, display_name, description, grid_help,
display_order, visible, searchable, editable, display_type, options_json
)
VALUES (?, ?, ?, ?, NULL, ?, ?, ?, ?, ?)
VALUES (?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?)
ON CONFLICT(table_name, column_name) DO UPDATE SET
display_name = excluded.display_name,
description = excluded.description,
grid_help = excluded.grid_help,
display_order = NULL,
visible = excluded.visible,
searchable = excluded.searchable,
@@ -147,6 +170,7 @@ def upsert_column_metadata(
column_name,
display_name,
description,
grid_help,
visible,
searchable,
editable,
@@ -171,6 +195,7 @@ def seed_column_metadata_batch(
column_name=str(column["column_name"]),
display_name=str(column["display_name"]),
description=str(column["description"]) if column.get("description") else None,
grid_help=str(column["grid_help"]) if column.get("grid_help") else None,
visible=int(column.get("visible", 1)),
searchable=int(column.get("searchable", 1)),
editable=int(column.get("editable", 1)),
@@ -252,6 +277,32 @@ def init_display_metadata(cursor) -> None:
init_display_metadata_tables(cursor)
def drop_deprecated_user_table(
cursor,
table_name: str,
*,
triggers: Iterable[str] | None = None,
) -> None:
"""永久退役用户可见表:删 trigger、DROP 物理表、清理展示元数据(幂等)。
用于废弃 task_logs 等旧表。禁止只删元数据而保留物理表。
见 references/SCHEMA.md「表增删改与退役」。
"""
for trigger_name in triggers or ():
cursor.execute(f"DROP TRIGGER IF EXISTS {trigger_name}")
default_updated_at = f"{table_name}_set_updated_at"
cursor.execute(f"DROP TRIGGER IF EXISTS {default_updated_at}")
cursor.execute(f"DROP TABLE IF EXISTS {_quote_identifier(table_name)}")
cursor.execute(
"DELETE FROM _jiangchang_columns WHERE table_name = ?",
(table_name,),
)
cursor.execute(
"DELETE FROM _jiangchang_tables WHERE table_name = ?",
(table_name,),
)
def iter_user_tables(cursor) -> Iterable[str]:
cursor.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name"

View File

@@ -30,6 +30,7 @@ _ENGLISH_PLACEHOLDER_NAME_PATTERNS = (
)
_KEBAB_SLUG = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
_CJK_RE = re.compile(r"[\u4e00-\u9fff]")
_FK_DDL_PATTERN = re.compile(r"\bFOREIGN\s+KEY\b|\bREFERENCES\b", re.I)
@dataclass
@@ -218,6 +219,8 @@ def validate_options_json(options_json: str | None) -> tuple[list[str], list[str
errors.append(f"options_json.{key} 必须是安全标识符,当前为 {raw!r}")
if parsed.get("filter_column") not in (None, "") and "filter_value" not in parsed:
warnings.append("options_json lookup 声明了 filter_column 但未提供 filter_value")
if "multiple" in parsed and not isinstance(parsed.get("multiple"), bool):
errors.append("options_json lookup.multiple 必须是布尔值")
return errors, warnings
if not isinstance(parsed, list):
@@ -232,6 +235,50 @@ def validate_options_json(options_json: str | None) -> tuple[list[str], list[str
return errors, warnings
def validate_filter_search_metadata(
cursor,
table_name: str,
) -> tuple[list[str], list[str]]:
"""对可见但未开启 searchable 的枚举 / boolean 列给出软建议。"""
warnings: list[str] = []
cursor.execute(
"""
SELECT column_name, visible, searchable, display_type, options_json
FROM _jiangchang_columns
WHERE table_name = ?
""",
(table_name,),
)
for column_name, visible, searchable, display_type, options_json in cursor.fetchall():
if visible is not None and int(visible) == 0:
continue
if searchable is not None and int(searchable) != 0:
continue
display_type_norm = str(display_type or "").strip()
is_boolean = display_type_norm == "boolean"
has_enum = False
if options_json is not None and str(options_json).strip():
opt_errors, _ = validate_options_json(options_json)
if not opt_errors:
try:
parsed = json.loads(str(options_json))
except json.JSONDecodeError:
parsed = None
has_enum = isinstance(parsed, list) and len(parsed) > 0
if not has_enum and not is_boolean:
continue
warnings.append(
f"{table_name}.{column_name}: 枚举/状态类列建议 searchable=1,"
"以便宿主列筛选与按中文 label 全局搜索(见 references/SCHEMA.md)"
)
return [], warnings
def validate_column_permissions(
cursor,
table_name: str,
@@ -412,6 +459,27 @@ def _table_exists(cursor, table_name: str) -> bool:
return cursor.fetchone() is not None
def validate_no_sqlite_foreign_keys(cursor) -> tuple[list[str], list[str]]:
"""业务表 DDL 不得含 FOREIGN KEY / REFERENCES(关联走 repository/service)。"""
errors: list[str] = []
cursor.execute(
"""
SELECT name, sql FROM sqlite_master
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
"""
)
for table_name, sql in cursor.fetchall():
if table_name in METADATA_TABLES:
continue
ddl = str(sql or "")
if ddl and _FK_DDL_PATTERN.search(ddl):
errors.append(
f"业务表 {table_name!r} 的 DDL 含 SQLite FOREIGN KEY/REFERENCES;"
"请改为业务 id 列 + repository/service 逻辑关联(见 references/SCHEMA.md)"
)
return errors, []
def validate_display_metadata(conn: sqlite3.Connection, *, check_column_order: bool = True) -> ValidationReport:
report = ValidationReport()
cur = conn.cursor()
@@ -425,6 +493,9 @@ def validate_display_metadata(conn: sqlite3.Connection, *, check_column_order: b
if not has_tables_meta or not has_columns_meta:
return report
fk_errors, _ = validate_no_sqlite_foreign_keys(cur)
report.errors.extend(fk_errors)
for table_name in iter_user_tables(cur):
cur.execute(
"SELECT display_name FROM _jiangchang_tables WHERE table_name = ?",
@@ -484,6 +555,9 @@ def validate_display_metadata(conn: sqlite3.Connection, *, check_column_order: b
report.errors.extend(perm_errors)
report.warnings.extend(perm_warnings)
_, filter_warnings = validate_filter_search_metadata(cur, table_name)
report.warnings.extend(filter_warnings)
cur.execute("SELECT table_name, column_name FROM _jiangchang_columns")
for table_name, column_name in cur.fetchall():
if table_name in METADATA_TABLES:
@@ -514,5 +588,5 @@ def validate_display_metadata(conn: sqlite3.Connection, *, check_column_order: b
def validate_template_database(conn: sqlite3.Connection) -> ValidationReport:
"""模板自检入口:校验展示元数据完整性。模板默认无业务表。"""
"""模板自检入口:校验展示元数据完整性,并扫描业务表 DDL 是否含 SQLite 外键。"""
return validate_display_metadata(conn)

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.86"
SKILL_VERSION = "1.0.104"
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
@@ -188,7 +196,7 @@ class TestActionsManifest(unittest.TestCase):
if replaces is not None:
self.assertIn(
replaces,
{"create", "update", "delete"},
{"create", "update", "delete", "import"},
msg=f"{action_id} invalid replaces {replaces!r}",
)
bind = action.get("bind") or {}
@@ -236,7 +244,7 @@ class TestActionsManifest(unittest.TestCase):
self.assertIn("interactionByPlacement", props)
by_placement = props["interactionByPlacement"]
self.assertIs(by_placement.get("additionalProperties"), False)
for key in ("toolbar", "row", "batch", "cron", "agent", "skill-detail", "create", "update", "delete"):
for key in ("toolbar", "row", "batch", "cron", "agent", "skill-detail", "create", "update", "delete", "import"):
self.assertIn(key, by_placement.get("properties") or {})
self.assertEqual(
set((by_placement["properties"][key].get("enum") or [])),
@@ -246,7 +254,7 @@ class TestActionsManifest(unittest.TestCase):
self.assertEqual(props["replaces"].get("$ref"), "#/$defs/mutationKind")
self.assertEqual(
set(schema["$defs"]["mutationKind"].get("enum") or []),
{"create", "update", "delete"},
{"create", "update", "delete", "import"},
)
def test_schema_defines_hover_hint(self) -> None:
@@ -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

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

View File

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

View File

@@ -225,6 +225,37 @@ class TestDisplayMetadata(unittest.TestCase):
finally:
conn.close()
def test_validate_rejects_sqlite_foreign_keys_on_business_tables(self) -> None:
from db.display_metadata import (
_JIANGCHANG_COLUMNS_DDL,
_JIANGCHANG_TABLES_DDL,
)
from db.display_metadata_validator import validate_display_metadata
conn = sqlite3.connect(":memory:")
try:
conn.execute(_JIANGCHANG_TABLES_DDL)
conn.execute(_JIANGCHANG_COLUMNS_DDL)
conn.execute("CREATE TABLE parent_rows (id INTEGER PRIMARY KEY, title TEXT)")
conn.execute(
"""
CREATE TABLE child_rows (
id INTEGER PRIMARY KEY,
parent_id INTEGER NOT NULL,
FOREIGN KEY (parent_id) REFERENCES parent_rows(id)
)
"""
)
report = validate_display_metadata(conn)
self.assertFalse(report.ok)
self.assertTrue(
any("FOREIGN KEY" in err or "REFERENCES" in err for err in report.errors),
msg=f"expected FK error, got: {report.errors}",
)
self.assertTrue(any("child_rows" in err for err in report.errors))
finally:
conn.close()
def test_validate_missing_both_metadata_tables(self) -> None:
conn = sqlite3.connect(":memory:")
try:
@@ -522,6 +553,41 @@ metadata:
finally:
conn.close()
def test_enum_column_without_searchable_warns(self) -> None:
conn = sqlite3.connect(":memory:")
try:
conn.executescript(
"""
CREATE TABLE filter_demo (id INTEGER PRIMARY KEY, enabled INTEGER NOT NULL DEFAULT 1);
"""
)
from db.display_metadata import (
init_display_metadata_tables,
upsert_column_metadata,
upsert_table_metadata,
)
from db.display_metadata_validator import validate_filter_search_metadata
cur = conn.cursor()
init_display_metadata_tables(cur)
upsert_table_metadata(cur, table_name="filter_demo", display_name="筛选演示", readonly=0)
upsert_column_metadata(
cur,
table_name="filter_demo",
column_name="enabled",
display_name="是否启用",
visible=1,
searchable=0,
editable=1,
display_type="boolean",
options_json='[{"value":0,"label":"停用"},{"value":1,"label":"启用"}]',
)
conn.commit()
_, warnings = validate_filter_search_metadata(cur, "filter_demo")
self.assertTrue(any("searchable=1" in item for item in warnings))
finally:
conn.close()
def test_schema_md_documents_readonly_and_seed_helpers(self) -> None:
path = os.path.join(get_skill_root(), "references", "SCHEMA.md")
with open(path, encoding="utf-8") as f:
@@ -578,6 +644,167 @@ metadata:
finally:
conn.close()
def test_grid_help_upsert_and_migrate_from_legacy_schema(self) -> None:
conn = sqlite3.connect(":memory:")
try:
from db.display_metadata import (
init_display_metadata_tables,
upsert_column_metadata,
upsert_table_metadata,
)
cur = conn.cursor()
cur.execute(
"""
CREATE TABLE _jiangchang_tables (
table_name TEXT PRIMARY KEY,
display_name TEXT NOT NULL,
description TEXT,
sort_order INTEGER,
visible INTEGER NOT NULL DEFAULT 1,
readonly INTEGER NOT NULL DEFAULT 0,
default_sort_json TEXT,
default_visible_columns_json TEXT
)
"""
)
cur.execute(
"""
CREATE TABLE _jiangchang_columns (
table_name TEXT NOT NULL,
column_name TEXT NOT NULL,
display_name TEXT NOT NULL,
description TEXT,
display_order INTEGER,
visible INTEGER NOT NULL DEFAULT 1,
searchable INTEGER NOT NULL DEFAULT 1,
editable INTEGER NOT NULL DEFAULT 1,
display_type TEXT,
options_json TEXT,
PRIMARY KEY (table_name, column_name)
)
"""
)
init_display_metadata_tables(cur)
cur.execute("PRAGMA table_info('_jiangchang_tables')")
self.assertIn("grid_help", {str(row[1]) for row in cur.fetchall()})
cur.execute("PRAGMA table_info('_jiangchang_columns')")
self.assertIn("grid_help", {str(row[1]) for row in cur.fetchall()})
upsert_table_metadata(
cur,
table_name="demo_items",
display_name="示例记录",
description="表单备注,不应出现在表格横幅",
grid_help="表格横幅说明",
)
upsert_column_metadata(
cur,
table_name="demo_items",
column_name="title",
display_name="标题",
description="填标题",
grid_help="列头问号说明",
)
upsert_table_metadata(
cur,
table_name="empty_help",
display_name="无表格说明",
)
conn.commit()
cur.execute(
"""
SELECT description, grid_help
FROM _jiangchang_tables
WHERE table_name = 'demo_items'
"""
)
self.assertEqual(cur.fetchone(), ("表单备注,不应出现在表格横幅", "表格横幅说明"))
cur.execute(
"""
SELECT description, grid_help
FROM _jiangchang_columns
WHERE table_name = 'demo_items' AND column_name = 'title'
"""
)
self.assertEqual(cur.fetchone(), ("填标题", "列头问号说明"))
cur.execute(
"""
SELECT grid_help FROM _jiangchang_tables
WHERE table_name = 'empty_help'
"""
)
self.assertTrue(not cur.fetchone()[0])
finally:
conn.close()
def test_drop_deprecated_user_table_removes_physical_and_metadata(self) -> None:
conn = sqlite3.connect(":memory:")
try:
cur = conn.cursor()
cur.execute(
"""
CREATE TABLE _jiangchang_tables (
table_name TEXT PRIMARY KEY,
display_name TEXT,
description TEXT,
sort_order INTEGER,
visible INTEGER,
readonly INTEGER
)
"""
)
cur.execute(
"""
CREATE TABLE _jiangchang_columns (
table_name TEXT,
column_name TEXT,
display_name TEXT,
description TEXT,
display_order INTEGER,
visible INTEGER,
searchable INTEGER,
editable INTEGER,
display_type TEXT,
options_json TEXT,
PRIMARY KEY (table_name, column_name)
)
"""
)
cur.execute(
"""
CREATE TABLE task_logs (
id INTEGER PRIMARY KEY,
status TEXT,
created_at INTEGER,
updated_at INTEGER
)
"""
)
cur.execute(
"INSERT INTO _jiangchang_tables VALUES ('task_logs', '任务日志', NULL, 1, 1, 0)"
)
cur.execute(
"INSERT INTO _jiangchang_columns VALUES "
"('task_logs', 'id', '编号', NULL, 1, 1, 1, 0, 'text', NULL)"
)
cur.execute("INSERT INTO task_logs (status) VALUES ('success')")
from db.display_metadata import drop_deprecated_user_table, iter_user_tables
drop_deprecated_user_table(cur, "task_logs")
cur.execute(
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='task_logs'"
)
self.assertIsNone(cur.fetchone())
cur.execute(
"SELECT COUNT(*) FROM _jiangchang_tables WHERE table_name='task_logs'"
)
self.assertEqual(int(cur.fetchone()[0]), 0)
self.assertNotIn("task_logs", list(iter_user_tables(cur)))
finally:
conn.close()
if __name__ == "__main__":
unittest.main()

View File

@@ -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")
@@ -301,6 +313,7 @@ class TestDocsStandards(unittest.TestCase):
self.assertTrue(os.path.isfile(os.path.join(root, "references", "ACTIONS.md")))
self.assertTrue(os.path.isfile(os.path.join(root, "references", "POLICY.md")))
self.assertTrue(os.path.isfile(os.path.join(root, "references", "CELL_PRESENTATION.md")))
self.assertTrue(os.path.isfile(os.path.join(root, "references", "WORK_ITEM_STATUS.md")))
self.assertTrue(
os.path.isfile(os.path.join(root, "assets", "schemas", "skill-actions.schema.json"))
)
@@ -348,6 +361,20 @@ class TestDocsStandards(unittest.TestCase):
"文件 / 图片上传",
"default_visible_columns_json",
"默认展示列",
"grid_help",
"表格说明 `grid_help`",
"表与表之间:禁止 SQLite 外键",
"禁止 SQLite 外键",
"跨表引用 lookup",
"\"multiple\": true",
"Lookup 多选",
"数据管理:列筛选与全局搜索",
"列筛选与全局搜索",
"searchable: 1",
"筛选栏控件",
"表增删改与退役",
"drop_deprecated_user_table",
"孤儿物理表",
):
self.assertIn(marker, text, msg=f"SCHEMA.md missing {marker!r}")
@@ -359,12 +386,15 @@ class TestDocsStandards(unittest.TestCase):
"跨表引用:Lookup",
"display_type: \"lookup\"",
"extra_column",
"multiple",
"数据管理新增/编辑",
"_jiangchang_columns.description",
"datetime_unix_seconds",
"SCHEMA.md",
"字段条件显隐",
"visibleWhen",
"grid_help",
"列筛选",
):
self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}")

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

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

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