Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e2f94dd10 | |||
| 72817f6cdd | |||
| 6644dedd0e | |||
| cef7e366cf | |||
| afe0788488 | |||
| c49fcba157 | |||
| bf3bb9cd10 | |||
| 166007b460 | |||
| 78c24a515d | |||
| dfe6ade2d8 | |||
| e32d04ec50 | |||
| a8403ce9b4 | |||
| 8c8811e777 | |||
| a70da6a076 | |||
| a125b00285 | |||
| 2a3e971c76 | |||
| db2ed089a3 | |||
| c43c26ad15 | |||
| fe4209b37f | |||
| 3bebd6468f | |||
| e7fa3c79b5 | |||
| 0ce0c3e852 | |||
| 201d4d1367 | |||
| 295b2356ac | |||
| 7257d09813 | |||
| 9d33c5ea2c | |||
| b5d120469b | |||
| d35d04f779 | |||
| cbebd19ab3 | |||
| 05764cac15 | |||
| 0a6b0171d8 | |||
| 43486afec4 | |||
| 225d5c0777 | |||
| fbf326d525 | |||
| 28c3c8d8ab | |||
| 571c135114 | |||
| d840745758 | |||
| cf3f0ae990 | |||
| a11cdf4e7e | |||
| 81096051dd | |||
| f221f81d4e | |||
| 3738f0bea2 | |||
| 6ebc570632 | |||
| 2d02ccff5d | |||
| 25b0c62dea | |||
| ff75aefcdd | |||
| 49e4d6e9a6 | |||
| b27dd5b5ba | |||
| 9fa5953713 | |||
| db2325dbe4 | |||
| 16ee121ab3 | |||
| 139d176841 | |||
| 0b6bf0b2fa | |||
| 51be1517c0 | |||
| 98f057e26b | |||
| 8878a90315 | |||
| 2ecb1172f4 | |||
| d2c8c3240c | |||
| 2c9f610a8e | |||
| 2c9bf56054 | |||
| 633ecff4e2 | |||
| 46bc573777 | |||
| 0002e86729 | |||
| caf0191366 |
25
.env.example
25
.env.example
@@ -1,6 +1,7 @@
|
||||
# 运行模式:决定本技能用模拟数据还是真实浏览器/接口去执行
|
||||
OPENCLAW_TEST_TARGET=real_rpa # 可选:mock=本地模拟,simulator_rpa=仿真站点,real_api=真实接口,real_rpa=真实浏览器;模板默认真实浏览器;本地单测/CI 请用 mock
|
||||
|
||||
# @required
|
||||
# 目标网站地址:技能要访问的网站或服务地址
|
||||
TARGET_BASE_URL=https://sandbox.jc2009.com # 填写完整网址(含 https://);换环境时再改,一般保持默认即可
|
||||
|
||||
@@ -31,23 +32,27 @@ STEP_DELAY_MAX=3.0 # 单位:秒;默认与最短等待组
|
||||
# 人工处理超时:遇到滑块、验证码、二次验证时,等待人工处理的最长时间
|
||||
HUMAN_WAIT_TIMEOUT=180 # 单位:秒;来不及处理时可调大,如 300
|
||||
|
||||
# 浏览器启动超时(可选):限制 launch_persistent_browser 最长时间;空=不限制(默认)
|
||||
# OPENCLAW_BROWSER_LAUNCH_TIMEOUT_MS=60000 # 单位:毫秒;慢机器/限频时可开启并调大
|
||||
|
||||
# 以下为可选目录配置。一般不用改;取消注释并填写后,才覆盖默认子文件夹。
|
||||
# 填写相对路径时,相对于本技能的数据目录(不是当前工作目录)。
|
||||
# 用户可见只有 input(上行)与 output(下行);见 development/DATA_PATHS.md。
|
||||
|
||||
# 下载目录:技能保存下载文件的位置
|
||||
# SKILL_DOWNLOAD_DIR=downloads # 相对本技能数据目录;留空或保持注释则用默认 downloads
|
||||
# 输入目录:用户/Agent 放入的待处理文件(清单、CSV、产品表等)
|
||||
# SKILL_INPUT_DIR=input # 相对本技能数据目录;留空或保持注释则用默认 input
|
||||
|
||||
# 导入目录:待导入文件放置的位置
|
||||
# SKILL_IMPORT_DIR=imports # 相对本技能数据目录;留空或保持注释则用默认 imports
|
||||
# 输出目录:技能/宿主交给用户拿走的结果(视频、报表、导出文件等)
|
||||
# SKILL_OUTPUT_DIR=output # 相对本技能数据目录;留空或保持注释则用默认 output
|
||||
|
||||
# 导出目录:技能导出文件的保存位置
|
||||
# SKILL_EXPORT_DIR=exports # 相对本技能数据目录;留空或保持注释则用默认 exports
|
||||
|
||||
# 上传目录:待上传文件放置的位置
|
||||
# 上传目录:待发布到外部平台的 staging(可选,不上侧栏)
|
||||
# SKILL_UPLOAD_DIR=uploads # 相对本技能数据目录;留空或保持注释则用默认 uploads
|
||||
|
||||
# 缓存目录:临时缓存文件的位置
|
||||
# 缓存目录:临时缓存文件的位置(不上侧栏)
|
||||
# SKILL_CACHE_DIR=cache # 相对本技能数据目录;留空或保持注释则用默认 cache
|
||||
|
||||
# 临时目录:运行中临时文件的位置
|
||||
# 临时目录:运行中临时文件的位置(不上侧栏)
|
||||
# SKILL_TEMP_DIR=temp # 相对本技能数据目录;留空或保持注释则用默认 temp
|
||||
|
||||
# MySQL 云库连接:技能走云库时在用户数据目录配置 data-sources.json,并在此填写 DSN
|
||||
# MYSQL_DSN=mysql://user:password@192.168.0.20:3306/your_database # 留空或不写=本地 SQLite;须 mysql:// 开头且含库名;匠厂 Host 须 ≥ 6.9.28;见 references/DATA_SOURCES.md
|
||||
|
||||
348
CHANGELOG.md
348
CHANGELOG.md
@@ -9,6 +9,354 @@
|
||||
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
|
||||
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
|
||||
|
||||
## 1.0.124
|
||||
|
||||
- 沉淀宿主能力:Excel 导入模板按列元数据自动加下拉(有可选集合的列展示 label;CSV 无下拉);技能只需声明 `_jiangchang_columns` 选项即可受益
|
||||
- 更新 `references/SCHEMA.md`「宿主负责」与 `options_json` 说明
|
||||
|
||||
|
||||
## 1.0.123
|
||||
|
||||
- 沉淀 `selectionMode: optional` 在**定时任务等无表勾选入口**的宿主约定:配置页按无勾选分支,隐藏空「编号列表」、只保留数量字段;勿为 cron 再拆 Action
|
||||
- 更新 ACTIONS / CHECKLIST A4-9 / RUNTIME / DEVELOPMENT 验收表;门禁用例覆盖关键表述
|
||||
|
||||
## 1.0.122
|
||||
|
||||
- 沉淀 Action **静态枚举中文 label**:`enum` + **`enumNames`**,或绑列复用列 `options_json` 数组
|
||||
- 约定宿主优先序:**绑列 options → enumNames → 纯 enum**;固定小枚举不要造假 Lookup
|
||||
- 更新 Schema / ACTIONS / SCHEMA / CHECKLIST A4-13;新增 `enum-names-action.example.json`
|
||||
|
||||
## 1.0.121
|
||||
|
||||
- 沉淀 Lookup **分页勾选小表**:`display_type: lookup` + `presentation: "paged_table"` + 可选 `page_size`(仍是 Lookup,不要新造控件类型)
|
||||
- 约定 `skill_cli` 追加 `--page` / `--limit`,stdout 为一行 `{"items","total"}`;0 条也是成功,界面显示「暂无可选项」
|
||||
- 只读列表命令不要在入口加载整包 platform-kit 或 Playwright;同库一个过滤条件优先 `local_sql`
|
||||
- 更新 Schema / ACTIONS / SCHEMA / CHECKLIST A4-12;新增 `lookup-paged-table.action.example.json`
|
||||
|
||||
## 1.0.120
|
||||
|
||||
- 正式沉淀行菜单显隐契约 **`bind.visibleWhen`**(宿主已支持:按当前行列值显示/隐藏行按钮)
|
||||
- 与表单字段 `visibleWhen`、Policy L3 预留消歧;更新 Schema / ACTIONS / POLICY / CHECKLIST A4-11 / RUNTIME
|
||||
- 新增示例 `bind-visible-when-row.action.example.json`
|
||||
|
||||
## 1.0.119
|
||||
|
||||
- 沉淀匠厂宿主 **Action Lookup 三源**:`local_sql`(同库)/ **`skill_cli`(兄弟技能 CLI JSON)** / `external_api`(预留)
|
||||
- 明确列 Lookup 仍仅 `local_sql`;跨技能账号列表用 `skill_cli`,**禁止**本库镜像缓存表
|
||||
- 更新 `ACTIONS.md` / `SCHEMA.md` / `skill-actions.schema.json` / CHECKLIST A4-6 / `ACCOUNT_MANAGER.md`;新增 `lookup-skill-cli.action.example.json`
|
||||
|
||||
## 1.0.118
|
||||
|
||||
- 沉淀浏览器 RPA 窗口/viewport 最佳实践(`RPA.md` §1.2.1):`no_viewport=True` + `--start-maximized`,禁止 `viewport=None` 与 CDP/JS 中途改窗
|
||||
- `RPA_BROWSER.md` 补充 Shadow DOM 双 input 陷阱与 Cookie 可选 dismiss 模式
|
||||
- 新增 `POLICY-RPA-007` 文档标记与 launch 禁止模式门禁;CHECKLIST A5-4 对齐
|
||||
|
||||
## 1.0.117
|
||||
|
||||
- 沉淀匠厂 Host ≥ 6.9.28 **多数据源**契约:可选 MySQL 云库 + `data-sources.json` 声明;默认仍为本地 SQLite
|
||||
- 新增 `references/DATA_SOURCES.md`、`assets/data-sources.example.json`(SQLite 默认)、`assets/examples/data-sources.mysql.example.json`
|
||||
- `scripts/db/backend.py`、`cursor_compat.py` 与双后端 `connection.py`:CLI / Action 与宿主 UI 读写同一库
|
||||
- `.env.example` 增加 `MYSQL_DSN` 说明;CHECKLIST / CONFIG / DATA_PATHS / SCHEMA / ACTIONS 口径同步
|
||||
|
||||
## 1.0.116
|
||||
|
||||
- 沉淀宿主 **年 / 年月** 表单字段契约:`display_type: year`(`YYYY`)、`year_month` / `month`(`YYYY-MM`);与 `date` / `datetime_unix_seconds` 对称,Action 与 CRUD 均适用
|
||||
- 更新 `ACTIONS.md`、`SCHEMA.md`、`skill-actions.schema.json`;新增 `assets/examples/year-month-action.example.json`;`display_metadata.py` 增加 `YEAR` / `YEAR_MONTH` 常量
|
||||
|
||||
## 1.0.115
|
||||
|
||||
- 沉淀匠厂配置管理必填项契约:`.env.example` 在行头上方用独立一行 `# @required` 声明;宿主显示红色 `*` 并在保存时校验非空
|
||||
- 模板示例已为 `TARGET_BASE_URL` 加上示范;新增 `POLICY-CONFIG-005` 门禁错误用法
|
||||
|
||||
## 1.0.114
|
||||
|
||||
- 技能根目录增加 `sync.ps1`,和 `release.ps1` 并排。在本机把明文同步到已安装的技能目录,用来在宿主里验收
|
||||
- 市场上的最新版本和加密包仍走 `release.ps1`。同步后退出资料管理再打开,不要点技能市场的「更新」
|
||||
|
||||
## 1.0.113
|
||||
|
||||
- 固化数据管理契约:`visible=0` 只藏界面;`default_sort_json` 仍可引用隐藏的非敏感物理列(含主键)
|
||||
- 元数据校验:排序列必须真实存在;引用隐藏列仅提示、不报错
|
||||
|
||||
## 1.0.112
|
||||
|
||||
- 新技能的名字不必再以平台或 `default` 结尾。没有抖音、小红书、亚马逊这类外部平台时,用动词加名词即可
|
||||
- 不要再把 `default` 当作占位平台。已经发布、名字末尾是 `default` 的技能不用改名
|
||||
|
||||
## 1.0.111
|
||||
|
||||
- 沉淀数据管理呈现加法:同一 `cellPresentation.enabled` 支持单元格、整行、整列、行列交叉区域(闭集 tone)
|
||||
- 新增 `_jiangchang_grid_presentation` 契约 / Schema / 示例;v1 `cells[]` 保持不变;禁止整表 `all`×`all`;整行色只涂表格、不铺到详情字段
|
||||
- 内部隐藏表补上两张呈现旁路表;模板默认仍不启用着色
|
||||
|
||||
## 1.0.110
|
||||
|
||||
- 数据管理侧栏除了生成出来的 HTML 快照,也会直接显示安装包里 `assets/html/pages/` 的页面,装完就有,不用点生成
|
||||
- 这些页面可以调用本技能已经声明的命令:宿主代为执行 Python,把 JSON 交回页面
|
||||
|
||||
## 1.0.109
|
||||
|
||||
- 沉淀任务中断与账号租约契约:禁止用裸 `except Exception` 吞掉 `JobStopped`;`finally` 先释租约再关浏览器;宿主只通知不代收租约
|
||||
- 补充 CHECKLIST A5-13(中断后租约须释放)及 ACCOUNT_MANAGER / LOGGING / ADAPTER / 样例 README
|
||||
|
||||
## 1.0.108
|
||||
|
||||
- 沉淀数据管理「用户可见文案」契约:弹窗 description / 确认黄条 / 参数说明 / 任务失败 message 须全中文产品用语
|
||||
- 明确禁止把字段名、CLI 子命令、async/pending、Agent 禁令写进用户可见文案;Agent 路由仍放 SKILL.md
|
||||
- 修正 `selection-mode-optional` 示例为口语文案;更新 ACTIONS / RUNTIME / CHECKLIST A4-10
|
||||
|
||||
## 1.0.107
|
||||
|
||||
- 沉淀数据管理 `bind.selectionMode` 契约(`required` / `none` / `optional`):同一按钮可「有勾选走 ids、无勾选走 pickCount」
|
||||
- 明确省略时的宿主兼容推断、空选数量字段名约定,以及勿再拆同义双按钮
|
||||
- 更新 ACTIONS.md / Schema / SKILL_ACTION_RUNTIME / CHECKLIST A4-9;新增 `selection-mode-optional.action.example.json`
|
||||
|
||||
## 1.0.106
|
||||
|
||||
- 文档契约:补充可选 `metadata.openclaw.brands`(OEM 市场编码 → `skill_market_relations`)
|
||||
- 明确未写则仅挂 `jiangchang`;与 `developer_ids` 分工;勿用 Gitea 组织名推断市场
|
||||
- 更新 `SKILL.md`、`DEVELOPMENT.md` §6、`development/README.md`、`REQUIREMENTS.md` 与发布检查清单
|
||||
|
||||
## 1.0.105
|
||||
|
||||
- 沉淀数据管理 form 与「已选择」摘要对齐契约:映射 `readOnly` 字段默认不占参数区(仍提交),与 confirm 信息架构一致
|
||||
- 新增可选 `revealInForm` 逃生阀;明确禁止用 `visibleWhen` 藏映射主键
|
||||
- 更新 ACTIONS.md / Schema / SKILL_ACTION_RUNTIME / CHECKLIST A4-8
|
||||
|
||||
## 1.0.104
|
||||
|
||||
- 新增发布契约 POLICY-RELEASE-001:`release.ps1` 打 tag 前强制校验 Next tag = `SKILL.md` version = `SKILL_VERSION`,避免市场一直显示「更新」
|
||||
- 日常测试增加 `SKILL.md` 与 `SKILL_VERSION` 对齐;更新 DEVELOPMENT / POLICY_MATRIX / CHECKLIST A9-4
|
||||
|
||||
## 1.0.103
|
||||
|
||||
- 用户市场文档:说明/教程正文禁止与宿主页头、Tab 重复的文首一级标题;主分节统一用 `##`
|
||||
- 更新 `TUTORIAL.md` / `README.md` 模板骨架、`TUTORIAL_SURFACES.md` §0.1、CHECKLIST A2-12 与契约测试
|
||||
|
||||
## 1.0.102
|
||||
|
||||
- 沉淀数据管理 Action「已选择」摘要契约:confirm/form 弹层由宿主默认展示勾选对象(#id + 可读标签);技能零改动即可用
|
||||
- 新增可选 `bind.labelColumn`(仅影响展示,不改提交 payload,不决定业务跳过逻辑)
|
||||
- 更新 `skill-actions.schema.json`、`ACTIONS.md`、`SKILL_ACTION_RUNTIME.md`;新增 `selection-label-column.action.example.json`
|
||||
|
||||
## 1.0.101
|
||||
|
||||
- 沉淀 Action 表单本地路径契约:`file` / `directory` 可选 **`pathRoot`**(相对技能数据根的起始目录;省略则宿主默认打开本技能 `input/`)
|
||||
- 更新 `skill-actions.schema.json`、`ACTIONS.md`、`DATA_PATHS.md`;新增 `assets/examples/path-root-file-action.example.json`
|
||||
- 与宿主导入面板默认 `input/` 划清边界;禁止技能写死用户 ID 或绝对盘符
|
||||
|
||||
## 1.0.100
|
||||
|
||||
- 沉淀用户市场文档契约 `development/TUTORIAL_SURFACES.md`:**说明 Tab ≠ 教程 Tab**;教程按数据管理 ∥ 对话、定时可选三入口结构
|
||||
- 重写根 `README.md` / `TUTORIAL.md` 模板骨架;CHECKLIST 增 A2-7~A2-11;`actions.json` 准备类优先 toolbar、skill-detail 不可作教程主路径
|
||||
- 新增 `tests/test_tutorial_surfaces.py`(POLICY-TUTORIAL-001);REQUIREMENTS §0.1 增教程章节列与用户文档对齐要求
|
||||
|
||||
## 1.0.99
|
||||
|
||||
- 沉淀数据管理**表注册门禁**契约:存在 `_jiangchang_tables` 时,宿主仅展示注册且 `visible=1` 的表;孤儿物理表不展示(匠厂宿主 ≥6.8.25)
|
||||
- 新增「表增删改与退役」:`drop_deprecated_user_table()` 标准迁移;禁止只删元数据留物理表;新技能禁止 `task_logs`
|
||||
- `LOGGING.md` 可观测性三分法;`DATA_PATHS.md` 卸载不删用户数据;Checklist A6-13 / A6-14;`tests/samples/test_table_deprecation_migration.py.sample`
|
||||
|
||||
## 1.0.98
|
||||
|
||||
- 沉淀数据管理**列筛选与全局搜索**契约:`searchable` 同时控制列筛选字段列表与顶栏全局搜索;存库 value / 展示 label 分工;筛选栏枚举/boolean/lookup 用下拉 Select(与表单开关/分段单选区分)
|
||||
- 全局搜索:options label 映射、boolean 别名、lookup 多选 token、`datetime_unix_seconds` 日期区间
|
||||
- `ACTIONS.md` 表单对照表补充「列筛选」脚注;`CHECKLIST` A6-11 / A6-12;`display_metadata_validator` 对未开 searchable 的枚举/boolean 列软 warning
|
||||
|
||||
## 1.0.97
|
||||
|
||||
- 沉淀数据管理 HTML 预览**布局契约**:`references/ACTIONS.md` 新增「宿主预览布局」(流体铺满 iframe、禁横向滚动、表格断行等)
|
||||
- 升级 `assets/html/example-report.html` 为通用布局基线;`test_html_page_service` 守护不退化
|
||||
- `DEVELOPMENT.md` / `CHECKLIST`(S0-12、A6-10)/ `TESTING.md` 交叉引用
|
||||
|
||||
## 1.0.96
|
||||
|
||||
- 在 `references/ACTIONS.md` 沉淀数据管理技能按钮中文名(`label`)参考表:同类通用动作用同一动词,不是白名单
|
||||
- 打开类改为「打开目录 / 打开浏览器」,一点即开、成功不弹「执行成功」;示例不再使用「打开文件位置」
|
||||
- 交活 Checklist A7-7;POLICY_MATRIX 软规则:不扫描 label 关键字
|
||||
|
||||
## 1.0.95
|
||||
|
||||
- 正式沉淀「业务行生命周期」通用契约(`references/WORK_ITEM_STATUS.md`):用可操作/进行中/成功·失败·跳过类终态等**角色**约束处理队列,不绑死某一技能的状态名
|
||||
- 默认只处理可操作态;开始抢占进行中;结束回写成功或失败(失败带原因);重做靠人工改回可操作
|
||||
- REQUIREMENTS §0.3 状态→角色映射;CHECKLIST S0-11 / A6-7~A6-9;SKILL / ACTIONS / SCHEMA / references 索引交叉引用
|
||||
|
||||
## 1.0.94
|
||||
|
||||
- 修正 1.0.93 更新日志重复小节
|
||||
|
||||
## 1.0.93
|
||||
|
||||
- 沉淀 Lookup **多选**契约:同一 `lookup` / `options_json` 对象上加 `multiple: true`(平行扩展单选,不另造 display_type)
|
||||
- 多选存 `TEXT` 逗号分隔 id;列表展示映射为多个 label;控件由宿主按选项数量选择(短列表勾选 / 长列表 Chip+下拉)
|
||||
- 更新 SCHEMA / ACTIONS / skill-actions.schema / lookup 示例与 options_json 校验
|
||||
|
||||
## 1.0.92
|
||||
|
||||
- 沉淀数据管理 `replaces: import`:导入 UI 统一宿主面板,确认后由技能写库
|
||||
- ACTIONS / schema / 守护测试对齐 create·update·delete·import;补充可抄示例 `replaces-*.action.example.json`
|
||||
- 修正旧文档「宿主导入仍直写」的过时说明
|
||||
|
||||
## 1.0.91
|
||||
|
||||
- 沉淀「表与表之间禁止 SQLite 外键」契约:只存业务 id,关联与级联在 repository/service;界面用 `lookup`
|
||||
- `display_metadata_validator` 扫描业务表 DDL,含 `FOREIGN KEY` / `REFERENCES` 时报错
|
||||
- 交活 Checklist 新增 A6-6;`lookup` 文档用语改为「跨表引用 id」,避免与 SQLite 外键混淆
|
||||
|
||||
## 1.0.90
|
||||
|
||||
- 沉淀数据管理表格说明契约:`_jiangchang_tables.grid_help` / `_jiangchang_columns.grid_help`
|
||||
- 有内容才显示表上方说明和列头「?」;不写则与今日一致
|
||||
- 与新增/编辑表单的 `description` 独立,禁止互拷(见 `references/SCHEMA.md`)
|
||||
|
||||
## 1.0.89
|
||||
|
||||
- 仓库增加 MIT 许可证,版权归属深圳匠厂科技有限公司
|
||||
- 增加 SECURITY.md:未公开漏洞请通过匠厂官网联系,不要发在公开 Issue
|
||||
- 模板 developer_ids 改为占位 12345,复制后请换成自己的匠厂用户 ID
|
||||
|
||||
## 1.0.88
|
||||
|
||||
- `release.ps1` 默认 tag 说明改为 UTF-8 字节写入,不再依赖脚本源码里的中文默认参数(Windows PowerShell 5.1 会把无 BOM 的 UTF-8 按系统代码页解析)
|
||||
- 不改写已发布 tag(v1.0.86 / v1.0.87 说明仍可能乱码)
|
||||
|
||||
## 1.0.87
|
||||
|
||||
- `release.ps1` 打 annotated tag / 自动 commit 改为 UTF-8 消息文件 + `git -F`,避免 `-m` 走控制台代码页
|
||||
- 默认说明仍可能乱码(见 1.0.88)
|
||||
|
||||
## 1.0.86
|
||||
|
||||
- 去掉 `release.ps1` 里通过 `cmd /c` 调 git 的写法;中文 tag 说明在部分 Windows 控制台代码页下仍可能乱码,见 1.0.87
|
||||
|
||||
## 1.0.85
|
||||
|
||||
- 新增开发主文 `development/PRODUCT_SURFACES.md`:说明对话 / 数据管理 / 定时任务平级关系、何时走哪个入口、如何做入口定位
|
||||
- 需求与交活清单增加入口定位表;纠正「只做对话即可」与「Action 一概可选」的误导语气
|
||||
- 数据管理明确为 UI 执行面 + 过程可见,与对话同内核
|
||||
|
||||
## 1.0.84
|
||||
|
||||
- 沉淀 Action 参数表单字段条件显隐契约 `visibleWhen`(先选模式再出对应参数)
|
||||
- 文档入口:`ACTIONS.md` / `SKILL_ACTION_RUNTIME.md` / `CHECKLIST` A4-7;与 Policy L3 行态 `visibleWhen` 消歧
|
||||
- 新增示例 `assets/examples/visible-when-action.example.json`;Schema 已支持该字段
|
||||
|
||||
## 1.0.83
|
||||
|
||||
- 沉淀跨表 Lookup 契约:Action 可用内联 `lookup` 或绑列下拉,禁止让用户手填内部编号
|
||||
- 统一展示:`编号 · 名称`,可选 `extra_column` 时为 `编号 · 名称(额外)`
|
||||
- 更新 `ACTIONS.md` / `SCHEMA.md` / `skill-actions.schema.json`、示例与验收清单
|
||||
|
||||
## 1.0.82
|
||||
|
||||
- 沉淀数据管理技能按钮可选悬停字段 `hoverHint`:短名看不懂或会覆盖数据时再写;不回退说明或确认黄条
|
||||
- 宿主内置删除/刷新/新增/导入/导出不走该字段;默认 health/version/config-path 不加悬停
|
||||
|
||||
## 1.0.81
|
||||
|
||||
- 模板默认不再创建或写入 `task_logs`:过程看运行日志,进度看任务中心,业务结果看业务表
|
||||
- `run` 录屏信息并入 `finish()`;去掉 CLI `logs` / `log-get`
|
||||
- 已安装技能的旧库不受影响
|
||||
|
||||
## 1.0.80
|
||||
|
||||
- 沉淀数据管理「HTML 页面报告」契约:写入 `{skill_data}/html/*.html`,重新生成 Action id 固定为 `render-html`
|
||||
- 新增 `get_html_dir()`、薄写入 helper(`html_page_service`)、示例页与 Action 示例 JSON
|
||||
- 文档:`ACTIONS.md` / `DATA_PATHS.md` / `SKILL_ACTION_RUNTIME.md` 等已区分 HTML 报告与 `export-*` / 宿主表导出
|
||||
|
||||
## 1.0.79
|
||||
|
||||
- 沉淀数据管理表级「默认展示列」契约:`_jiangchang_tables.default_visible_columns_json`
|
||||
- 已配置则宿主默认只显示名单列;未配置全显;用户改过「列」后以本机偏好为准
|
||||
- 与 `_jiangchang_columns.visible=0` 硬隐藏区分:默认隐藏仍可在「列」面板勾回
|
||||
- `upsert_table_metadata` / 旧库幂等 `ALTER` 已支持该字段(见 `references/SCHEMA.md`)
|
||||
|
||||
## 1.0.78
|
||||
|
||||
- 反哺 RPA 有界就绪等待约定:状态等待统一用 kit `poll_until`(需 `jiangchang-platform-kit>=1.2.11`)
|
||||
- 错误码表补充 `UI_NOT_READY` / `BROWSER_START_TIMEOUT` / `ENV_DEGRADED`,明确慢机器勿误判为配置错或频控
|
||||
- 模板默认 `platform_kit_min_version` 上调至 `1.2.11`;`.env.example` 增加可选 `OPENCLAW_BROWSER_LAUNCH_TIMEOUT_MS`
|
||||
- `examples/real_browser_rpa` 搜索结果等待改为 `poll_until` 示例写法
|
||||
|
||||
## 1.0.77
|
||||
|
||||
- 固化 Action / CRUD **对称**表单字段类型契约:text、textarea/json、number、boolean、enum/lookup、sensitive、date、datetime_unix_seconds、url/email(薄层)
|
||||
- 明确可编辑业务时间用 Unix **秒**读写;审计 `created_at`/`updated_at` 仍只读展示
|
||||
- 明确本版**不做**文件上传、多选/标签数组、复杂 nested 表单,避免宿主反复扩面
|
||||
- 更新 `SCHEMA.md`、`ACTIONS.md`、`skill-actions.schema.json`(`display_type`/`format`)
|
||||
|
||||
## 1.0.76
|
||||
|
||||
- 固化数据管理「新增/编辑」表单契约:列 `description` 须在标签下展示;创建默认值取自 SQLite 列 `DEFAULT`(见 `references/SCHEMA.md`)
|
||||
- 明确禁止把说明塞进 `display_name`;与 Skill Action 的 `inputSchema.description` / `default` 分工写清(见 `ACTIONS.md`)
|
||||
- 宿主须按该契约实现下发与预填;技能侧用元数据说明 + DDL DEFAULT 即可,第一期不新增表单默认值元数据列
|
||||
|
||||
## 1.0.75
|
||||
|
||||
- 正式沉淀数据管理「单元格呈现」通用契约(`references/CELL_PRESENTATION.md`):技能声明某表某行某列的语义 tone,宿主映射主题样式
|
||||
- 用途不限(超时未填、库存预警、状态异常、审核驳回等);推荐旁路表 `_jiangchang_cell_presentation`;填完后重算即取消强调
|
||||
- 新增 Schema / 示例;`actions.json` 可选顶层 `cellPresentation.enabled`
|
||||
|
||||
## 1.0.74
|
||||
|
||||
- 沉淀宿主 Skill Action 编排契约:`hostEffects`(`onSuccess` / `onFailure`)与 `steps`(`before` / `afterSuccess` / `afterFailure`)
|
||||
- 支持成功后 `reloadTable` 等 UI 副作用与级联删除步骤;需匠厂宿主 ≥2.8.5;省略则不自动刷表
|
||||
- 更新 `skill-actions.schema.json`、ACTIONS.md、SKILL_ACTION_RUNTIME
|
||||
|
||||
## 1.0.73
|
||||
|
||||
- 正式发布 Data Policy 契约(`references/POLICY.md` v1.0):可选身份授权,默认 L0 零配置
|
||||
- 新增 Snapshot Schema 与示例;`actions.json` 支持顶层 `policy` 与 Action 可选 `authz` 提示
|
||||
- ACTIONS 扩展为六职责正交(含 policy)
|
||||
|
||||
## 1.0.72
|
||||
|
||||
- 澄清 `placements: "row"` 契约:表示数据管理**行级入口**;技能配置决定菜单项,宿主可呈现为操作列、行右键和/或行内「⋯」(同一批 Action,无独立菜单 DSL / `row-context-menu` placement)
|
||||
- 更新 ACTIONS.md、SKILL_ACTION_RUNTIME、DEVELOPMENT 验收表与 skill-actions.schema 描述
|
||||
|
||||
## 1.0.71
|
||||
|
||||
- 文档对齐:Skill Action `sync` 默认超时约 **60s**(`SKILL_ACTION_SYNC_TIMEOUT_MS`);接近或超过约 60s 的任务应标 `async`
|
||||
- 更新 `SKILL_ACTION_RUNTIME.md`
|
||||
|
||||
## 1.0.70
|
||||
|
||||
- 对齐宿主:数据管理表顶栏技能按钮全部直接展示,取消「更多操作」溢出菜单
|
||||
- 补充宽窄布局说明:够宽左右横排一行;不够宽左右分栏、栏内竖排
|
||||
- 更新 ACTIONS.md 与 skill-actions.schema.json
|
||||
|
||||
## 1.0.69
|
||||
|
||||
- 用户数据目录收敛为上行 `input/` 与下行 `output/`(取代 downloads / imports / exports 三分法)
|
||||
- `runtime_paths` 提供 `get_input_dir()` / `get_output_dir()`;相对输入路径解析到 `input/`
|
||||
- 更新 DATA_PATHS.md、CONFIG、`.env.example`;缓存 / 临时 / RPA 录屏目录仍为内部目录
|
||||
|
||||
## 1.0.68
|
||||
|
||||
- 明确数据管理技能按钮顺序契约:同一 placement(toolbar / row / batch)下按 `actions` 数组相对顺序展示;调整数组先后即可改序
|
||||
- 说明宿主内置刷新 / 导出 / 查看等不参与该顺序;无独立 `sortOrder` 字段
|
||||
- 更新 ACTIONS.md 与 skill-actions.schema.json
|
||||
|
||||
## 1.0.67
|
||||
|
||||
- 移除模板内嵌的 `tools/ui_inspector`;UI 录制器产品迁至独立仓库 `jiangchang-ui-recorder`
|
||||
- 相关开发文档改为指向 Gitea 产品仓库入口
|
||||
|
||||
## 1.0.66
|
||||
|
||||
- 数据管理支持表级默认排序:`_jiangchang_tables.default_sort_json`(多字段 JSON);宿主优先采用,未配置则仍按 updated_at → created_at → 主键
|
||||
- 更新 SCHEMA.md 与 `upsert_table_metadata`;旧库幂等补列
|
||||
|
||||
## 1.0.65
|
||||
|
||||
- `replaces` create/update 可复用表列 lookup:字段名对齐列名,或使用 `bind.columnMapping` / 字段 `column`
|
||||
- 更新 ACTIONS.md 与 skill-actions.schema.json
|
||||
|
||||
## 1.0.64
|
||||
|
||||
- Skill Action 新增可选 `replaces: create|update|delete`:接管数据管理默认新增/编辑/删除,无配置时宿主仍直写 SQLite
|
||||
- 更新 ACTIONS.md 与 skill-actions.schema.json;`replaces` 须配合 `bind.tables`,可与空 `placements` 并用
|
||||
|
||||
## 1.0.63
|
||||
|
||||
- 数据管理支持字段级 lookup:`options_json` 可声明同库关联表(`ref_table` / `value_column` / `label_column`),宿主新建/编辑时渲染下拉
|
||||
|
||||
10
DEMO.md
10
DEMO.md
@@ -15,9 +15,13 @@ demo_video_url: ""
|
||||
|
||||
## 建议观看顺序
|
||||
|
||||
1. 如何发起一次典型任务
|
||||
2. 执行过程中会看到什么提示 / 进度
|
||||
3. 成功后的结果长什么样
|
||||
建议与 `TUTORIAL.md` 三入口结构对齐(有则拍、无则跳过):
|
||||
|
||||
1. 数据管理:准备数据 → 主流程按钮 → 结果表/报告
|
||||
2. 对话任务(若有):一句自然语言触发同一能力
|
||||
3. 定时任务(若有):到点自动跑;若本技能无定时,演示中可说明「不需要定时」
|
||||
|
||||
成功后的结果长什么样(表、任务中心、HTML 等)
|
||||
|
||||
## 没有视频时
|
||||
|
||||
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 深圳匠厂科技有限公司
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
63
README.md
63
README.md
@@ -1,14 +1,14 @@
|
||||
---
|
||||
description: "用用户能理解的方式说明这个技能能做什么、适合什么场景,以及如何开始使用。"
|
||||
description: "用用户能理解的方式说明这个技能能做什么、适合什么场景,以及边界与结果形态。"
|
||||
---
|
||||
|
||||
> **模板说明(复制后删除本段)**
|
||||
> 本文是技能市场详情与普通用户说明,**不是** Agent 参考索引、CLI 契约或开发文档。
|
||||
> frontmatter 的 `description` 必须是用户能看懂的一句话能力描述;**不得**写成 CLI、schema、references、development、契约、数据结构、开发规范或 Agent 参考索引。
|
||||
> 正文只回答用户关心的问题,**不要**把 Playwright、DOM、Python、RPA、references、development、schema 等作为面向用户的主说明内容;若业务上必须出现技术词,须用用户能理解的方式解释,且不能喧宾夺主。
|
||||
> **复制后验收章节(标题可微调,建议保留):**「能帮你做什么」「适合什么场景」「使用前需要准备什么」「你可以这样告诉 Agent」「执行后你会得到什么」「注意事项」「常见问题」。
|
||||
|
||||
# 【技能名称】
|
||||
> 本文是技能市场 **「说明」** Tab(`readme_md`),**不是**使用教程。
|
||||
> frontmatter 的 `description` 必须是用户能看懂的一句话能力描述;**不得**写成 CLI、schema、references、development、契约或「如何开始使用」类教程承诺。
|
||||
> **禁止**写入:分步操作(1/2/3)、Agent 示例话术块、「入口一/入口二」专章、逐步排障。这些一律写在根目录 `TUTORIAL.md`。
|
||||
> 正文只回答:是什么、能做什么、适合谁、准备什么、得到什么、注意事项。完整契约见 `development/TUTORIAL_SURFACES.md` §1。
|
||||
> **复制后验收章节(标题可微调):**「能帮你做什么」「适合什么场景」「使用前需要准备什么」「怎么使用」「执行后你会得到什么」「注意事项」「常见问题」。
|
||||
> **标题层级:** 宿主页头已有技能名——**禁止**文首再写 `# 技能名`;正文主分节用 `##`。
|
||||
|
||||
> 用一两句话说明这个技能帮助用户完成什么工作。
|
||||
> 这里不要解释技术实现,要从用户的业务目标出发描述价值。
|
||||
@@ -17,7 +17,7 @@ description: "用用户能理解的方式说明这个技能能做什么、适合
|
||||
|
||||
## 这个技能能帮你做什么
|
||||
|
||||
用 3 到 6 条业务能力说明技能价值,例如:
|
||||
用 3 到 6 条业务能力说明技能价值(**不要**写成操作步骤),例如:
|
||||
|
||||
- 【自动整理/查询/生成/同步/核对/提交】某类业务数据
|
||||
- 帮你减少重复操作,降低人工录入或核对成本
|
||||
@@ -38,42 +38,33 @@ description: "用用户能理解的方式说明这个技能能做什么、适合
|
||||
|
||||
## 使用前需要准备什么
|
||||
|
||||
说明用户在使用前要准备的东西,例如:
|
||||
说明**前置条件**(装什么、要不要账号、要不要外站登录),**不要**写「去哪张表点哪个按钮」:
|
||||
|
||||
- **网页自动化类:** 首次运行可由技能自动登记平台账号(浏览器会话由系统隔离管理)。也可在「账号管理」里指定登录标识。登记账号 ≠ 已登录目标站
|
||||
- **账号在哪里看:** 已登记账号可在「数据管理 / 账号管理」中查看;每个账号使用独立浏览器会话,批量或多账号任务不会共用登录状态
|
||||
- **是否要登录目标站:** 按本技能说明(有的平台必须登录,有的公开页可不登录);与「是否有账号记录」不是一回事
|
||||
- **网页自动化类:** 可能需要在账号管理中登记平台账号;登记账号 ≠ 已登录目标站(细节见使用教程)
|
||||
- 相关业务权限(查询、导出、提交等)已具备
|
||||
- 需要处理的文件、时间范围、订单号、客户名称、项目名称等信息
|
||||
- 如果涉及验证码或人工确认,请保证能及时在弹出的浏览器窗口中操作
|
||||
- 需要处理的文件、时间范围、单号等信息(若适用)
|
||||
- 若涉及验证码或人工确认,使用时请能及时在弹出窗口中操作
|
||||
|
||||
---
|
||||
|
||||
## 你可以这样告诉 Agent
|
||||
## 怎么使用
|
||||
|
||||
用自然语言示例展示用户可以怎么发起任务,例如:
|
||||
本技能支持哪些宿主入口,用**一两句话**概括即可,例如:
|
||||
|
||||
> “帮我查询今天的【业务对象】,整理成表格。”
|
||||
>
|
||||
> “帮我核对这些【数据/单据】是否有异常,并告诉我问题在哪里。”
|
||||
>
|
||||
> “帮我把【时间范围】内的【业务数据】导出并生成处理报告。”
|
||||
>
|
||||
> “帮我根据这个文件里的内容,逐条完成【业务动作】。”
|
||||
- 支持 **数据管理**(点表操作)与 **对话任务**(自然语言);是否提供定时任务按本技能说明。
|
||||
|
||||
> 注意:示例应是自然语言,不要写命令行命令。
|
||||
**逐步操作、示例话术与排障,请查看技能市场「使用教程」Tab。**
|
||||
(完整写法见 `development/TUTORIAL_SURFACES.md`。)
|
||||
|
||||
---
|
||||
|
||||
## 执行后你会得到什么
|
||||
|
||||
说明用户会看到什么结果,例如:
|
||||
说明用户会看到什么**结果形态**(不要教如何点出来),例如:
|
||||
|
||||
- 任务是否成功完成
|
||||
- 处理了多少条数据
|
||||
- 哪些内容成功、哪些失败
|
||||
- 失败原因和建议处理方式
|
||||
- 生成的文件、表格、报告或记录
|
||||
- 处理了多少条数据;哪些成功、哪些失败
|
||||
- 生成的文件、表格、报告或业务表记录
|
||||
|
||||
---
|
||||
|
||||
@@ -90,18 +81,16 @@ description: "用用户能理解的方式说明这个技能能做什么、适合
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 为什么任务没有查到数据?
|
||||
> FAQ 写**概念与边界**;需要「点哪里」的排障写到「使用教程」。
|
||||
|
||||
> 可能是时间范围、关键词、账号权限或系统筛选条件不正确。请先确认输入信息是否准确。
|
||||
### 这个技能适合我吗?
|
||||
|
||||
### 为什么任务执行到一半需要我操作?
|
||||
> 若你的场景与上文「适合什么场景」一致,可以安装后按「使用教程」完成第一次操作。
|
||||
|
||||
> 可能遇到了登录、验证码、权限确认、二次审批或系统异常。请根据 Agent 的提示完成必要操作。
|
||||
### 我不会操作,从哪里开始?
|
||||
|
||||
### 结果文件在哪里查看?
|
||||
|
||||
> 请根据 Agent 返回的结果说明查看生成文件、任务记录或处理报告。
|
||||
> 请打开技能市场 **「使用教程」** Tab,按「入口一:数据管理」或「入口二:对话任务」任选一种跟做。
|
||||
|
||||
### 已经登记的账号在哪里查看?
|
||||
|
||||
> 请到匠厂的「数据管理 / 账号管理」查看账号记录和状态。若任务仍提示账号异常,请把账号编号、任务时间和界面提示原文提供给支持人员;不要发送密码、Cookie 或验证码。
|
||||
> 请到匠厂的「数据管理 / 账号管理」查看账号记录和状态。不要发送密码、Cookie 或验证码。
|
||||
|
||||
20
SECURITY.md
Normal file
20
SECURITY.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# 安全漏洞披露
|
||||
|
||||
本仓库是匠厂技能开发模板。请把**未公开的安全漏洞**发给深圳匠厂科技有限公司,不要在公开 Issue、PR 或讨论区里贴完整复现步骤、PoC 或利用细节。
|
||||
|
||||
## 怎么报告
|
||||
|
||||
请通过匠厂官网联系渠道提交:[https://www.jc2009.com/](https://www.jc2009.com/)
|
||||
|
||||
建议在联系时注明:
|
||||
|
||||
- 标题或开头带 `[Security]`
|
||||
- 影响的仓库 / 版本(例如 `skill-template`、tag `v1.0.x`)
|
||||
- 问题类型、影响范围、复现步骤
|
||||
- 若已有修复思路,可一并说明
|
||||
|
||||
我们会确认收悉,并在有结论后告知是否修复、是否公开披露。
|
||||
|
||||
## 不在本页范围
|
||||
|
||||
本页只说明**如何向我们报告漏洞**。技能运行时的权限、RPA 合规、加密发布等产品安全设计,见 `development/` 与 `references/`,不要写进本文件。
|
||||
52
SKILL.md
52
SKILL.md
@@ -1,17 +1,19 @@
|
||||
---
|
||||
name: 技能开发模板(通用业务版)
|
||||
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
|
||||
version: 1.0.63
|
||||
version: 1.0.124
|
||||
author: 深圳匠厂科技有限公司
|
||||
metadata:
|
||||
openclaw:
|
||||
slug: your-skill-slug
|
||||
platform_kit_min_version: "1.2.2"
|
||||
platform_kit_min_version: "1.2.11"
|
||||
emoji: "📦"
|
||||
category: "通用"
|
||||
developer_ids:
|
||||
- 10032
|
||||
- 12428
|
||||
- 12345 # 占位,请换成你的匠厂用户 ID
|
||||
# brands: # 可选;未写则发布后仅挂匠厂市场(jiangchang)
|
||||
# - jiangchang # 匠厂主站
|
||||
# - huashang # 华商等 OEM:按目标市场填写,可多选
|
||||
allowed-tools:
|
||||
- bash
|
||||
---
|
||||
@@ -31,18 +33,26 @@ allowed-tools:
|
||||
|
||||
### 架构铁律(先读)
|
||||
|
||||
- **宿主产品入口平级**:对话任务 / 数据管理 / 定时任务是同一技能能力的不同触发场景,**同等重要**;挂哪些由本技能 UX 定义(入口定位表),详见 `development/PRODUCT_SURFACES.md`。以后宿主若增加新入口,仍是「新触发场景 + 同一内核」。
|
||||
- **单业务内核、多入口**:数据管理 / 定时任务 / Agent / 技能详情 / 直接 CLI 必须复用同一 domain service;禁止按入口复制业务逻辑,禁止按 `source` 分叉业务行为。
|
||||
- **`placements`**:只决定 Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail)。
|
||||
- **数据管理**:既是 **UI 执行面**(点按钮走与对话相同的闭环),也是数据与过程可见;不是「只读库」。
|
||||
- **`placements`**:只决定 Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail)。其中 `row` 表示**行级入口**(宿主可呈现为操作列、行右键、行内「⋯」等;技能配什么就显示什么,无独立菜单 DSL)。数据管理技能按钮顺序见 `actions` **数组相对顺序**(详见 `references/ACTIONS.md`),无独立 sortOrder。
|
||||
- **`bind.tables`**:只决定数据管理出现在哪些表;含 `toolbar` 时**必须**显式非空绑定。
|
||||
- **`bind.selectionMode`(可选)**:表勾选如何驱动 Action(`required` / `none` / `optional`);省略由宿主推断。同一能力勿拆两个同义顶栏按钮;定时任务配置页按无勾选分支隐藏 `$selection.ids`,见 `references/ACTIONS.md`。
|
||||
- **`bind.visibleWhen`(可选)**:`row` 菜单/按钮按当前行列值显隐(如仅已采用可入队);省略则始终显示。≠ 表单字段 `visibleWhen`,≠ Policy L3。见 `references/ACTIONS.md`「行菜单显隐」。
|
||||
- **用户可见文案**:数据管理弹窗 / 任务中心文案须全中文产品用语;勿把 `pickCount`、`async`、`pending`、CLI 子命令写进弹窗(见 `references/ACTIONS.md`「用户可见文案」)。
|
||||
- **`executionProfile`**:只决定执行方式——`sync` 当场返回结果,`async` 进**任务中心**。与 placements / 入口**无关**。心智上多数为 sync,仅长任务 / RPA 标 async;宿主省略该字段时按 sync。每个 Action **仍必须**显式写 `sync` 或 `async`。
|
||||
- **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;特殊业务报告才另做 `export-*`。
|
||||
- **`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**:壳子页放 `assets/html/pages/`,快照写 `html/` 并用固定 Action id `render-html`;页面用 `jiangchang.call` 调已声明命令(见 `references/ACTIONS.md`)。
|
||||
- **准备 vs 执行(适用则拆)**:若技能存在「先写入/准备本地数据」与「再对外产生副作用」两段,Agent 必须能分开触发;禁止默认绑成不可拆的一步。一键无准备态的技能可跳过本条。
|
||||
|
||||
### 如何触发(能力类型)
|
||||
|
||||
| 你要做什么 | Agent 怎么做 | 进任务中心? |
|
||||
|------------|--------------|--------------|
|
||||
| 短查询 / 只读 / 秒级(`health` / `version` / `stats` / `list` / `get` / `logs` 等) | OpenClaw bash/exec + **共享 Python** 跑 CLI(见下方「共享 Python」) | 否(CLI 当场结束) |
|
||||
| 短查询 / 只读 / 秒级(`health` / `version` / `stats` / `list` / `get` 等) | OpenClaw bash/exec + **共享 Python** 跑 CLI(见下方「共享 Python」) | 否(CLI 当场结束) |
|
||||
| 已在 `actions.json` 声明且 `executionProfile: "sync"` 的能力 | 可用 `run_skill_action`,或同等效果的短 CLI(同一内核) | 否 |
|
||||
| 长耗时 / RPA / 浏览器 / 需要进度或取消;或 `executionProfile: "async"` | **必须** `run_skill_action`(先 `list_skill_actions` / `get_skill_action`) | **是**(仅因为该 Action 是 async) |
|
||||
| 用户只问用法 / 能做什么 | 读根目录 `README.md`;逐步操作再读 `TUTORIAL.md`;**不要**为答用法去列目录或默认打开 `references/` | — |
|
||||
@@ -64,11 +74,12 @@ allowed-tools:
|
||||
1. **禁止**对长任务使用 `exec`/`bash` 跑 `…/scripts/main.py run`(或等价业务命令)再 `process poll` 干等。
|
||||
2. **禁止**用 `uv run python …` 跑技能脚本(会建临时环境,缺 `jiangchang_skill_core`)。
|
||||
3. **禁止** Agent 自行 `pip install`。
|
||||
4. Action **可选**:无数据管理 / 定时 / Agent 结构化调用需求时可不提供 `actions.json`;有长任务 / RPA 时**必须**提供至少一条 `executionProfile: "async"` 且 `placements` 含 `agent` 的 Action。
|
||||
4. **入口按定位表挂载**:复制业务技能后须在 `development/REQUIREMENTS.md` 填写入口定位表(见 `development/PRODUCT_SURFACES.md`)。定位需要数据管理 / 定时 / Agent 结构化调用时,必须提供 `actions.json` 并声明对应 `placements`;定位确认无需任何宿主结构化入口时可不提供。有长任务 / RPA 时**必须**提供至少一条 `executionProfile: "async"` 且 `placements` 含 `agent` 的 Action。
|
||||
5. **一个业务能力一个 Action**,用多个 `placements` 挂入口;底层仍是同一 `scripts/main.py` CLI。
|
||||
6. 是否进任务中心**只**看该 Action 的 `executionProfile`,与数据管理 / 定时 / Agent 入口无关。
|
||||
7. **处理业务行**时遵守状态角色契约(可操作 / 进行中 / 终态);禁止对已成功/失败/跳过默认重复处理。详见 `references/WORK_ITEM_STATUS.md`。
|
||||
|
||||
Agent 执行契约细节:`references/CLI.md`、`references/ACTIONS.md`、`references/SCHEMA.md`(不要把 `development/` 当 Agent 主读路径)。
|
||||
Agent 执行契约细节:`references/CLI.md`、`references/ACTIONS.md`、`references/SCHEMA.md`、按需 `references/POLICY.md` / `references/WORK_ITEM_STATUS.md`(不要把 `development/` 当 Agent 主读路径)。
|
||||
|
||||
## 共享 Python(必读)
|
||||
|
||||
@@ -81,7 +92,7 @@ python {baseDir}/scripts/main.py config-path
|
||||
python {baseDir}/scripts/main.py init-db
|
||||
```
|
||||
|
||||
- 公共库来自共享 venv 的 `jiangchang-platform-kit>=1.2.2`(包名路径 `jiangchang_skill_core`);**不要 vendor** `scripts/jiangchang_skill_core/`。
|
||||
- 公共库来自共享 venv 的 `jiangchang-platform-kit>=1.2.11`(包名路径 `jiangchang_skill_core`);**不要 vendor** `scripts/jiangchang_skill_core/`。
|
||||
- 根目录 `requirements.txt` **只声明技能特有**依赖;不要写 `jiangchang-platform-kit` / `playwright`。
|
||||
- `metadata.openclaw.platform_kit_min_version` 是宿主兼容声明,**不是** pip 依赖。
|
||||
- `health` 用 `collect_runtime_diagnostics` 做只读诊断。
|
||||
@@ -104,10 +115,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 臆造按钮路径。
|
||||
|
||||
**文档读取边界:**
|
||||
|
||||
@@ -120,13 +132,13 @@ python {baseDir}/scripts/main.py init-db
|
||||
|
||||
| 文档 | 读者 | 用途 |
|
||||
|------|------|------|
|
||||
| 根目录 `README.md` | 普通用户 | 市场 **说明** Tab(`readme_md`) |
|
||||
| 根目录 `TUTORIAL.md` | 普通用户 | 市场 **教程** Tab(`tutorial_md`) |
|
||||
| 根目录 `README.md` | 普通用户 | 市场 **说明** Tab(`readme_md`):能力/场景/边界;**非教程** |
|
||||
| 根目录 `TUTORIAL.md` | 普通用户 | 市场 **教程** Tab(`tutorial_md`):分入口操作;结构见 `development/TUTORIAL_SURFACES.md` |
|
||||
| 根目录 `DEMO.md` | 普通用户 | 市场 **演示** Tab(`demo_video_url` + `demo_md`) |
|
||||
| 根目录 `CHANGELOG.md` | 普通用户 | 市场 **更新日志** Tab(按版本小节 → `changelog`) |
|
||||
| `SKILL.md`(本文) | LLM / OpenClaw 平台 | 技能入口、触发与运行契约摘要 |
|
||||
| `references/` | Agent 编排 / 调用 | CLI、Action、schema 契约(Agent 主读) |
|
||||
| `development/` | 开发者 / AI 编程代理 | 需求、开发教程、测试、深度技术规范 |
|
||||
| `development/` | 开发者 / AI 编程代理 | 需求、开发教程、测试、深度技术规范;**产品入口**见 `development/PRODUCT_SURFACES.md`;**用户文档契约**见 `development/TUTORIAL_SURFACES.md` |
|
||||
|
||||
## 目录约定
|
||||
|
||||
@@ -141,14 +153,14 @@ python {baseDir}/scripts/main.py init-db
|
||||
1. 从本模板创建新业务技能时,**不得保留模板 `.git`**;优先使用 [`tools/scaffold_skill.ps1`](tools/scaffold_skill.ps1)(见 [`tools/README.md`](tools/README.md))。
|
||||
2. 业务技能仓库中**不得**存在 `.openclaw-skill-template`(仅 template 源仓库保留)。
|
||||
3. 复制目录为你的新 skill 仓库,全局替换 `your-skill-slug` 等占位词。
|
||||
4. 按 `development/DEVELOPMENT.md` 完成开发与文档定制。
|
||||
4. 按 `development/DEVELOPMENT.md` 完成开发与文档定制;开发前先读 `development/PRODUCT_SURFACES.md` 并填写入口定位表。
|
||||
5. 用户市场四 Tab 材料写入根 `README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md`;Agent 调用契约见 `references/CLI.md`、`references/SCHEMA.md`、`references/ACTIONS.md`。
|
||||
6. 同步修改 `scripts/util/constants.py` 中的 `SKILL_SLUG` / `SKILL_VERSION`。
|
||||
7. 本地 SQLite 的中文表名/字段名、标准时间字段与字段顺序规范见 `references/SCHEMA.md`。
|
||||
7. 本地 SQLite 或可选 MySQL 云库:数据源声明见 `references/DATA_SOURCES.md`;中文表名/字段名、标准时间字段与字段顺序规范见 `references/SCHEMA.md`。
|
||||
|
||||
## 技能命名与 slug
|
||||
|
||||
- 标准格式:`{verb}-{noun-phrase}-{platform}`(详见 `development/NAMING.md`)。
|
||||
- 标准格式:`{verb}-{noun-phrase}`,绑定外部平台时才加 `-{platform}`(详见 `development/NAMING.md`)。不要用 `default` 占位。
|
||||
- slug / repo / 目录 / `SKILL_SLUG` / DB 文件名:**英文** kebab-case;`name`、`description`、根 `README.md`:**中文**。
|
||||
- 复制为新业务技能前**必须**阅读 `development/NAMING.md`。
|
||||
- `name`(frontmatter):**用户可见的中文业务名称**;`description` 是说明,不是名称。
|
||||
@@ -161,3 +173,9 @@ python {baseDir}/scripts/main.py init-db
|
||||
- 开发期技能常为不公开:不加本人 ID,开发者自己也无法在市场安装自测。取 ID 与写入步骤见 `development/DEVELOPMENT.md` §6。
|
||||
- 当 `access_scope = 0`(不公开)时,平台会把 `developer_ids` 中的用户自动补写到 `skill_user_access`。
|
||||
- `developer_ids` 建议写为正整数数组;第一个 ID 会作为主开发者同步到 `skills.developer_id`。
|
||||
- `metadata.openclaw.brands`(**可选**):OEM 技能市场编码列表,写入平台 `skill_market_relations.market_code`。兼容别名 `oem_clients` / `client_keys`。
|
||||
- **未写或为空**:发布时仅幂等补默认 `jiangchang`(匠厂主站),兼容老技能。
|
||||
- **显式列表**(如 `jiangchang`、`huashang`):发布时幂等补写对应市场(**只补不删**);客户端靠 Header `X-Market-Code` 过滤目录。
|
||||
- **不是** Gitea 组织名推断:仓库在 `client-huashang` 下 ≠ 自动出现在华商市场,必须以 `brands` 声明。
|
||||
- 与 `developer_ids` 分工:`brands` 管「进哪个 OEM 市场目录」;`developer_ids` 管「不公开时谁能看见/安装」。
|
||||
- 完整步骤与验收见 `development/DEVELOPMENT.md` §6「关于 brands」。
|
||||
|
||||
143
TUTORIAL.md
143
TUTORIAL.md
@@ -1,12 +1,33 @@
|
||||
> **模板说明(复制后删除本段)**
|
||||
> 本文对应技能市场 **「使用教程」** Tab(发布时同步为 `tutorial_md`)。
|
||||
> 面向普通用户写**可跟着做的步骤**,不要写 CLI、目录结构、release、开发规范。
|
||||
> 与根 `README.md`(技能说明)分工:README 讲「是什么 / 能做什么」;本文讲「第一次怎么用完」。
|
||||
> **复制后验收章节(标题可微调):**「开始前」「第一步…」「第二步…」「常见卡住怎么办」。
|
||||
> 与根 `README.md`(技能说明)分工:README 讲「是什么 / 能做什么 / 边界」;本文讲「从哪个入口怎么用完」。
|
||||
> **完整契约:** `development/TUTORIAL_SURFACES.md`(三入口结构、说明≠教程、skill-detail 不可作主路径)。
|
||||
> **复制后验收章节(标题可微调,语义不可缺):**「本技能做什么」「开始前」「入口一:数据管理」「入口二:对话任务」「入口三:定时任务」「主能力对照表」「常见卡住怎么办」。
|
||||
> 若本技能 §0.1 未挂数据管理 / 对话 / 定时,对应入口节改为「不适用」并写一句理由(与 REQUIREMENTS 一致)。
|
||||
> **标题层级:** 宿主页头已有技能名、Tab 已有「使用教程」——**禁止**文首再写 `# 技能名 · 使用教程`;正文主分节用 `##`,步骤用 `###`。
|
||||
|
||||
# 【技能名称】使用教程
|
||||
本技能有两种**平行**的使用方式(按本技能入口定位填写;未挂的入口删掉或写不适用):
|
||||
|
||||
用最短路径带用户完成一次成功使用。步骤要具体,可用「打开匠厂 → …」这类操作语言。
|
||||
- **数据管理**:在左侧表上点工具栏 / 行内按钮
|
||||
- **对话任务**:用自然语言让 Agent 完成同一能力
|
||||
|
||||
两种方式触发同一套业务能力,结果一致,选你习惯的即可。
|
||||
定时任务是否提供:见下方「入口三」(没有则写「本技能不需要定时」+ 理由)。
|
||||
|
||||
---
|
||||
|
||||
## 本技能做什么
|
||||
|
||||
用业务语言画清闭环(**不要**写库表英文名或命令行),例如:
|
||||
|
||||
```text
|
||||
【输入/准备】 → 【核心动作】 → 【结果可见处】 → 【可选报告/导出】
|
||||
```
|
||||
|
||||
- **输入**:……
|
||||
- **输出**:……
|
||||
- **说明**:边界(教学/正式、是否接外站等)
|
||||
|
||||
---
|
||||
|
||||
@@ -14,43 +35,105 @@
|
||||
|
||||
确认:
|
||||
|
||||
1. 已安装并登录匠厂客户端
|
||||
2. 已在技能市场安装本技能(或更新到最新版)
|
||||
3. 【网页自动化类技能】首次运行一般会自动登记本平台账号;也可在「账号管理」中指定登录标识。浏览器会话由系统隔离管理——**不要**用本机日常浏览器用户目录去跑真实站
|
||||
4. 【按平台】若本技能说明「需要先登录目标站」,请在弹出的有头浏览器中按提示登录或扫码;「登记账号」不等于「已登录目标站」。若说明「可不登录也能用」,仍须有可用的账号会话目录
|
||||
5. 【网页自动化类技能】已登记账号可在「数据管理 / 账号管理」查看;每个账号都有独立浏览器会话
|
||||
6. 【如需】需要的文件 / 时间范围 / 单号等已准备好
|
||||
1. 已安装并登录匠厂客户端
|
||||
2. 已在技能市场安装本技能(建议更新到最新版)
|
||||
3. 【网页自动化类】账号登记 / 目标站登录要求(按本技能;登记 ≠ 已登录)
|
||||
4. 【如需】文件 / 时间范围 / 单号等已准备好
|
||||
5. 【如需】首次须先完成「载入样例 / 导入 / 同步」等准备动作(见入口一或入口二)
|
||||
|
||||
---
|
||||
|
||||
## 第一步:打开技能并说清目标
|
||||
## 入口一:数据管理
|
||||
|
||||
在匠厂里新建任务或打开对话,用自然语言说明要做什么,例如:
|
||||
> 若本技能无业务表、无 toolbar/row/batch:整节改为「不适用」+ 理由。
|
||||
|
||||
> “帮我【完成某件业务事】,范围是【时间 / 对象 / 条件】。”
|
||||
适合:对照数据、喜欢点界面、机房/运营用户。
|
||||
|
||||
建议一次只交代一件事;范围说清楚,减少来回确认。
|
||||
### 左侧各表是干什么的
|
||||
|
||||
| 表名(中文) | 作用 |
|
||||
|--------------|------|
|
||||
| 【表 A】 | …… |
|
||||
| 【表 B】 | …… |
|
||||
|
||||
### 首次准备(若需要)
|
||||
|
||||
1. 进入 **数据管理** → **【主表】**
|
||||
2. 点击表顶 **「【准备类按钮 label】」**(若为空表可点,写明是否须选中行)
|
||||
3. 成功后应看到:……
|
||||
|
||||
> **注意:** 准备/载入/导入类按钮须挂在数据管理 **toolbar**(用户看得见)。不要把「技能详情页点按钮」写成主路径——详情页常只显示四 Tab,不一定渲染 Action 按钮。
|
||||
|
||||
### 主流程
|
||||
|
||||
1. 进入 **【主表】**
|
||||
2. 【是否选中行】
|
||||
3. 点击 **「【主能力 label】」**
|
||||
4. 到 **【结果表 / 任务中心 / 报告】** 核对结果
|
||||
|
||||
### 行级 / 批量(若有)
|
||||
|
||||
| 你想做什么 | 操作 |
|
||||
|------------|------|
|
||||
| …… | **【表】** 行内 → **「【label】」** |
|
||||
| …… | 勾选多行 → **「【label】」** |
|
||||
|
||||
---
|
||||
|
||||
## 第二步:按提示补充信息
|
||||
## 入口二:对话任务
|
||||
|
||||
如果 Agent 问你缺什么(账号、文件、筛选条件),按提示补充即可。
|
||||
> 若本技能不挂 `agent`:整节改为「不适用」+ 理由。
|
||||
|
||||
- 有文件时:按界面提示选择或拖入
|
||||
- 有账号时:确认已在对应平台登录,且具备操作权限
|
||||
适合:临时提问、要解释、不想切换表。
|
||||
|
||||
### 什么时候用对话
|
||||
|
||||
- ……
|
||||
- 与入口一效果相同;习惯看表时优先用数据管理
|
||||
|
||||
### 示例话术
|
||||
|
||||
| 你想做什么 | 可以这样说 |
|
||||
|------------|------------|
|
||||
| …… | 「……」 |
|
||||
| …… | 「……」 |
|
||||
|
||||
### Agent 可能追问什么
|
||||
|
||||
- 缺参数时:……
|
||||
- 失败时:可改用数据管理点按钮
|
||||
|
||||
---
|
||||
|
||||
## 第三步:等待结果并核对
|
||||
## 入口三:定时任务
|
||||
|
||||
任务结束后,请核对:
|
||||
> **有 `cron`:** 写如何新建定时、选哪个 Action、默认参数、预期结果。
|
||||
> **无 `cron`:** 用下面「不需要」模板,删掉「如何配置」小节。
|
||||
|
||||
- 是否成功完成
|
||||
- 处理了多少条 / 哪些失败
|
||||
- 生成的文件、表格或记录是否在预期位置
|
||||
### 本技能不需要定时时(示例文案,按需保留/改写)
|
||||
|
||||
若失败,先看失败原因说明,再按下一节排查。
|
||||
本技能**未提供定时任务入口**,原因:【一次性 / 须人指定对象 / 须当场确认 / ……】。
|
||||
请使用 **数据管理** 或 **对话任务**。
|
||||
|
||||
### 本技能需要定时时(示例结构)
|
||||
|
||||
1. 打开侧栏 **定时任务** → 新建
|
||||
2. 选择本技能 → Action **「【label】」**
|
||||
3. 填写参数 → 保存;可「立即运行」验证
|
||||
4. 到点后在【结果表 / 任务中心】查看
|
||||
|
||||
---
|
||||
|
||||
## 主能力对照表
|
||||
|
||||
同一能力,两种主入口(按本技能实际填写;只挂一端的注明):
|
||||
|
||||
| 想做什么 | 数据管理 | 对话任务 |
|
||||
|----------|----------|----------|
|
||||
| …… | **【表】** → **「【label】」** | 「……」 |
|
||||
| …… | …… | …… |
|
||||
|
||||
按钮名必须与 `assets/actions.json` 的 `label` 一致。
|
||||
|
||||
---
|
||||
|
||||
@@ -58,10 +141,10 @@
|
||||
|
||||
| 现象 | 你可以先试 |
|
||||
|------|------------|
|
||||
| 提示未登录 / 无权限 | 确认匠厂已登录;业务账号是否在线、权限是否足够 |
|
||||
| 提示账号不存在 / 被占用 | 到「数据管理 / 账号管理」确认账号记录和状态,稍后再试;不要重复创建账号 |
|
||||
| 提示缺少必要信息 | 把时间范围、单号、文件等说完整后再试一次 |
|
||||
| 一直没有结果 | 到任务中心查看进度或失败原因;必要时重新发起 |
|
||||
| 结果不对 | 用更明确的条件再说一次,或缩小范围重试 |
|
||||
| 主表是空的 | 先做「首次准备」或对话载入/导入 |
|
||||
| 按钮点不了 | 是否须先选中行;是否装错版本 |
|
||||
| 一直没有结果 | 看任务中心 / 运行日志;async 任务看进度 |
|
||||
| 【网页自动化】未登录 / 无权限 | 确认账号在线与目标站登录状态 |
|
||||
| 结果不对 | 缩小范围重试,或改用数据管理核对表数据 |
|
||||
|
||||
仍无法解决时,把「你说了什么 + 界面提示原文」发给支持人员,便于排查。
|
||||
仍无法解决时,把「你用的入口 + 操作步骤 + 界面提示原文」发给支持人员。
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
# assets
|
||||
|
||||
- `actions.json`:Skill Action manifest 最小示例(**可选**;不需要宿主入口时可删除)。每个 action **必须**显式声明 `executionProfile`(`sync` \| `async`)。默认仅 `health` / `version` / `config-path`,不放 toolbar,故无 `bind` 示例。
|
||||
- `examples/`:CLI 成功输出形状示例(虚构路径与数据)。
|
||||
- `schemas/`:轻量 JSON Schema(`skill-actions.schema.json` 为**新技能严格规范**;`task-log-record.schema.json` 等)。
|
||||
- `actions.json`:Skill Action manifest 最小示例(按入口定位表决定是否保留/扩展;见 `development/PRODUCT_SURFACES.md`)。每个 action **必须**显式声明 `executionProfile`(`sync` \| `async`)。默认仅 `health` / `version` / `config-path`,不放 toolbar,故无 `bind` 示例。
|
||||
- **业务技能注意:** 若主路径在数据管理,准备/载入/导入类 Action 应挂 `toolbar` + `bind.tables`(用户看得见),**不要**只挂 `skill-detail` 却在教程里写成「技能详情点按钮」。见 `development/TUTORIAL_SURFACES.md` §5、`references/ACTIONS.md`「用户可见性」。
|
||||
- `html/example-report.html`:HTML 快照页最小壳(`{{TITLE}}` / `{{GENERATED_AT}}` / `{{BODY}}`);含数据管理 iframe 预览布局基线(流体铺满、禁横向滚动);渲染后须写入数据目录 `html/`。不要放进 `pages/`。
|
||||
- `html/pages/*.html`:装完就出现在侧栏的壳子页(宿主直接读安装包,不经生成)。页面用 `jiangchang.call` 调本技能已声明的命令。见 `references/ACTIONS.md`「HTML 页面报告」。模板仓库不要在这里放示例页。
|
||||
- `data-sources.example.json`:声明式数据源默认示例(SQLite `{skillSlug}.db`);宿主安装时可复制到用户目录。MySQL 参考见 `examples/data-sources.mysql.example.json`。
|
||||
- `examples/`:CLI 成功输出形状示例、Action 形状参考(含 `data-sources.mysql.example.json`、`path-root-file-action.example.json`、`selection-label-column.action.example.json`、`selection-mode-optional.action.example.json`、`render-html.action.example.json` 等;虚构路径与数据)。
|
||||
- `schemas/`:轻量 JSON Schema(`skill-actions.schema.json` 为**新技能严格规范**)。
|
||||
|
||||
`skill-actions.schema.json` 使用 `additionalProperties: false`。正式支持 `bind.tables`(toolbar Action 必填)与可选 `bind.inputMapping`;placements 含 `toolbar` / `row` / `batch` / `cron` / `agent` / `skill-detail`(对齐匠厂宿主)。`concurrency`/`locks` 仍为预留。`placements` 与 `executionProfile` 正交——进任务中心只看 `async`,与入口无关。表级 Action 与 Agent 契约见 [`references/ACTIONS.md`](../references/ACTIONS.md);开发深度规范见 [`development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md)。
|
||||
`skill-actions.schema.json` 使用 `additionalProperties: false`。正式支持 `bind.tables`(toolbar Action 必填)与可选 `bind.inputMapping` / `columnMapping` / **`labelColumn`**(选择摘要标签列)/ **`selectionMode`**(`required` \| `none` \| `optional`:表勾选如何驱动 Action;省略则宿主兼容推断)/ **`visibleWhen`**(行菜单/行按钮按当前行列值显隐)。Action Lookup 可选 **`presentation: "paged_table"`** + **`page_size`**(分页勾选小表,仍是 `display_type: lookup`)。placements 含 `toolbar` / `row` / `batch` / `cron` / `agent` / `skill-detail`(对齐匠厂宿主)。数据管理 confirm/form 弹层在会应用勾选时展示「已选择」摘要;**form + 有摘要时映射 `readOnly` 默认不占参数区(仍提交)**,可选 **`revealInForm`** 强制显示。弹窗 `description` / 确认黄条 / 参数说明须**用户可见中文**(勿写 `pickCount`/`async`/`pending`/CLI 子命令;见 ACTIONS.md「用户可见文案」)。可选 `hoverHint` 为数据管理工具栏悬停(省略不显示,不回退)。`concurrency`/`locks` 仍为预留。`placements` 与 `executionProfile` 正交——进任务中心只看 `async`,与入口无关。表级 Action 与 Agent 契约见 [`references/ACTIONS.md`](../references/ACTIONS.md);开发深度规范见 [`development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md)。
|
||||
|
||||
- 用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.md)
|
||||
- 用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.md);写法契约见 [`development/TUTORIAL_SURFACES.md`](../development/TUTORIAL_SURFACES.md)
|
||||
- Agent 调用/编排参考见 [`references/`](../references/)(含 [`ACTIONS.md`](../references/ACTIONS.md))
|
||||
- 开发规范见 [`development/`](../development/)
|
||||
|
||||
11
assets/data-sources.example.json
Normal file
11
assets/data-sources.example.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": 1,
|
||||
"sources": [
|
||||
{
|
||||
"id": "primary",
|
||||
"kind": "sqlite",
|
||||
"displayName": "本地库",
|
||||
"path": "{skillSlug}.db"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,17 @@
|
||||
# 示例 JSON
|
||||
|
||||
- `data-sources.mysql.example.json`:MySQL 云库 `data-sources.json` 形状参考(复制到用户数据目录并配 `.env` 中 `MYSQL_DSN`;见 `references/DATA_SOURCES.md`)。
|
||||
- `version-response.json`:`python main.py version` 成功时 stdout 单行对象的形状参考。
|
||||
- `log-get-response.json`:若技能有日志表,`log-get` 成功时单条对象的形状参考。
|
||||
- `skill-policy-snapshot.*.example.json`:Data Policy Snapshot 形状参考(见 `references/POLICY.md`)。
|
||||
- `skill-cell-presentation.example.json`:数据管理单元格呈现文档形状参考(见 `references/CELL_PRESENTATION.md`)。
|
||||
- `skill-grid-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(**local_sql**)形状参考(单选 + `multiple: true`;同库跨表勿填编号;见 `references/ACTIONS.md`「跨表引用:Lookup」)。
|
||||
- `lookup-skill-cli.action.example.json`:Action Lookup **`skill_cli`** 形状参考(打开表单时宿主调兄弟技能 CLI 抽 JSON 选项;跨技能账号列表勿再造本库缓存表;见同节)。
|
||||
- `lookup-paged-table.action.example.json`:Lookup **分页勾选小表** `presentation: "paged_table"` 形状参考(仍是 `display_type: lookup`;见 `references/ACTIONS.md`「分页勾选小表」)。
|
||||
- `year-month-action.example.json`:Action / CRUD 账期月份字段 `display_type: year_month` 形状参考(见 `references/ACTIONS.md`「表单字段类型对照」)。
|
||||
- `visible-when-action.example.json`:Action 参数字段条件显隐 `visibleWhen` 形状参考(互斥模式联动;见 `references/ACTIONS.md`「字段条件显隐」;≠ `bind.visibleWhen`,≠ Policy L3)。
|
||||
- `bind-visible-when-row.action.example.json`:行菜单显隐 `bind.visibleWhen` 形状参考(按当前行状态显示/隐藏行按钮;见 `references/ACTIONS.md`「行菜单显隐」)。
|
||||
- `selection-label-column.action.example.json`:选择摘要 `bind.labelColumn` 形状参考(见 `references/ACTIONS.md`「选择摘要」)。
|
||||
- `selection-mode-optional.action.example.json`:`bind.selectionMode: "optional"` 智能勾选/按数量合并形状参考(含 `cron`:配置页按无勾选分支,只见数量字段);用户可见文案为口语中文(见 `references/ACTIONS.md`「勾选驱动模式」「用户可见文案」)。
|
||||
- `replaces-create.action.example.json` / `replaces-update.action.example.json` / `replaces-delete.action.example.json` / `replaces-import.action.example.json`:数据管理默认按钮接管(`replaces`)形状参考;**import 时 UI 仍用宿主「导入数据」面板**,确认后调技能写库(见 `references/ACTIONS.md`「`replaces`」)。
|
||||
|
||||
39
assets/examples/bind-visible-when-row.action.example.json
Normal file
39
assets/examples/bind-visible-when-row.action.example.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"id": "approve-item",
|
||||
"label": "采用",
|
||||
"description": "把当前记录改为已采用。仅待确认或已跳过的行显示此按钮。",
|
||||
"placements": ["row", "agent"],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "direct",
|
||||
"bind": {
|
||||
"tables": ["business_items"],
|
||||
"visibleWhen": {
|
||||
"field": "status",
|
||||
"in": ["pending_review", "skipped"]
|
||||
},
|
||||
"labelColumn": "display_name",
|
||||
"inputMapping": {
|
||||
"itemId": "$row.$pk"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["itemId"],
|
||||
"properties": {
|
||||
"itemId": {
|
||||
"type": "number",
|
||||
"title": "编号",
|
||||
"description": "行内触发时自动填入,一般无需修改",
|
||||
"readOnly": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "approve",
|
||||
"args": ["--id", "{{itemId}}"]
|
||||
},
|
||||
"hostEffects": {
|
||||
"onSuccess": ["reloadTable"]
|
||||
}
|
||||
}
|
||||
11
assets/examples/data-sources.mysql.example.json
Normal file
11
assets/examples/data-sources.mysql.example.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": 1,
|
||||
"sources": [
|
||||
{
|
||||
"id": "primary",
|
||||
"kind": "mysql",
|
||||
"displayName": "业务云库",
|
||||
"connectionEnv": "MYSQL_DSN"
|
||||
}
|
||||
]
|
||||
}
|
||||
51
assets/examples/enum-names-action.example.json
Normal file
51
assets/examples/enum-names-action.example.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"_comment": "形状参考:Action 静态枚举用 enum + enumNames 显示中文;提交仍是 enum value。有对应表列时优先 column 绑列复用 options_json。完整契约见 references/ACTIONS.md「静态枚举 label」。",
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actions": [
|
||||
{
|
||||
"id": "create-ticket",
|
||||
"label": "新建工单(示例)",
|
||||
"description": "演示 enumNames:界面显示中文,CLI 仍收到英文枚举值。",
|
||||
"placements": ["toolbar", "agent"],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "form",
|
||||
"bind": {
|
||||
"tables": ["tickets"]
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "ticket-create",
|
||||
"args": [
|
||||
"--category", "{{category}}",
|
||||
"--priority", "{{priority}}",
|
||||
"--title", "{{title}}"
|
||||
]
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["title", "category"],
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"title": "标题"
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"title": "类型",
|
||||
"column": "category",
|
||||
"enum": ["incident", "request", "consult", "other"],
|
||||
"enumNames": ["故障", "需求", "咨询", "其他"]
|
||||
},
|
||||
"priority": {
|
||||
"type": "string",
|
||||
"title": "优先级",
|
||||
"enum": ["low", "medium", "high", "urgent"],
|
||||
"enumNames": ["低", "中", "高", "紧急"],
|
||||
"description": "无对应列时仅靠 enumNames 展示中文"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"id": 1,
|
||||
"task_type": "demo_task",
|
||||
"target_id": "demo_target_1",
|
||||
"input_id": "demo_input_42",
|
||||
"input_title": "示例任务输入标题",
|
||||
"status": "success",
|
||||
"error_msg": null,
|
||||
"result_summary": "{\"processed\": 10, \"skipped\": 0}",
|
||||
"created_at": "2026-04-01T10:00:00",
|
||||
"updated_at": "2026-04-01T10:00:00"
|
||||
}
|
||||
83
assets/examples/lookup-action.example.json
Normal file
83
assets/examples/lookup-action.example.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"_comment": "形状参考:Action 参数引用同库另一张表时用 Lookup(local_sql)。单选与多选同一 lookup 对象,多选只加 multiple:true。跨技能列表见 lookup-skill-cli.action.example.json。完整契约见 references/ACTIONS.md「跨表引用:Lookup」。",
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actions": [
|
||||
{
|
||||
"id": "send-with-template",
|
||||
"label": "按模板发送(示例)",
|
||||
"description": "演示内联 lookup 单选:从 message_templates 选模板;-- 表示自动挑选。",
|
||||
"placements": ["toolbar"],
|
||||
"executionProfile": "async",
|
||||
"interaction": "form",
|
||||
"bind": {
|
||||
"tables": ["leads"]
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "engage",
|
||||
"args": ["--template-id", "{{templateId}}", "--content", "{{content}}"]
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string",
|
||||
"title": "正文",
|
||||
"description": "填了就按这里发;留空则用下面的话术模板。",
|
||||
"display_type": "textarea"
|
||||
},
|
||||
"templateId": {
|
||||
"type": "number",
|
||||
"title": "话术模板",
|
||||
"description": "仅在正文留空时生效。选 -- = 自动挑选。",
|
||||
"display_type": "lookup",
|
||||
"lookup": {
|
||||
"ref_table": "message_templates",
|
||||
"value_column": "id",
|
||||
"label_column": "name",
|
||||
"extra_column": "channel",
|
||||
"filter_column": "is_active",
|
||||
"filter_value": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "probe-with-engines",
|
||||
"label": "按引擎抽检(多选示例)",
|
||||
"description": "演示 lookup.multiple:从 engines 多选,提交为逗号分隔 id。",
|
||||
"placements": ["toolbar"],
|
||||
"executionProfile": "async",
|
||||
"interaction": "form",
|
||||
"bind": {
|
||||
"tables": ["queries"]
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "query",
|
||||
"args": ["batch-probe", "--engine-ids", "{{engineIds}}"]
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"engineIds": {
|
||||
"type": "string",
|
||||
"title": "将使用的引擎",
|
||||
"description": "可多选;提交值为逗号分隔编号,如 1,2。",
|
||||
"display_type": "lookup",
|
||||
"lookup": {
|
||||
"ref_table": "engines",
|
||||
"value_column": "id",
|
||||
"label_column": "display_name",
|
||||
"filter_column": "enabled",
|
||||
"filter_value": 1,
|
||||
"multiple": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
47
assets/examples/lookup-paged-table.action.example.json
Normal file
47
assets/examples/lookup-paged-table.action.example.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"_comment": "形状参考:Action Lookup 分页勾选小表。仍是 display_type=lookup,加 presentation=paged_table。完整契约见 references/ACTIONS.md「分页勾选小表」。不要新造 display_type。skill_cli 的 stdout 须为一行 {items,total};0 条也成功退出。列表命令不要在 main.py 入口加载整包 jiangchang_skill_core 或 Playwright。",
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actions": [
|
||||
{
|
||||
"id": "import-picked",
|
||||
"label": "导入文章",
|
||||
"description": "从已生成文章里勾选要导入的,可翻页。",
|
||||
"placements": ["toolbar"],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "form",
|
||||
"bind": {
|
||||
"tables": ["articles"],
|
||||
"selectionMode": "none"
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "import-articles",
|
||||
"args": ["--ids", "{{articleIds}}"]
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["articleIds"],
|
||||
"properties": {
|
||||
"articleIds": {
|
||||
"type": "string",
|
||||
"title": "选择文章",
|
||||
"description": "勾选要导入的文章。可翻页,可本页全选。",
|
||||
"display_type": "lookup",
|
||||
"lookup": {
|
||||
"source": "skill_cli",
|
||||
"skill": "geo-generate-article-all",
|
||||
"command": "title",
|
||||
"args": ["list-generated-json", "--platform", "toutiaohao"],
|
||||
"value_column": "id",
|
||||
"label_column": "title",
|
||||
"multiple": true,
|
||||
"presentation": "paged_table",
|
||||
"page_size": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
51
assets/examples/lookup-skill-cli.action.example.json
Normal file
51
assets/examples/lookup-skill-cli.action.example.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"_comment": "形状参考:Action Lookup 的 skill_cli 源——打开表单时宿主 sync 调用兄弟技能 CLI,从 stdout JSON 抽选项。完整契约见 references/ACTIONS.md「跨表引用:Lookup」。本例对标 account-manager 遗留 list-json(stdout 为账号数组,字段 id / account_label);其它依赖技能请核对自己的命令与 JSON 形状。",
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actions": [
|
||||
{
|
||||
"id": "publish-with-account",
|
||||
"label": "发布(示例)",
|
||||
"description": "演示跨技能选账号:skill_cli 调 account-manager,勿在本技能维护账号缓存表。",
|
||||
"placements": ["toolbar", "cron"],
|
||||
"executionProfile": "async",
|
||||
"interaction": "form",
|
||||
"bind": {
|
||||
"tables": ["articles"],
|
||||
"selectionMode": "none"
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "run",
|
||||
"args": ["--account-id", "{{accountId}}", "--pick", "{{pickCount}}"]
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["pickCount"],
|
||||
"properties": {
|
||||
"pickCount": {
|
||||
"type": "number",
|
||||
"title": "发布数量",
|
||||
"description": "从未发布队列领取的条数",
|
||||
"default": 1,
|
||||
"minimum": 1
|
||||
},
|
||||
"accountId": {
|
||||
"type": "string",
|
||||
"title": "发布账号",
|
||||
"description": "不选则由系统自动分配;选定后本次全部使用该号。",
|
||||
"display_type": "lookup",
|
||||
"lookup": {
|
||||
"source": "skill_cli",
|
||||
"skill": "account-manager",
|
||||
"command": "list-json",
|
||||
"args": ["toutiao"],
|
||||
"value_column": "id",
|
||||
"label_column": "account_label"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
36
assets/examples/path-root-file-action.example.json
Normal file
36
assets/examples/path-root-file-action.example.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"_comment": "形状参考:Action 表单 file/directory 用 pathRoot 指定原生对话框起始目录(相对技能数据根)。省略则宿主默认 {skill_data}/input。完整契约见 references/ACTIONS.md「本地路径选择与 pathRoot」。勿与宿主导入面板(replaces:import)混用本示例。",
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actions": [
|
||||
{
|
||||
"id": "import-pack",
|
||||
"label": "导入数据包",
|
||||
"description": "从本技能输入目录子文件夹选择 JSON 并写入业务表。",
|
||||
"placements": ["toolbar", "agent"],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "form",
|
||||
"bind": { "tables": ["business_items"] },
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["file"],
|
||||
"properties": {
|
||||
"file": {
|
||||
"type": "string",
|
||||
"title": "数据包文件",
|
||||
"description": "请选择 input/数据包/ 下的 JSON。",
|
||||
"format": "file",
|
||||
"display_type": "file",
|
||||
"pathRoot": "input/数据包"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "import-pack",
|
||||
"args": ["--file", "{{file}}"]
|
||||
},
|
||||
"steps": [{ "when": "afterSuccess", "do": "reloadTable" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
28
assets/examples/render-html.action.example.json
Normal file
28
assets/examples/render-html.action.example.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"id": "render-html",
|
||||
"label": "生成 HTML",
|
||||
"description": "根据业务数据生成 HTML 报告,写入技能数据目录 html/(示例:请改 bind.tables / entrypoint)。",
|
||||
"placements": [
|
||||
"toolbar",
|
||||
"skill-detail",
|
||||
"agent"
|
||||
],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "direct",
|
||||
"bind": {
|
||||
"tables": [
|
||||
"your_source_table"
|
||||
]
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "render-html",
|
||||
"args": []
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"when": "afterSuccess",
|
||||
"do": "refreshCatalog"
|
||||
}
|
||||
]
|
||||
}
|
||||
28
assets/examples/replaces-create.action.example.json
Normal file
28
assets/examples/replaces-create.action.example.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"id": "item-create",
|
||||
"label": "新增记录",
|
||||
"description": "新增业务记录并执行领域副作用(走技能 CLI,非宿主直写库)。",
|
||||
"replaces": "create",
|
||||
"placements": [],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "form",
|
||||
"bind": {
|
||||
"tables": ["items"]
|
||||
},
|
||||
"hostEffects": {
|
||||
"onSuccess": ["reloadTable"]
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["name"],
|
||||
"properties": {
|
||||
"name": { "type": "string", "title": "名称" },
|
||||
"note": { "type": "string", "title": "备注" }
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "item",
|
||||
"args": ["create", "--name", "{{name}}", "--note", "{{note}}"]
|
||||
}
|
||||
}
|
||||
33
assets/examples/replaces-delete.action.example.json
Normal file
33
assets/examples/replaces-delete.action.example.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"id": "item-delete",
|
||||
"label": "删除记录",
|
||||
"description": "删除业务记录并清理关联副作用。",
|
||||
"replaces": "delete",
|
||||
"placements": [],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "confirm",
|
||||
"confirmation": {
|
||||
"message": "确定删除该记录?关联数据将一并清理。"
|
||||
},
|
||||
"bind": {
|
||||
"tables": ["items"],
|
||||
"inputMapping": {
|
||||
"id": "$row.$pk"
|
||||
}
|
||||
},
|
||||
"hostEffects": {
|
||||
"onSuccess": ["reloadTable", "clearSelection"]
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["id"],
|
||||
"properties": {
|
||||
"id": { "type": "number", "title": "编号", "readOnly": true }
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "item",
|
||||
"args": ["delete", "--id", "{{id}}"]
|
||||
}
|
||||
}
|
||||
38
assets/examples/replaces-import.action.example.json
Normal file
38
assets/examples/replaces-import.action.example.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "item-import",
|
||||
"label": "导入记录",
|
||||
"description": "由宿主「导入数据」面板调用:解析 Excel/CSV 并写入业务表(含领域校验与派生字段)。",
|
||||
"replaces": "import",
|
||||
"placements": [],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "form",
|
||||
"bind": {
|
||||
"tables": ["items"]
|
||||
},
|
||||
"hostEffects": {
|
||||
"onSuccess": ["reloadTable"]
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["filePath"],
|
||||
"properties": {
|
||||
"filePath": {
|
||||
"type": "string",
|
||||
"title": "导入文件",
|
||||
"description": "由宿主导入面板传入的 Excel/CSV 路径。",
|
||||
"format": "file"
|
||||
},
|
||||
"onError": {
|
||||
"type": "string",
|
||||
"title": "导入规则(出错时)",
|
||||
"enum": ["跳过该行,继续导入", "立即停止,不再导入后续行"],
|
||||
"default": "跳过该行,继续导入"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "item",
|
||||
"args": ["import", "--file", "{{filePath}}", "--on-error", "{{onError}}"]
|
||||
}
|
||||
}
|
||||
34
assets/examples/replaces-update.action.example.json
Normal file
34
assets/examples/replaces-update.action.example.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "item-update",
|
||||
"label": "编辑记录",
|
||||
"description": "编辑业务记录并重算派生字段。",
|
||||
"replaces": "update",
|
||||
"placements": [],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "form",
|
||||
"bind": {
|
||||
"tables": ["items"],
|
||||
"inputMapping": {
|
||||
"id": "$row.$pk",
|
||||
"name": "$row.name",
|
||||
"note": "$row.note"
|
||||
}
|
||||
},
|
||||
"hostEffects": {
|
||||
"onSuccess": ["reloadTable"]
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["id", "name"],
|
||||
"properties": {
|
||||
"id": { "type": "number", "title": "编号", "readOnly": true },
|
||||
"name": { "type": "string", "title": "名称" },
|
||||
"note": { "type": "string", "title": "备注" }
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "item",
|
||||
"args": ["update", "--id", "{{id}}", "--name", "{{name}}", "--note", "{{note}}"]
|
||||
}
|
||||
}
|
||||
38
assets/examples/selection-label-column.action.example.json
Normal file
38
assets/examples/selection-label-column.action.example.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "process-selected",
|
||||
"label": "处理勾选",
|
||||
"description": "对勾选行执行业务处理。示例:声明 labelColumn,让确认弹层「已选择」摘要显示可读名称。",
|
||||
"placements": ["toolbar", "batch"],
|
||||
"executionProfile": "async",
|
||||
"interaction": "confirm",
|
||||
"confirmation": {
|
||||
"message": "将按技能业务规则处理勾选记录(具体是否跳过某些状态由 CLI 决定)。"
|
||||
},
|
||||
"bind": {
|
||||
"tables": ["business_items"],
|
||||
"inputMapping": {
|
||||
"ids": "$selection.ids"
|
||||
},
|
||||
"labelColumn": "display_name"
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["ids"],
|
||||
"properties": {
|
||||
"ids": {
|
||||
"type": "string",
|
||||
"title": "编号列表",
|
||||
"description": "逗号分隔主键;宿主会预填勾选行",
|
||||
"readOnly": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "process",
|
||||
"args": ["--ids", "{{ids}}"]
|
||||
},
|
||||
"hostEffects": {
|
||||
"onSuccess": ["reloadTable"]
|
||||
}
|
||||
}
|
||||
46
assets/examples/selection-mode-optional.action.example.json
Normal file
46
assets/examples/selection-mode-optional.action.example.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"id": "process-queue",
|
||||
"label": "处理队列",
|
||||
"description": "从业务队列处理待处理记录。有勾选则处理勾选的;没有勾选则按下方数量从队列领取。已成功或失败的会自动跳过。进度可在任务中心查看。",
|
||||
"placements": ["toolbar", "batch", "cron", "agent"],
|
||||
"executionProfile": "async",
|
||||
"interaction": "form",
|
||||
"confirmation": {
|
||||
"message": "将按规则处理记录(有勾选则处理勾选的;未勾选则按数量从队列领取),是否继续?"
|
||||
},
|
||||
"bind": {
|
||||
"tables": ["business_items"],
|
||||
"selectionMode": "optional",
|
||||
"labelColumn": "display_name",
|
||||
"inputMapping": {
|
||||
"itemIds": "$selection.ids"
|
||||
}
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["itemIds", "pickCount"],
|
||||
"properties": {
|
||||
"itemIds": {
|
||||
"type": "string",
|
||||
"title": "编号列表",
|
||||
"description": "有勾选时自动填入,一般无需修改",
|
||||
"readOnly": true
|
||||
},
|
||||
"pickCount": {
|
||||
"type": "number",
|
||||
"title": "处理数量",
|
||||
"description": "没有勾选时:从待处理队列里领取的条数",
|
||||
"default": 1,
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "process",
|
||||
"args": ["--ids", "{{itemIds}}", "--pick", "{{pickCount}}"]
|
||||
},
|
||||
"hostEffects": {
|
||||
"onSuccess": ["reloadTable", "clearSelection"]
|
||||
}
|
||||
}
|
||||
29
assets/examples/skill-cell-presentation.example.json
Normal file
29
assets/examples/skill-cell-presentation.example.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"table": "business_records",
|
||||
"rowKey": "id",
|
||||
"generatedAt": "2026-08-10T06:00:00Z",
|
||||
"cells": [
|
||||
{
|
||||
"rowId": "42",
|
||||
"column": "qty",
|
||||
"tone": "danger",
|
||||
"message": "库存低于安全线",
|
||||
"alert": true
|
||||
},
|
||||
{
|
||||
"rowId": "42",
|
||||
"column": "status",
|
||||
"tone": "warning",
|
||||
"message": "待审核"
|
||||
},
|
||||
{
|
||||
"rowId": "99",
|
||||
"column": "print_done_at",
|
||||
"tone": "danger",
|
||||
"message": "环节到期未填",
|
||||
"alert": true
|
||||
}
|
||||
]
|
||||
}
|
||||
32
assets/examples/skill-grid-presentation.example.json
Normal file
32
assets/examples/skill-grid-presentation.example.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"table": "business_records",
|
||||
"rowKey": "id",
|
||||
"generatedAt": "2026-08-10T06:00:00Z",
|
||||
"rules": [
|
||||
{
|
||||
"ruleKey": "qty-low-rows",
|
||||
"rows": ["42", "99"],
|
||||
"columns": "all",
|
||||
"tone": "danger",
|
||||
"message": "库存低于安全线",
|
||||
"alert": true
|
||||
},
|
||||
{
|
||||
"ruleKey": "status-column",
|
||||
"rows": "all",
|
||||
"columns": ["status"],
|
||||
"tone": "warning",
|
||||
"message": "待审核"
|
||||
},
|
||||
{
|
||||
"ruleKey": "overdue-print-region",
|
||||
"rows": ["7", "8"],
|
||||
"columns": ["print_done_at", "ship_done_at"],
|
||||
"tone": "danger",
|
||||
"message": "环节到期未填",
|
||||
"alert": true
|
||||
}
|
||||
]
|
||||
}
|
||||
13
assets/examples/skill-policy-snapshot.allowlist.example.json
Normal file
13
assets/examples/skill-policy-snapshot.allowlist.example.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actor": {
|
||||
"userId": "u1",
|
||||
"displayName": "张三",
|
||||
"roles": ["clerk"]
|
||||
},
|
||||
"actions": {
|
||||
"mode": "allowlist",
|
||||
"ids": ["whoami", "create-record", "sync-records"]
|
||||
}
|
||||
}
|
||||
28
assets/examples/skill-policy-snapshot.scope.example.json
Normal file
28
assets/examples/skill-policy-snapshot.scope.example.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actor": {
|
||||
"userId": "u1"
|
||||
},
|
||||
"actions": {
|
||||
"mode": "open"
|
||||
},
|
||||
"data": {
|
||||
"tables": {
|
||||
"orders": {
|
||||
"rows": {
|
||||
"mode": "scope",
|
||||
"scope": {
|
||||
"all": [
|
||||
{
|
||||
"field": "owner_user_id",
|
||||
"op": "eq",
|
||||
"valueFrom": "actor.userId"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.92",
|
||||
"skill": "your-skill-slug"
|
||||
}
|
||||
|
||||
72
assets/examples/visible-when-action.example.json
Normal file
72
assets/examples/visible-when-action.example.json
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"_comment": "形状参考:互斥模式用 inputSchema.properties.*.visibleWhen 联动显隐,勿只靠 description 灰字。完整契约见 references/ACTIONS.md「字段条件显隐」。勿与 POLICY.md L3 行态 visibleWhen 混淆。",
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actions": [
|
||||
{
|
||||
"id": "scrape-links-demo",
|
||||
"label": "采集链接(示例)",
|
||||
"description": "演示按采集方式切换参数:关键词路径 vs 作者主页路径。",
|
||||
"placements": ["toolbar"],
|
||||
"executionProfile": "async",
|
||||
"interaction": "form",
|
||||
"bind": {
|
||||
"tables": ["demo_items"]
|
||||
},
|
||||
"confirmation": {
|
||||
"message": "将按所选方式采集链接(示例确认文案)。是否继续?"
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "run",
|
||||
"args": [
|
||||
"--collect-mode",
|
||||
"{{collectMode}}",
|
||||
"--keyword",
|
||||
"{{keyword}}",
|
||||
"--author-url",
|
||||
"{{authorUrl}}",
|
||||
"--max-count",
|
||||
"{{maxCount}}"
|
||||
]
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["collectMode"],
|
||||
"properties": {
|
||||
"collectMode": {
|
||||
"type": "string",
|
||||
"title": "采集方式",
|
||||
"description": "先选方式,下方只出现对应参数。",
|
||||
"enum": ["按关键词采集", "按作者主页采集"],
|
||||
"default": "按关键词采集"
|
||||
},
|
||||
"keyword": {
|
||||
"type": "string",
|
||||
"title": "关键词",
|
||||
"description": "例如 rpa。",
|
||||
"visibleWhen": {
|
||||
"field": "collectMode",
|
||||
"equals": "按关键词采集"
|
||||
}
|
||||
},
|
||||
"authorUrl": {
|
||||
"type": "string",
|
||||
"title": "作者主页链接",
|
||||
"description": "例如 https://example.com/author/...",
|
||||
"display_type": "url",
|
||||
"visibleWhen": {
|
||||
"collectMode": "按作者主页采集"
|
||||
}
|
||||
},
|
||||
"maxCount": {
|
||||
"type": "number",
|
||||
"title": "数量上限",
|
||||
"default": 50,
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
40
assets/examples/year-month-action.example.json
Normal file
40
assets/examples/year-month-action.example.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"_comment": "形状参考:账期/月份字段用 year_month,宿主渲染年月选择器。完整契约见 references/ACTIONS.md「表单字段类型对照」与 references/SCHEMA.md。",
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actions": [
|
||||
{
|
||||
"id": "create-period-job",
|
||||
"label": "新建账期作业(示例)",
|
||||
"description": "演示 year_month 字段:提交值为 YYYY-MM 字符串,不要用 date。",
|
||||
"placements": ["toolbar"],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "form",
|
||||
"bind": {
|
||||
"tables": ["close_jobs"]
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "create-job",
|
||||
"args": ["--period", "{{period}}", "--title", "{{title}}"]
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["period"],
|
||||
"properties": {
|
||||
"period": {
|
||||
"type": "string",
|
||||
"title": "月份",
|
||||
"description": "格式 YYYY-MM,例如 2026-05",
|
||||
"display_type": "year_month"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"title": "标题",
|
||||
"description": "可留空,默认按月份生成"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
119
assets/html/example-report.html
Normal file
119
assets/html/example-report.html
Normal file
@@ -0,0 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{TITLE}}</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f6f8fb;
|
||||
--ink: #0f172a;
|
||||
--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 {
|
||||
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: var(--page-pad-y) var(--page-pad-x) 8px;
|
||||
background: var(--card);
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
header h1 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
header p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
main {
|
||||
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);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
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>
|
||||
<header>
|
||||
<h1>{{TITLE}}</h1>
|
||||
<p>报告生成于 · {{GENERATED_AT}}</p>
|
||||
</header>
|
||||
<main>
|
||||
<div class="card">
|
||||
{{BODY}}
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -11,3 +11,4 @@ lianlian
|
||||
single-window
|
||||
export-rebate
|
||||
etax
|
||||
colipu
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
all
|
||||
batch
|
||||
multi
|
||||
default
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://openclaw.local/skill-template/skill-actions.schema.json",
|
||||
"title": "SkillActionManifest",
|
||||
"description": "匠厂 skill-template 对新技能的严格 Action manifest 规范(schemaVersion 1)。additionalProperties=false 表示模板推荐契约;宿主运行时可能为兼容历史 manifest 接受部分缺省字段,但新技能不应依赖缺省行为。",
|
||||
"description": "匠厂 skill-template 对新技能的严格 Action manifest 规范(schemaVersion 1)。additionalProperties=false 表示模板推荐契约;宿主运行时可能为兼容历史 manifest 接受部分缺省字段,但新技能不应依赖缺省行为。含可选 hostEffects/steps(匠厂宿主 ≥2.8.5)与可选 hoverHint(数据管理工具栏悬停,需宿主已支持该字段)。",
|
||||
"type": "object",
|
||||
"required": ["schemaVersion", "skill", "actions"],
|
||||
"additionalProperties": false,
|
||||
@@ -19,18 +19,119 @@
|
||||
"actions": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/action" }
|
||||
"items": { "$ref": "#/$defs/action" },
|
||||
"description": "Action 列表。同一 placement(toolbar/row/batch)下,宿主按本数组中匹配项的相对顺序展示。toolbar/batch:表顶栏全部直接展示(无「更多操作」溢出)。row:行级入口,宿主可呈现为操作列、行右键和/或行内「⋯」(同一批 Action、同一顺序);不在本字段配置排序、截断或菜单树。调整顺序即调整数组先后。无独立 sortOrder 字段。宿主内置刷新/导出/查看等不参与本顺序。表顶栏宽时左右横排,窄时左右分栏且栏内竖排。"
|
||||
},
|
||||
"policy": {
|
||||
"$ref": "#/$defs/policyDeclaration",
|
||||
"description": "可选。启用 Data Policy(L1/L2)。缺省或不 enabled = L0,宿主不拉 Snapshot。详见 references/POLICY.md。"
|
||||
},
|
||||
"cellPresentation": {
|
||||
"$ref": "#/$defs/cellPresentationDeclaration",
|
||||
"description": "可选。启用数据管理呈现(P1:格子/行/列/区域)。缺省或不 enabled = P0,宿主不读标色。详见 references/CELL_PRESENTATION.md。"
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"cellPresentationDeclaration": {
|
||||
"type": "object",
|
||||
"required": ["enabled"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "仅 true 启用;false 等同 P0。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"policyDeclaration": {
|
||||
"type": "object",
|
||||
"required": ["enabled"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "仅 true 启用;false 等同 L0。"
|
||||
},
|
||||
"entrypoint": {
|
||||
"$ref": "#/$defs/entrypoint",
|
||||
"description": "enabled=true 时必填。推荐 command=policy,args 可含 {{table}}/{{skill}}/{{userId}}。"
|
||||
},
|
||||
"appliesTo": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["data-management"]
|
||||
},
|
||||
"description": "缺省 [\"data-management\"]。本期宿主只实现 data-management。"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": { "enabled": { "const": true } },
|
||||
"required": ["enabled"]
|
||||
},
|
||||
"then": {
|
||||
"required": ["entrypoint"]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"placement": {
|
||||
"type": "string",
|
||||
"enum": ["toolbar", "row", "batch", "cron", "agent", "skill-detail"],
|
||||
"description": "稳定支持 toolbar/row/batch/cron/agent/skill-detail(对齐匠厂宿主)。placements 与 executionProfile 正交,不做条件限制。"
|
||||
"description": "稳定支持 toolbar/row/batch/cron/agent/skill-detail(对齐匠厂宿主)。row=行级入口(操作列/行右键/⋯ 等由宿主呈现,无独立 row-context-menu placement)。placements 与 executionProfile 正交,不做条件限制。"
|
||||
},
|
||||
"mutationKind": {
|
||||
"type": "string",
|
||||
"enum": ["create", "update", "delete", "import"],
|
||||
"description": "可选。接管数据管理默认按钮:create=新增、update=编辑、delete=删除、import=导入。须配合非空 bind.tables;无配置时宿主仍直写已注册数据源(默认 SQLite)。import 时 UI 仍用宿主「导入数据」面板,确认后调用本 Action 写库。"
|
||||
},
|
||||
"scalarArg": {
|
||||
"type": ["string", "number", "boolean"]
|
||||
},
|
||||
"visibleWhenRule": {
|
||||
"description": "条件显隐规则。支持 {field,equals} / {field,in:[...]},或单键简写 {status:\"approved\"} / {status:[\"a\",\"b\"]}。",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["field", "equals"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"field": { "type": "string", "minLength": 1 },
|
||||
"equals": { "type": ["string", "number", "boolean"] }
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["field", "in"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"field": { "type": "string", "minLength": 1 },
|
||||
"in": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "type": ["string", "number", "boolean"] }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"minProperties": 1,
|
||||
"maxProperties": 1,
|
||||
"additionalProperties": {
|
||||
"oneOf": [
|
||||
{ "type": ["string", "number", "boolean"] },
|
||||
{
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "type": ["string", "number", "boolean"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"inputProperty": {
|
||||
"type": "object",
|
||||
"required": ["type"],
|
||||
@@ -46,15 +147,192 @@
|
||||
"default": {},
|
||||
"enum": {
|
||||
"type": "array",
|
||||
"minItems": 1
|
||||
"minItems": 1,
|
||||
"description": "静态枚举取值(提交/存库用)。展示中文时配 enumNames,或绑列复用 options_json;勿用 Lookup 冒充固定小枚举。"
|
||||
},
|
||||
"enumNames": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"description": "可选。与 enum 一一对应的展示文案(JSON Schema / OpenAPI 风格)。缺项或空串时宿主回退为对应 enum 原文。有对应表列时优先绑列复用 options_json(见 ACTIONS.md「静态枚举 label」)。"
|
||||
},
|
||||
"sensitive": { "type": "boolean" },
|
||||
"readOnly": {
|
||||
"type": "boolean",
|
||||
"description": "宿主数据管理表单只读展示该字段(仍会随提交传入 entrypoint);用于行内/勾选预填的主键等"
|
||||
"description": "只读。有「已选择」且字段来自 inputMapping(或约定 id 字段)时,宿主 form 默认不渲染该控件,预设值仍提交;无摘要时只读展示。勿用 visibleWhen 代替(visibleWhen 隐藏不提交)。"
|
||||
},
|
||||
"revealInForm": {
|
||||
"type": "boolean",
|
||||
"description": "可选逃生阀。true 时即使弹层有「已选择」摘要,也仍渲染该 readOnly 字段。不改变提交 payload。省略=映射只读字段在有摘要时默认折叠。"
|
||||
},
|
||||
"column": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-z][a-z0-9_]*$",
|
||||
"description": "可选。绑定业务表物理列名;Action 弹窗(toolbar/batch)与 replaces create/update 抽屉可复用该列的中文名与 options(静态枚举 options_json 数组,或已解析的 lookup)。"
|
||||
},
|
||||
"display_type": {
|
||||
"type": "string",
|
||||
"enum": ["textarea", "year", "year_month", "month", "date", "datetime_unix_seconds", "url", "email", "lookup", "directory", "file"],
|
||||
"description": "可选。表单控件提示;与 format 二选一即可。year/year_month=年或年月选择器(YYYY / YYYY-MM;month 为 year_month 别名)。lookup=关联下拉(须配 lookup 对象或绑到已有 lookup 列)。file/directory=本地路径选择(可选 pathRoot 指定起始目录,见 ACTIONS.md)。须与 CRUD display_type 语义对齐。"
|
||||
},
|
||||
"format": {
|
||||
"type": "string",
|
||||
"enum": ["textarea", "year", "year_month", "month", "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": "可选。Action 内联 Lookup:local_sql(同库表)/ skill_cli(兄弟技能 CLI JSON)/ external_api(预留)。打开表单时由宿主解析。推荐同时设 display_type=lookup。见 ACTIONS.md「跨表引用:Lookup」。"
|
||||
},
|
||||
"visibleWhen": {
|
||||
"description": "可选。表单字段条件显隐。宿主在数据管理/定时任务参数表单中:规则不满足则隐藏该字段、提交时省略、且不校验 required。Agent/CLI 无 UI 时同样按当前 input 求值后剥离隐藏字段。≠ bind.visibleWhen(行菜单),≠ Policy L3。",
|
||||
"allOf": [{ "$ref": "#/$defs/visibleWhenRule" }]
|
||||
}
|
||||
}
|
||||
},
|
||||
"lookupConfig": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"source": {
|
||||
"type": "string",
|
||||
"enum": ["local_sql", "skill_cli", "external_api"],
|
||||
"description": "可选。显式数据源;省略时:有 ref_table→local_sql,有 skill→skill_cli,有 url→external_api。"
|
||||
},
|
||||
"lookup_source": {
|
||||
"type": "string",
|
||||
"enum": ["local_sql", "skill_cli", "external_api"],
|
||||
"description": "可选。source 别名。"
|
||||
},
|
||||
"ref_table": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
|
||||
"description": "local_sql:同库被引用表名"
|
||||
},
|
||||
"value_column": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
|
||||
"description": "提交值列;local_sql 必填;skill_cli/external_api 默认可为 id"
|
||||
},
|
||||
"label_column": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
|
||||
"description": "主文案列;local_sql 必填;skill_cli 默认可为 display_label"
|
||||
},
|
||||
"extra_column": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
|
||||
"description": "可选。第三展示列;有值时宿主拼成「value · label(extra)」"
|
||||
},
|
||||
"filter_column": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$",
|
||||
"description": "可选。local_sql 等值过滤列"
|
||||
},
|
||||
"filter_value": {
|
||||
"type": ["string", "number", "boolean", "null"],
|
||||
"description": "可选。与 filter_column 配对的等值"
|
||||
},
|
||||
"skill": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "skill_cli:兄弟/依赖技能 slug"
|
||||
},
|
||||
"skill_slug": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "skill_cli:skill 别名"
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "skill_cli:CLI 子命令(argv[0])"
|
||||
},
|
||||
"cli_command": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "skill_cli:command 别名"
|
||||
},
|
||||
"args": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
{ "type": "string" }
|
||||
],
|
||||
"description": "skill_cli:追加 argv;数组或逗号分隔串"
|
||||
},
|
||||
"items_path": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "skill_cli / external_api:stdout JSON 内选项数组点路径,默认 items"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "external_api(预留):HTTP URL"
|
||||
},
|
||||
"endpoint": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "external_api:url 别名"
|
||||
},
|
||||
"method": {
|
||||
"type": "string",
|
||||
"enum": ["GET", "POST"],
|
||||
"description": "external_api:HTTP 方法,默认 GET"
|
||||
},
|
||||
"multiple": {
|
||||
"type": "boolean",
|
||||
"description": "可选。true=多选 Lookup(存/提交 TEXT 逗号分隔 id);省略或 false=单选(默认)。"
|
||||
},
|
||||
"presentation": {
|
||||
"type": "string",
|
||||
"enum": ["select", "paged_table"],
|
||||
"description": "可选。省略或 select=下拉/勾选/Chip。paged_table=分页勾选小表(编号|文案,可翻页、可本页全选)。仍是 lookup,不要新造 display_type。"
|
||||
},
|
||||
"page_size": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"description": "可选。presentation=paged_table 时每页条数,默认 10,最大 100。"
|
||||
}
|
||||
},
|
||||
"oneOf": [
|
||||
{
|
||||
"required": ["ref_table", "value_column", "label_column"],
|
||||
"description": "local_sql(同库表)"
|
||||
},
|
||||
{
|
||||
"anyOf": [
|
||||
{ "required": ["skill", "command"] },
|
||||
{ "required": ["skill", "cli_command"] },
|
||||
{ "required": ["skill_slug", "command"] },
|
||||
{ "required": ["skill_slug", "cli_command"] }
|
||||
],
|
||||
"description": "skill_cli(兄弟技能 CLI JSON)"
|
||||
},
|
||||
{
|
||||
"anyOf": [
|
||||
{ "required": ["url"] },
|
||||
{ "required": ["endpoint"] }
|
||||
],
|
||||
"description": "external_api(预留)"
|
||||
}
|
||||
],
|
||||
"description": "Action Lookup:local_sql / skill_cli / external_api。展示:value · label;有 extra 时 value · label(extra)。宿主亦接受 camelCase。列 Lookup 仅 local_sql,见 SCHEMA.md。"
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["type", "properties"],
|
||||
@@ -120,9 +398,102 @@
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string", "minLength": 1 },
|
||||
"description": "可选。将 $row.$pk / $row.col / $selection.ids 等映射到 inputSchema 字段名,供 row/batch 预填。"
|
||||
},
|
||||
"columnMapping": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-z][a-z0-9_]*$"
|
||||
},
|
||||
"description": "可选。将 inputSchema 属性名映射到业务表物理列名,供 Action 弹窗与 replaces create/update 抽屉复用列 displayName / lookup options。"
|
||||
},
|
||||
"labelColumn": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-z][a-z0-9_]*$",
|
||||
"description": "可选。数据管理 Action 确认/表单弹层「已选择」摘要用的可读标签列(#id + 标签)。仅影响展示,不改变提交 payload。省略时宿主按常见列名启发(title/name 等),再不行只显示编号。"
|
||||
},
|
||||
"selectionMode": {
|
||||
"type": "string",
|
||||
"enum": ["required", "none", "optional"],
|
||||
"description": "可选。数据管理勾选如何驱动本 Action:required=必须有勾选(无勾选禁用);none=忽略表勾选(不出现假「已选择」);optional=有勾选走 $selection.ids,无勾选走 pickCount/maxLeads 等空选数量字段。省略时宿主推断:有 $selection.ids 或 batch → required,否则 → none。与 placements/executionProfile 正交。"
|
||||
},
|
||||
"visibleWhen": {
|
||||
"description": "可选。placements 含 row 时:按当前行原始列值决定是否出现在行菜单/行按钮(宿主已支持)。形状同表单字段 visibleWhen。省略=始终显示。≠ inputSchema 字段 visibleWhen,≠ Policy L3 预留。见 ACTIONS.md「行菜单显隐」。",
|
||||
"allOf": [{ "$ref": "#/$defs/visibleWhenRule" }]
|
||||
}
|
||||
},
|
||||
"description": "表级绑定;toolbar 必填。可选 inputMapping 供行内/批量预填。concurrency/locks 勿写入。"
|
||||
"description": "表级绑定;toolbar 必填。可选 inputMapping / columnMapping / labelColumn / selectionMode / visibleWhen。concurrency/locks 勿写入。"
|
||||
},
|
||||
"hostEffectId": {
|
||||
"type": "string",
|
||||
"enum": ["reloadTable", "refreshCatalog", "clearSelection"],
|
||||
"description": "数据管理 UI 副作用(匠厂宿主 ≥2.8.5)。reloadTable=刷新当前表(含 Data Policy);refreshCatalog=刷新技能目录;clearSelection=清空多选。"
|
||||
},
|
||||
"hostEffects": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"onSuccess": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/hostEffectId" },
|
||||
"uniqueItems": true,
|
||||
"description": "sync 退出 0,或 async 任务已被任务中心接收时触发(不是 Job 跑完)。"
|
||||
},
|
||||
"onFailure": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/hostEffectId" },
|
||||
"uniqueItems": true,
|
||||
"description": "sync 失败,或 async 启动被拒时触发。"
|
||||
}
|
||||
},
|
||||
"description": "可选。UI 副作用简写;省略则宿主不额外改 UI。与 placements/executionProfile/interaction 正交;cron/agent 忽略。新技能有编排需求时优先用 steps。"
|
||||
},
|
||||
"actionStep": {
|
||||
"type": "object",
|
||||
"required": ["when", "do"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"when": {
|
||||
"type": "string",
|
||||
"enum": ["before", "afterSuccess", "afterFailure"],
|
||||
"description": "before=CLI 前;afterSuccess/afterFailure 时机同 hostEffects.onSuccess/onFailure。"
|
||||
},
|
||||
"do": {
|
||||
"type": "string",
|
||||
"enum": ["deleteByFk", "deleteRows", "reloadTable", "refreshCatalog", "clearSelection"],
|
||||
"description": "闭环操作或 UI 效果。deleteByFk/deleteRows 须配合 table(及 fk)。"
|
||||
},
|
||||
"table": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-z][a-z0-9_]*$",
|
||||
"description": "deleteByFk / deleteRows 目标表(snake_case)。"
|
||||
},
|
||||
"fk": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-z][a-z0-9_]*$",
|
||||
"description": "deleteByFk 时子表外键列名。"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": { "do": { "const": "deleteByFk" } },
|
||||
"required": ["do"]
|
||||
},
|
||||
"then": { "required": ["table", "fk"] }
|
||||
},
|
||||
{
|
||||
"if": {
|
||||
"properties": { "do": { "const": "deleteRows" } },
|
||||
"required": ["do"]
|
||||
},
|
||||
"then": { "required": ["table"] }
|
||||
}
|
||||
]
|
||||
},
|
||||
"action": {
|
||||
"type": "object",
|
||||
@@ -142,12 +513,18 @@
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"hoverHint": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "可选。数据管理工具栏悬停文案。与 description / confirmation.message 独立;省略则不显示悬停,不回退。需宿主已支持该字段。当前实现为顶栏技能按钮,不含宿主内置删除/刷新/新增/导入/导出。"
|
||||
},
|
||||
"placements": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"minItems": 0,
|
||||
"items": { "$ref": "#/$defs/placement" },
|
||||
"description": "决定 Action 出现在哪里;与 executionProfile 正交"
|
||||
"description": "决定 Action 出现在哪里;与 executionProfile 正交。仅 replaces 接管默认 CRUD 时可为空数组。数据管理 toolbar/row/batch 的顺序由顶层 actions 数组相对顺序决定。表顶栏 toolbar/batch 全部直接展示(无溢出菜单);row 为行级入口(操作列/行右键/⋯),不在本字段配置排序、截断或菜单树。"
|
||||
},
|
||||
"replaces": { "$ref": "#/$defs/mutationKind" },
|
||||
"entrypoint": { "$ref": "#/$defs/entrypoint" },
|
||||
"inputSchema": { "$ref": "#/$defs/inputSchema" },
|
||||
"confirmation": { "$ref": "#/$defs/confirmation" },
|
||||
@@ -170,11 +547,33 @@
|
||||
"batch": { "type": "string", "enum": ["direct", "confirm", "form"] },
|
||||
"cron": { "type": "string", "enum": ["direct", "confirm", "form"] },
|
||||
"agent": { "type": "string", "enum": ["direct", "confirm", "form"] },
|
||||
"skill-detail": { "type": "string", "enum": ["direct", "confirm", "form"] }
|
||||
"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"] },
|
||||
"import": { "type": "string", "enum": ["direct", "confirm", "form"] }
|
||||
},
|
||||
"description": "按 placement 覆盖 interaction(只能写 concrete 模式,不能写 auto)。常用于同一 Action 在 row 直通、toolbar 仍要表单。"
|
||||
"description": "按 placement / replaces 覆盖 interaction(只能写 concrete 模式,不能写 auto)。"
|
||||
},
|
||||
"bind": { "$ref": "#/$defs/bind" }
|
||||
"bind": { "$ref": "#/$defs/bind" },
|
||||
"hostEffects": { "$ref": "#/$defs/hostEffects" },
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"maxItems": 8,
|
||||
"items": { "$ref": "#/$defs/actionStep" },
|
||||
"description": "可选。有序宿主编排(before / afterSuccess / afterFailure);单次最多 8 步。与 hostEffects 可并存(同效果可能各跑一次)。新技能优先用 steps。省略则无额外步骤。需匠厂宿主 ≥2.8.5。"
|
||||
},
|
||||
"authz": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"capability": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "可选文档/静态提示用能力名;宿主不得仅凭此显隐按钮,运行时以 policy Snapshot 为准。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"if": {
|
||||
"properties": {
|
||||
@@ -187,7 +586,17 @@
|
||||
"then": {
|
||||
"required": ["bind"]
|
||||
},
|
||||
"description": "concurrency/locks 为未稳定预留字段,勿写入新技能 manifest。placements 含 toolbar 时必须声明 bind.tables。"
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"required": ["replaces"]
|
||||
},
|
||||
"then": {
|
||||
"required": ["bind"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "concurrency/locks 为未稳定预留字段,勿写入新技能 manifest。placements 含 toolbar 或声明 replaces 时必须声明 bind.tables。无 replaces 时 placements 建议非空(模板测试强制)。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
78
assets/schemas/skill-cell-presentation.schema.json
Normal file
78
assets/schemas/skill-cell-presentation.schema.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://openclaw.local/skill-template/skill-cell-presentation.schema.json",
|
||||
"title": "SkillCellPresentationDocument",
|
||||
"description": "匠厂数据管理「单元格呈现」文档(schemaVersion 1)。技能按业务规则为「某表某行某列」声明语义 tone(非任意 CSS);宿主映射主题样式。整行/整列/区域见 skill-grid-presentation.schema.json,同一 cellPresentation.enabled。用途不限(时效超时、库存预警、状态异常、必填缺失等)。详见 references/CELL_PRESENTATION.md。",
|
||||
"type": "object",
|
||||
"required": ["schemaVersion", "skill", "table", "rowKey", "cells"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"schemaVersion": {
|
||||
"type": "integer",
|
||||
"const": 1
|
||||
},
|
||||
"skill": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
|
||||
"minLength": 1,
|
||||
"description": "与 SKILL.md metadata.openclaw.slug / actions.json skill 一致"
|
||||
},
|
||||
"table": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-z][a-z0-9_]*$",
|
||||
"description": "业务表物理名(snake_case)"
|
||||
},
|
||||
"rowKey": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-z][a-z0-9_]*$",
|
||||
"description": "用于匹配行的业务表列名(如 id、feishu_record_id);cells[].rowId 的值取自该列"
|
||||
},
|
||||
"generatedAt": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "可选。ISO-8601 或技能约定的生成时间,便于排障"
|
||||
},
|
||||
"cells": {
|
||||
"type": "array",
|
||||
"description": "当前应对该表生效的呈现项;空数组 = 该表无特殊呈现(清除旧样式)。同 (rowId, column) 重复时宿主可后者覆盖前者。",
|
||||
"items": { "$ref": "#/$defs/cellItem" }
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"tone": {
|
||||
"type": "string",
|
||||
"enum": ["danger", "warning", "info", "success", "muted"],
|
||||
"description": "语义色板(闭集)。宿主映射到底色/字色/左边线等;禁止技能下发原始 CSS 或任意 hex。"
|
||||
},
|
||||
"cellItem": {
|
||||
"type": "object",
|
||||
"required": ["rowId", "column", "tone"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"rowId": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "rowKey 列的字符串形式主键/业务键"
|
||||
},
|
||||
"column": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-z][a-z0-9_]*$",
|
||||
"description": "要强调的业务列名(snake_case),必须是真实列"
|
||||
},
|
||||
"tone": { "$ref": "#/$defs/tone" },
|
||||
"message": {
|
||||
"type": "string",
|
||||
"maxLength": 500,
|
||||
"description": "可选。悬停/无障碍说明(短文案);不要塞长报告"
|
||||
},
|
||||
"alert": {
|
||||
"type": "boolean",
|
||||
"description": "可选,默认 false。true = 可计入进页/同步后的提醒汇总(如 Toast);填完数据并重算后该项消失则提醒自然关闭"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
93
assets/schemas/skill-grid-presentation.schema.json
Normal file
93
assets/schemas/skill-grid-presentation.schema.json
Normal file
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://openclaw.local/skill-template/skill-grid-presentation.schema.json",
|
||||
"title": "SkillGridPresentationDocument",
|
||||
"description": "匠厂数据管理「Grid Presentation」文档(schemaVersion 1)。技能按业务规则为 rows×columns 声明语义 tone(整行、整列、区域或单格);禁止 all×all。非任意 CSS。与 skill-cell-presentation.schema.json(v1 cells[])并存,同一 cellPresentation.enabled。详见 references/CELL_PRESENTATION.md。",
|
||||
"type": "object",
|
||||
"required": ["schemaVersion", "skill", "table", "rowKey", "rules"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"schemaVersion": {
|
||||
"type": "integer",
|
||||
"const": 1
|
||||
},
|
||||
"skill": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
|
||||
"minLength": 1,
|
||||
"description": "与 SKILL.md metadata.openclaw.slug / actions.json skill 一致"
|
||||
},
|
||||
"table": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-z][a-z0-9_]*$",
|
||||
"description": "业务表物理名(snake_case)"
|
||||
},
|
||||
"rowKey": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-z][a-z0-9_]*$",
|
||||
"description": "用于匹配行的业务表列名(如 id);rules[].rows 的 id 取自该列"
|
||||
},
|
||||
"generatedAt": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "可选。ISO-8601 或技能约定的生成时间,便于排障"
|
||||
},
|
||||
"rules": {
|
||||
"type": "array",
|
||||
"description": "当前应对该表生效的 grid 规则;空数组 = 该表无 grid 呈现(清除旧行/列/区域样式)。同 ruleKey 由 UNIQUE 约束覆盖。",
|
||||
"items": { "$ref": "#/$defs/ruleItem" }
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"tone": {
|
||||
"type": "string",
|
||||
"enum": ["danger", "warning", "info", "success", "muted"],
|
||||
"description": "语义色板(闭集)。宿主映射到底色/字色/左边线等;禁止技能下发原始 CSS 或任意 hex。"
|
||||
},
|
||||
"axisTarget": {
|
||||
"description": "\"all\" = 该轴全部;非空字符串数组 = 有限集合(行 id 或列名)",
|
||||
"oneOf": [
|
||||
{ "type": "string", "const": "all" },
|
||||
{
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
]
|
||||
},
|
||||
"ruleItem": {
|
||||
"type": "object",
|
||||
"required": ["ruleKey", "rows", "columns", "tone"],
|
||||
"additionalProperties": false,
|
||||
"not": {
|
||||
"properties": {
|
||||
"rows": { "const": "all" },
|
||||
"columns": { "const": "all" }
|
||||
},
|
||||
"required": ["rows", "columns"]
|
||||
},
|
||||
"properties": {
|
||||
"ruleKey": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 200,
|
||||
"description": "本表内唯一。建议稳定业务键(重算时覆盖同一条)"
|
||||
},
|
||||
"rows": { "$ref": "#/$defs/axisTarget" },
|
||||
"columns": { "$ref": "#/$defs/axisTarget" },
|
||||
"tone": { "$ref": "#/$defs/tone" },
|
||||
"message": {
|
||||
"type": "string",
|
||||
"maxLength": 500,
|
||||
"description": "可选。悬停/无障碍说明(短文案);不要塞长报告"
|
||||
},
|
||||
"alert": {
|
||||
"type": "boolean",
|
||||
"description": "可选,默认 false。true = 按本条规则计入进页/同步提醒(不按命中格子数)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
196
assets/schemas/skill-policy-snapshot.schema.json
Normal file
196
assets/schemas/skill-policy-snapshot.schema.json
Normal file
@@ -0,0 +1,196 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://openclaw.local/skill-template/skill-policy-snapshot.schema.json",
|
||||
"title": "SkillDataPolicySnapshot",
|
||||
"description": "匠厂 Skill Data Policy Snapshot(schemaVersion 1)。由技能 policy CLI 输出到 stdout;宿主用于数据管理按钮/行列过滤。详见 references/POLICY.md。",
|
||||
"type": "object",
|
||||
"required": ["schemaVersion", "skill", "actor", "actions"],
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"schemaVersion": {
|
||||
"type": "integer",
|
||||
"const": 1
|
||||
},
|
||||
"skill": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
|
||||
"minLength": 1
|
||||
},
|
||||
"actor": {
|
||||
"type": "object",
|
||||
"required": ["userId"],
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"userId": { "type": "string", "minLength": 1 },
|
||||
"displayName": { "type": "string" },
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1 }
|
||||
},
|
||||
"attributes": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": ["string", "number", "boolean", "null"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"type": "object",
|
||||
"required": ["mode"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["allowlist", "open"],
|
||||
"description": "v1 仅允许 allowlist/open;denylist 非法。"
|
||||
},
|
||||
"ids": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
|
||||
},
|
||||
"description": "mode=allowlist 时必填;mode=open 时应省略或 []。"
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"if": {
|
||||
"properties": { "mode": { "const": "allowlist" } },
|
||||
"required": ["mode"]
|
||||
},
|
||||
"then": {
|
||||
"required": ["ids"]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"hostCapabilities": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["refresh", "export", "view", "edit", "delete", "import"]
|
||||
},
|
||||
"description": "省略=不限制宿主内置能力;写出则按 allowlist(含 []=全禁)。"
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"tables": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "$ref": "#/$defs/tablePolicy" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensions": {
|
||||
"type": "object",
|
||||
"description": "技能私有扩展;宿主必须忽略未知键。"
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"predicate": {
|
||||
"type": "object",
|
||||
"required": ["field", "op"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"field": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"pattern": "^[a-z][a-z0-9_]*$"
|
||||
},
|
||||
"op": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"eq",
|
||||
"ne",
|
||||
"in",
|
||||
"not_in",
|
||||
"gt",
|
||||
"gte",
|
||||
"lt",
|
||||
"lte",
|
||||
"is_null",
|
||||
"is_not_null"
|
||||
]
|
||||
},
|
||||
"value": {
|
||||
"description": "与 valueFrom 互斥;in/not_in 时为非空数组。"
|
||||
},
|
||||
"valueFrom": {
|
||||
"type": "string",
|
||||
"const": "actor.userId",
|
||||
"description": "v1 仅允许 actor.userId。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scope": {
|
||||
"oneOf": [
|
||||
{ "$ref": "#/$defs/predicate" },
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["all"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"all": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/predicate" }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["any"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"any": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/predicate" }
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"tablePolicy": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"accessible": { "type": "boolean" },
|
||||
"columns": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"visible": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9_]*$"
|
||||
}
|
||||
},
|
||||
"writable": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z][a-z0-9_]*$"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"rows": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": ["scope", "open", "delegate"]
|
||||
},
|
||||
"scope": { "$ref": "#/$defs/scope" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://openclaw.local/skill-template/task-log-record.schema.json",
|
||||
"title": "TaskLogRecord",
|
||||
"description": "通用业务技能 log-get 返回的单条任务日志对象模板",
|
||||
"type": "object",
|
||||
"required": ["id", "task_type", "status", "created_at", "updated_at"],
|
||||
"properties": {
|
||||
"id": { "type": "integer" },
|
||||
"task_type": { "type": "string" },
|
||||
"target_id": { "type": ["string", "null"] },
|
||||
"input_id": { "type": ["string", "null"] },
|
||||
"input_title": { "type": ["string", "null"] },
|
||||
"status": { "type": "string" },
|
||||
"error_msg": { "type": ["string", "null"] },
|
||||
"result_summary": { "type": ["string", "null"] },
|
||||
"created_at": { "type": ["string", "null"] },
|
||||
"updated_at": { "type": ["string", "null"] }
|
||||
},
|
||||
"additionalProperties": true
|
||||
}
|
||||
@@ -93,6 +93,17 @@ finally:
|
||||
|
||||
## 5. 账号选择与多账号
|
||||
|
||||
### 5.0 数据管理 / 定时任务表单选账号(Action Lookup)
|
||||
|
||||
若 Action 参数需要用户**点选账号**(而非手填编号):
|
||||
|
||||
- 在 `actions.json` 用 **`display_type: lookup` + `lookup.source: skill_cli`**,指向 `account-manager` 能吐 JSON 选项列表的 CLI(字段名 / `items_path` 以 account-manager 文档与实际 stdout 为准)。
|
||||
- **禁止**在本技能库再建「账号选项缓存表」再 `local_sql`——易过期,且定时任务等入口容易空下拉。
|
||||
- 契约与示例见 [`references/ACTIONS.md`](../references/ACTIONS.md)「跨表引用:Lookup」、`assets/examples/lookup-skill-cli.action.example.json`。
|
||||
|
||||
表单 Lookup **只负责填参数**;真正开浏览器前的 ensure / pick / 租约仍按下文与 `account_client` 执行,二者不要混成「打开表单就 pick-web」。
|
||||
|
||||
### 5.1 运行时选择顺序
|
||||
选择优先级:
|
||||
|
||||
1. 用户明确指定纯数字 `DEFAULT_ACCOUNT_ID`:先确认账号存在且平台一致,再通过可定位该账号的 pick 流程取得租约;不得只 `get` 后返回空租约。
|
||||
@@ -173,7 +184,9 @@ python scripts/main.py account open <account_id>
|
||||
- 租约持有者应使用技能 slug 或可追踪任务标识,`purpose` 写明用途。
|
||||
- 租约 TTL 应覆盖正常任务时长;长任务需要平台支持的续租策略,不能依赖永不过期。
|
||||
- `finally release_lease(...)` 是最低要求,浏览器关闭失败也不能跳过释放尝试。
|
||||
- 进程被强制结束后可能留下租约;先用 `lease list --active` 核对,确认没有真实任务仍在使用后再释放或清理。
|
||||
- **清理顺序**:`finally` 内**先 `release_lease`,再关浏览器**。关浏览器(含 CDP / 强杀 Profile 进程)可能耗时或卡住;先释租约可避免「窗还没关完、账号已被下一任务抢走失败」的空窗拉长。
|
||||
- **任务中心中断**:宿主只下发 `stop` 并等待技能退出;技能须在闸门抛出 `JobStopped` 后进入 `finally` 自清。禁止用 `except Exception` 吞掉 `JobStopped`(详见 [`LOGGING.md`](LOGGING.md) §2.5)。不要指望宿主代收租约。
|
||||
- 进程被强制结束后可能留下租约;先用 `lease list --active` 核对,确认没有真实任务仍在使用后再释放或清理;亦可等待 TTL 过期。
|
||||
- 禁止看到租约冲突就直接删数据库记录、删 Profile、换账号或新建重复账号。
|
||||
|
||||
## 9. 工业级测试
|
||||
@@ -184,6 +197,7 @@ python scripts/main.py account open <account_id>
|
||||
- 账号不存在、平台不匹配、Profile 缺失、返回格式异常;
|
||||
- 同一账号并发抢占和 `LEASE_CONFLICT`;
|
||||
- 业务成功、业务失败、用户取消、浏览器崩溃后的租约释放;
|
||||
- **NEED_MANUAL** 任务中心运行中点「中断」:租约变为 `released`(或无 active),马上再采不因残留租约 `LEASE_CONFLICT`;
|
||||
- 登录过期、验证码 / 二次验证、账号风险和权限不足;
|
||||
- 两个以上账号轮换且 Profile、登录态、结果互不污染;
|
||||
- 关闭重开后登录保持;
|
||||
@@ -200,7 +214,7 @@ python scripts/main.py account open <account_id>
|
||||
3. 执行 `account list/get`,核对账号 ID、`profile_dir`、`session_status`。
|
||||
4. 在文件系统确认 `profile_dir` 是否存在、是否错用了其他用户目录。
|
||||
5. 执行 `lease list --active`,确认是否被其他任务占用或残留。
|
||||
6. 查看任务日志、错误码、失败截图和浏览器当前页面。
|
||||
6. 查看运行日志、任务中心终态、错误码、失败截图和浏览器当前页面。
|
||||
7. 判断问题属于账号不存在、租约冲突、登录失效、Profile 损坏、页面变化还是业务失败。
|
||||
|
||||
仍需协助时必须提供:技能 slug、平台、脱敏账号标识 / 账号 ID、任务时间、错误码、账号与租约检查结果、`profile_dir` 是否存在、脱敏日志和截图。禁止发送密码、Cookie、Token 或验证码。
|
||||
|
||||
@@ -102,7 +102,7 @@ def get_adapter():
|
||||
2. **普通兄弟技能调用**,优先走统一 **`service.sibling_bridge`**(`call_sibling_json`),**不要**在 `task_service.py`、`task_rpa.py` 等业务流程文件中到处散落 `subprocess.run`。
|
||||
3. **account-manager 账号/租约能力**是例外:可参考 `examples/real_browser_rpa/scripts/service/account_client.py` 与 `examples/simulator_browser_rpa/scripts/service/account_client.py`,封装为**单一** `account_client.py`;允许在该文件内部集中通过 subprocess 调 account-manager CLI。**禁止** import `rpa_helpers` 等 account-manager 内部模块;`simulator_rpa` 与 `real_rpa` 均走同一 `pick_web_account` / `release_lease` 模式。
|
||||
4. **不允许**直接 `import account-manager` 的内部 Python 模块(如 `service/`、`util/`、`db/`)。
|
||||
5. **所有选择路径都必须取得 lease**:包括纯数字 `DEFAULT_ACCOUNT_ID`。`account get <id>` 只能读取账号,不能单独作为运行结果;必须再通过可定位该账号的 pick 流程取得非空 `lease_token`。随后必须 `finally release lease`;进程被 kill 后可能残留 lease,需按 [`ACCOUNT_MANAGER.md`](ACCOUNT_MANAGER.md) 排查。
|
||||
5. **所有选择路径都必须取得 lease**:包括纯数字 `DEFAULT_ACCOUNT_ID`。`account get <id>` 只能读取账号,不能单独作为运行结果;必须再通过可定位该账号的 pick 流程取得非空 `lease_token`。随后必须 `finally` **先 `release_lease`、再关浏览器**;进程被 kill 后可能残留 lease,需按 [`ACCOUNT_MANAGER.md`](ACCOUNT_MANAGER.md) 排查。宿主中断只通知技能,**不**代收租约;禁止 `except Exception` 吞掉 `JobStopped`(见 [`LOGGING.md`](LOGGING.md) §2.5)。
|
||||
6. **网页 RPA 硬规则**(详见 [`RPA.md`](RPA.md) §0.2):必须先经 `pick_web_account`(底层 **`ensure-web` / `pick-web --ensure`**)拿到 `profile_dir` 再开浏览器;拿不到则失败退出;生产有头;**是否站点登录**按平台约定,但**不等于**可以跳过 account-manager。登记账号 ≠ 站点已登录。
|
||||
7. **账号选择优先级**:纯数字 `DEFAULT_ACCOUNT_ID` → 校验指定账号后带租约 pick;`DEFAULT_LOGIN_ID` / 非数字标识 → `ensure-web --login-id --lease`;皆空 → 按平台 `ensure-web --lease`。人读展示优先 `login_id` / `account_label`,不要只用数字 id。多条匹配或无法确保拿到指定 ID → `ERROR:AMBIGUOUS_ACCOUNT`。
|
||||
8. **运行前校验**:`profile_dir` 和 `lease_token` 均须非空;账号平台必须匹配;缺任一字段立即失败,不打开浏览器。
|
||||
@@ -119,10 +119,12 @@ result = call_sibling_json("account-manager", ["list", "--limit", "10"])
|
||||
from service.account_client import pick_web_account, release_lease
|
||||
|
||||
account = pick_web_account(platform="target_platform", login_id=None)
|
||||
lease_token = account.get("lease_token")
|
||||
try:
|
||||
...
|
||||
finally:
|
||||
release_lease(account.get("lease_token"))
|
||||
release_lease(lease_token) # 先释租约
|
||||
# 再关浏览器 / Playwright
|
||||
```
|
||||
|
||||
## 相关文档
|
||||
|
||||
@@ -81,7 +81,7 @@ Agent 与编排层必须确认:
|
||||
|
||||
- 先验证业务成功标志,再记录成功;不得把“点击完成”当成“业务成功”。
|
||||
- 返回成功、部分成功、失败、停止或等待人工等明确状态。
|
||||
- 保存必要的任务日志、错误码、失败截图和可选录屏。
|
||||
- 保存必要的运行日志、错误码、失败截图和可选录屏。
|
||||
- Agent 根据结构化结果解释完成数量、失败原因和下一步,不直接粘贴技术日志。
|
||||
|
||||
## 4. 合规与技术红线
|
||||
@@ -141,6 +141,13 @@ Agent 与编排层必须确认:
|
||||
|
||||
页面加载、控件可用、弹窗消失、保存成功、下载完成等必须等待**真实条件**并设置超时。固定等待不能代替状态判断。
|
||||
|
||||
自 **platform-kit 1.2.11** / 本模板 **1.0.78** 起:
|
||||
|
||||
- 统一使用 `jiangchang_skill_core.rpa.poll_until`(或 `poll_until_sync`),内部已接 `interruptible_sleep`。
|
||||
- 先有界等待就绪,再区分「加载慢」与「页不对」;超时优先 `ERROR:UI_NOT_READY`。
|
||||
- 不要把慢机器症状映射成配置错误、网络错误或频控,除非有明确证据。
|
||||
- 详细表与纪律见 [`RPA.md`](RPA.md) §0.3 / §4。
|
||||
|
||||
### 操作节奏
|
||||
|
||||
真实业务环境中,两个正常业务动作之间默认随机等待 **1~3 秒**:
|
||||
|
||||
@@ -52,11 +52,15 @@ checklist_version: 1.0
|
||||
- [ ] S0-1 交互形态:纯本地 / API / 网页RPA / 桌面RPA / 手机RPA / 编排(调兄弟技能)
|
||||
- [ ] S0-2 目标站登录策略:required / optional / not_needed / 不适用
|
||||
- [ ] S0-3 是否依赖账号会话(account-manager 或等价 Profile):是 / 否
|
||||
- [ ] S0-4 持久化:无 / 仅任务日志 / 另有业务表
|
||||
- [ ] S0-4 持久化:无 / 另有业务表
|
||||
- [ ] S0-5 Skill Action:无 / 仅同步短任务 / 含异步长任务
|
||||
- [ ] S0-6 是否需要在任务中心查看进度或结果:是 / 否
|
||||
- [ ] S0-7 外部系统账号、数据、设备与操作权限的授权来源:
|
||||
- [ ] S0-8 是否包含提交、付款、删除、审批、发布、发送等不可逆副作用:是 / 否
|
||||
- [ ] S0-9 **入口定位表**(见 `PRODUCT_SURFACES.md` / `REQUIREMENTS` §0.1):已填写 / 未填写
|
||||
- [ ] S0-10 本技能宣称的主用户路径涉及:对话 / 数据管理(执行或仅展示) / 定时(可多选;与定位表一致)
|
||||
- [ ] S0-11 **业务行生命周期**(见 `references/WORK_ITEM_STATUS.md` / `REQUIREMENTS` §0.3):有处理队列且已声明状态→角色映射 / 不适用
|
||||
- [ ] S0-12 **HTML 页面报告**(见 `references/ACTIONS.md`「HTML 页面报告」):壳子页 `assets/html/pages/*.html` / 快照 `html/*.html` + `render-html` / 不适用
|
||||
|
||||
---
|
||||
|
||||
@@ -73,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)
|
||||
|
||||
---
|
||||
|
||||
@@ -86,6 +96,8 @@ checklist_version: 1.0
|
||||
- [ ] A3-2 **P0** 缺参或非法输入时稳定失败(可读错误,不假成功)
|
||||
- [ ] A3-3 **P1** 失败可诊断:能从任务记录/提示判断失败阶段,而非只有“失败了”
|
||||
- [ ] A3-4 **P0** 同一能力从已声明的不同入口触发时,业务结果一致(不出现入口分叉成两套逻辑)
|
||||
- [ ] A3-5 **P0** `REQUIREMENTS` 入口定位表已填写;定位为「挂」的入口在 `actions.json`(及必要表元数据)中有对应声明
|
||||
- [ ] A3-6 **P1** 定位为「不挂」的入口有书面理由(不是空白遗漏)
|
||||
|
||||
---
|
||||
|
||||
@@ -96,7 +108,14 @@ checklist_version: 1.0
|
||||
- [ ] A4-3 **P0** 异步任务在任务中心能看到进行中与结束状态;结束有成功或失败结论
|
||||
- [ ] A4-4 **P1** 进行中的进度/步骤信息对用户可理解
|
||||
- [ ] A4-5 **P1** 已对外暴露的动作参数覆盖主路径关键输入(无“命令行能做、动作入口不能做”的关键缺口)
|
||||
|
||||
- [ ] A4-6 **P0** 若 Action 需引用业务行(话术/平台/账号等):参数为 Lookup 下拉(**不是**手填内部编号);同库用 `local_sql`(内联或绑列),跨技能用 **`skill_cli`**(勿在本库造镜像缓存表);可选「自动」可留空提交(见 `references/ACTIONS.md`)
|
||||
- [ ] A4-7 **P1** 若 Action 有互斥采集/操作模式(如「按关键词 / 按主页」):用 `inputSchema` 字段级 **`visibleWhen`** 联动显隐,勿只靠 `description` 灰字说明「仅某某时填写」(见 `references/ACTIONS.md`;勿与 **`bind.visibleWhen`** 行菜单、Policy L3 混淆)
|
||||
- [ ] A4-8 **P1** 行内/勾选打开的 `form`:有「已选择」时参数区不应再重复映射主键(`inputMapping` + `readOnly`);需要强制露出时用 `revealInForm`,**禁止**用表单 `visibleWhen` 藏主键(会不提交)
|
||||
- [ ] A4-9 **P1** 若同一能力既支持「勾选处理」又支持「按数量从队列领取」:用一个 Action + `bind.selectionMode: "optional"`(勿拆两个同义顶栏 / cron 按钮);无勾选可点、有勾选显示「已选择」、空选数量字段用 `pickCount` 等约定名;**定时任务配置页**只应出现数量字段、不应出现空的「编号列表」(见 `references/ACTIONS.md`)
|
||||
- [ ] A4-10 **P0** 数据管理弹窗与任务失败文案:全中文产品用语;禁止在 `description` / `confirmation.message` / 参数 `description` / 用户向 `message` 写英文字段名、CLI 子命令、`async`/`pending`/`status=` 等(Agent 路由放 `SKILL.md`;见 `references/ACTIONS.md`「用户可见文案」)
|
||||
- [ ] A4-11 **P1** 若行操作应按业务状态显隐(如仅待确认可「采用」、仅已采用可「加入待写」):用 **`bind.visibleWhen`**(对当前行列值求值;存库原始值如 `approved`);勿指望 Policy L3;CLI 侧仍须校验状态(见 `references/ACTIONS.md`「行菜单显隐」)
|
||||
- [ ] A4-12 **P1** 若要在表单里翻页勾选一批业务行(文章、平台等):用 Lookup **`presentation: "paged_table"`**(仍是 `display_type: lookup`,勿新造类型);`skill_cli` 的 stdout 为一行 `{"items","total"}`,0 条也成功退出;该列表命令不要在入口加载整包 platform-kit 或 Playwright(见 `references/ACTIONS.md`「分页勾选小表」)
|
||||
- [ ] A4-13 **P1** 若 Action 有固定小枚举(状态/船司/优先级等)需中文展示:用 `enum` + **`enumNames`**,或绑列复用列 `options_json` 数组;宿主优先序为绑列 options → enumNames → 纯 enum;**禁止**为中文展示造假 Lookup(见 `references/ACTIONS.md`「静态枚举 label」)
|
||||
---
|
||||
|
||||
## 5. 账号与浏览器会话(网页 RPA 适用;其他形态标 N/A 或改测等价项)
|
||||
@@ -104,15 +123,16 @@ checklist_version: 1.0
|
||||
- [ ] A5-1 **P0** 未准备好账号/浏览器档案时,主路径失败并提示,不会默默用本机默认浏览器身份跑起来
|
||||
- [ ] A5-2 **P0** 登录策略为 required 时:未登录不能把做不成的事当成成功
|
||||
- [ ] A5-3 **P0** 交付联调默认能看见浏览器窗口(有头);不能把无头当作唯一可验收形态
|
||||
- [ ] A5-4 **P1** 浏览器窗口启动形态满足交付约定(如最大化或可视区域足够操作与人工介入)
|
||||
- [ ] A5-4 **P1** 浏览器窗口启动形态满足交付约定(如最大化或可视区域足够操作与人工介入);`no_viewport=True` + `--start-maximized`,无「先小后大」或右侧留白/scrollbar 后遗症(见 `RPA.md` §1.2.1)
|
||||
- [ ] A5-5 **P1** 验证码或人工确认:有等待与超时;超时后任务失败可理解,不无限挂死或假成功
|
||||
- [ ] A5-6 **P0** **NEED_MANUAL** 真实站或约定仿真站主路径至少完整跑通 1 次;仅模拟数据通过不算交活
|
||||
- [ ] A5-7 **P0** 账号创建后可在宿主数据管理和 `account list/get` 查到,返回的 `profile_dir` 在当前用户数据目录真实存在
|
||||
- [ ] A5-8 **P0** 每个账号使用独立且稳定的 Profile;不同账号不共用,本机日常浏览器目录不参与运行
|
||||
- [ ] A5-9 **P0** 指定账号 ID、指定登录标识和自动选择均取得非空租约;成功、失败、取消和异常路径最终释放
|
||||
- [ ] A5-9 **P0** 指定账号 ID、指定登录标识和自动选择均取得非空租约;成功、失败、取消和异常路径最终释放;`finally` 先释租约再关浏览器
|
||||
- [ ] A5-10 **P0** **NEED_MANUAL** 登录后关闭浏览器并释放租约,再取同一账号重开,登录状态仍有效;失效时明确提示重新登录
|
||||
- [ ] A5-11 **P1** 至少两个账号连续或轮换测试通过,Profile、登录态、结果和失败互不污染
|
||||
- [ ] A5-12 **P1** 租约冲突、残留租约和账号登录失效能按文档自助定位,不通过删库、删 Profile 或自动换号处理
|
||||
- [ ] A5-13 **P0** **NEED_MANUAL** 任务中心运行中点「中断」:技能响应 `JobStopped` 并释放租约(数据管理「账号租约」为 `released` / 无 active);立即再跑不因残留租约失败;进度 `emit` 不得用裸 `except Exception` 吞掉停止
|
||||
|
||||
---
|
||||
|
||||
@@ -123,6 +143,18 @@ 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 **P2** 账期/月份/生效月等 `YYYY-MM` 字段用 `display_type: year_month`(或 Action 同义 `format`),**不要**只用 `description` 写格式却留纯文本框;完整日才用 `date`
|
||||
- [ ] 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`)
|
||||
- [ ] A6-15 **P1** 若启用 `cellPresentation`:旁路表不得出现在数据管理侧栏;整行着色不得铺到详情每个字段;禁止 `all`×`all`;tone 闭集。未启用 → **N/A**(见 `references/CELL_PRESENTATION.md`)
|
||||
- [ ] A6-16 **P0** 若走 **MySQL 云库**:用户目录 `data-sources.json` + `.env` DSN 已配置;Host ≥ 6.9.28;CLI / Action 与数据管理 UI 读写**同一库**(抽测 list/create);业务 DDL 支持所选后端。纯 SQLite → **N/A**(见 `references/DATA_SOURCES.md`)
|
||||
|
||||
---
|
||||
|
||||
@@ -133,7 +165,8 @@ checklist_version: 1.0
|
||||
- [ ] A7-3 **P0** 故意失败一条时,文件日志先有 `task_failed`/`warning`,再有 `ERROR:CODE` 或任务中心失败态
|
||||
- [ ] A7-4 **P1** 关键步骤有据可查(能对应到开始/关键节点/结束;RPA 宜有多个 `stage=`)
|
||||
- [ ] A7-5 **P1** 若宣称支持录屏或截图存证:打开开关后关键步骤或失败能留下可查看产物;宣称支持却完全无产物 → **FAIL**
|
||||
- [ ] A7-6 **P2** 用户可见文案偏业务语言,而非大段开发黑话
|
||||
- [ ] A7-6 **P2** 用户可见文案偏业务语言,而非大段开发黑话(数据管理弹窗 / 任务失败见 **A4-10**,更高优先级)
|
||||
- [ ] A7-7 **P2** 数据管理技能按钮(toolbar / row / batch)中文名对照 `references/ACTIONS.md`「Action 按钮中文名」;不与宿主「查看 / 编辑 / 删除」抢名;打开类成功不弹成功条。行业专有按钮名可自创
|
||||
|
||||
---
|
||||
|
||||
@@ -162,8 +195,9 @@ checklist_version: 1.0
|
||||
|
||||
- [ ] A9-1 **P0** 仓库默认测试套件通过(执行技能约定的一键测试命令,如 `python tests/run_tests.py -v`)
|
||||
- [ ] A9-2 **P0** 不以「仅模拟档通过」作为交活依据(有真实档要求时必须真实跑通或书面阶段性豁免)
|
||||
- [ ] A9-3 **P0** **NEED_MANUAL** 已声明的宿主入口(详情/Agent/工具栏/定时等)按声明抽测可通过
|
||||
- [ ] A9-4 **P1** 对外版本一致:详情、version、更新日志、发布标签能对上
|
||||
- [ ] A9-3 **P0** **NEED_MANUAL** 入口定位表中定为「挂」的宿主入口(详情/Agent/工具栏/定时等)按声明抽测可通过;未在定位表中规划且未声明的入口可标 N/A(须引用定位表理由),**禁止**用「未声明」掩盖「从未做入口定位」
|
||||
- [ ] A9-4 **P0** 对外版本一致(POLICY-RELEASE-001):`SKILL.md` version = `SKILL_VERSION` = 发布 git tag(无前缀);`release.ps1` 打 tag 前会硬校验,漏改不得发布;CHANGELOG 有对应小节
|
||||
- [ ] A9-4b **P1** 本机宿主验收用根目录 `.\sync.ps1` 覆盖已安装明文;市场上的最新版本和加密包仍走 `.\release.ps1`。同步后退出资料管理再打开,不要点技能市场「更新」
|
||||
- [ ] A9-5 **P1** 开发者按平台规则能安装并测到该技能(若平台有开发者可见性要求)
|
||||
|
||||
---
|
||||
|
||||
@@ -26,6 +26,45 @@
|
||||
|
||||
**不要**依赖 `# ── 分组 ──` 装饰线给整组做唯一说明:宿主只会把行头挂到**紧挨着的下一个活跃 key**,后面的 key 不会自动继承。分组可用空行区分;**逐 key 自带行头(+ 行尾)才是硬要求**。
|
||||
|
||||
### 必填项:`# @required`(匠厂配置管理)
|
||||
|
||||
匠厂宿主「数据管理 → 配置管理」支持把部分配置项标为**必填**:模板在**行头注释上方**加独立一行 `# @required`,宿主会在 key 前显示红色 `*`,并在键值模式保存时拦截空值。
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| **写法** | `# @required` 单独占一行,紧挨在该 key 的**中文行头**上方 |
|
||||
| **语义** | 保存时该 key 的值不得为空(trim 后);模板里可以有默认值 |
|
||||
| **展示** | `@required` 行本身**不展示**给用户,只用于打标 |
|
||||
| **声明位置** | 只在仓库 `.env.example` 声明;用户数据目录 `.env` 里的 `@required` 不算 |
|
||||
|
||||
正例:
|
||||
|
||||
```ini
|
||||
# @required
|
||||
# 目标网站地址:技能要访问的网站或服务地址
|
||||
TARGET_BASE_URL=https://sandbox.jc2009.com # 填写完整网址(含 https://);换环境时再改
|
||||
|
||||
# 默认登录账号:仅填写非敏感的账号标识(如工号/登录名)
|
||||
DEFAULT_LOGIN_ID= # 留空=按平台自动选用;本项未标 @required,宿主不强制
|
||||
```
|
||||
|
||||
反例(禁止):
|
||||
|
||||
```ini
|
||||
# 目标网站地址(必填)
|
||||
TARGET_BASE_URL= # @required ← 禁止写在行尾
|
||||
|
||||
# @required
|
||||
TARGET_BASE_URL= # ← 禁止:@required 下方缺少中文行头
|
||||
|
||||
# @required
|
||||
# 共用说明
|
||||
KEY_A=
|
||||
KEY_B= # ← 禁止:一个 @required 给多个 key 共用
|
||||
```
|
||||
|
||||
**机器门禁**:`POLICY-CONFIG-005` 校验 `@required` 用法;与 `POLICY-CONFIG-004`(行头/行尾中文)一并生效。
|
||||
|
||||
### 必须做到
|
||||
|
||||
1. 每个会进入配置管理的活跃 `KEY=` 都有完整中文**行头**。
|
||||
@@ -104,25 +143,28 @@ STEP_DELAY_MAX=3.0 # 单位:秒;默认与最短等待组
|
||||
|
||||
# 人工处理超时:遇到滑块、验证码、二次验证时,等待人工处理的最长时间
|
||||
HUMAN_WAIT_TIMEOUT=180 # 单位:秒;来不及处理时可调大,如 300
|
||||
|
||||
# 浏览器启动超时(可选):限制 launch 最长时间;注释掉或不设=不限制
|
||||
# OPENCLAW_BROWSER_LAUNCH_TIMEOUT_MS=60000 # 单位:毫秒;慢机器时可开启
|
||||
```
|
||||
|
||||
四档运行模式的**实现与测试约定**见 [`ADAPTER.md`](ADAPTER.md);录屏管线见 [`RPA.md`](RPA.md)。这些内容**不要**写回 `.env.example` 注释。
|
||||
|
||||
**业务专属配置必须带技能前缀**(如 `DEMO_XXX`、`MY_SKILL_XXX`、`SKILL_DOWNLOAD_DIR`),**不要污染全局命名空间**(禁止 `SCRAPE_1688_*`、`RECEIVE_ORDER_*` 等跨技能前缀写入模板)。
|
||||
**业务专属配置必须带技能前缀**(如 `DEMO_XXX`、`MY_SKILL_XXX`、`SKILL_OUTPUT_DIR`),**不要污染全局命名空间**(禁止 `SCRAPE_1688_*`、`RECEIVE_ORDER_*` 等跨技能前缀写入模板)。
|
||||
|
||||
### 数据目录子路径(下载 / 导入 / 导出)
|
||||
### 数据目录子路径(输入 / 输出)
|
||||
|
||||
技能**自己写入**的文件默认在 `{JIANGCHANG_DATA_ROOT}/{USER_ID}/{slug}/` 下标准子目录(`downloads/`、`imports/`、`exports/` 等)。须通过 `util.runtime_paths.resolve_data_path()` 或 `get_*_dir()` 解析,**禁止** `os.path.abspath(config.get(...))` 相对 CWD。
|
||||
用户可见目录:文件浏览只有 **`input/`(上行)** 与 **`output/`(下行)**;另有可选 **`html/`** 供宿主扫描 HTML 报告页(见 `DATA_PATHS.md`)。技能写入的业务文件产出默认落在 `output/`;整页报告落在 `html/`;用户放入的待处理文件落在 `input/`。须通过 `util.runtime_paths.resolve_data_path()` 或 `get_input_dir()` / `get_output_dir()` / `get_html_dir()` 解析,**禁止** `os.path.abspath(config.get(...))` 相对 CWD。
|
||||
|
||||
- 权威说明(开发):[`DATA_PATHS.md`](DATA_PATHS.md)
|
||||
- `.env.example` 中路径类配置**留空或保持注释即用默认**;若写相对路径,必须相对 **skill 数据目录**,**禁止** `./outputs/...` 这类相对 workspace 的写法
|
||||
- 可选 env:`SKILL_DOWNLOAD_DIR`、`SKILL_IMPORT_DIR`、`SKILL_EXPORT_DIR` 等;若作为活跃 key 出现在配置管理中,同样须有用户向行头/行尾(见模板根 `.env.example`)
|
||||
- 可选 env:`SKILL_INPUT_DIR`、`SKILL_OUTPUT_DIR` 等;若作为活跃 key 出现在配置管理中,同样须有用户向行头/行尾(见模板根 `.env.example`)
|
||||
|
||||
### 录屏开关(`OPENCLAW_RECORD_VIDEO`)
|
||||
|
||||
- **模板默认 `OPENCLAW_RECORD_VIDEO=0`**,降低本地开发 / 调试成本(不启 ffmpeg、不生成 MP4)。
|
||||
- 需要**生产留证、合规记录、客户现场复现或排障**时,可在用户 `.env` 或进程环境变量中设为 `OPENCLAW_RECORD_VIDEO=1`。
|
||||
- **默认关闭不代表可以删除录屏接入**:RPA / 长任务代码仍须保留 `RpaVideoSession`、`video.add_step` 与 video artifact 写入 `result_summary`(见 `RPA.md` §5.3)。
|
||||
- **默认关闭不代表可以删除录屏接入**:RPA / 长任务代码仍须保留 `RpaVideoSession`、`video.add_step` 与 video artifact 写入 `finish()`(见 `RPA.md` §5.3)。
|
||||
- **已落盘的用户 `.env` 不会被模板整体覆盖**;老用户保留原值,技能升级时仅通过 `merge_missing_env_keys` **追加** `.env.example` 中新增的 key。
|
||||
|
||||
## 🚫 红线:敏感信息不进 `.env`
|
||||
@@ -167,7 +209,8 @@ config.get_float("STEP_DELAY_MIN", 1.0)
|
||||
### 空值、行尾注释与假默认(硬规则)
|
||||
|
||||
- **留空即未配置**:`KEY=` 或 `KEY= # 说明` 必须读成空字符串,再走技能自己的「自动 / ensure」逻辑;**禁止**把示例说明、`# 可填…` 当成真实配置值。
|
||||
- **需要 platform-kit ≥ 1.2.2**:旧版解析曾把「空值 + 行尾 `#`」误当成 `# 可填…` 这种假值。技能若依赖正确空值语义,须在 `SKILL.md` 声明 `platform_kit_min_version: "1.2.2"`(或更高)。
|
||||
- **需要 platform-kit ≥ 1.2.2**:旧版解析曾把「空值 + 行尾 `#`」误当成 `# 可填…` 这种假值。技能若依赖正确空值语义,须在 `SKILL.md` 声明不低于该版本。
|
||||
- **需要 platform-kit ≥ 1.2.11(本模板默认)**:RPA 有界就绪等待 `poll_until`、可选 `OPENCLAW_BROWSER_LAUNCH_TIMEOUT_MS`、health 中只读 `machine_power`。新技能直接声明 `"1.2.11"`。
|
||||
- **网页账号配置语义**(有则用):
|
||||
- `DEFAULT_ACCOUNT_ID`:仅**纯数字**视为账号主键;校验指定记录后仍必须取得该账号租约,不能只 `account get`
|
||||
- `DEFAULT_LOGIN_ID`:手机号 / 用户名等;留空则按平台 `ensure-web`;非数字 `DEFAULT_ACCOUNT_ID` 可兼容为 login_id,但不推荐
|
||||
@@ -187,8 +230,25 @@ config.get_float("STEP_DELAY_MIN", 1.0)
|
||||
- `python scripts/main.py doctor`:检查浏览器、Profile/账号、`.env` 落盘、设备等。
|
||||
- `python scripts/main.py setup`:初始化演示账号、首次落盘 `.env`。
|
||||
|
||||
## MySQL 云库(可选)
|
||||
|
||||
技能若走 MySQL 云库(匠厂 Host **≥ 6.9.28**),须同时满足:
|
||||
|
||||
1. 用户数据目录有 [`data-sources.json`](../references/DATA_SOURCES.md)(声明 `kind: mysql` 与 `connectionEnv`)
|
||||
2. `.env` 配置对应 DSN,模板默认为 `MYSQL_DSN`
|
||||
|
||||
```env
|
||||
# MySQL 云库连接(留空=本地 SQLite)
|
||||
MYSQL_DSN=mysql://user:password@host:3306/database_name
|
||||
```
|
||||
|
||||
**不要**把 DSN 写进 `data-sources.json`。技能 CLI 通过 `scripts/db/backend.py` 读取同一 key,与宿主 UI 保持一致。
|
||||
|
||||
完整契约见 [`../references/DATA_SOURCES.md`](../references/DATA_SOURCES.md)。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- `DATA_SOURCES.md` — 多数据源声明(SQLite 默认 / MySQL opt-in)
|
||||
- `RPA.md` — 三端 RPA 标准与各开关含义(开发向;勿写入 `.env` 注释)
|
||||
- `ACCOUNT_MANAGER.md` — 账号选择、Profile、租约、数据位置与验收
|
||||
- `ADAPTER.md` — `OPENCLAW_TEST_TARGET` 四档模式(开发向;勿写入 `.env` 注释)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 用户数据目录与文件路径黄金标准
|
||||
|
||||
> 本文是 skill **读写本地文件** 的权威约定。凡涉及下载、导入、导出、缓存、RPA 存证等路径,复制模板后须遵守本文;Agent 实现业务 skill 时也应先读本文。
|
||||
> 本文是 skill **读写本地文件** 的权威约定。凡涉及输入、输出、缓存、RPA 存证等路径,复制模板后须遵守本文;Agent 实现业务 skill 时也应先读本文。
|
||||
|
||||
---
|
||||
|
||||
@@ -30,21 +30,49 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
||||
|
||||
## 2. 标准子目录树
|
||||
|
||||
用户可见 · **文件目录**(数据管理「输入 / 输出」文件浏览器只挂这两个):
|
||||
|
||||
| 子目录 | 方向 | 用途 | 默认 helper |
|
||||
|--------|------|------|-------------|
|
||||
| `input/` | 上行 | 用户/Agent 放入的待处理材料(urls.txt、batch.json、CSV、产品表…) | `get_input_dir()` |
|
||||
| `output/` | 下行 | 技能/宿主交给用户拿走的结果(视频、回单、xlsx/csv/json…) | `get_output_dir()` |
|
||||
|
||||
用户可见 · **HTML 报告页**(宿主扫描挂到技能下,**不是** input/output 文件浏览器):
|
||||
|
||||
| 子目录 | 用途 | 默认 helper |
|
||||
|--------|------|-------------|
|
||||
| `html/` | 技能写出的静态 `.html` 报告;宿主一层扫描 `*.html`,侧栏预览 / 右键重新生成 | `get_html_dir()` |
|
||||
|
||||
内部 / 可选目录(**不上侧栏**):
|
||||
|
||||
| 子目录 | 用途 | 默认 helper |
|
||||
|--------|------|-------------|
|
||||
| `downloads/` | 从外部拉取的原始文件(视频、回单、PDF…) | `get_downloads_dir()` |
|
||||
| `imports/` | 用户/Agent 放入的待处理清单(urls.txt、batch.json、CSV) | `get_imports_dir()` |
|
||||
| `exports/` | 导出给用户的结果(xlsx/csv/json) | `get_exports_dir()` |
|
||||
| `uploads/` | 待发布到外部平台的 staging(可选) | `get_uploads_dir()` |
|
||||
| `cache/` | 可再生缓存(页面快照、解析中间结果) | `get_cache_dir()` |
|
||||
| `temp/` | 单次任务 scratch,任务结束可删 | `get_temp_dir()` |
|
||||
| `rpa-artifacts/` | RPA 失败截图、录屏中间产物 | `get_rpa_artifacts_dir(batch_id)` |
|
||||
| `videos/` | RPA 录屏成片 MP4 | `get_videos_dir()` |
|
||||
| `{skill_slug}.db` | SQLite | `get_db_path()` |
|
||||
| `.env` | 用户配置 | `config.get_env_file_path()` |
|
||||
| `{skill_slug}.db` | SQLite(默认本地库) | `get_db_path()` |
|
||||
| `data-sources.json` | 声明式数据源(可选;不配则宿主自动扫描 `.db`) | 用户目录根;格式见 [`../references/DATA_SOURCES.md`](../references/DATA_SOURCES.md) |
|
||||
| `.env` | 用户配置(含可选 `MYSQL_DSN`) | `config.get_env_file_path()` |
|
||||
|
||||
复制后若业务不需要某目录,可不创建;**但一旦写入该类文件,必须走对应 helper**。
|
||||
|
||||
**开发者硬规则:**
|
||||
|
||||
- 用户/Agent **放进来**的文件 → `input/`(`get_input_dir()`)
|
||||
- 技能/宿主 **交给用户拿走**的文件 → `output/`(`get_output_dir()`)
|
||||
- 可浏览的 **HTML 报告页** → `html/`(`get_html_dir()` / `service.html_page_service`);**不要**把整页报告塞进 `output/` 指望侧栏预览
|
||||
- 禁止再引入业务「文件浏览器」第三目录(如旧的 `downloads` / `imports` / `exports` 三分法);`html/` 是报告页扫描面,不是第三种文件浏览器
|
||||
- 缓存、临时、RPA 录屏用 `cache` / `temp` / `videos`,不要当成用户可见的「输出目录」
|
||||
|
||||
宿主约定(与本模板对齐):
|
||||
|
||||
- 工具栏「导入」**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(兼容)
|
||||
- 数据管理扫描两处 HTML(都只扫一层):安装包 `assets/html/pages/*.html` 是装完就有的壳子页;`{skill_data}/html/*.html` 是快照,「重新生成」调用 Action id **`render-html`**。页面里用 `jiangchang.call` 调本技能已声明的命令。详见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「HTML 页面报告」
|
||||
|
||||
---
|
||||
|
||||
## 3. 两类路径
|
||||
@@ -54,6 +82,7 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
||||
技能**创建或更新**的文件:下载结果、导出报表、失败截图、队列 sidecar 等。
|
||||
|
||||
- **默认**必须在 `{skill_data_dir}/<子目录>/` 下。
|
||||
- 用户可拿走的业务产出 → **`output/`**。
|
||||
- **必须**通过 `resolve_data_path()` 或 `get_*_dir()` 解析,**禁止** `os.path.abspath(config.get(...))` 相对 CWD。
|
||||
|
||||
### B. 用户显式输入(user-owned)
|
||||
@@ -61,7 +90,7 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
||||
用户在 CLI / Agent 对话中给出的**已有文件**绝对路径,例如「发布这个视频 `D:\素材\x.mp4`」。
|
||||
|
||||
- 技能**只读**,路径可以是用户磁盘任意位置。
|
||||
- 使用 `resolve_input_path()`:绝对路径原样;**相对路径**解析为 `{skill_data_dir}/imports/<相对路径>`,**不**相对 workspace/CWD。
|
||||
- 使用 `resolve_input_path()`:绝对路径原样;**相对路径**解析为 `{skill_data_dir}/input/<相对路径>`,**不**相对 workspace/CWD。
|
||||
|
||||
---
|
||||
|
||||
@@ -72,9 +101,8 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
||||
```ini
|
||||
# ── 数据目录子路径(见 development/DATA_PATHS.md)──
|
||||
# 相对路径相对于 {JIANGCHANG_DATA_ROOT}/{USER_ID}/{slug}/,不是 workspace
|
||||
# SKILL_DOWNLOAD_DIR=downloads/videos
|
||||
# SKILL_IMPORT_DIR=imports
|
||||
# SKILL_EXPORT_DIR=exports
|
||||
# SKILL_INPUT_DIR=input
|
||||
# SKILL_OUTPUT_DIR=output/videos
|
||||
# SKILL_UPLOAD_DIR=uploads
|
||||
# SKILL_CACHE_DIR=cache
|
||||
# SKILL_TEMP_DIR=temp
|
||||
@@ -83,8 +111,8 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| 未配置 | `{skill_data_dir}/{default_subdir}` |
|
||||
| 相对值 `downloads/videos` | `{skill_data_dir}/downloads/videos` |
|
||||
| 绝对值 `D:\nas\exports` | 高级用户/NAS 挂载,原样使用 |
|
||||
| 相对值 `output/videos` | `{skill_data_dir}/output/videos` |
|
||||
| 绝对值 `D:\nas\output` | 高级用户/NAS 挂载,原样使用 |
|
||||
| **禁止** `./outputs/...` | 会随 CWD 漂移,不得作为 `.env.example` 活跃配置行 |
|
||||
|
||||
业务专属路径 env key 须带 **`SKILL_` 前缀**(或技能 slug 前缀),避免污染全局命名空间。
|
||||
@@ -95,20 +123,20 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
||||
|
||||
```python
|
||||
from util.runtime_paths import (
|
||||
get_downloads_dir,
|
||||
get_imports_dir,
|
||||
get_input_dir,
|
||||
get_output_dir,
|
||||
resolve_data_path,
|
||||
resolve_input_path,
|
||||
list_resolved_data_paths,
|
||||
)
|
||||
|
||||
# 下载保存(A 类)
|
||||
dest = os.path.join(get_downloads_dir(), f"{vid}.mp4")
|
||||
# 下行产出(A 类)——视频、报表等
|
||||
dest = os.path.join(get_output_dir(), f"{vid}.mp4")
|
||||
|
||||
# 或带 env 覆盖
|
||||
out_dir = resolve_data_path("SKILL_DOWNLOAD_DIR", "downloads")
|
||||
out_dir = resolve_data_path("SKILL_OUTPUT_DIR", "output")
|
||||
|
||||
# CLI -i urls.txt(B 类,相对路径 → imports/)
|
||||
# CLI -i urls.txt(B 类,相对路径 → input/)
|
||||
manifest = resolve_input_path(args.input_path)
|
||||
|
||||
# health 诊断
|
||||
@@ -123,6 +151,8 @@ os.path.abspath(config.get("DOWNLOAD_OUTPUT_DIR"))
|
||||
|
||||
# ❌ .env.example 里写 ./outputs/videos
|
||||
DOWNLOAD_OUTPUT_DIR=./outputs/videos
|
||||
|
||||
# ❌ 再拆 downloads / imports / exports 三个用户目录
|
||||
```
|
||||
|
||||
---
|
||||
@@ -138,7 +168,7 @@ DOWNLOAD_OUTPUT_DIR=./outputs/videos
|
||||
- 失败截图、录屏中间文件:`rpa-artifacts/{batch_id}/`(见 [`RPA.md`](RPA.md) §5.1)
|
||||
- 录屏成片:`videos/`(见 [`RPA.md`](RPA.md) §5.3)
|
||||
|
||||
业务 `downloads/` 与 RPA `videos/` **分工不同**:前者是业务产出文件,后者是操作过程录屏。
|
||||
业务 `output/` 与 RPA `videos/` **分工不同**:前者是业务产出文件,后者是操作过程录屏。
|
||||
|
||||
---
|
||||
|
||||
@@ -148,6 +178,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. 相关文档
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
本文默认你开发的是当前最常见的一类业务 skill:
|
||||
|
||||
- 有明确的 `scripts/main.py` CLI 入口
|
||||
- 可能需要读写本地 SQLite(例如任务日志 `task_logs`)
|
||||
- 可能需要读写本地 SQLite(业务表按需创建,不是默认必有)
|
||||
- 可能需要调用兄弟技能或外部 HTTP / RPA
|
||||
- 业务逻辑主要放在 `scripts/service/`
|
||||
|
||||
@@ -74,6 +74,7 @@ your-skill/
|
||||
├─ CHANGELOG.md
|
||||
├─ requirements.txt
|
||||
├─ release.ps1
|
||||
├─ sync.ps1
|
||||
└─ SKILL.md
|
||||
```
|
||||
|
||||
@@ -85,6 +86,8 @@ your-skill/
|
||||
- `DEMO.md`:市场 **演示** Tab(`demo_video_url` + `demo_md`)
|
||||
- `CHANGELOG.md`:市场 **更新日志** Tab(按版本小节同步 `changelog`)
|
||||
- `SKILL.md`:面向 LLM / 平台的技能入口
|
||||
- `release.ps1`:发布到服务器(提交、tag、CI 加密包)
|
||||
- `sync.ps1`:把明文同步到本机宿主,供界面验收
|
||||
- `references/`:Agent 运行/编排/调用时渐进加载的补充上下文(如 CLI、SCHEMA)
|
||||
- `development/`:面向开发者与 AI 编程代理的开发资料、需求、测试与技术规范
|
||||
- `scripts/`:放真正的代码
|
||||
@@ -115,8 +118,8 @@ scripts/
|
||||
不写具体业务逻辑
|
||||
|
||||
- `db/`
|
||||
作用:SQLite 连接、建表、repository
|
||||
不写页面操作和业务编排
|
||||
作用:数据库连接(默认 SQLite;配 `MYSQL_DSN` 时为 MySQL)、建表、repository
|
||||
不写页面操作和业务编排;多数据源契约见 [`../references/DATA_SOURCES.md`](../references/DATA_SOURCES.md)
|
||||
|
||||
- `service/`
|
||||
作用:核心业务逻辑
|
||||
@@ -127,7 +130,7 @@ scripts/
|
||||
作用:常量、日志、路径、时间工具、通用帮助函数
|
||||
`util/logging_config.py` 是 `jiangchang_skill_core.unified_logging` 的**薄封装**,业务代码应通过它获取 logger
|
||||
|
||||
公共能力(config、logging、runtime_env、rpa、media_assets、video_session、runtime_diagnostics、activity/SRCP)从共享 runtime 的 `jiangchang-platform-kit>=1.2.2` import,**不得**在 `scripts/` 下保留 `jiangchang_skill_core/` 副本。
|
||||
公共能力(config、logging、runtime_env、rpa、media_assets、video_session、runtime_diagnostics、activity/SRCP)从共享 runtime 的 `jiangchang-platform-kit>=1.2.11` import,**不得**在 `scripts/` 下保留 `jiangchang_skill_core/` 副本。
|
||||
|
||||
## 3.2 开发 RPA 类 skill
|
||||
|
||||
@@ -170,9 +173,10 @@ scripts/
|
||||
技能根目录的 `requirements.txt` 是**标准文件**,用于声明本技能**特有** Python 三方依赖。
|
||||
|
||||
- **公共依赖**(`jiangchang-platform-kit`、`playwright`、config、runtime diagnostics、RPA 公共能力等)由**宿主共享 runtime** 提供,**不要**写入技能 `requirements.txt`。
|
||||
- `SKILL.md` 的 `metadata.openclaw.platform_kit_min_version`(当前 `1.2.2`)是运行契约/兼容性声明,供宿主安装与启用时校验,**不是** pip 依赖声明。
|
||||
- `SKILL.md` 的 `metadata.openclaw.platform_kit_min_version`(当前 `1.2.11`)是运行契约/兼容性声明,供宿主安装与启用时校验,**不是** pip 依赖声明。
|
||||
- 匠厂宿主安装/更新技能后,会将技能 `requirements.txt` 安装到共享 venv:`{JIANGCHANG_DATA_ROOT}/python-runtime/.venv`。
|
||||
- **不要**在业务代码中 `subprocess` / `pip install`;缺依赖由 `health` 报错,由宿主负责安装。
|
||||
- 根目录 `sync.ps1` 是开发者手动执行的本机同步,允许用 `uv pip install` 把本技能 `requirements.txt` 装进宿主共享 venv。业务代码和其它脚本仍然不要自己装包。
|
||||
- **版本约束尽量收窄**,降低多技能共享 venv 时的冲突风险。推荐范围写法:
|
||||
- `pkg>=1.2.0,<2`
|
||||
- 对**原生扩展 / 高风险依赖**(如 `chromadb`、`onnxruntime`)建议 pin 或窄范围,避免无上限:
|
||||
@@ -335,7 +339,7 @@ git remote -v
|
||||
|
||||
应该重点写:
|
||||
|
||||
- YAML frontmatter:`name`、`description`、`version`、`metadata.openclaw`(含 `slug`、`platform_kit_min_version`、`developer_ids` 等)
|
||||
- YAML frontmatter:`name`、`description`、`version`、`metadata.openclaw`(含 `slug`、`platform_kit_min_version`、`developer_ids`、可选 `brands` 等)
|
||||
- 何时触发本技能、CLI 入口摘要、运行契约要点
|
||||
- 指向 `references/`(Agent 调用)与 `development/`(开发规范)的分工说明
|
||||
|
||||
@@ -352,9 +356,9 @@ git remote -v
|
||||
| `DEMO.md` | 演示 | `demo_md` + frontmatter `demo_video_url` |
|
||||
| `CHANGELOG.md` | 更新日志 | 匹配当前 tag 版本的 `##` 小节 → `changelog` |
|
||||
|
||||
**`README.md`**:能力与场景说明。含 frontmatter `description`;写能做什么、场景、准备、如何告诉 Agent、结果、注意事项、FAQ。不要写开发教程、目录规范、release、requirements。
|
||||
**`README.md`(说明 Tab)**:能力、场景、前置条件、结果形态、注意事项、概念类 FAQ。含 frontmatter `description`(**不要**写「如何开始使用」)。**禁止**分步操作、Agent 话术块、「入口一/二」专章、逐步排障——这些属教程。**禁止**文首再写 `# 技能名`(页头已有);正文主分节用 `##`。完整契约见 [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md) §0.1 / §1。
|
||||
|
||||
**`TUTORIAL.md`**:第一次怎么用完的逐步操作。写「开始前 → 步骤 → 卡住怎么办」;不要写 CLI / 目录 / 开发规范。
|
||||
**`TUTORIAL.md`(教程 Tab)**:按 [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md) 写三入口结构(数据管理 ∥ 对话;定时有则教、无则 N/A)+ 主能力对照表 + 常见卡住。步骤必须与 `REQUIREMENTS` §0.1、`actions.json` 及宿主真实 UI 对齐;**禁止**把仅 `skill-detail` 的 Action 写成用户主路径。**禁止**文首 `# 技能名 · 使用教程`;正文最高用 `##`。不要写 CLI / 目录 / 开发规范。
|
||||
|
||||
**`DEMO.md`**:演示视频与补充说明。frontmatter 必须含 `demo_video_url`(无视频时用 `""`);正文可选。
|
||||
|
||||
@@ -407,6 +411,51 @@ metadata:
|
||||
- 一期只做“补授权”,不会因为你 later 修改数组而自动撤销旧授权
|
||||
- **首次正式 release 前**就应配好;配错或漏配时,CI 可能成功,但你在技能市场仍找不到技能
|
||||
|
||||
### 关于 `metadata.openclaw.brands`(OEM 市场,按需)
|
||||
|
||||
#### 目的
|
||||
|
||||
匠厂与华商、Lili、GDCM 等 OEM 客户端共用同一技能库,但技能市场目录按 **`market_code`** 过滤(客户端 Header `X-Market-Code`;缺省为 `jiangchang`)。
|
||||
|
||||
发布接口 `POST /api/skill/update` 会把 `metadata.openclaw.brands` 幂等写入 `skill_market_relations`(**只补不删**)。**未声明 `brands` 时仅补默认 `jiangchang`**,因此只写在 Gitea 的 `client-huashang` 组织下、却不写 `brands: [huashang]`,华商市场仍然搜不到该技能。
|
||||
|
||||
`brands` 与 `developer_ids` 正交:
|
||||
|
||||
| 字段 | 管什么 |
|
||||
|------|--------|
|
||||
| `brands` | 技能出现在哪些 OEM 市场目录 |
|
||||
| `developer_ids` | 技能不公开(`access_scope = 0`)时,哪些用户仍可见并可安装 |
|
||||
|
||||
#### 何时填写
|
||||
|
||||
- **仅匠厂验收 / 通用技能**:可省略(默认匠厂)。
|
||||
- **须在华商或其他 OEM 客户端出现**:必须显式列出对应编码(通常同时保留 `jiangchang` 以便主站也能看到)。
|
||||
|
||||
常见编码(小写机器码,非中文展示名):`jiangchang`、`huashang`、`lili`、`gdcm` 等。非法编码会导致发布接口 400,CI 失败。
|
||||
|
||||
#### 在 `SKILL.md` 中声明
|
||||
|
||||
```yaml
|
||||
metadata:
|
||||
openclaw:
|
||||
slug: your-skill-slug
|
||||
category: 通用
|
||||
developer_ids:
|
||||
- 12345
|
||||
brands:
|
||||
- jiangchang
|
||||
- huashang
|
||||
```
|
||||
|
||||
兼容别名(平台按优先级取第一个出现的键):`brands` → `oem_clients` → `client_keys`。模板与文档统一推荐写 **`brands`**。
|
||||
|
||||
约定:
|
||||
|
||||
- **可选字段**:未写或空列表 → 行为与历史一致,只挂 `jiangchang`
|
||||
- **一期只补不删**:从 `brands` 去掉某市场后,不会自动删除库中已有关联;若需下架请走运营/后台或后续平台能力
|
||||
- **不要**指望 Gitea 组织名、目录名 `client-*` 自动映射市场
|
||||
- 发布后在对应 OEM 客户端(须带正确 `X-Market-Code`)搜索验收;也可查库 `skill_market_relations`
|
||||
|
||||
## 7. 文档目录分工
|
||||
|
||||
当前规范 skill 的文档按读者拆分:
|
||||
@@ -448,10 +497,10 @@ metadata:
|
||||
宿主 Action 入口 manifest(可选;见 `references/ACTIONS.md`)
|
||||
|
||||
- `examples/`
|
||||
比如 `version` 输出示例、`log-get` 输出示例
|
||||
比如 `version` 输出示例
|
||||
|
||||
- `schemas/`
|
||||
比如 `skill-actions.schema.json`、日志记录、机读 JSON 的 schema
|
||||
比如 `skill-actions.schema.json`、机读 JSON 的 schema
|
||||
|
||||
不要把正式研发文档放到 `assets/`。
|
||||
用户市场四 Tab 进根 `README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md`;Agent 编排资料进 `references/`;开发规范进 `development/`。
|
||||
@@ -466,7 +515,7 @@ metadata:
|
||||
也就是说,`cli/app.py` 的职责是:
|
||||
|
||||
1. 打印帮助
|
||||
2. 定义 `run / logs / log-get / health / version / init-db`(及业务子命令)
|
||||
2. 定义 `run / health / version / init-db`(及业务子命令)
|
||||
3. 把参数转交给 `service.task_service`(或薄适配后再进 domain service)
|
||||
|
||||
不要在 `cli/app.py` 里直接写:
|
||||
@@ -505,7 +554,7 @@ assets/actions.json → placements / bind / executionProfile / entry
|
||||
|
||||
`service` 层是**关键业务日志**的落点,须遵循 [`LOGGING.md`](LOGGING.md):
|
||||
|
||||
- `cmd_run` 及主任务入口必须:`log.info("task_start ...")`、`log.info("task_log_saved ...")`;失败路径必须 `log.exception("task_failed ...")`。
|
||||
- `cmd_run` 及主任务入口必须:`log.info("task_start ...")`;失败路径必须 `log.exception("task_failed ...")`。
|
||||
- 外部 API、兄弟技能、RPA 操作须记录**开始 / 结束 / 耗时 / 状态**(`external_call_start` / `external_call_done elapsed_ms=...`)。
|
||||
- 长任务 / RPA 须配合 `activity.emit` 或 `RpaVideoSession.add_step` 输出用户可见进度;`cmd_run` 须 `job_context` + 各出口 `finish`(见 [`LOGGING.md`](LOGGING.md) §2.5)。
|
||||
- 使用 `from util.logging_config import get_skill_logger`,**不要**用 `print` 替代 logger 做排障日志。
|
||||
@@ -526,10 +575,11 @@ assets/actions.json → placements / bind / executionProfile / entry
|
||||
如果你的 skill 需要记录日志或本地状态,建议:
|
||||
|
||||
- `db/connection.py`
|
||||
只做连接和建表
|
||||
只做连接和建表(模板默认只建 `_jiangchang_*` 元数据表;业务表按需 CREATE)
|
||||
**业务表之间禁止 SQLite `FOREIGN KEY` / `REFERENCES`**:只存业务 id,存在性 / 级联 / 删除在 repository/service 处理(见 [`references/SCHEMA.md`](../references/SCHEMA.md)「表与表之间:禁止 SQLite 外键」)
|
||||
|
||||
- `db/task_logs_repository.py`
|
||||
只做增删查改(模板默认表:`task_logs`)
|
||||
- `db/<domain>_repository.py`
|
||||
只做业务表增删查改(有业务表才建;模板不再默认提供任务审计表)
|
||||
- `db/display_metadata.py`
|
||||
创建 `_jiangchang_tables` / `_jiangchang_columns` 并幂等写入中文展示名(见 `references/SCHEMA.md`)
|
||||
- `db/display_metadata_validator.py`
|
||||
@@ -543,17 +593,20 @@ assets/actions.json → placements / bind / executionProfile / entry
|
||||
- 打印用户提示
|
||||
- 拼接业务流程
|
||||
|
||||
## 11.5 Skill Action manifest(可选)
|
||||
## 11.5 Skill Action manifest(按入口定位挂载)
|
||||
|
||||
若技能需要出现在宿主**数据管理按钮**、**技能详情**、**定时任务**或 **Agent 直接调用**中,提供 `assets/actions.json`。
|
||||
先读 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md):对话 / 数据管理 / 定时任务**平级**;哪个主能力出现在哪个入口,由本技能 UX 在 `REQUIREMENTS` 入口定位表中定义。
|
||||
|
||||
若定位表要求出现在宿主**数据管理按钮**、**技能详情**、**定时任务**或 **Agent 直接调用**中,提供 `assets/actions.json` 并写入对应 `placements`。
|
||||
|
||||
- **产品入口与何时走哪个**见 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md)
|
||||
- **运行时金标准**见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md)
|
||||
- 字段契约见 [`references/ACTIONS.md`](../references/ACTIONS.md)(`placements` / `bind.tables` / `executionProfile` 正交)
|
||||
- JSON Schema 见 [`assets/schemas/skill-actions.schema.json`](../assets/schemas/skill-actions.schema.json)
|
||||
- 模板最小示例仅暴露 `health` / `version` / `config-path`(均显式 `executionProfile: "sync"`;**不**自动加 toolbar);CLI 另含 `init-db` 供宿主静默建库(不必做成 Action)
|
||||
- 含 `toolbar` 的 Action:**必须**声明合法 `bind.tables`(`POLICY-SKILL-ACTION-003`)
|
||||
- 含浏览器 RPA / 长耗时的业务技能:**必须**增加至少一条 `"executionProfile": "async"` 且 `placements` 含 `agent` 的 action(`POLICY-SKILL-ACTION-001`)
|
||||
- 没有宿主直接操作需求时,**可以删除** `actions.json`;删除后勿保留失效引用
|
||||
- 入口定位表确认无任何宿主结构化入口需求时,**可以删除** `actions.json`;删除后勿保留失效引用
|
||||
- scaffold **不会**移除 Schema 中的 `bind` 支持,**不会**生成平台业务 Action 或业务表名;仅替换占位 slug
|
||||
|
||||
宿主执行方式:
|
||||
@@ -586,6 +639,7 @@ cmd_stats() # 只读 sync:JSON,无 job_context
|
||||
- 批量数量用 **`pickCount` / `--pick N` 参数**(任意正整数),不写死条数。
|
||||
- **顺序执行、不并发**同一浏览器 Profile / account lease。
|
||||
- **同步数据**与**宿主表导出**职责分离:同步不写 CSV/Excel;表导出交给数据管理。
|
||||
- **HTML 页面报告**(可选):壳子页放 `assets/html/pages/*.html`,装完即进侧栏;快照才写入 `html/` + Action `render-html`。页面用 `jiangchang.call` 调已声明命令。契约见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「HTML 页面报告」(含**宿主预览布局**底线)、[`DATA_PATHS.md`](DATA_PATHS.md)。快照可从 `assets/html/example-report.html` 复制壳,不要把该文件放进 `pages/`。
|
||||
- 详情见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md);契约样例见 `tests/samples/test_sync_contract.py.sample`、`tests/samples/test_action_core_contract.py.sample`。
|
||||
|
||||
### 队列 + pick(可选)
|
||||
@@ -666,18 +720,10 @@ python scripts/main.py <your-command> -h
|
||||
如果你的 skill 需要本地日志或数据库,再继续:
|
||||
|
||||
```bash
|
||||
python scripts/main.py <your-log-command>
|
||||
python scripts/main.py <your-detail-command> <id>
|
||||
python scripts/main.py <your-read-command>
|
||||
```
|
||||
|
||||
如果你沿用了模板中的通用任务日志骨架,那么这里可以具体对应成:
|
||||
|
||||
```bash
|
||||
python scripts/main.py logs
|
||||
python scripts/main.py log-get 1
|
||||
```
|
||||
|
||||
除 `health` / `version` / `logs` 外,执行一次 `run`(或你的主任务命令)后,应确认**统一日志文件**已生成并可检索:
|
||||
执行一次 `run`(或你的主任务命令)后,应确认**统一日志文件**已生成并可检索:
|
||||
|
||||
```text
|
||||
{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/logs/jiangchang.log
|
||||
@@ -705,6 +751,33 @@ python scripts/main.py <your-command>
|
||||
|
||||
详见 `TESTING.md`。
|
||||
|
||||
## 14.6 同步到本机宿主
|
||||
|
||||
`sync.ps1` 和 `release.ps1` 并排,都在技能根目录执行。`release.ps1` 发布到服务器并触发加密打包;`sync.ps1` 把当前明文覆盖到本机已安装的技能目录,用来在宿主里验收,不必每次都走服务器构建。
|
||||
|
||||
在技能根目录:
|
||||
|
||||
```powershell
|
||||
.\sync.ps1
|
||||
.\sync.ps1 -DryRun
|
||||
.\sync.ps1 -SkipDeps
|
||||
.\sync.ps1 -ForceDeps
|
||||
.\sync.ps1 -SkipInitDb
|
||||
```
|
||||
|
||||
脚本会:
|
||||
|
||||
- 按 `SKILL.md` 的 slug 镜像到 `%USERPROFILE%\.openclaw\skills\<slug>`,排除 `.git` 等开发目录
|
||||
- 去掉安装目录里残留的 `pyarmor_runtime_*`
|
||||
- 把 `requirements.txt` 装进宿主共享 venv(与宿主安装技能同一条路径)。依赖未变时可跳过;`-SkipDeps` 只跳过安装,仍会跑 `init-db`
|
||||
- 用宿主 `settings.json` 里的当前登录用户跑 `scripts\main.py init-db`。没有登录用户时停止,不会猜用户 ID
|
||||
|
||||
同步完成后:先停掉该技能正在跑的任务,退出资料管理再打开。不要点技能市场的「更新」,那会用加密包装掉这份明文。
|
||||
|
||||
技能至少要从市场安装过一次,宿主才会启用它。`sync.ps1` 覆盖的是已经装上的那一份。市场上的「最新版本」仍只在 `release.ps1` 的 CI 成功后变化;本机「当前版本」来自这次同步写进去的 `SKILL.md`。
|
||||
|
||||
`sync.ps1` 须为 UTF-8 无 BOM,且不要写中文:Windows PowerShell 5.1 会把无 BOM 的中文脚本解析错。
|
||||
|
||||
## 15. 发布到正式环境验证
|
||||
|
||||
### release workflow 通道
|
||||
@@ -723,9 +796,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)(或注明由既有测试覆盖)。
|
||||
|
||||
@@ -825,9 +904,14 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
|
||||
- 安装后状态正常
|
||||
- 不会出现缺文件、缺入口或安装失败的问题
|
||||
|
||||
### 第七步:按声明做宿主多入口验收(已声明的入口都要测;须看用户体验)
|
||||
### 第七步:按入口定位与声明做宿主多入口验收(须看用户体验)
|
||||
|
||||
安装完成后,不要只停留在“已安装”状态,也**不要**只在本地 mock 通就认为完成。宿主侧栏有多条与技能相关的入口;**按本技能 `assets/actions.json` 的 `placements` / `executionProfile` 声明逐项自测**(契约见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md)、[`../references/ACTIONS.md`](../references/ACTIONS.md))。未声明的入口可以跳过;**已声明却测不到,视为验收失败**。不要只测对话、忽略其它入口。
|
||||
安装完成后,不要只停留在“已安装”状态,也**不要**只在本地 mock 通就认为完成。对话任务 / 数据管理 / 定时任务在宿主上**平级**(见 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md))。先核对本技能 `REQUIREMENTS` **入口定位表**,再按 `assets/actions.json` 的 `placements` / `executionProfile` 对「定位为挂」的入口逐项自测(字段契约见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md)、[`../references/ACTIONS.md`](../references/ACTIONS.md))。
|
||||
|
||||
- 定位为「挂」却未声明或测不到 → **验收失败**
|
||||
- 定位为「不挂」且写了理由 → 可跳过该入口
|
||||
- **禁止**从未做入口定位、却用「未声明所以跳过」交差
|
||||
- 不要只测对话、忽略定位表中已挂的其它入口
|
||||
|
||||
**用户体验(与功能入口并列,后台通了不算完):**
|
||||
|
||||
@@ -835,15 +919,16 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
|
||||
- 长任务(async)是否有可见进度,能否取消或等待说明
|
||||
- 网页 RPA 是否有头运行;需登录/验证码时是否停下等人,而不是静默失败
|
||||
- 数据管理:表名/字段中文、按钮文案是否清楚;结果是否写回用户看得见的地方
|
||||
- 市场说明 / 教程是否与真实行为一致
|
||||
- 市场说明 / 教程是否与真实行为一致;**说明 Tab 不含分步操作**,教程覆盖已挂入口(见 [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md))
|
||||
|
||||
| 宿主入口(侧栏 / 界面) | 技能侧如何挂上 | 建议验收什么 |
|
||||
|-------------------------|----------------|--------------|
|
||||
| **新建任务**(对话 Agent) | `placements` 含 `agent`;或短查询走共享 Python CLI | 自然语言能触发主流程;长任务 / RPA 须走 `run_skill_action`,禁止 bash 干等 |
|
||||
| **数据管理 · 表顶栏** | `placements` 含 `toolbar`(须合法非空 `bind.tables`) | 左侧能看到本技能库表;表顶栏出现按钮;点按行为符合预期 |
|
||||
| **数据管理 · 行内** | `placements` 含 `row`(建议 `bind.tables` + `bind.inputMapping` 预填主键) | 行内操作链接可见(需有主键列);点按后入参/行为正确 |
|
||||
| **数据管理 · HTML 报告** | 壳子页放 `assets/html/pages/*.html`;快照写出 `html/*.html` 并提供 `render-html` | 壳子装完即现、右键无「重新生成」;快照可重新生成;页面 `jiangchang.call` 能拿到 JSON |
|
||||
| **数据管理 · 行级** | `placements` 含 `row`(建议 `bind.tables` + `bind.inputMapping` 预填主键) | 操作列和/或行右键(及可选「⋯」)出现本技能行级 Action(需有主键列);点按后入参/行为与壳无关、均正确 |
|
||||
| **数据管理 · 批量** | `placements` 含 `batch` | 勾选行后批量按钮可用;无勾选时禁用;批量行为正确 |
|
||||
| **定时任务** | `placements` 含 `cron`(创建时选「技能直调」) | 能选到本技能 Action、保存并触发;到点或「立即运行」行为正确 |
|
||||
| **定时任务** | `placements` 含 `cron`(创建时选「技能直调」) | 能选到本技能 Action、保存并触发;到点或「立即运行」行为正确;`selectionMode: optional` 时参数区只见数量字段、不见空「编号列表」 |
|
||||
| **任务中心** | 不是 placement;由 `executionProfile: "async"` 决定是否进 Job | 长任务出现进度 / 可取消;来源标签与触发入口一致 |
|
||||
| **技能市场 → 技能详情** | `placements` 含 `skill-detail`(详情直调) | **必验**安装与四 Tab。详情页「技能直调」按钮随宿主版本上线;**已声明则须在具备该 UI 的宿主上完成按钮自测**(与其它入口同等对待,不要只靠 Agent) |
|
||||
|
||||
@@ -871,21 +956,23 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
|
||||
每个新 skill 发布前,建议技术人员逐条确认:
|
||||
|
||||
- [ ] 目录结构符合当前模板
|
||||
- [ ] slug 符合 [`NAMING.md`](NAMING.md)(verb-noun-platform)
|
||||
- [ ] slug 符合 [`NAMING.md`](NAMING.md)(动词-名词,平台可选;不要用 `default` 占位)
|
||||
- [ ] 目录名、`SKILL.md` slug、`constants.SKILL_SLUG` 三者一致
|
||||
- [ ] `SKILL.md` 中 slug、名称、描述都已替换
|
||||
- [ ] `SKILL.md` 的 `developer_ids` 已换成**本人**匠厂用户 ID(设置 → 用户信息 → 复制),不是模板示例 ID
|
||||
- [ ] 若须在华商等 OEM 市场出现:`SKILL.md` 的 `brands` 已声明(如 `jiangchang` + `huashang`);未写则仅匠厂;不要用 Gitea 组织名代替
|
||||
- [ ] `scripts/util/constants.py` 已修改
|
||||
- [ ] `../references/CLI.md` 示例命令已改成真实命令
|
||||
- [ ] `service` 下的核心业务文件(如 `task_service.py`)已按领域改名并实现
|
||||
- [ ] 没有残留旧平台名
|
||||
- [ ] `health` / `version` 可运行
|
||||
- [ ] `.env.example` 注释已按 [`CONFIG.md`](CONFIG.md)「行头 = 是什么、行尾 = 怎么填」写全(面向配置管理用户;无 `见 development/...` 等开发文档指向)
|
||||
- [ ] 业务必填项已在 `.env.example` 用独立一行 `# @required` 标记(见 [`CONFIG.md`](CONFIG.md) §必填项);未标则宿主不显示红色 `*`
|
||||
- [ ] `.gitignore` 生效,没有把 `__pycache__` 提交进去
|
||||
- [ ] `release.ps1` 存在(发布前会自动跑 `python tests/run_tests.py -v`;失败不得打 tag)
|
||||
- [ ] `.github/workflows/release_skill.yaml` 存在
|
||||
- [ ] `python tests/run_tests.py -v` 全部通过(含 `test_development_policy_guard.py`)
|
||||
- [ ] 主任务入口有关键日志事件词:`task_start`、`task_log_saved`;失败路径 `task_failed error_code=` + `logger.exception`/`warning`(见 [`LOGGING.md`](LOGGING.md) §0、§3)
|
||||
- [ ] 主任务入口有关键日志事件词:`task_start`;失败路径 `task_failed error_code=` + `logger.exception`/`warning`(见 [`LOGGING.md`](LOGGING.md) §0、§3)
|
||||
- [ ] RPA / 域模块(`scripts/service/<domain>/` 等)非零文件日志;`print(ERROR:)` 旁有 logger(`POLICY-LOGGING-006`~`008`)
|
||||
- [ ] 本地跑一次主任务后,统一日志能 grep 到 §3.4 最低可验收串(不只 `cli_start`)
|
||||
- [ ] 日志不泄露 password / token / cookie 等敏感明文(见 [`LOGGING.md`](LOGGING.md))
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
| 通道 | 给谁看 | 能否单独当排障依据 |
|
||||
|------|--------|-------------------|
|
||||
| 统一文件日志 `jiangchang.log` | 运维 / 离线打包 | **必须能单独定位到失败阶段与 error_code** |
|
||||
| Activity / 任务中心 `emit` | 用户进度 | **不能**替代文件日志 |
|
||||
| Activity / 任务中心 `emit` / `finish` | 用户进度与终态 | **不能**替代文件日志 |
|
||||
| `print` / stdout | CLI 一眼结果 | **禁止**作为唯一失败记录 |
|
||||
| `task_logs` | 终态审计 | 摘要层;**不能**替代逐步 trace |
|
||||
| 业务表 | 用户可见业务结果 | 有业务才建;**不是**默认排障通道 |
|
||||
|
||||
硬规定:
|
||||
|
||||
@@ -48,7 +48,7 @@ OpenClaw 技能常在客户电脑上异步执行,且大量依赖 RPA、浏览
|
||||
|
||||
## 2. 日志分层
|
||||
|
||||
技能日志分四层,职责互不替代:
|
||||
技能日志分三层,职责互不替代:
|
||||
|
||||
### 2.1 统一文件日志(开发 / 运维排查)
|
||||
|
||||
@@ -122,17 +122,35 @@ RPA 等待用 `interruptible_sleep`,**不要**裸 `asyncio.sleep`(否则暂
|
||||
|
||||
Journal 会写入 `type: lifecycle` 事件(`paused` / `resumed` / `stopping`),供宿主任务中心显示状态。
|
||||
|
||||
**宿主中断(`stop`)与 `JobStopped`(必读):**
|
||||
|
||||
### 2.3 `task_logs`(任务结果审计)
|
||||
- 用户在任务中心点「中断」时,宿主写入 `control.json` `command=stop`,并**等待技能进程协作退出**(超时后才可能强杀)。
|
||||
- 技能在 `emit` / `checkpoint` / `interruptible_sleep` / `rpa_step` 等闸门处会抛出 `JobStopped`;业务代码须让其冒泡到 `job_context` / 外层 `finally`,由技能**自己**释放租约、关闭浏览器等资源。
|
||||
- **禁止**用裸 `except Exception:` 包住上述闸门调用。`JobStopped` 是 `Exception` 子类,会被吞掉,导致 UI 已停、进程仍占账号租约,下次采集必 `LEASE_CONFLICT`。
|
||||
- 若必须兜底「emit 写 Journal 失败」,正确写法:
|
||||
|
||||
**来源**:`db/task_logs_repository.save_task_log`
|
||||
```python
|
||||
from jiangchang_skill_core.activity import JobStopped, emit
|
||||
|
||||
- 适合 `python scripts/main.py logs` / `log-get <id>` 查询。
|
||||
- 记录任务最终状态、`result_summary`(可含 `video_path`、`video_log` 等)。
|
||||
- 面向「这次任务成功还是失败、摘要是什么」,不是逐步 trace。
|
||||
- **鉴权失败与未捕获异常也必须写入 `task_logs`**,保证 `logs` / `log-get` 排查链不断(见模板 `cmd_run` 示范)。
|
||||
try:
|
||||
emit("正在处理…", skill=SKILL_SLUG, stage="fetch")
|
||||
except JobStopped:
|
||||
raise
|
||||
except Exception:
|
||||
log.debug("emit_failed", exc_info=True)
|
||||
```
|
||||
|
||||
表结构与字段见 [`references/SCHEMA.md`](../references/SCHEMA.md)。
|
||||
- 宿主**不会**按技能类型统一代收 account-manager 租约(许多技能根本不用账号管理)。强杀后的残留靠租约 TTL / 数据管理手工 force-release;主路径必须是技能自清。
|
||||
|
||||
### 2.3 可观测性三分法(禁止 task_logs)
|
||||
|
||||
| 信息类型 | 去哪里 | 技能怎么做 |
|
||||
|----------|--------|------------|
|
||||
| **过程**(步骤、异常栈、排查) | `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(用户可见动作与存证)
|
||||
|
||||
@@ -140,7 +158,7 @@ Journal 会写入 `type: lifecycle` 事件(`paused` / `resumed` / `stopping`
|
||||
|
||||
- video artifact 是**标准可启用能力**,**不保证默认每次生成 MP4**(模板默认 `OPENCLAW_RECORD_VIDEO=0`)。
|
||||
- **`OPENCLAW_RECORD_VIDEO=0`**:不生成最终录屏,但 RPA / 长任务仍须保留 `RpaVideoSession`、`video.add_step` 与 Activity 进度同步。
|
||||
- **`OPENCLAW_RECORD_VIDEO=1`**:`result_summary` 应包含 `video_path`、`raw_video`、`video_log`、`video_warnings` 等字段;CLI 可打印录屏路径与诊断。
|
||||
- **`OPENCLAW_RECORD_VIDEO=1`**:`finish()` 字段应包含 `video_path`、`raw_video`、`video_log`、`video_warnings` 等;CLI 可打印录屏路径与诊断。
|
||||
- 用户可见的中文步骤(「打开登录页」「点击提交」)。
|
||||
- 失败截图:`{数据目录}/rpa-artifacts/{batch_id}/...`(受 `OPENCLAW_ARTIFACTS_ON_FAILURE` 控制,默认开)
|
||||
|
||||
@@ -161,7 +179,6 @@ Journal 会写入 `type: lifecycle` 事件(`paused` / `resumed` / `stopping`
|
||||
| `entitlement_ok` / `entitlement_failed` | info / warning | 鉴权结果 |
|
||||
| `item_start` / `item_done` | info | 批量逐条时;单条可用 `task_start`/`task_success` 代替 |
|
||||
| `task_success` 或 `task_failed` | info / warning\|exception | 终态;失败须含 `error_code=` |
|
||||
| `task_log_saved` | info | 写入 `task_logs` 后 |
|
||||
|
||||
### 3.2 RPA / 桌面 / 浏览器额外最低集
|
||||
|
||||
@@ -198,7 +215,6 @@ rpa_start stage=open_window
|
||||
rpa_start stage=locate_control
|
||||
rpa_start stage=submit
|
||||
task_failed error_code=ELEMENT_NOT_FOUND ...
|
||||
task_log_saved status=failed
|
||||
```
|
||||
|
||||
成功路径则出现 `task_success` 而非 `task_failed`。若只有 `cli_start` → **不合格**。
|
||||
@@ -224,7 +240,7 @@ task_log_saved status=failed
|
||||
|
||||
| 字段 | 含义 |
|
||||
|------|------|
|
||||
| `task_type` | 任务类型(与 `task_logs.task_type` 对齐) |
|
||||
| `task_type` | 任务类型 |
|
||||
| `target_id` | 目标标识(平台、店铺、报表类型等) |
|
||||
| `input_id` | 输入记录 ID |
|
||||
| `batch_id` | 批次 / 录屏批次 |
|
||||
@@ -251,7 +267,7 @@ batch_progress current=7 total=20 target_id=store-001
|
||||
|
||||
| 级别 | 使用场景 |
|
||||
|------|----------|
|
||||
| **info** | 关键阶段、成功路径、外部调用开始/结束、批量进度、`task_log_saved` |
|
||||
| **info** | 关键阶段、成功路径、外部调用开始/结束、批量进度 |
|
||||
| **warning** | 可恢复问题、重试、跳过、人工介入、部分失败、鉴权失败(业务可预期) |
|
||||
| **exception** | 异常路径;**必须**用 `log.exception(...)` 保留 traceback |
|
||||
| **debug** | 低频诊断;**默认不依赖 debug 才能排查问题** |
|
||||
@@ -264,7 +280,7 @@ batch_progress current=7 total=20 target_id=store-001
|
||||
|
||||
### 禁止记录原始值
|
||||
|
||||
以下字段**不得**以明文出现在日志、Activity、task_logs 的 `error_msg` / `result_summary`、stdout:
|
||||
以下字段**不得**以明文出现在日志、Activity、`finish()` 字段、stdout:
|
||||
|
||||
- `password`、`token`、`cookie`、`secret`、`api_key`、`authorization`
|
||||
- 验证码、动态口令全文
|
||||
@@ -318,8 +334,6 @@ def cmd_run(target=None, input_id=None):
|
||||
"erp", "submit", elapsed_ms, "ok",
|
||||
)
|
||||
|
||||
tlr.save_task_log(..., status="success", ...)
|
||||
log.info("task_log_saved task_type=%s status=success", task_type)
|
||||
finish(status="success", message="任务完成", skill=SKILL_SLUG)
|
||||
return 0
|
||||
except Exception:
|
||||
@@ -328,7 +342,6 @@ def cmd_run(target=None, input_id=None):
|
||||
task_type, target, input_id,
|
||||
)
|
||||
emit("任务失败,已记录诊断信息", type="warn", skill=SKILL_SLUG, stage="run")
|
||||
tlr.save_task_log(..., status="failed", error_msg="...", ...)
|
||||
finish(status="failed", message="任务执行异常", skill=SKILL_SLUG)
|
||||
return 1
|
||||
```
|
||||
@@ -342,11 +355,10 @@ def cmd_run(target=None, input_id=None):
|
||||
客户现场或测试机排障,建议按以下顺序(由快到慢、由用户可见到运维细节):
|
||||
|
||||
1. **CLI stdout** — 是否有 `ERROR:<CODE>:`、`HINT:`、模板提示文案。
|
||||
2. **`python scripts/main.py logs`** — 最近任务是否写入 `task_logs`、status 与 `error_msg`。
|
||||
3. **`python scripts/main.py log-get <id>`** — 单条 JSON,含 `result_summary`(video 路径等)。
|
||||
4. **统一日志文件** — `{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/logs/jiangchang.log`(grep `trace_id`、`task_failed`)。
|
||||
5. **Run Journal** — `{JIANGCHANG_DATA_ROOT}/.jiangchang/runs/{job_id}.jsonl`(宿主 UI 进度来源)。
|
||||
6. **RPA artifacts** — `rpa-artifacts/` 失败截图;若 `OPENCLAW_RECORD_VIDEO=1` 还有录屏 MP4、`video_log`。未开启录屏时可能只有截图或日志,需要完整录屏请在用户 `.env` 中设置 `OPENCLAW_RECORD_VIDEO=1`。
|
||||
2. **任务中心 / Run Journal** — `{JIANGCHANG_DATA_ROOT}/.jiangchang/runs/{job_id}.jsonl`(进度与 `finish` 终态)。
|
||||
3. **统一日志文件** — `{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/logs/jiangchang.log`(grep `trace_id`、`task_failed`)。
|
||||
4. **业务表** — 宿主数据管理中该技能自己的业务结果(有才建)。
|
||||
5. **RPA artifacts** — `rpa-artifacts/` 失败截图;若 `OPENCLAW_RECORD_VIDEO=1` 还有录屏 MP4、`video_log`。未开启录屏时可能只有截图或日志,需要完整录屏请在用户 `.env` 中设置 `OPENCLAW_RECORD_VIDEO=1`。
|
||||
|
||||
长时间无 stdout **不代表卡死**;先看 Run Journal / Activity,再看统一日志。
|
||||
|
||||
@@ -359,8 +371,8 @@ def cmd_run(target=None, input_id=None):
|
||||
| [`RUNTIME.md`](RUNTIME.md) | 统一日志能力来源、`util.logging_config` 薄封装、数据根约定 |
|
||||
| [`TESTING.md`](TESTING.md) | 日志相关 hard policy 测试(`test_development_policy_guard.py`) |
|
||||
| [`RPA.md`](RPA.md) | RPA step、`RpaVideoSession`、video / artifacts 标准 |
|
||||
| [`SCHEMA.md`](../references/SCHEMA.md) | `task_logs` 表结构与查询命令 |
|
||||
| [`CLI.md`](../references/CLI.md) | `logs` / `log-get` 命令与手工排查入口 |
|
||||
| [`SCHEMA.md`](../references/SCHEMA.md) | 业务表结构与展示元数据 |
|
||||
| [`CLI.md`](../references/CLI.md) | 最小命令与手工排查入口 |
|
||||
| [`CONFIG.md`](CONFIG.md) | 敏感配置不进日志;`.env` 边界 |
|
||||
| [`DEVELOPMENT.md`](DEVELOPMENT.md) | `service/` 层日志职责与发布前检查清单 |
|
||||
| [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) | 任务中心 / async Job 与 `job_context` 边界 |
|
||||
|
||||
@@ -13,23 +13,24 @@
|
||||
| ❌ **不修改** | 其他仓库(尤其 `account-manager`)不在本文约束范围内 |
|
||||
| ✅ **模板占位** | 模板本体的 `your-skill-slug` 占位必须继续通过全部测试 |
|
||||
|
||||
**新技能勿仿**历史 2 段无平台形态(如 `reconcile-finance`)或不符合 verb-noun-platform 的旧 repo 名;校验器对历史 repo **不追溯**,但规范中明确标注为反模式。
|
||||
**新技能**使用「动词-名词」,外部平台可选。不要再为了凑段数追加 `default`。校验器不追溯已上线仓库;已经发布、末尾是 `default` 的技能不用改名。
|
||||
|
||||
---
|
||||
|
||||
## 2. 命名格式
|
||||
|
||||
标准形态(约 95% 新技能):
|
||||
标准形态:
|
||||
|
||||
```text
|
||||
{verb}-{noun-phrase}-{platform}
|
||||
{verb}-{noun-phrase}
|
||||
{verb}-{noun-phrase}-{platform} # 仅当技能绑定外部平台时
|
||||
```
|
||||
|
||||
| 部分 | 规则 |
|
||||
|------|------|
|
||||
| **verb** | 1 个词,来自 [动词白名单](#5-动词白名单) |
|
||||
| **noun-phrase** | 1~3 个词(按 `-` 分段),描述业务对象 |
|
||||
| **platform** | 1 个词,来自 [平台白名单](#6-平台白名单);可含连字符(如 `single-window`) |
|
||||
| **platform** | **可选**。没有抖音、小红书、亚马逊这类外部系统时省略。有平台时 1 个词,来自 [平台白名单](#6-平台白名单);可含连字符(如 `single-window`) |
|
||||
|
||||
**展示层分工不变:**
|
||||
|
||||
@@ -44,8 +45,8 @@
|
||||
|
||||
| 约束 | 值 |
|
||||
|------|-----|
|
||||
| 格式 | `{verb}-{noun-phrase}-{platform}`(标准型,默认) |
|
||||
| 总段数(按 `-` 分割) | **3~5 段**(标准型);scope 型见 [§4](#4-三类合法形态) |
|
||||
| 格式 | `{verb}-{noun-phrase}`;有外部平台时才加 `-{platform}` |
|
||||
| 总段数(按 `-` 分割) | **2~5 段** |
|
||||
| 总长度 | **≤ 48 字符** |
|
||||
| 字符集 | 小写字母、数字、连字符;kebab-case |
|
||||
| 禁止 | 中文 slug、下划线、**平台放最前**(如 `amazon-download-xxx`) |
|
||||
@@ -56,12 +57,26 @@
|
||||
|
||||
## 4. 三类合法形态
|
||||
|
||||
### 4.1 标准型(95%)
|
||||
### 4.1 无平台(常见)
|
||||
|
||||
`verb-noun-platform` 或 `verb-noun-noun-platform`(3~5 段)
|
||||
`verb-noun` 或 `verb-noun-noun`(2~4 段)
|
||||
|
||||
- 第 1 段 ∈ 动词白名单
|
||||
- 最后 1 段(或 hyphenated 平台的最后 2 段)∈ 平台白名单
|
||||
- 其余为 noun-phrase(1~3 段)
|
||||
- **不要**追加 `default`
|
||||
|
||||
示例:
|
||||
|
||||
- `generate-monthly-statements`
|
||||
- `reconcile-finance`
|
||||
- `process-article`
|
||||
|
||||
### 4.2 标准型(绑定外部平台)
|
||||
|
||||
`verb-noun-platform`(在无平台形态后加平台,总段数仍 ≤ 5)
|
||||
|
||||
- 第 1 段 ∈ 动词白名单
|
||||
- 最后 1 段(或 hyphenated 平台的最后若干段)∈ 平台白名单
|
||||
- 中间为 noun-phrase(1~3 段)
|
||||
|
||||
示例:
|
||||
@@ -71,19 +86,19 @@
|
||||
- `enter-sales-receipt-kingdee`
|
||||
- `fill-export-declaration-single-window`
|
||||
|
||||
### 4.2 scope 型(少见)
|
||||
### 4.3 scope 型(少见)
|
||||
|
||||
`verb-noun-scope`
|
||||
|
||||
- 最后 1 段 ∈ scope 白名单:`all` | `batch` | `multi` | `default`
|
||||
- 最后 1 段 ∈ scope 白名单:`all` | `batch` | `multi`
|
||||
- 第 1 段 ∈ 动词白名单
|
||||
- 总段数 3~5
|
||||
- 至少还有一段名词(因此最少 3 段)
|
||||
|
||||
示例:`generate-article-all`
|
||||
|
||||
> 说明:`geo-generate-article-all` 若作为新技能且不在豁免表、首段 `geo` 不在动词表,应写为 `generate-article-all`。
|
||||
> `default` 已从 scope 词表移除。它不是平台,也不表示范围。新技能不要写 `generate-article-default` 这类占位。已发布且末尾为 `default` 的仓库保持原 slug;同步本校验后只会警告,不会因此失败。
|
||||
|
||||
### 4.3 历史豁免(不校验语义)
|
||||
### 4.4 历史豁免(不校验语义)
|
||||
|
||||
以下 slug 跳过 verb/platform 语义校验(见 `LEGACY_EXEMPT_SLUGS`):
|
||||
|
||||
@@ -121,7 +136,7 @@
|
||||
| lianlian | single-window | export-rebate |
|
||||
| etax | | |
|
||||
|
||||
scope 词表:[`assets/naming/scopes.txt`](../assets/naming/scopes.txt) — `all`、`batch`、`multi`、`default`
|
||||
scope 词表:[`assets/naming/scopes.txt`](../assets/naming/scopes.txt) — `all`、`batch`、`multi`。不含 `default`。
|
||||
|
||||
---
|
||||
|
||||
@@ -164,7 +179,7 @@ slug **末段 platform** 与 `account-manager` 中的 `platform_key` **语义对
|
||||
|
||||
## 9. 复制新技能流程
|
||||
|
||||
1. **先定 slug** — 按本文规范拟定 `{verb}-{noun-phrase}-{platform}`,确认在白名单内且 ≤ 48 字符
|
||||
1. **先定 slug** — `{verb}-{noun-phrase}`,仅在绑定外部平台时加 `-{platform}`。确认动词/平台在白名单内且 ≤ 48 字符。不要用 `default` 占位。
|
||||
2. **复制目录** — **推荐** [`tools/scaffold_skill.ps1`](../tools/scaffold_skill.ps1)(`-Slug` 与目录名一致);勿整包复制带走 `.git`
|
||||
2.5. **清理 Git(若未用 scaffold)** — 删除 `.git`、`.openclaw-skill-template`、`.pytest_cache`;再 `git init` 与 `git remote add origin`
|
||||
3. **全局替换** — `your-skill-slug` → 新 slug;更新 `SKILL.md`、`constants.py`、文档中的占位
|
||||
@@ -179,10 +194,12 @@ slug **末段 platform** 与 `account-manager` 中的 `platform_key` **语义对
|
||||
|
||||
| slug | 说明 |
|
||||
|------|------|
|
||||
| `download-settlement-report-amazon` | 标准型,4 段 |
|
||||
| `add-product-shopee` | 标准型,3 段 |
|
||||
| `enter-sales-receipt-kingdee` | 标准型 |
|
||||
| `fill-export-declaration-single-window` | 标准型,平台含连字符 |
|
||||
| `generate-monthly-statements` | 无平台 |
|
||||
| `reconcile-finance` | 无平台,动词 + 一个名词 |
|
||||
| `download-settlement-report-amazon` | 有平台 |
|
||||
| `add-product-shopee` | 有平台 |
|
||||
| `enter-sales-receipt-kingdee` | 有平台 |
|
||||
| `fill-export-declaration-single-window` | 有平台,平台含连字符 |
|
||||
| `generate-article-all` | scope 型 |
|
||||
|
||||
### ❌ 反例
|
||||
@@ -191,7 +208,7 @@ slug **末段 platform** 与 `account-manager` 中的 `platform_key` **语义对
|
||||
|------|------|
|
||||
| `amazon-download-settlement-report` | 平台在最前 |
|
||||
| `enter-cross-border-sales-receipt-voucher-kingdee` | 过长、noun 臃肿 |
|
||||
| `reconcile-finance` | 新技能禁止 2 段无平台 |
|
||||
| `generate-article-default` | `default` 不是平台;应写成 `generate-article` |
|
||||
| `download_settlement_amazon` | 非 kebab-case(下划线) |
|
||||
|
||||
---
|
||||
@@ -201,14 +218,14 @@ slug **末段 platform** 与 `account-manager` 中的 `platform_key` **语义对
|
||||
- **平台前置**:`amazon-download-...`、`shopee-add-...`
|
||||
- **英文占位 name**:复制后仍保留 `My Skill` 或纯英文 slug 风格名称
|
||||
- **只改目录名**:`SKILL.md` slug 与 `constants.SKILL_SLUG` 未同步
|
||||
- **noun 堆砌**:中间段过多导致超 48 字符或难以阅读
|
||||
- **仿历史 2 段 repo**:`reconcile-finance`、`content-manager` 等
|
||||
- **用 default 占位**:`generate-article-default`。没有外部平台就省略末段
|
||||
- **名词堆砌**:中间段过多导致超 48 字符或难以阅读
|
||||
|
||||
---
|
||||
|
||||
## 12. 演进说明(当前不启用)
|
||||
|
||||
当技能库规模 **> 200** 时,可再评估引入 `{domain}-verb-noun-platform` 前缀形态。**当前不启用**,新技能一律使用本文 verb-noun-platform 标准型或 scope 型。
|
||||
当技能库规模 **> 200** 时,可再评估引入 `{domain}-verb-noun-platform` 前缀形态。**当前不启用**。新技能使用动词-名词;只有绑定外部平台时才使用 verb-noun-platform,或使用 scope 型。
|
||||
|
||||
---
|
||||
|
||||
@@ -218,9 +235,9 @@ slug **末段 platform** 与 `account-manager` 中的 `platform_key` **语义对
|
||||
|
||||
| 历史 slug | 说明 |
|
||||
|-----------|------|
|
||||
| `account-manager` | 跨平台账号管理,非 verb-noun-platform |
|
||||
| `reconcile-finance` | 2 段无 platform |
|
||||
| `scrape-contacts-lianjia` | 平台名不在当前白名单(链家) |
|
||||
| `account-manager` | 跨平台账号管理,非动词-名词 |
|
||||
| `reconcile-finance` | 现已是合法的无平台形态;旧文档曾把它标成反例 |
|
||||
| `scrape-contacts-lianjia` | 平台名不在当前白名单(链家);新技能若要表达链家,先把平台加入白名单 |
|
||||
| `skill-template` | 模板仓库本身 |
|
||||
|
||||
校验器通过 `LEGACY_EXEMPT_SLUGS` 对模板占位与少数豁免 slug 跳过语义检查;**新复制出去的业务 skill 不在豁免表内**,必须满足 §3 硬约束。
|
||||
|
||||
@@ -9,25 +9,28 @@
|
||||
| POLICY-RUNTIME-002 | `requirements.txt` 非注释依赖行不得声明 `jiangchang-platform-kit` / `playwright` | development/RUNTIME.md §共享 Python Runtime;development/DEVELOPMENT.md §3.1 requirements.txt 依赖规范 | hard | 解析依赖行 | `tests/test_development_policy_guard.py::TestPolicyRuntime002`(另见 `tests/test_platform_import.py`) |
|
||||
| POLICY-RUNTIME-003 | Agent/CLI 文档须要求共享 Python(python-runtime / PATH 上的 `python` + `{baseDir}`);须明确**禁止**对技能脚本使用 `uv run python` | development/RUNTIME.md;references/CLI.md;SKILL.md | hard | 文档标记扫描 | `tests/test_docs_standards.py::test_cli_md_requires_shared_python_not_uv_run`;`tests/test_docs_standards.py::test_skill_md_requires_shared_python_not_uv_run` |
|
||||
| POLICY-AGENT-001 | `SKILL.md` 须含「用户意图路由表」结构;`CLI.md` 须含短 CLI 白名单 / Action 强制黑名单表;用法回答须禁止无故粘贴 CLI、禁止为答用法列目录挖 references | SKILL.md;references/CLI.md | hard | 文档标记扫描 | `tests/test_docs_standards.py::test_skill_md_has_intent_routing_table`;`tests/test_docs_standards.py::test_cli_md_has_agent_routing_tables`;`tests/test_docs_standards.py::test_skill_md_how_to_forbids_cli_paste` |
|
||||
| POLICY-INSTALL-001 | 交付执行文件不得含 `pip install` / `python -m pip install` / `uv pip install` / `playwright install` | development/RUNTIME.md §共享 Python Runtime;development/RPA.md §1.4 Playwright 与安装边界;development/DEVELOPMENT.md §3.1 | hard | 扫描 `scripts/**/*.py`、`*.ps1`、`*.sh`、`requirements.txt`、`.github/workflows/*`(不扫 `development/*.md`) | `tests/test_development_policy_guard.py::TestPolicyInstall001` |
|
||||
| POLICY-INSTALL-001 | 交付执行文件不得含 `pip install` / `python -m pip install` / `uv pip install` / `playwright install` | development/RUNTIME.md §共享 Python Runtime;development/RPA.md §1.4 Playwright 与安装边界;development/DEVELOPMENT.md §3.1 | hard | 扫描 `scripts/**/*.py`、`*.ps1`、`*.sh`、`requirements.txt`、`.github/workflows/*`(不扫 `development/*.md`;根目录 `sync.ps1` 除外,它是开发者手动执行的本机同步,用 `uv pip install` 装进宿主共享 venv) | `tests/test_development_policy_guard.py::TestPolicyInstall001` |
|
||||
| POLICY-CONFIG-001 | 仓库根目录必须存在 `.env.example` | development/CONFIG.md §核心原则、§标准配置项 | hard | 文件存在性 | `tests/test_development_policy_guard.py::TestPolicyConfig001` |
|
||||
| POLICY-CONFIG-002 | `.gitignore` 必须忽略 `.env` 与 `*.env.local` | development/CONFIG.md §红线:敏感信息不进 .env | hard | 解析 `.gitignore` 行 | `tests/test_development_policy_guard.py::TestPolicyConfig002` |
|
||||
| POLICY-CONFIG-003 | 业务代码不得直接 `os.environ` / `os.getenv` / `os.putenv` / `environ.get`;须经 `jiangchang_skill_core.config.get*`(允许 `main.py`、测试、config_bootstrap/runtime 边界文件) | development/CONFIG.md §配置 bootstrap;development/ADAPTER.md §档位 dispatch | hard | 扫描 `scripts/**/*.py` 并排除边界文件 | `tests/test_development_policy_guard.py::TestPolicyConfig003` |
|
||||
| POLICY-CONFIG-004 | `.env.example` 每个活跃 `KEY=` 必须有中文**行头**(是什么)与中文**行尾**(怎么填);注释禁止 `development/`、`*.md` 文档指向、`account-manager`、`adapter 档位` 等开发黑话 | development/CONFIG.md §.env.example 注释 = 配置管理用户文案 | hard | 解析 `.env.example` 活跃行与注释 | `tests/test_development_policy_guard.py::TestPolicyConfig004` |
|
||||
| POLICY-CONFIG-005 | 若 `.env.example` 使用 `# @required`,须单独占一行且在下一行中文行头之上;禁止写在行尾;禁止无中文行头或多 key 共用 | development/CONFIG.md §必填项:# @required | hard | 解析 `.env.example` 活跃行与 `@required` 标记 | `tests/test_development_policy_guard.py::TestPolicyConfig005` |
|
||||
| POLICY-TESTING-001 | 默认根目录 `tests/test_*.py` 不得硬编码 `real_api` / `real_rpa` / `OPENCLAW_TEST_TARGET`(白名单仅 `test_adapter_profile_policy.py`,用于档位 helper 自测) | development/TESTING.md §默认必跑测试要做什么、§4 测试目标档位 | hard | 扫描 `tests/test_*.py` 非注释行;白名单外禁止上述文本 | `tests/test_development_policy_guard.py::TestPolicyTesting001` |
|
||||
| POLICY-TESTING-002 | `tests/integration/` 下 Python 测试文件若存在,默认应为 `.sample`,不得进入默认套件 | development/TESTING.md §7 真实联调测试的安全约束、§8 新 skill 的最小测试清单 | hard | 检查 `tests/integration/*.py` 无 `.sample` 后缀 | `tests/test_development_policy_guard.py::TestPolicyTesting002` |
|
||||
| POLICY-RPA-001 | 不得 import/use account-manager 内部 `rpa_helpers` / `inject_account_manager_scripts_path` / `get_account_credential` | development/ADAPTER.md §兄弟依赖;development/RPA.md §1.7 | hard | 扫描 `scripts/**/*.py` 禁止模式 | `tests/test_development_policy_guard.py::TestPolicyRpa001`(另见 `tests/test_no_rpa_helpers_import.py`) |
|
||||
| POLICY-RPA-002 | RPA 交付代码不得直接执行 ffmpeg 拼 MP4;应使用 `RpaVideoSession`(允许 `ffmpeg_path` 等诊断字段) | development/RPA.md §5.3 录屏成片标准 | hard | 扫描 `scripts/**/*.py` 禁止 `subprocess` / `os.system` / `os.popen` 直接调用 ffmpeg(`task_run_support.py` 保留豁免) | `tests/test_development_policy_guard.py::TestPolicyRpa002` |
|
||||
| POLICY-RPA-003 | 即使默认 `OPENCLAW_RECORD_VIDEO=0`,RPA / 长任务模板也必须保留录屏能力:`RpaVideoSession`、`video.add_step`、video artifact merge 到 `result_summary` | development/RPA.md §5.3;development/LOGGING.md;development/CONFIG.md | hard | 扫描 `scripts/service/*.py`(service 层整体须含上述接入) | `tests/test_development_policy_guard.py::TestPolicyRpa003` |
|
||||
| POLICY-RPA-003 | 即使默认 `OPENCLAW_RECORD_VIDEO=0`,RPA / 长任务模板也必须保留录屏能力:`RpaVideoSession`、`video.add_step`、video artifact merge 进 `finish()` 字段 | development/RPA.md §5.3;development/LOGGING.md;development/CONFIG.md | hard | 扫描 `scripts/service/*.py`(service 层整体须含上述接入) | `tests/test_development_policy_guard.py::TestPolicyRpa003` |
|
||||
| POLICY-RPA-004 | 网页 RPA:`.env.example` 若声明 `OPENCLAW_BROWSER_HEADLESS` 则默认必须为 `0`;`scripts/` 若使用 `launch_persistent_*` 则必须同时存在 `pick_web_account`(先 Profile 再开浏览器;实现上应走 ensure-web);登录不全局强制,但不得跳过 account-manager | development/RPA.md §0.2;development/ADAPTER.md §兄弟依赖 | hard | 解析 `.env.example` + 扫描 `scripts/**/*.py` | `tests/test_development_policy_guard.py::TestPolicyRpa004` |
|
||||
| POLICY-RPA-005 | 必须保留 AI + RPA 总纲,明确 Agent / RPA 职责、授权范围、禁止破解/逆向/私有协议/安全验证绕过、人工介入、状态等待;真实业务默认动作节奏为 1~3 秒 | development/AI_RPA.md §4–§6;development/RPA.md §0.0/§0.3 | hard(文档与默认配置) / soft(真实行为) | 检查总纲关键边界;解析 `.env.example` 的 `STEP_DELAY_MIN=1.0` / `STEP_DELAY_MAX=3.0` | `tests/test_development_policy_guard.py::TestPolicyRpa005` |
|
||||
| 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-RPA-007 | 网页 RPA 启动须 `no_viewport=True` + `--start-maximized`;禁止 `viewport=None`、CDP/JS 中途改窗;`RPA.md` §1.2.1 须保留窗口标准 | development/RPA.md §1.2.1;development/RPA_BROWSER.md §4;examples/real_browser_rpa | hard | 文档标记 + 扫描含 `launch_persistent_*` 的 Python 禁止模式;校验 real_browser 示例 | `tests/test_development_policy_guard.py::TestPolicyRpa007` |
|
||||
| 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` |
|
||||
| POLICY-LOGGING-003 | 主任务入口须含 `log.info`/`logger.info`、`log.exception`/`logger.exception`,并写入 `save_task_log` | development/LOGGING.md | hard | 扫描 `scripts/service/task_service.py` | `tests/test_development_policy_guard.py::TestPolicyLogging003` |
|
||||
| POLICY-LOGGING-003 | 主任务入口须含 `log.info`/`logger.info`、`log.exception`/`logger.exception` | development/LOGGING.md | hard | 扫描 `scripts/service/task_service.py` | `tests/test_development_policy_guard.py::TestPolicyLogging003` |
|
||||
| POLICY-LOGGING-004 | 长任务 / RPA 模板须使用 Activity 或 RPA video step 输出进度 | development/LOGGING.md;development/RPA.md | hard | 扫描 `scripts/service/task_service.py` 含 `emit(` / `activity.emit` / `video.add_step` | `tests/test_development_policy_guard.py::TestPolicyLogging004` |
|
||||
| POLICY-LOGGING-005 | 交付代码不得在 logging 调用行以 `key=value` 形式记录明显敏感字段 | development/LOGGING.md;development/CONFIG.md | hard | 扫描 `scripts/**/*.py` 的 logging 调用行(不含注释) | `tests/test_development_policy_guard.py::TestPolicyLogging005` |
|
||||
| POLICY-LOGGING-006 | `scripts/service/**/*.py` 业务域文件须有 `log/logger/_logger.(info|warning|error|exception)`;豁免薄工具:`__init__.py`、`task_run_support.py`、`entitlement_service.py`、`sibling_bridge.py` | development/LOGGING.md §0、§3 | hard | 扫描 service 树 | `tests/test_development_policy_guard.py::TestPolicyLogging006` |
|
||||
@@ -41,6 +44,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` |
|
||||
|
||||
@@ -58,12 +62,17 @@
|
||||
| 是否满足完整快照语义 / 空结果保护实现 | references/SCHEMA.md | 见 `test_sync_contract.py.sample` |
|
||||
| 是否把系统导出与业务同步混淆 | references/ACTIONS.md;SCHEMA.md | 文档 + sample;勿做源码关键词误报扫描 |
|
||||
| `db/` 层不得调浏览器或拼业务流程 | development/DEVELOPMENT.md §11 | 需跨文件调用图 |
|
||||
| slug 命名语义(verb-noun-platform) | development/NAMING.md | 部分覆盖于 `tests/test_slug_naming.py`,非本矩阵 hard policy |
|
||||
| slug 命名语义(动词-名词,平台可选) | development/NAMING.md | 部分覆盖于 `tests/test_slug_naming.py`,非本矩阵 hard policy |
|
||||
| 复制模板不得保留 `.git` / template marker | development/DEVELOPMENT.md §4 | 由 `tests/test_scaffold_guard.py` 守护,属脚手架场景 |
|
||||
| 共享 runtime 导入来源非技能目录 | development/RUNTIME.md | 运行时依赖已安装包,见 `tests/test_platform_import.py` |
|
||||
| RPA 真实轨迹、选择器纪律、状态等待、HITL 超时与有限重试是否正确实现 | development/AI_RPA.md;development/RPA.md §0–§1 | 文档和默认节奏已守护,真实行为与 DOM 质量仍需人工验收 |
|
||||
| RPA 阶段日志是否「够细」(每个控件) | development/LOGGING.md §3.2 | hard 已要求里程碑事件词与域模块有日志;逐步粒度靠 sample 契约与验收 |
|
||||
| adapter 四档契约测试覆盖 timeout/unauthorized 等 | development/ADAPTER.md §contract tests | 需业务实现后人工补测 |
|
||||
| `SKILL.md` / `constants.SKILL_SLUG` 一致性 | development/DEVELOPMENT.md §16 | 已有 `tests/test_skill_metadata.py` |
|
||||
| platform_kit_min_version >= 1.2.2 | development/RUNTIME.md | 已有 `tests/test_platform_import.py` |
|
||||
| platform_kit_min_version >= 1.2.11 | development/RUNTIME.md | 已有 `tests/test_platform_import.py` |
|
||||
| 宿主按 `bind.tables` 过滤 toolbar 按钮 | references/ACTIONS.md | 待宿主升级后完整生效;模板侧已强制显式绑定 |
|
||||
| form +「已选择」时映射 `readOnly` 默认不占参数区(仍提交);`revealInForm` 逃生阀 | references/ACTIONS.md「选择摘要」 | soft(宿主 UX);文档与 schema 已声明;勿用 `visibleWhen` 代替 |
|
||||
| 数据管理技能按钮中文名(`label`)对照 ACTIONS.md 建议表;打开类成功静默 | references/ACTIONS.md「Action 按钮中文名」 | 参考非白名单;行业词允许自创;勿做 label 关键字扫描 |
|
||||
| Data Policy 未启用(L0)时宿主不得拉 Snapshot / 不得改按钮与行列行为 | references/POLICY.md | soft;宿主实现契约 |
|
||||
| Data Policy 启用后 fail-closed(禁止回退 L0 全开);写路径 domain 必须再鉴权 | references/POLICY.md §6–§7 | soft;`POLICY-SKILL-AUTHZ-*` 硬门禁可后续加 |
|
||||
| `actions.mode` 仅 allowlist/open;`hostCapabilities` 省略=不限制;scope `valueFrom` 仅 `actor.userId` | references/POLICY.md §0.1 | soft;Schema 已约束 |
|
||||
|
||||
188
development/PRODUCT_SURFACES.md
Normal file
188
development/PRODUCT_SURFACES.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# 宿主产品入口(对话 / 数据管理 / 定时任务)
|
||||
|
||||
本文给**技能开发者**与 **AI 编程助手**读:说明匠厂宿主上几个**平级**业务入口各自干什么、**什么时候走哪个**、以及如何为本技能做入口定位。
|
||||
|
||||
字段怎么写见 [`../references/ACTIONS.md`](../references/ACTIONS.md)、[`../references/SCHEMA.md`](../references/SCHEMA.md);任务中心 / sync·async 见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md)。
|
||||
|
||||
**与其它分类正交:**
|
||||
|
||||
| 文档维度 | 回答的问题 |
|
||||
|----------|------------|
|
||||
| 本文(产品入口) | 用户从宿主哪条路径触发同一能力 |
|
||||
| Type A–D / 目录分层 | 技能有没有库、会不会调兄弟技能 |
|
||||
| RPA 四象限 | 外部系统怎么接 |
|
||||
|
||||
不要把三者合并成一个大枚举。
|
||||
|
||||
---
|
||||
|
||||
## 1. 入口是什么(先立规矩)
|
||||
|
||||
技能跑在**匠厂宿主**上,不是独立软件。宿主侧栏里的入口,是同一技能能力的不同**触发场景**:
|
||||
|
||||
| 今日常见入口 | 触发方式 | 底层 |
|
||||
|--------------|----------|------|
|
||||
| **对话任务**(新建任务) | 自然语言 → Agent | 短 CLI,或 `run_skill_action`(`placements` 含 `agent`) |
|
||||
| **数据管理** | 点工具栏 / 行 / 批量 / 表单(UI) | `run_skill_action`(`source=data-management`)→ **同一 CLI** |
|
||||
| **定时任务** | 到点「技能直调」或定时 Agent | `run_skill_action`(`cron` / `cron-agent`)→ **同一 CLI** |
|
||||
|
||||
**铁律:**
|
||||
|
||||
1. **入口平级**:没有「对话主、数据管理/定时辅」。只是触发场景与交互方式不同,各自有优势。
|
||||
2. **同一业务能力、同一 domain service**:禁止按入口复制逻辑,禁止按 `source` 分叉业务结果。
|
||||
3. **同能力可挂多个入口**:不是三选一;按本技能 UX 决定挂哪些。
|
||||
4. **可扩展**:以后若宿主增加新入口,仍是「新触发场景 + 同一内核」,定位方法不变(定位表多一列即可)。
|
||||
5. **任务中心不是业务入口**:只表示 `executionProfile: async` 的执行壳;与从哪个入口点进来无关。
|
||||
|
||||
---
|
||||
|
||||
## 2. 什么时候走哪个入口
|
||||
|
||||
### 2.1 对话任务——「用嘴发起」
|
||||
|
||||
**适合:**
|
||||
|
||||
- 用户用自然语言描述意图,条件一次说清或还要追问
|
||||
- 探索式:「能不能帮我……」「先看看再决定」
|
||||
- 对象临时指定(贴链接、说一个范围)
|
||||
- 需要 Agent 解释结果、失败原因、下一步
|
||||
- 用户不想打开数据管理,只想在聊天里办完
|
||||
|
||||
**技术自问:** 用户会不会用一句话说清这件事?需要模型帮理解和确认吗?——会,就为该能力挂 `agent`(长任务 / RPA 必须 Action + 通常 `async`;短查询可用共享 Python CLI)。
|
||||
|
||||
**注意:** 若技能还有「对着表运营 / 定时啃队列」等场景,**不要把对话当成唯一路径**,否则传统用户与省 token 路径会丢掉。
|
||||
|
||||
### 2.2 数据管理——「用界面发起 + 看着数据做」
|
||||
|
||||
数据管理有两层,**同等重要**:
|
||||
|
||||
1. **执行面**:工具栏 / 行级 / 批量 / `replaces` 接管新增·编辑·删除·**导入** —— 与对话一样触发同一 CLI,走完整业务闭环(传统用户可不聊天)。导入按钮的 UX 始终是宿主「导入数据」面板;有 `replaces:import` 时确认后由技能写库。
|
||||
2. **运营面**:业务表、状态流向、配置、input/output、日志、HTML —— 看清数据从哪来、到哪去、卡在哪。
|
||||
|
||||
**适合:**
|
||||
|
||||
- 在业务数据上操作:看队列、选一行重试、勾选批量、顶栏「处理 N 条 / 同步」
|
||||
- 已知流程、参数结构化(表单 / 确认 / 一点即跑)——**省 token**,不必每次过模型
|
||||
- 准备与执行拆开:表里先准备,再在表上点执行
|
||||
- 服务不愿用 AI 对话的用户
|
||||
|
||||
**技术自问:** 用户会不会打开这张表,靠点选就把主流程跑完?——要,就挂 `toolbar` / `row` / `batch` / `replaces`,并做好 `_jiangchang_*` 中文元数据。
|
||||
|
||||
**不要:** 把数据管理理解成「只给开发者看 SQLite」;也不要在完全没有结构化操作对象时硬造空表按钮。
|
||||
|
||||
### 2.3 定时任务——「到点自动发起」
|
||||
|
||||
**适合:**
|
||||
|
||||
- 同一能力需按节奏重复:消费队列、定期同步、巡检
|
||||
- 无人值守:默认参数即可跑,人不在场也正确、安全
|
||||
- 与对话 / 数据管理挂**同一个** Action,只改触发方式
|
||||
|
||||
**技术自问:** 这件事明天能不能在人不盯着时自动再跑一遍?——能,挂 `cron`;不能,定位表写明不挂及理由。
|
||||
|
||||
**慎挂:** 每次对象都不同须人指定;强不可逆且须当场确认;主路径依赖验证码 / 人工介入。
|
||||
|
||||
### 2.4 场景对照(贴需求用)
|
||||
|
||||
| 若用户场景是… | 优先考虑 | 说明 |
|
||||
|---------------|----------|------|
|
||||
| 「帮我处理这个……」(临时、口语) | 对话 | 灵活、可追问 |
|
||||
| 「对着表点一下就跑」 | 数据管理 | 结构化、省 token、传统用户 |
|
||||
| 「每天自动跑,不用我管」 | 定时 | 无人值守 |
|
||||
| 「偶尔对话下发,平时表上运营,夜里自动啃队列」 | **多个入口都挂同一能力** | 平级互补,不是重复开发 |
|
||||
| 「只看结果,从不在表上触发」 | 表可仅展示;执行挂对话/定时 | 由本技能 UX 定 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 如何为本技能做入口定位(必做)
|
||||
|
||||
模板**不**规定「某类技能必须三入口都挂满」。由技术按**本技能干什么 + 用户体验**填写定位表(写入 [`REQUIREMENTS.md`](REQUIREMENTS.md))。
|
||||
|
||||
对每个**主能力**(一条业务 CLI / 一个 Action)填一行:
|
||||
|
||||
| 主能力(Action id / CLI) | 对话 | 数据管理 | 定时 | 不挂理由(若不挂) |
|
||||
|---------------------------|------|----------|------|--------------------|
|
||||
| (例)`download-run-pick` | 挂 | toolbar | 挂 | — |
|
||||
| (例)`download-video` | 挂 | 不挂 | 不挂 | 对象每次不同,须人指定链接 |
|
||||
|
||||
**数据管理列怎么写:** `不挂` / `toolbar` / `row` / `batch` / `replaces` / `仅展示表(无业务按钮)`(可组合)。
|
||||
|
||||
**规则只有三条:**
|
||||
|
||||
1. 定位为「挂」的入口:必须在 `actions.json`(及必要元数据)中声明,且宿主可测通,并走同一内核。
|
||||
2. 定位为「不挂」的入口:必须有一句理由;禁止空白「忘了」。
|
||||
3. 未来新入口:表上多一列,方法不变。
|
||||
|
||||
完整 ≠ 每个命令塞满所有入口;完整 = **想清楚用户路径后,该出现的入口都挂上了,且同内核**。
|
||||
|
||||
---
|
||||
|
||||
## 4. 怎么挂(最小路径)
|
||||
|
||||
| 我想让用户… | 主要改什么 |
|
||||
|-------------|------------|
|
||||
| 能在对话里触发 | `SKILL.md` 意图路由 + `actions.json` 的 `placements` 含 `agent`(长任务 `async`) |
|
||||
| 能在数据管理看见结果 / 过程 | 业务表 + `init-db` + `_jiangchang_*`(见 SCHEMA) |
|
||||
| 能在表上点按钮走闭环 | `placements` 含 `toolbar`/`row`/`batch`,`toolbar` 须 `bind.tables` |
|
||||
| 能设定时自动跑 | 同一 Action 的 `placements` 含 `cron`;参数适合无人值守 |
|
||||
|
||||
细节与 Schema:[`../references/ACTIONS.md`](../references/ACTIONS.md)。运行时:[`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 反例(不要这样做)
|
||||
|
||||
- 只做对话 Action,从未填写入口定位表,也未考虑数据管理 / 定时用户路径
|
||||
- 为 toolbar / cron / agent 复制三套业务实现,或按 `source` 分叉结果
|
||||
- 把数据管理当成只读调试窗,主闭环只能聊天完成(若 UX 上本应支持 UI)
|
||||
- 不可逆、须确认的主路径默认挂 `cron` 且无产品确认
|
||||
- 用「Action 可选 / 定时以后再说」当作「可以不思考入口」
|
||||
|
||||
---
|
||||
|
||||
## 6. 附录:定位表示例(参考,非强制)
|
||||
|
||||
### 示例 A:队列消费类(对话 + 表上处理 + 定时)
|
||||
|
||||
| 主能力 | 对话 | 数据管理 | 定时 | 理由摘要 |
|
||||
|--------|------|----------|------|----------|
|
||||
| 入队 / 准备 | 挂 | 可 replaces 或表新增 | 不挂 | 准备态由人或对话写入 |
|
||||
| 处理 N 条 pending | 挂 | toolbar | 挂 | 运营点选与无人值守同一内核 |
|
||||
| 单条立即执行 | 挂 | 可选 row | 不挂 | 对象临时指定 |
|
||||
|
||||
### 示例 B:单次确认发布类
|
||||
|
||||
| 主能力 | 对话 | 数据管理 | 定时 | 理由摘要 |
|
||||
|--------|------|----------|------|----------|
|
||||
| 发布一条 | 挂 | 按是否落草稿表决定 | 不挂 | 不可逆,需人确认;不做无人值守 |
|
||||
|
||||
### 示例 C:无库短查询
|
||||
|
||||
| 主能力 | 对话 | 数据管理 | 定时 | 理由摘要 |
|
||||
|--------|------|----------|------|----------|
|
||||
| 查询 | 挂(或短 CLI) | 不挂 | 不挂 | 无本地业务表与重复批跑需求 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 用户教程如何投影本文
|
||||
|
||||
入口定位表填完后,**必须**落到用户市场文档(见 [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md)):
|
||||
|
||||
| 定位结果 | `TUTORIAL.md` | `README.md` |
|
||||
|----------|---------------|-------------|
|
||||
| 挂数据管理 | 「入口一」专章 + 表/按钮步骤 | 仅一句「支持数据管理」 |
|
||||
| 挂对话 | 「入口二」专章 + 话术 | 仅一句「支持对话」;**禁止**话术块 |
|
||||
| 挂定时 | 「入口三」教怎么建 | 可选一句边界 |
|
||||
| 不挂定时 | 「入口三」写不需要 + 理由 | 可选一句「不提供定时」 |
|
||||
|
||||
准备/载入类 Action:优先 `toolbar` + `bind.tables`,避免教程写「技能详情点按钮」而宿主详情页不渲染。
|
||||
|
||||
---
|
||||
|
||||
## 8. 相关文档
|
||||
|
||||
- 需求里填定位表:[`REQUIREMENTS.md`](REQUIREMENTS.md)
|
||||
- **用户说明 / 教程怎么写:** [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md)
|
||||
- 交活怎么验:[`CHECKLIST.md`](CHECKLIST.md)
|
||||
- 开发步骤中的 Action 挂载:[`DEVELOPMENT.md`](DEVELOPMENT.md) §11.5、§15
|
||||
- Agent 侧字段契约:[`../references/ACTIONS.md`](../references/ACTIONS.md)
|
||||
@@ -9,13 +9,16 @@
|
||||
| 步骤 | 做什么 | 详见 |
|
||||
|------|--------|------|
|
||||
| 1 | **拿到仓库**:① 项目经理在 [自建 Gitea](https://git.jc2009.com/)(`git.jc2009.com`,非 Gitea 官网)开仓并授权后 `git clone`;或 ② 本地已有 `skill-template` 时用 scaffold / 复制出新目录(遵守 Git 红线) | [`DEVELOPMENT.md`](DEVELOPMENT.md) §1、§4 |
|
||||
| 2 | **先读 AI + RPA 总纲**;界面技能先完成元素拾取/element map,再按浏览器、桌面或手机读分端手册 | [`AI_RPA.md`](AI_RPA.md)、[`RPA_ELEMENT_LOCATING.md`](RPA_ELEMENT_LOCATING.md) |
|
||||
| 3 | 按 [`NAMING.md`](NAMING.md) 确认 slug;**先填** [`REQUIREMENTS.md`](REQUIREMENTS.md),再写业务代码 | [`REQUIREMENTS.md`](REQUIREMENTS.md)、[`NAMING.md`](NAMING.md) |
|
||||
| 4 | 替换标识与占位,按四象限 / examples 实现 `scripts/service/` | [`DEVELOPMENT.md`](DEVELOPMENT.md) §4~§14 |
|
||||
| 5 | **配置 `developer_ids`(开发自测必做)**:开发期技能在匠厂默认不公开;不加则开发者自己也装测不了。从宿主设置复制用户 ID 写入 `SKILL.md` | [`DEVELOPMENT.md`](DEVELOPMENT.md) §6「关于 developer_ids」 |
|
||||
| 6 | 本地单测(mock)通过后,按业务默认 **`real_rpa`** 真实跑通;再 `release.ps1`;看 Gitea CI。**mock 通 ≠ 完成** | [`ADAPTER.md`](ADAPTER.md);[`DEVELOPMENT.md`](DEVELOPMENT.md) §15 |
|
||||
| 7 | 匠厂安装后,按已声明入口自测,并看**用户体验**(进度、失败提示、有头 RPA 等) | [`DEVELOPMENT.md`](DEVELOPMENT.md) §15;[`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) |
|
||||
| 8 | **交活验收(第三方)**:对照 [`CHECKLIST.md`](CHECKLIST.md) 验收;报告写入仓库根目录 [`acceptance-reports/`](../acceptance-reports/) | [`CHECKLIST.md`](CHECKLIST.md) |
|
||||
| 2 | **先读宿主产品入口**:对话 / 数据管理 / 定时任务平级;何时走哪个;为本技能填入口定位表 | [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md) |
|
||||
| 2b | **用户市场文档**:说明 Tab ≠ 教程 Tab;教程按三入口写;与 §0.1 / actions 对齐 | [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md) |
|
||||
| 3 | **先读 AI + RPA 总纲**;界面技能先完成元素拾取/element map,再按浏览器、桌面或手机读分端手册 | [`AI_RPA.md`](AI_RPA.md)、[`RPA_ELEMENT_LOCATING.md`](RPA_ELEMENT_LOCATING.md) |
|
||||
| 4 | 按 [`NAMING.md`](NAMING.md) 确认 slug;**先填** [`REQUIREMENTS.md`](REQUIREMENTS.md)(含入口定位表),再写业务代码 | [`REQUIREMENTS.md`](REQUIREMENTS.md)、[`NAMING.md`](NAMING.md) |
|
||||
| 5 | 替换标识与占位,按四象限 / examples 实现 `scripts/service/` | [`DEVELOPMENT.md`](DEVELOPMENT.md) §4~§14 |
|
||||
| 6 | **配置 `developer_ids`(开发自测必做)**:开发期技能在匠厂默认不公开;不加则开发者自己也装测不了。从宿主设置复制用户 ID 写入 `SKILL.md` | [`DEVELOPMENT.md`](DEVELOPMENT.md) §6「关于 developer_ids」 |
|
||||
| 6b | **按需配置 `brands`(OEM 市场)**:须在华商等 OEM 客户端出现时声明 `brands`(如 `jiangchang` + `huashang`);未写则仅挂匠厂。Gitea 组织名不会自动映射市场 | [`DEVELOPMENT.md`](DEVELOPMENT.md) §6「关于 brands」 |
|
||||
| 7 | 本地单测(mock)通过后,按业务默认 **`real_rpa`** 真实跑通;再 `release.ps1`;看 Gitea CI。**mock 通 ≠ 完成** | [`ADAPTER.md`](ADAPTER.md);[`DEVELOPMENT.md`](DEVELOPMENT.md) §15 |
|
||||
| 8 | 匠厂安装后,按**入口定位表已挂**的入口自测,并看**用户体验**(进度、失败提示、有头 RPA 等) | [`DEVELOPMENT.md`](DEVELOPMENT.md) §15;[`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md);[`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) |
|
||||
| 9 | **交活验收(第三方)**:对照 [`CHECKLIST.md`](CHECKLIST.md) 验收;报告写入仓库根目录 [`acceptance-reports/`](../acceptance-reports/) | [`CHECKLIST.md`](CHECKLIST.md) |
|
||||
|
||||
脚手架与 Git 防串库:[`../tools/README.md`](../tools/README.md)(`scaffold_skill.ps1`)。业务仓也可直接从自建 Gitea clone,见 [`DEVELOPMENT.md`](DEVELOPMENT.md) §4 来源 A。
|
||||
|
||||
@@ -25,23 +28,25 @@
|
||||
|
||||
开始定制 skill 前,建议按以下顺序阅读:
|
||||
|
||||
1. [`AI_RPA.md`](AI_RPA.md) — **AI Agent + RPA 总纲**:职责边界、合规红线、风控、操作节奏与完整闭环
|
||||
2. [`ACCOUNT_MANAGER.md`](ACCOUNT_MANAGER.md) — **浏览器账号标准**:一账号一 Profile、租约、多账号、数据查看、验收与排障
|
||||
3. [`SHARED_REPOS.md`](SHARED_REPOS.md) — **公共仓库目录**(模板 / 账号管理 / platform-kit 等,可扩展)
|
||||
4. [`REQUIREMENTS.md`](REQUIREMENTS.md) — 需求文档模板与验收标准
|
||||
5. [`NAMING.md`](NAMING.md) — slug / 仓库名命名规范(复制模板前必读)
|
||||
6. [`DEVELOPMENT.md`](DEVELOPMENT.md) — 完整开发步骤与目录规范
|
||||
7. [`TESTING.md`](TESTING.md) — 测试分层、隔离数据根与档位开关
|
||||
8. [`LOGGING.md`](LOGGING.md) — 日志分层、必打节点、敏感信息红线(**涉及长任务、RPA、外部系统时必读**)
|
||||
9. [`ADAPTER.md`](ADAPTER.md) — 涉及外部系统对接时
|
||||
10. [`RPA.md`](RPA.md) — 涉及浏览器 / 桌面 / 手机自动化时的会话、风控和工程总纲
|
||||
11. [`RPA_ELEMENT_LOCATING.md`](RPA_ELEMENT_LOCATING.md) — **所有界面技能必读**:真实拾取、element map、唯一性、降级和 AI 编程约束
|
||||
12. 按执行端选读:[`RPA_BROWSER.md`](RPA_BROWSER.md) / [`RPA_DESKTOP.md`](RPA_DESKTOP.md) / [`RPA_MOBILE.md`](RPA_MOBILE.md)
|
||||
13. [`CONFIG.md`](CONFIG.md) — `.env` 规范与 bootstrap 机制
|
||||
14. [`DATA_PATHS.md`](DATA_PATHS.md) — 下载/导入/导出等本地文件路径标准(涉及文件读写时必读)
|
||||
15. [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) — Skill Action sync/async、任务中心、Agent 禁令、队列 pick(涉及 RPA / 长任务 / 数据管理按钮 / Cron / Agent 时必读)
|
||||
16. [`RUNTIME.md`](RUNTIME.md) — 共享 runtime、数据路径、发布打包与编码约定
|
||||
17. [`CHECKLIST.md`](CHECKLIST.md) — **交活验收清单(第三方视角)**;报告输出到 [`../acceptance-reports/`](../acceptance-reports/)
|
||||
1. [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md) — **宿主产品入口**:对话 / 数据管理 / 定时任务平级;何时走哪个;入口定位表
|
||||
1b. [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md) — **用户说明 / 教程契约**:README≠TUTORIAL;三入口结构;skill-detail 不可作主路径
|
||||
2. [`AI_RPA.md`](AI_RPA.md) — **AI Agent + RPA 总纲**:职责边界、合规红线、风控、操作节奏与完整闭环
|
||||
3. [`ACCOUNT_MANAGER.md`](ACCOUNT_MANAGER.md) — **浏览器账号标准**:一账号一 Profile、租约、多账号、数据查看、验收与排障
|
||||
4. [`SHARED_REPOS.md`](SHARED_REPOS.md) — **公共仓库目录**(模板 / 账号管理 / platform-kit 等,可扩展)
|
||||
5. [`REQUIREMENTS.md`](REQUIREMENTS.md) — 需求文档模板与验收标准(含入口定位表)
|
||||
6. [`NAMING.md`](NAMING.md) — slug / 仓库名命名规范(复制模板前必读)
|
||||
7. [`DEVELOPMENT.md`](DEVELOPMENT.md) — 完整开发步骤与目录规范
|
||||
8. [`TESTING.md`](TESTING.md) — 测试分层、隔离数据根与档位开关
|
||||
9. [`LOGGING.md`](LOGGING.md) — 日志分层、必打节点、敏感信息红线(**涉及长任务、RPA、外部系统时必读**)
|
||||
10. [`ADAPTER.md`](ADAPTER.md) — 涉及外部系统对接时
|
||||
11. [`RPA.md`](RPA.md) — 涉及浏览器 / 桌面 / 手机自动化时的会话、风控和工程总纲
|
||||
12. [`RPA_ELEMENT_LOCATING.md`](RPA_ELEMENT_LOCATING.md) — **所有界面技能必读**:真实拾取、element map、唯一性、降级和 AI 编程约束
|
||||
13. 按执行端选读:[`RPA_BROWSER.md`](RPA_BROWSER.md) / [`RPA_DESKTOP.md`](RPA_DESKTOP.md) / [`RPA_MOBILE.md`](RPA_MOBILE.md)
|
||||
14. [`CONFIG.md`](CONFIG.md) — `.env` 规范与 bootstrap 机制
|
||||
15. [`DATA_PATHS.md`](DATA_PATHS.md) — 下载/导入/导出等本地文件路径标准(涉及文件读写时必读)
|
||||
16. [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md) — Skill Action sync/async、任务中心、Agent 禁令、队列 pick(涉及 RPA / 长任务 / 数据管理按钮 / Cron / Agent 时必读)
|
||||
17. [`RUNTIME.md`](RUNTIME.md) — 共享 runtime、数据路径、发布打包与编码约定
|
||||
18. [`CHECKLIST.md`](CHECKLIST.md) — **交活验收清单(第三方视角)**;报告输出到 [`../acceptance-reports/`](../acceptance-reports/)
|
||||
|
||||
Agent 调用契约见 [`../references/CLI.md`](../references/CLI.md)、[`../references/SCHEMA.md`](../references/SCHEMA.md)、[`../references/ACTIONS.md`](../references/ACTIONS.md)。
|
||||
用户市场四 Tab 见根目录 [`README.md`](../README.md) / [`TUTORIAL.md`](../TUTORIAL.md) / [`DEMO.md`](../DEMO.md) / [`CHANGELOG.md`](../CHANGELOG.md),不要写进本目录。
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
- 中文 description(一句话):
|
||||
- account-manager platform_key(如有):
|
||||
- 命名形态:标准型 / scope 型
|
||||
- **目标 OEM 市场 `brands`(可选)**:未写则发布后仅挂匠厂(`jiangchang`)。须在华商等 OEM 客户端可见时填写,例如 `jiangchang`、`huashang`(见 `DEVELOPMENT.md` §6「关于 brands」)。**不要**用 Gitea 组织名代替本字段。
|
||||
- **网页 RPA(如有)— 登录策略(三选一,须写明):**
|
||||
- `required`:目标站必须登录才能闭环(用户须完成/保持站点登录;技能每轮主路径须有登录门禁 + 验证码/人工等待)
|
||||
- `optional`:登录可提升能力,但不登录也能完成主路径(**仅当**公开页确可闭环时选用;会话依赖型采集/评论/私信**禁止**默认抄成 optional)
|
||||
@@ -31,7 +32,30 @@
|
||||
- **登记 ≠ 登录**:ensure/add-web 只建账号与 Profile;站点登录由本技能在有头浏览器中完成
|
||||
- **反模式**:把「需要登录态才能采到数据」的需求写成 `optional`,再省略 `ensure_logged_in`
|
||||
|
||||
## 0.1 AI + RPA 协作与授权边界(涉及外部系统时必填)
|
||||
## 0.1 宿主入口定位(必填)
|
||||
|
||||
先读 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md)。对话任务 / 数据管理 / 定时任务在宿主上**平级**;挂哪些由本技能业务与 UX 定,模板不规定「必须三入口都挂满」。用户文档如何写见 [`TUTORIAL_SURFACES.md`](TUTORIAL_SURFACES.md)。
|
||||
|
||||
对每个主能力填一行(可增行;「不挂」必须写理由):
|
||||
|
||||
| 主能力(Action id / CLI) | 对话 | 数据管理 | 定时 | 教程章节 | 不挂理由(若不挂) |
|
||||
|---------------------------|------|----------|------|----------|--------------------|
|
||||
| (例)`<action-or-cli>` | 挂 / 不挂 | 不挂 / toolbar / row / batch / replaces / 仅展示表 | 挂 / 不挂 | 入口一 / 入口二 / 入口三 / N/A | |
|
||||
|
||||
说明:
|
||||
|
||||
- 数据管理列可写组合(如 `toolbar + row`)
|
||||
- 「仅展示表」= 用户要看过程/结果,但不在表上触发该能力
|
||||
- 定位为「挂」的入口须在 `assets/actions.json`(及必要表元数据)中落实,并与对话路径共用同一 domain service
|
||||
- **教程章节**:该能力在 `TUTORIAL.md` 中落在哪一节;整技能无定时时,入口三仍须写「不需要 + 理由」
|
||||
|
||||
### 0.1.1 用户市场文档(与入口表一致)
|
||||
|
||||
- `README.md`(说明 Tab):只写能力 / 场景 / 边界;**禁止**分步操作与 Agent 话术(见 `TUTORIAL_SURFACES.md` §1)
|
||||
- `TUTORIAL.md`(教程 Tab):覆盖 §0.1 中所有「挂」的入口;准备/载入类优先数据管理 toolbar,**不要**把 skill-detail 当主路径
|
||||
- 定位为「不挂」定时:教程须有 N/A 声明,与本表理由一致
|
||||
|
||||
## 0.2 AI + RPA 协作与授权边界(涉及外部系统时必填)
|
||||
|
||||
### Agent 设计
|
||||
|
||||
@@ -69,6 +93,20 @@
|
||||
- 需要保存哪些日志、截图、视频和业务结果:
|
||||
- 定位失败时如何保存 DOM/UIA/XML、候选数量和环境版本:
|
||||
|
||||
## 0.3 业务行生命周期(有「处理某一行」类能力时必填)
|
||||
|
||||
若本技能存在对业务表某一行做生成、触达、私信、发布等**有副作用**的处理,须遵守 [`../references/WORK_ITEM_STATUS.md`](../references/WORK_ITEM_STATUS.md),并在此声明**本技能状态 → 角色**映射(状态名自定,角色固定):
|
||||
|
||||
| 角色 | 本技能状态值(可多个) | 说明 |
|
||||
|------|------------------------|------|
|
||||
| 可操作(operable) | (例)新建 / 待生成 / … | 默认唯一可被处理 Action 捞起 |
|
||||
| 进行中(in_progress) | (例)生成中 / 触达中 / … | 抢占后、结束前 |
|
||||
| 成功类终态 | (例)已生成 / 已私信 / … | 默认不可再处理 |
|
||||
| 失败类终态 | (例)失败 / 触达失败 / … | 默认不可再处理;失败原因字段: |
|
||||
| 跳过类终态 | (例)已跳过 / … | 默认不可再处理 |
|
||||
|
||||
无此类处理队列时整节写「不适用」即可。
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
说明这份需求文档的作用,以及它解决什么问题。
|
||||
@@ -86,7 +124,7 @@
|
||||
|
||||
- 当前用户在 `<业务场景>` 中需要反复处理 `<输入数据>`,人工操作耗时且易错。
|
||||
- 现有流程主要依赖人工处理,存在 `<效率低 / 容易出错 / 不可批量 / 不可追踪>` 等问题。
|
||||
- 因此需要开发 `your-skill-slug`,用于自动完成 `<核心动作>`,输出 `<输出结果>`,并写入 `task_logs` 便于追踪。
|
||||
- 因此需要开发 `your-skill-slug`,用于自动完成 `<核心动作>`,输出 `<输出结果>`。
|
||||
|
||||
填写提示:
|
||||
|
||||
@@ -104,7 +142,7 @@
|
||||
- 解析并校验 `<输入数据>` 的关键字段
|
||||
- 通过 adapter 完成 mock / simulator / real 档位下的 `<核心动作>`
|
||||
- 输出结构化结果(stdout JSON 或固定文本 + 机读字段)
|
||||
- 写入 `task_logs`(及必要的业务表,见 `references/SCHEMA.md`)
|
||||
- 写入必要的业务表(见 `references/SCHEMA.md`;无业务表也可)
|
||||
- 支持失败可诊断(稳定错误码、`ERROR:` 前缀、必要截图/日志)
|
||||
- 完成 CLI 入口与 `health` / `version` / `init-db` 最小可运行验证
|
||||
- 完成正式环境发布与安装验证(如适用)
|
||||
@@ -116,7 +154,7 @@
|
||||
模板写法:
|
||||
|
||||
- 支持 `<命令1>`(如 `run`)
|
||||
- 支持 `<命令2>`(如 `logs` / `log-get`)
|
||||
- 支持 `<命令2>`(如 `health` / 业务只读查询)
|
||||
- 支持读取 `<数据来源>`(兄弟 skill / 本地 DB / 外部 API)
|
||||
- 支持执行 `<核心业务动作>`
|
||||
- 支持记录 `<日志 / 状态 / 结果>`
|
||||
@@ -130,13 +168,14 @@
|
||||
- 支持 `python scripts/main.py init-db`(幂等建库;宿主安装/更新后可静默调用)
|
||||
- 支持 `python scripts/main.py <your-main-command>`
|
||||
- 支持核心业务编排(HTTP / 批处理 / 可选 RPA,按四档 adapter 选型)
|
||||
- 支持写入任务日志(`task_logs`)
|
||||
- 支持失败可诊断(统一文件日志 + 任务中心 `finish`)
|
||||
- 完成 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md) 入口定位表,并按表落实已挂入口(对话 / 数据管理 / 定时按本技能 UX)
|
||||
|
||||
### 可后续扩展
|
||||
|
||||
- 支持定时批跑
|
||||
- 支持失败自动重试
|
||||
- 支持多 `<目标系统>` 轮询
|
||||
- 入口定位表中已标明「本期不挂」且写了理由的能力(例如暂不挂定时)
|
||||
|
||||
## 5. 非功能要求
|
||||
|
||||
@@ -207,14 +246,16 @@
|
||||
- `health`、`version`、`init-db` 命令执行正常
|
||||
- `python tests/run_tests.py -v` 必跑测试全部通过(mock / 离线门禁;**mock 通 ≠ 完成**)
|
||||
- 主命令在业务默认档(模板为 `real_rpa`)真实跑通;不得只 mock 通就交活
|
||||
- `task_logs` 写入和查询符合 `references/SCHEMA.md`(含 `created_at` / `updated_at` Unix 秒级规范)
|
||||
- 业务表(若有)写入和查询符合 `references/SCHEMA.md`(含 `created_at` / `updated_at` Unix 秒级规范)
|
||||
- `init_db()` 已写入 `_jiangchang_tables` / `_jiangchang_columns`;用户可见表/字段具备中文 `display_name`
|
||||
- 字段展示顺序与 `PRAGMA table_info(task_logs)` 的 cid 一致;不依赖 `display_order`
|
||||
- 字段展示顺序与 `PRAGMA table_info(<业务表>)` 的 cid 一致;不依赖 `display_order`
|
||||
- `tests/test_display_metadata.py` 通过
|
||||
- 真实联调样例仍放 `tests/integration/`,默认套件不包含真实外联(与业务 `.env` 默认 `real_rpa` 不冲突)
|
||||
- `SKILL.md` 的 `developer_ids` 已配置为开发者本人匠厂用户 ID(开发期不公开时,否则本人无法在市场安装自测;取 ID 步骤见 `development/DEVELOPMENT.md` §6)
|
||||
- 若目标含华商等 OEM 客户端:`SKILL.md` 的 `brands` 已声明对应 `market_code`(通常含 `jiangchang` + 目标 OEM);未写则仅匠厂市场可见
|
||||
- 发布后 Gitea 工作流成功;匠厂技能市场对开发者账号可见最新版本并可安装
|
||||
- 安装后按本技能 `actions.json` 声明完成宿主多入口验收(见 `development/DEVELOPMENT.md` §15);**须看用户体验**(进度可见、失败可读、RPA 有头、文案与真实行为一致等),后台通了不算完
|
||||
- 若声明了 OEM `brands`:在对应 OEM 客户端(`X-Market-Code` 正确)也能检索到该技能
|
||||
- 安装后按本技能入口定位表与 `actions.json` 声明完成宿主多入口验收(见 `development/DEVELOPMENT.md` §15、`PRODUCT_SURFACES.md`);**须看用户体验**(进度可见、失败可读、RPA 有头、文案与真实行为一致等),后台通了不算完
|
||||
- 已人工核验授权与技术路线:无破解、逆向、私有协议、验证码绕过、Hook / 注入、自动换号或其他规避平台限制的实现
|
||||
- RPA 真实主路径已验证状态等待、`1~3` 秒正常操作节奏、有限重试、账号 / 设备串行、人工介入、结果核验和结束存证
|
||||
- RPA 关键元素已在真实界面拾取并登记 element map;关键动作唯一定位,重启会话后复测通过
|
||||
@@ -259,19 +300,18 @@
|
||||
## 2. 业务背景
|
||||
|
||||
当前用户在 `<业务场景>` 中需要反复处理 `<输入数据>`,人工操作耗时且易错。
|
||||
因此需要开发 `your-skill-slug`,自动完成 `<核心动作>`,输出 `<输出结果>`,并写入 task_logs。
|
||||
因此需要开发 `your-skill-slug`,自动完成 `<核心动作>`,输出 `<输出结果>`。
|
||||
|
||||
## 3. 开发目标
|
||||
|
||||
- 解析并校验输入
|
||||
- 通过 adapter 完成 mock / simulator / real 档位动作
|
||||
- 输出结构化结果并写入 task_logs
|
||||
- 输出结构化结果;业务结果写入业务表(有才建)
|
||||
- 支持失败可诊断
|
||||
|
||||
## 4. 功能范围
|
||||
|
||||
- 支持 health / version / init-db / run
|
||||
- 支持 logs / log-get
|
||||
|
||||
## 5. 非功能要求
|
||||
|
||||
@@ -288,7 +328,7 @@
|
||||
|
||||
- 成功 JSON / 摘要
|
||||
- ERROR: 稳定错误码
|
||||
- task_logs 记录
|
||||
- 任务中心终态(`finish`)
|
||||
|
||||
## 7. 依赖的兄弟技能或外部系统
|
||||
|
||||
@@ -303,9 +343,9 @@
|
||||
## 9. 验收标准
|
||||
|
||||
- 命令可运行;tests/run_tests.py -v 通过
|
||||
- task_logs 符合 SCHEMA
|
||||
- 业务表(若有)符合 SCHEMA;过程看运行日志,进度看任务中心
|
||||
- `developer_ids` 已配置为「你的用户ID」(匠厂设置中复制的正整数)
|
||||
- 正式环境安装验证通过;已声明的宿主入口(新建任务 / 数据管理 / 定时任务 / 任务中心 / 技能详情等)按 DEVELOPMENT §15 自测
|
||||
- 正式环境安装验证通过;已声明的宿主入口(新建任务 / 数据管理 / 定时任务 / 任务中心 / 技能详情等)按 DEVELOPMENT §15 与入口定位表自测
|
||||
## 10. 开发注意事项
|
||||
|
||||
- 不修改无关项目;不引入旧模板结构
|
||||
|
||||
@@ -99,6 +99,16 @@ async def open_login(page):
|
||||
|
||||
**状态等待优先:**页面加载、控件可用、弹窗消失、保存成功、下载完成和业务结果出现,必须等待真实条件并设置超时;禁止用固定 `sleep(3)` 假设成功。
|
||||
|
||||
**有界就绪等待(platform-kit ≥ 1.2.11 必用):**
|
||||
|
||||
- 业务技能的状态等待统一使用共享 API:`jiangchang_skill_core.rpa.poll_until`(async)或 `poll_until_sync`。
|
||||
- **禁止**在 service 主路径再发明平行的 `deadline + sleep` / 私有 `_wait_*` 轮询作为标准答案(HITL 专用等待除外,仍可用 kit 的 `wait_for_login` / `wait_for_captcha_pass`)。
|
||||
- 必须区分:**还在加载**(超时内继续等)vs **真的不是目标页/目标态**(有界等待失败后再判)。
|
||||
- 超时默认映射 `ERROR:UI_NOT_READY`(或业务等价就绪码);**不要**把「慢」直接判成 `PAGE_TYPE_MISMATCH`、配置门禁失败或 `ERROR:RATE_LIMITED`。
|
||||
- 超时秒数须允许环境变量 / `.env` 覆盖(慢机器、限频、轻量电源场景可调大)。
|
||||
- 可选浏览器启动超时:`launch_persistent_browser(..., timeout_ms=...)` 或 `OPENCLAW_BROWSER_LAUNCH_TIMEOUT_MS`;超时码 `ERROR:BROWSER_START_TIMEOUT`。默认不开启,与历史行为兼容。
|
||||
- `health` 可带只读 `machine_power`(供电/限频提示);**不得**仅凭该信号 hard-fail 全技能。
|
||||
|
||||
**操作节奏:**
|
||||
|
||||
- 两个正常业务动作之间默认随机等待 `1~3` 秒。
|
||||
@@ -179,18 +189,35 @@ BrowserType.launch_persistent_context: Arguments can not specify page to be open
|
||||
|
||||
**正确做法(生产路径):**
|
||||
|
||||
1. `launch_persistent_context(user_data_dir=profile_dir, executable_path=chrome, args=chrome_args, ...)`
|
||||
1. `launch_persistent_context(user_data_dir=profile_dir, executable_path=chrome, args=chrome_args, locale="zh-CN", no_viewport=True, ...)`
|
||||
2. `page = await context.new_page()`
|
||||
3. `await page.goto(start_url, wait_until="domcontentloaded", timeout=60000)`
|
||||
|
||||
对强风控站点,如果需要更拟人,可以启动后通过**地址栏输入 / 真实点击进入**目标页,但**仍然不能把 URL 塞进 launch args**。
|
||||
|
||||
#### 1.2.1 窗口与 viewport(`POLICY-RPA-007`)
|
||||
|
||||
persistent profile 会记住上次窗口大小;**最大化与 layout 同步**须按下列标准,避免「先小后大、右侧留白、scrollbar 后遗症」:
|
||||
|
||||
| 必须 | 禁止 |
|
||||
|------|------|
|
||||
| `no_viewport=True`(页面 layout 跟随真实窗口) | `viewport=None` 或固定 `viewport={width,height}` 冒充全屏 |
|
||||
| launch args 含 `--start-maximized` | CDP `Browser.setWindowBounds` / JS `resizeTo` **中途改窗** |
|
||||
| 真实站:`browser_session` 内 **启动后尽快 `goto` 业务页** | 长时间停空白页再导航、goto 后再 CDP 最大化 |
|
||||
| 优先复制 `examples/real_browser_rpa/scripts/service/browser_session.py` | 自造 maximize / CDP 窗口 hack |
|
||||
|
||||
说明:
|
||||
|
||||
- **`no_viewport=True` ≠ `viewport=None`**。后者在 Playwright 中不等于「跟窗口走」,易导致 OS 窗口已最大化但页面仍按窄 viewport 排版。
|
||||
- **仿真站**(`examples/simulator_browser_rpa`)可在 `browser_session` 只做 launch、由上层 `goto`;仍须 **`no_viewport=True`**。
|
||||
- 共享库 `jiangchang_skill_core.rpa.launch_persistent_browser` 已内置 `no_viewport=True`;自写 `browser_session` 时勿漏。
|
||||
|
||||
完整参考实现见:
|
||||
|
||||
- `examples/real_browser_rpa/README.md`(**开发真实浏览器 RPA 类 skill 时必须先读**)
|
||||
- `examples/real_browser_rpa/scripts/service/browser_session.py`
|
||||
|
||||
**生产路径不要依赖 CDP 接管现有页面**;CDP 仅用于诊断或桌面宿主已打开浏览器的场景。
|
||||
**生产路径不要依赖 CDP 接管现有页面**;CDP 仅用于诊断或桌面宿主已打开浏览器的场景(**含禁止用 CDP 最大化窗口**)。
|
||||
|
||||
### 1.3 HITL / 验证码
|
||||
|
||||
@@ -359,11 +386,20 @@ skill 退出/抛错统一用 `ERROR:` 前缀 + 稳定码,方便宿主与上层
|
||||
| `ERROR:AMBIGUOUS_ACCOUNT` | `--login-id` / `--label` 匹配到多条 | 改用数字 id 或更精确标识 |
|
||||
| `ERROR:RATE_LIMITED` | 触发频控 | 退避后重试 |
|
||||
| `ERROR:MISSING_BROWSER` | 未检测到 Chrome/Edge | 提示安装 |
|
||||
| `ERROR:BROWSER_START_TIMEOUT` | 浏览器启动超时(可选 timeout) | 可重试;检查机器负载/供电;可调大 `OPENCLAW_BROWSER_LAUNCH_TIMEOUT_MS` |
|
||||
| `ERROR:UI_NOT_READY` | 有界等待内页面/控件未就绪 | **默认可重试**;勿与「页类型错误/配置未开」混用;勿轻易标 session fatal |
|
||||
| `ERROR:ENV_DEGRADED` | 环境降级提示类(限频/供电不足等,技能选用) | 可重试;提示接电源/退出省电/减小并发;**不是**配置错误 |
|
||||
| `ERROR:DEVICE_NOT_READY` | 手机未连接/未授权 | 检查 USB/ADB |
|
||||
| `ERROR:WINDOW_NOT_FOUND` | 桌面目标窗口未找到 | 检查程序是否启动 |
|
||||
|
||||
业务空结果(如「本页无评论」)用业务码或成功空列表表达,**不要**复用上表账号/登录错误码。
|
||||
|
||||
**归因纪律(防误诊):**
|
||||
|
||||
- 「未开 `ALLOW_REAL_RPA` / 档位门禁」必须用技能自己的配置类错误码,**禁止**与真实启动失败、启动超时共用一个 hint。
|
||||
- 页面慢导致的超时优先 `UI_NOT_READY` / 业务就绪码;仅当有明确频控 DOM/文案时才用 `RATE_LIMITED`。
|
||||
- 是否列入技能 `SESSION_FATAL` / `RETRYABLE` 由技能自定,但 **UI 未就绪 / 启动超时默认不应整批不可重试终止**(除非会话已明确不可恢复,如未登录、验证码人工超时)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 存证与录屏规范
|
||||
@@ -402,13 +438,13 @@ skill 退出/抛错统一用 `ERROR:` 前缀 + 稳定码,方便宿主与上层
|
||||
- **skill-template 默认 `OPENCLAW_RECORD_VIDEO=0`**(开发调试更快,不启 ffmpeg)。
|
||||
- **`OPENCLAW_RECORD_VIDEO=1` 时启用录屏**:字幕、旁白、背景音、MP4 成片由 platform-kit 统一处理。
|
||||
- 使用 platform-kit 的 **`RpaVideoSession`**;**skill 不要自行合成视频**(不要自己调 ffmpeg 拼 MP4)。
|
||||
- **默认关闭 ≠ 可以删除录屏接入**:RPA / 长任务 **service 层**(`scripts/service/*.py`)**必须保留** `RpaVideoSession`、`video.add_step`、video artifact merge 到 `result_summary`(见 `POLICY-RPA-003`)。
|
||||
- **默认关闭 ≠ 可以删除录屏接入**:RPA / 长任务 **service 层**(`scripts/service/*.py`)**必须保留** `RpaVideoSession`、`video.add_step`、video artifact merge 进 `finish()`(见 `POLICY-RPA-003`)。
|
||||
- `OPENCLAW_RECORD_VIDEO=0` 时 session **无副作用**(不启 ffmpeg、不写字幕文件、不生成最终 MP4),但 **`RpaVideoSession.add_step` 仍可同步 Activity 进度**。
|
||||
- 生产留证 / 合规 / 客户现场复现时,由用户在 `.env` 或进程环境变量中开启 `OPENCLAW_RECORD_VIDEO=1`。
|
||||
- **ffmpeg 是唯一录屏器**(Windows:`gdigrab` + `desktop`)。
|
||||
- **最终视频**(`OPENCLAW_RECORD_VIDEO=1` 时):`{skill_data_dir}/videos/{skill_slug}_{yyyyMMdd_HHmmss}_{batch_id}.mp4`
|
||||
- **中间产物**:`rpa-artifacts/{batch_id}/capture.mp4`、`subtitles/`、`logs/` 等。
|
||||
- 任务完成后 CLI / `result_summary` 应包含 video 字段(开启录屏时有路径;关闭时仍保留 `video` 结构与诊断占位):`video_path`、`raw_video`、`video_log`、`video_warnings`、`music_path`、`voiceover_path`、`audio_warnings`(见 `scripts/service/task_run_support.py`)。
|
||||
- 任务完成后 CLI / `finish()` 应包含 video 字段(开启录屏时有路径;关闭时仍保留 `video` 结构与诊断占位):`video_path`、`raw_video`、`video_log`、`video_warnings`、`music_path`、`voiceover_path`、`audio_warnings`(见 `scripts/service/task_run_support.py`)。
|
||||
|
||||
模板最小示范见 `scripts/service/task_service.py` 的 `_run_template_demo()`。
|
||||
|
||||
|
||||
@@ -90,6 +90,19 @@ button = row.get_by_role("button", name="添加")
|
||||
|
||||
Playwright CSS locator 默认可穿透 open Shadow DOM;XPath 不穿透。closed Shadow DOM 无法正常读取时,评估目标应用是否提供可访问性属性,不能注入破解。
|
||||
|
||||
**Web Component 双 input 陷阱**(常见于 `<mc-input>` 等宿主):
|
||||
|
||||
- 轻 DOM 内可能有 **hidden 同步用 input**(`aria-hidden="true"`、`tabindex="-1"`)。
|
||||
- 裸写 `host >> input` 时 Playwright 常默认匹配**第一个** hidden input,等 `visible` 会超时。
|
||||
- 做法:等 **host 可见**作 page ready;输入用 shadow 内真实字段(如 `>> #mc-input-track-input` 或 `>> input.input:not([aria-hidden="true"])`);或 click host + keyboard。
|
||||
|
||||
### Cookie / 同意条(可选出现)
|
||||
|
||||
Profile 已点过「允许全部」后,下次可能**不再出现** Cookie 条。不要无限等待:
|
||||
|
||||
- 短超时(约 2~3s)探测可见 → 点击;
|
||||
- 超时或无元素 → **skip 继续**,不要阻塞主流程。
|
||||
|
||||
### 动态列表
|
||||
|
||||
等待列表完成加载后再读取;`locator.all()` 不会等待动态列表稳定。滚动或刷新后重新获取 locator。
|
||||
@@ -109,6 +122,7 @@ Playwright CSS locator 默认可穿透 open Shadow DOM;XPath 不穿透。close
|
||||
## 6. 禁止模式
|
||||
|
||||
- 用全页所有 `input` 的尺寸和位置猜搜索框。
|
||||
- 对 Web Component 裸用 `host >> input` 且不等 shadow 内真实可编辑 input。
|
||||
- 用 `.first` 绕过严格模式。
|
||||
- 从 DevTools 复制深层 `nth-child` 或绝对 XPath。
|
||||
- 只等 URL 含 `search` 就判定结果成功。
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
|
||||
未完成第 2~6 步时,代码只能保留明确的 `TODO: NEED_REAL_UI_INSPECTION`,不得写“看起来可能正确”的 selector。
|
||||
|
||||
开发期推荐使用独立仓 [jiangchang-ui-recorder](https://git.jc2009.com/client-jiangchang/jiangchang-ui-recorder)(本机常见路径 `D:\OpenClaw\client-jiangchang\jiangchang-ui-recorder`,`python run.py`):以 **Raw Event Journal → Action → Element Catalog → 异步 Observation** 录制,优先把 `action_flow.json` 与 `element_catalog.json` 交给 AI 编程工具;原始事件可重新归并,tree/index 作为诊断证据。默认不再让完整页面 dump 阻塞动作采集。本模板仓内的 `tools/ui_inspector` 已移除,勿再从 skill-template 启动。
|
||||
|
||||
## 3. 元素清单
|
||||
|
||||
每个关键输入、按钮、列表、弹窗、成功状态和错误状态都必须登记。推荐 YAML:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 共享 Python Runtime
|
||||
|
||||
**skill-template** 及复制出的新技能,公共能力均来自宿主匠厂安装的共享 Python Runtime(`jiangchang-platform-kit>=1.2.2` 及其传递依赖,含 `playwright`)。`jiangchang_skill_core` **不得**在技能仓库内 vendored,应由共享 venv 的 site-packages 提供。
|
||||
**skill-template** 及复制出的新技能,公共能力均来自宿主匠厂安装的共享 Python Runtime(`jiangchang-platform-kit>=1.2.11` 及其传递依赖,含 `playwright`)。`jiangchang_skill_core` **不得**在技能仓库内 vendored,应由共享 venv 的 site-packages 提供。
|
||||
|
||||
技能根目录 `requirements.txt` **只声明技能特有依赖**;**不要**重复声明 `jiangchang-platform-kit` 或 `playwright`。`SKILL.md` 的 `platform_kit_min_version` 是运行契约,**不是** pip 依赖声明。
|
||||
|
||||
@@ -12,7 +12,7 @@ kit **源码仓**(可 clone 学习,勿 vendor)与其它公开公共仓登
|
||||
|
||||
| 情况 | 做法 |
|
||||
|------|------|
|
||||
| 技能依赖 kit 某次修复/新 API(如 `.env` 空值+行尾 `#` 解析、新 RPA helper) | 把 `metadata.openclaw.platform_kit_min_version` 调到**需要的最低版本**(例如 `"1.2.2"`) |
|
||||
| 技能依赖 kit 某次修复/新 API(如 `.env` 空值+行尾 `#` 解析、`poll_until`、启动超时) | 把 `metadata.openclaw.platform_kit_min_version` 调到**需要的最低版本**(模板默认 `"1.2.11"`) |
|
||||
| PyPI/私有源刚发了更新,但本技能行为未依赖 | **不必**仅为「追新」抬版本 |
|
||||
| 宿主升级时机 | 宿主在技能/宿主声明的最低版本**未满足**时按需升级共享 venv;**不会**仅因源上有更新就后台拉 latest |
|
||||
|
||||
@@ -39,7 +39,7 @@ Unix(未注入时):
|
||||
|
||||
共享解释器通常位于 `{JIANGCHANG_DATA_ROOT}/python-runtime/.venv`。数据根由宿主注入(见 `jiangchang_skill_core.runtime_env`)。
|
||||
|
||||
## Runtime 诊断(platform-kit 1.2.2+)
|
||||
## Runtime 诊断(platform-kit 1.2.11+;含既有 1.2.2+ 字段)
|
||||
|
||||
`health` 命令通过 **`jiangchang_skill_core.collect_runtime_diagnostics`** 输出共享 runtime 诊断,**不在技能内重复实现**。典型字段:
|
||||
|
||||
@@ -48,6 +48,7 @@ Unix(未注入时):
|
||||
- `JIANGCHANG_DATA_ROOT` / `resolved_data_root`
|
||||
- `media_assets_root`、`ffmpeg_path`、`background_music_*`
|
||||
- `record_video_enabled`、`runtime_issue[warning|error]`
|
||||
- `machine_power` — 只读供电/限频探测(探不到则 `probed=false`);**不得**单独作为 hard-fail 依据
|
||||
|
||||
`health` 是**只读诊断**,**不会**触发 media-assets 下载或修复。并补充 `env_path` / `env_exists` / `example_path` 配置路径字段。
|
||||
|
||||
@@ -57,7 +58,7 @@ Unix(未注入时):
|
||||
- 用户实际 `.env`:`{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/{skill_slug}/.env`。
|
||||
- `scripts/main.py` 与 `cli.app.main()` 启动时调用 `util.config_bootstrap.bootstrap_skill_config()`。
|
||||
- 配置优先级:**进程环境变量** > **用户 `.env`** > **`.env.example` 默认值**。
|
||||
- 公共 `config` / `merge_missing_env_keys` 来自共享 runtime 的 `jiangchang-platform-kit>=1.2.2`,**不得** vendored `scripts/jiangchang_skill_core/`。
|
||||
- 公共 `config` / `merge_missing_env_keys` 来自共享 runtime 的 `jiangchang-platform-kit>=1.2.11`,**不得** vendored `scripts/jiangchang_skill_core/`。
|
||||
|
||||
## media-assets / ffmpeg / 背景音乐
|
||||
|
||||
@@ -104,7 +105,7 @@ RPA 录屏成片(`RpaVideoSession`)、ffmpeg 路径、背景音乐探测均
|
||||
{...}/{skill_slug}.db
|
||||
```
|
||||
|
||||
**业务文件(下载、导入、导出、缓存等)** 须落在上述数据目录的标准子目录下,**不得**相对 workspace/CWD 写入。完整子目录树、env 覆盖规则、`resolve_data_path()` 用法见 **[`DATA_PATHS.md`](DATA_PATHS.md)**。
|
||||
**业务文件(输入、输出、缓存等)** 须落在上述数据目录的标准子目录下,**不得**相对 workspace/CWD 写入。用户可见目录为 `input/`(上行)与 `output/`(下行)。完整子目录树、env 覆盖规则、`resolve_data_path()` 用法见 **[`DATA_PATHS.md`](DATA_PATHS.md)**。
|
||||
|
||||
- 业务表使用英文 snake_case;中文表名/字段名写入 `_jiangchang_tables` / `_jiangchang_columns`(见 `references/SCHEMA.md`)。
|
||||
- 界面字段顺序由 `CREATE TABLE` 列定义顺序决定,**不要**维护 `display_order` 来调整顺序。
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
|
||||
本文是匠厂宿主 **Skill Action / 任务中心** 的技能侧金标准。涉及浏览器 RPA、长耗时任务、数据管理按钮、定时任务或 Agent 直接调用时**必读**。
|
||||
|
||||
**产品入口(平级关系、何时挂哪个)**先读 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md);本文管 sync/async、任务中心与编排。
|
||||
|
||||
**边界:**
|
||||
|
||||
| 文档 | 职责 |
|
||||
|------|------|
|
||||
| 本文 | sync/async、任务中心、Agent 禁令、单内核编排、队列 pick、同步 vs 导出 |
|
||||
| [`../references/ACTIONS.md`](../references/ACTIONS.md) | `actions.json` 字段、placements、`bind.tables`、inputSchema |
|
||||
| [`../references/ACTIONS.md`](../references/ACTIONS.md) | `actions.json` 字段、placements、`bind.tables`、`bind.selectionMode`、`bind.visibleWhen`(行菜单显隐)、**用户可见文案**、inputSchema、跨表 Lookup(含分页勾选 `presentation: paged_table`)、字段条件显隐 `visibleWhen`、本地路径 `file`/`directory` + 可选 **`pathRoot`**、选择摘要「已选择」与映射 **`readOnly` / `revealInForm`**、**按钮中文名 `label` 参考** |
|
||||
| [`LOGGING.md`](LOGGING.md) | `job_context` / `emit` / `finish` / Run Journal |
|
||||
| [`DATA_PATHS.md`](DATA_PATHS.md) | 下载/导入等落盘路径 |
|
||||
|
||||
@@ -19,7 +21,7 @@
|
||||
|
||||
1. **宿主管调度,技能管业务** — 不自行实现任务中心 / Job 轮询 / 后台守护进程。
|
||||
2. **单业务内核、多入口** — 数据管理、定时任务、Agent、技能详情与直接 CLI 复用同一 domain service;禁止按入口复制业务逻辑。
|
||||
3. **五职责正交** — `placements` / `bind.tables` / `executionProfile` / `entrypoint` / domain service 各管一事(见 ACTIONS.md)。
|
||||
3. **五职责正交** — `placements` / `bind.tables` / `executionProfile` / `entrypoint` / domain service 各管一事(见 ACTIONS.md)。可选第六轴 **policy**(见 `references/POLICY.md`):未启用 = L0 与今日一致。可选 **`hostEffects` / `steps`**(见 ACTIONS.md):数据管理交互前后的 UI/级联编排;省略 = 不额外改 UI(匠厂宿主 ≥2.8.5)。可选 **`hoverHint`**(见 ACTIONS.md):数据管理工具栏悬停;省略 = 不显示,不回退说明或确认黄条。可选 **表单 `visibleWhen`**(见 ACTIONS.md):Action 参数表单字段条件显隐;省略 = 字段始终平铺;**勿与** **`bind.visibleWhen`**(行菜单)或 Policy L3 混淆;**勿**用其藏映射主键。可选 **`pathRoot`**(见 ACTIONS.md):`file`/`directory` 原生选路对话框起始目录(相对技能数据根;省略默认 `{skill_data}/input`)。可选 **`bind.labelColumn`**(见 ACTIONS.md):选择摘要可读标签列;省略 = 宿主常见列名启发。可选 **`bind.selectionMode`**(见 ACTIONS.md):表勾选如何驱动 Action(`required` / `none` / `optional`);省略 = 宿主按 `$selection.ids` / `batch` 推断;**定时任务等无表勾选入口按无勾选分支**(隐藏 `$selection.ids`,保留 `pickCount`)。可选 **`bind.visibleWhen`**(见 ACTIONS.md「行菜单显隐」):`row` 菜单按当前行列值显隐;省略 = 始终显示。可选 **`revealInForm`**(见 ACTIONS.md):有「已选择」时仍强制显示某 `readOnly` 字段;省略 = 映射只读默认不占 form 参数区(仍提交)。可选 **`cellPresentation`**(见 `references/CELL_PRESENTATION.md`):单元格/整行/整列/区域语义标色;省略 = P0。**用户可见文案**(见 ACTIONS.md「用户可见文案」):弹窗 `description` / 确认黄条 / 参数说明 / 任务失败 `message` 须全中文产品用语;Agent 路由勿塞进弹窗。
|
||||
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 流**。
|
||||
@@ -38,7 +40,7 @@
|
||||
|
||||
| 值 | 宿主行为 | 超时 | 任务中心 |
|
||||
|----|----------|------|----------|
|
||||
| **`sync`**(或省略时的宿主缺省) | 调用方等待 CLI 结束,直接返回结构化结果或结构化错误;**不**创建后台 Job | 默认约 **30s**(`SKILL_ACTION_SYNC_TIMEOUT_MS`) | ❌ 不进 |
|
||||
| **`sync`**(或省略时的宿主缺省) | 调用方等待 CLI 结束,直接返回结构化结果或结构化错误;**不**创建后台 Job | 默认约 **60s**(`SKILL_ACTION_SYNC_TIMEOUT_MS`) | ❌ 不进 |
|
||||
| **`async`**(须显式声明) | 后台 spawn Job,立即返回 `jobId`;`emit` / `checkpoint` / `finish`;长任务应支持暂停/停止/取消检查 | Job 本身无默认墙钟上限 | ✅ 进入 |
|
||||
|
||||
直接执行 CLI:终端等待进程结束。宿主经 Action 调用同一 CLI:再按 `executionProfile` 分流。技能不得自建任务中心。
|
||||
@@ -48,7 +50,7 @@
|
||||
| 场景 | 建议 |
|
||||
|------|------|
|
||||
| 浏览器 / Playwright / RPA 主路径 | `async` + 建议 `confirmation` |
|
||||
| 单次可能超过 ~20s 的网络/下载/同步 | `async` |
|
||||
| 单次可能接近或超过约 60s 的网络/下载/同步 | `async` |
|
||||
| `health` / `version` / `config-path` / 纯统计 / 秒级查询 | `sync` |
|
||||
| 任意 placement(含 toolbar) | 允许 sync 或 async;以 manifest 为准 |
|
||||
|
||||
@@ -66,7 +68,7 @@
|
||||
|-------------------------|-------------------|--------|--------------|
|
||||
| **新建任务**(对话 Agent) | `agent` | `placements` 含 `agent`;`run_skill_action` | 侧栏「新建任务」→ 对话触发 |
|
||||
| **数据管理 · 顶栏** | `data-management` | `toolbar` + 必填 `bind.tables` | 侧栏「数据管理」→ 表顶栏按钮 |
|
||||
| **数据管理 · 行内** | `data-management` | `row`(建议 `bind.tables` + `inputMapping`) | 同行操作链接(需主键) |
|
||||
| **数据管理 · 行级** | `data-management` | `row`(建议 `bind.tables` + `inputMapping`) | 操作列和/或**行右键**(及可选「⋯」)可见并触发(需主键);技能配什么就显示什么 |
|
||||
| **数据管理 · 批量** | `data-management` | `batch` | 勾选行后点批量按钮 |
|
||||
| **定时任务** | `cron` | `placements` 含 `cron` | 侧栏「定时任务」→「技能直调」 |
|
||||
| **任务中心** | (各入口触发的 Job) | `executionProfile: "async"` | 侧栏「任务中心」查看进度 / 取消;**不是** placement |
|
||||
@@ -141,7 +143,7 @@ cmd_stats() # 只读 sync:JSON stdout,无 job_context
|
||||
| 能力 | 约定 |
|
||||
|------|------|
|
||||
| 队列表 | `status`: `pending` → `processing` → `success` / `failed` |
|
||||
| 入库 | 数据管理 CRUD,或 `import-*`(相对 `{skill_data}/imports/`,见 [`DATA_PATHS.md`](DATA_PATHS.md)) |
|
||||
| 入库 | 数据管理 CRUD,或 `import-*`(相对 `{skill_data}/input/`,见 [`DATA_PATHS.md`](DATA_PATHS.md)) |
|
||||
| 消费 | `run --pick N`,默认仅 `pending` |
|
||||
| 重试 | `reset` 或数据管理改 `pending`;**failed 不要自动重试** |
|
||||
| 批次 | 可选 `batch_id`,便于筛选与任务中心对照 |
|
||||
@@ -164,9 +166,10 @@ cmd_stats() # 只读 sync:JSON stdout,无 job_context
|
||||
|------|------|--------|
|
||||
| 同步 / refresh / collect | 外部来源 → 技能本地 SQLite | 技能 domain service |
|
||||
| 导出当前数据表 | 本地库 → CSV/Excel | **宿主**数据管理统一导出 |
|
||||
| 特殊业务报告 | 多表合并 / 特殊格式 / 附加计算 | 仅系统导出不足时提供 `export-*` |
|
||||
| 特殊业务报告(文件) | 多表合并 / 特殊格式 → `output/` | 仅系统导出不足时提供 `export-*` |
|
||||
| HTML 页面报告 | 壳子页 `assets/html/pages/*.html`;快照写入 `{skill_data}/html/*.html` | 壳子装完即现;快照由技能生成,重新生成 Action id 固定为 **`render-html`**。页面用 `jiangchang.call` 调已声明命令 |
|
||||
|
||||
同步 Action 默认写库、不写导出文件。通用幂等 / 事务 / 空结果保护见 [`../references/SCHEMA.md`](../references/SCHEMA.md)。
|
||||
同步 Action 默认写库、不写导出文件。HTML 报告契约见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「HTML 页面报告」与 [`DATA_PATHS.md`](DATA_PATHS.md)。通用幂等 / 事务 / 空结果保护见 [`../references/SCHEMA.md`](../references/SCHEMA.md)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -22,19 +22,20 @@
|
||||
|
||||
必跑套件要像一个紧张的守门员:**快、确定、离线**。典型覆盖:
|
||||
|
||||
- CLI:导入 [`cli.app`](../scripts/cli/app.py) 走解析链路、`health`(runtime diagnostics)/ `version` / `init-db` / `logs` / `log-get` 冒烟;
|
||||
- CLI:导入 [`cli.app`](../scripts/cli/app.py) 走解析链路、`health`(runtime diagnostics)/ `version` / `init-db` 冒烟;
|
||||
- 架构守护:无 `scripts/jiangchang_skill_core/`、`platform-kit>=1.2.0` 导入来源、文档/runtime 标准(见 `test_platform_import.py` 等);
|
||||
- **真实 subprocess**:[`tests/test_entrypoint_subprocess.py`](../tests/test_entrypoint_subprocess.py) 再调用一遍 `python scripts/main.py`,防路径漂移;
|
||||
- 运行时:`runtime_paths` 与 **`JIANGCHANG_*` 隔离**;
|
||||
- `SKILL.md` YAML slug vs [`constants.SKILL_SLUG`](../scripts/util/constants.py);
|
||||
- SQLite 骨架:`task_logs` 创建幂等与仓储读写;
|
||||
- SQLite 骨架:`init_db` 幂等创建元数据表,默认不建业务表;
|
||||
- 数据管理元数据:`_jiangchang_*` 幂等初始化、中文展示名、`PRAGMA table_info` 字段顺序、editable 权限(见 `test_display_metadata.py`、`test_data_management_contract.py`);
|
||||
- Skill Action manifest:`assets/actions.json` 结构、CLI 映射、placements 与 inputSchema 守护(见 `test_actions_manifest.py`);
|
||||
- 标准时间字段:`created_at` / `updated_at` 默认值、trigger 维护与 `datetime_unix_seconds` 元数据(见 `test_timestamp_columns.py`);
|
||||
- **adapter profile**:[`tests/test_adapter_profile_policy.py`](../tests/test_adapter_profile_policy.py) + [`adapter_test_utils`](../tests/adapter_test_utils.py) ——验证在未授权情况下绝不误判开启真实网络/RPA。
|
||||
- **发布打包守护**:[`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 命名规范校验。
|
||||
- **slug 语义**:[`tests/test_slug_naming.py`](../tests/test_slug_naming.py) ——动词-名词命名;平台可选,`default` 不再作为占位。
|
||||
- **HTML 报告壳**:[`tests/test_html_page_service.py`](../tests/test_html_page_service.py) 守护 `assets/html/example-report.html` 的数据管理 iframe 预览布局基线,并守护 ACTIONS.md 里的壳子页目录 `assets/html/pages` 与 `jiangchang.call`。业务技能若有自定义 `assets/html/*.html` 或 `pages/*.html`,建议在自有仓库加同类静态断言(见 `references/ACTIONS.md`「宿主预览布局」)。不要把 `example-report.html` 放进 `pages/`。
|
||||
|
||||
**原则:`tests/test_*.py` 不允许隐形访问外网、不允许拉起真实浏览器、不允许读写开发者机器的真实数据根**。如果需要仿真服务器,也应仅在 integration(并由档位变量放行)。
|
||||
|
||||
@@ -198,7 +199,7 @@ Golden fixture 流程同理([`tests/samples/test_golden_cases.py.sample`](../t
|
||||
[`tests/test_development_policy_guard.py`](../tests/test_development_policy_guard.py) 中的 `POLICY-LOGGING-*` 规则会检查 logging **hard policy**(见 [`POLICY_MATRIX.md`](POLICY_MATRIX.md)):
|
||||
|
||||
- CLI 是否调用 `setup_skill_logging`(`POLICY-LOGGING-001`)
|
||||
- `task_service.py` 是否使用 `get_skill_logger`、含 `log.info` / `log.exception`、写入 `save_task_log`(`POLICY-LOGGING-002` / `003`)
|
||||
- `task_service.py` 是否使用 `get_skill_logger`、含 `log.info` / `log.exception`(`POLICY-LOGGING-002` / `003`)
|
||||
- 长任务模板是否使用 Activity 或 RPA video step(`POLICY-LOGGING-004`)
|
||||
- 交付代码是否在 logging 调用中拼接明显敏感字段(`POLICY-LOGGING-005`)
|
||||
- **域模块**(`scripts/service/**`,豁免薄工具)是否有文件日志调用(`POLICY-LOGGING-006`)
|
||||
@@ -242,7 +243,7 @@ Golden fixture 流程同理([`tests/samples/test_golden_cases.py.sample`](../t
|
||||
## 12. RPA / video 测试标准
|
||||
|
||||
- 模板 `.env.example` 默认 **`OPENCLAW_RECORD_VIDEO=0`**;默认必跑测试**不应启动真实 ffmpeg**(patch session 或显式设 `OPENCLAW_RECORD_VIDEO=0`)。
|
||||
- **但测试必须守护录屏接入能力**(`POLICY-RPA-003` 扫描 `scripts/service/*.py`):`RpaVideoSession` 被构造、`video.add_step` 被调用、video artifact 结构 merge 进 `result_summary`(`video` / `video_path` 等字段),与默认是否录屏无关。
|
||||
- **但测试必须守护录屏接入能力**(`POLICY-RPA-003` 扫描 `scripts/service/*.py`):`RpaVideoSession` 被构造、`video.add_step` 被调用、video artifact 结构 merge 进 `finish()`(`video` / `video_path` 等字段),与默认是否录屏无关。
|
||||
- 断言 `title` / `closing_title` 是**中文**业务文案。
|
||||
- 断言 step 文案贴近用户动作,不是技术日志(如「准备执行示例任务」而非「enter cmd_run」)。
|
||||
- 若需测试**真实录屏**(ffmpeg 成片),必须显式设 `OPENCLAW_RECORD_VIDEO=1`,并放在 `tests/integration/` 或手动触发场景,**不得**进入默认 unittest 套件。
|
||||
@@ -265,7 +266,7 @@ Golden fixture 流程同理([`tests/samples/test_golden_cases.py.sample`](../t
|
||||
|
||||
- [ ] `requirements.txt` **不含** `jiangchang-platform-kit` / `playwright`
|
||||
- [ ] 无 `scripts/jiangchang_skill_core/` vendored 副本
|
||||
- [ ] `platform_kit_min_version` **>= 1.2.2**(`SKILL.md` + `constants.py`)
|
||||
- [ ] `platform_kit_min_version` **>= 1.2.11**(`SKILL.md` + `constants.py`)
|
||||
- [ ] `health` 能输出 `platform_kit_version_ok`(或等价诊断行)
|
||||
- [ ] `config-path` 可输出用户 `.env` 路径 JSON
|
||||
- [ ] `pytest.ini` 存在且 `python_files` 只收集 `test_*.py` / `*_test.py`
|
||||
|
||||
222
development/TUTORIAL_SURFACES.md
Normal file
222
development/TUTORIAL_SURFACES.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# 用户市场文档契约(说明 Tab / 教程 Tab)
|
||||
|
||||
本文给**技能开发者**与 **AI 编程助手**读:规定根目录 `README.md`(技能说明)与 `TUTORIAL.md`(使用教程)**写什么、不写什么**,以及如何与宿主三入口、入口定位表、`actions.json` 对齐。
|
||||
|
||||
**单一真相链:**
|
||||
|
||||
```text
|
||||
REQUIREMENTS.md §0.1 入口定位表
|
||||
↓
|
||||
assets/actions.json(placements / label / bind)
|
||||
↓
|
||||
TUTORIAL.md(用户可读、分入口操作)
|
||||
README.md(能力与边界;不写步骤)
|
||||
```
|
||||
|
||||
产品入口「挂什么」见 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md)。本文只管**用户市场文档怎么投影**。
|
||||
|
||||
---
|
||||
|
||||
## 0. 与四 Tab 的关系
|
||||
|
||||
| 文件 | 市场 Tab | 本文角色 |
|
||||
|------|----------|----------|
|
||||
| `README.md` | **说明** | §1:是什么 / 给谁 / 边界 |
|
||||
| `TUTORIAL.md` | **教程** | §2~§6:怎么用(分入口) |
|
||||
| `DEMO.md` | 演示 | 观看顺序可对齐三入口,不另造标准 |
|
||||
| `CHANGELOG.md` | 更新日志 | 与文档契约无关 |
|
||||
|
||||
**铁律:** 说明就是说明,教程就是教程。**禁止**在 README 里写分步操作、Agent 话术块、排障「点哪里」;这些一律进 `TUTORIAL.md`。
|
||||
|
||||
### 0.1 标题层级(说明 / 教程共用)
|
||||
|
||||
宿主详情页**已经**展示技能名;「说明 / 使用教程」Tab(及数据管理右侧教程抽屉)**已经**标明文档类型。因此:
|
||||
|
||||
| 层级 | 用途 |
|
||||
|------|------|
|
||||
| `#` | **正文不要用**(会与页头/Tab 标题重复抢视觉) |
|
||||
| `##` | 主分节(能帮你做什么、入口一:数据管理…) |
|
||||
| `###` | 分节内步骤 |
|
||||
|
||||
**禁止:**
|
||||
|
||||
- `README.md` 文首 `# 【技能名】` / `# 审计疑点辅助` 这类与页头重复的 H1
|
||||
- `TUTORIAL.md` 文首 `# 【技能名】· 使用教程` 这类与 Tab/抽屉标题重复的 H1
|
||||
|
||||
去掉重复 H1 后,**不要**把原来的 `##` 全部升成 `#`。
|
||||
|
||||
---
|
||||
|
||||
## 1. README.md(说明 Tab)
|
||||
|
||||
### 1.1 必须回答
|
||||
|
||||
| 问题 | 写法 |
|
||||
|------|------|
|
||||
| 这是什么 | 开篇一两句价值(**普通段落或引用**,不要再写技能名 H1) |
|
||||
| 能做什么 | 3~6 条能力 bullet(**无步骤编号**) |
|
||||
| 适合谁 / 场景 | 业务语言 |
|
||||
| 使用前准备 | 安装、账号、权限等**前置条件**;不写「去哪张表点哪」 |
|
||||
| 执行后得到什么 | 结果形态(表、报告、文件),**不教怎么生成** |
|
||||
| 注意事项 | 边界、风险、人工复核 |
|
||||
| 怎么使用(可选,短) | **一两句**:支持哪些入口 + **「步骤见使用教程 Tab」** |
|
||||
|
||||
### 1.2 禁止写入 README
|
||||
|
||||
- `1.` `2.` `3.` 操作步骤(如「选品任务 → 点某某按钮」)
|
||||
- 「你可以这样告诉 Agent」整节或大块 `>` 示例话术
|
||||
- 「入口一 / 入口二」专章结构
|
||||
- 逐步排障(「先点 A 再点 B」)
|
||||
- frontmatter `description` 不要写「以及如何开始使用」类教程承诺
|
||||
|
||||
### 1.3 FAQ 边界
|
||||
|
||||
- README FAQ:**概念类**(和真实系统区别、从哪开始学、是否支持定时)→ 可指向「使用教程」Tab
|
||||
- **操作类**排障 → 只写在 `TUTORIAL.md`「常见卡住怎么办」
|
||||
|
||||
### 1.4 建议章节(标题可微调)
|
||||
|
||||
```text
|
||||
(开篇一两句价值;不要写 # 技能名)
|
||||
## 这个技能能帮你做什么
|
||||
## 适合什么场景
|
||||
## 使用前需要准备什么
|
||||
## 怎么使用 ← 仅短摘要 + 指向教程
|
||||
## 执行后你会得到什么
|
||||
## 注意事项
|
||||
## 常见问题 ← 概念类
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. TUTORIAL.md(教程 Tab)必选结构
|
||||
|
||||
复制业务技能后,**验收章节(标题可微调,语义不可缺)**:
|
||||
|
||||
```text
|
||||
(开篇:数据管理与对话平行;定时有/无一句。不要写 # 技能名 · 使用教程)
|
||||
|
||||
## 本技能做什么 ← 业务数据流(非 CLI/SQL)
|
||||
## 开始前
|
||||
## 入口一:数据管理 ← §0.1 有 toolbar/row/batch/replaces 则必填
|
||||
## 入口二:对话任务 ← §0.1 有 agent 则必填
|
||||
## 入口三:定时任务 ← 有 cron:怎么用;无 cron:不需要 + 理由
|
||||
## 主能力对照表 ← 双挂能力必填
|
||||
## 常见卡住怎么办
|
||||
```
|
||||
|
||||
### 2.1 与入口定位表的关系
|
||||
|
||||
| §0.1 / actions | TUTORIAL 要求 |
|
||||
|----------------|---------------|
|
||||
| 任一能力挂数据管理(toolbar/row/batch/replaces) | **必须**有「入口一:数据管理」专章 |
|
||||
| 任一能力挂 `agent` | **必须**有「入口二:对话任务」专章(**禁止**用「或者你也可以说…」替代专章) |
|
||||
| 任一能力挂 `cron` | **必须**有「入口三」且写清如何建定时、选哪个 Action |
|
||||
| **无** `cron` | **必须**写「本技能不需要定时」+ 一句理由(与 §0.1 一致) |
|
||||
| 某能力只挂一端 | 对照表注明「仅数据管理」或「仅对话」 |
|
||||
|
||||
### 2.2 平行关系(数据管理 ∥ 对话)
|
||||
|
||||
- 两者是**平行主入口**,不是「对话主、点表辅」或反过来
|
||||
- 禁止全文只有「第一步:打开对话说清目标」且无数据管理专章
|
||||
- 允许用户只用其中一种;教程须写清「两种效果一致,选你习惯的」
|
||||
|
||||
---
|
||||
|
||||
## 3. 入口一:数据管理(怎么写)
|
||||
|
||||
适合写:
|
||||
|
||||
1. 左侧有哪些业务表、各干什么(业务语言)
|
||||
2. 首次准备(载入/导入/新建)——**按钮必须出现在用户能看到的地方**(见 §5)
|
||||
3. 主流程:进哪张表 → 是否要选中行 → 点哪个 `label` → 去哪看结果
|
||||
4. 行级 / 批量操作(若有 `row` / `batch`)
|
||||
|
||||
按钮中文名 = `assets/actions.json` 的 `label`,不要另造别名。
|
||||
|
||||
---
|
||||
|
||||
## 4. 入口二:对话任务(怎么写)
|
||||
|
||||
适合写:
|
||||
|
||||
1. 什么时候用对话(与数据管理互补)
|
||||
2. 示例话术表(与主能力一一对应)
|
||||
3. Agent 可能追问什么、用户怎么答
|
||||
|
||||
**不要**把对话写成唯一路径;也不要把话术塞进 README。
|
||||
|
||||
---
|
||||
|
||||
## 5. 准确性铁律(用户看得见才写)
|
||||
|
||||
教程步骤必须对应 **宿主真实 UI**,不是只对应 manifest 声明。
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| **placements 对齐** | 教程写的入口 ∈ `actions.json` 已挂 placement |
|
||||
| **skill-detail 不可作主路径** | 声明了 `skill-detail` **不等于**详情页一定渲染出按钮;当前宿主常只显示四 Tab。**禁止**把「技能详情 → 点某某」写成用户主路径 |
|
||||
| **准备/载入/导入类** | 优先挂 **数据管理 `toolbar`** + `bind.tables`(空表也可点的能力尤其如此);可保留 `agent`;`skill-detail` 仅作可选兼容 |
|
||||
| **写入后刷新** | 建议 `steps: [{ "when": "afterSuccess", "do": "refreshCatalog" }]`,避免用户点完表不刷新 |
|
||||
| **对照表** | 「想做什么 \| 数据管理 \| 对话」覆盖双挂主能力;`label` 与界面一致 |
|
||||
|
||||
字段细节见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「用户可见性与教程准确性」。
|
||||
|
||||
---
|
||||
|
||||
## 6. 入口三:定时任务(可选)
|
||||
|
||||
### 6.1 何时挂 `cron`(教程教「怎么用定时」)
|
||||
|
||||
- 参数相对固定、按节奏重复(同步、巡检、消费队列)
|
||||
- 无人值守仍安全、可幂等或可失败重试
|
||||
- 与数据管理是「白天点选 + 夜里自动啃」组合
|
||||
|
||||
教程写:侧栏定时任务 → 新建 → 选本技能 → 选 Action → 填参 → 保存/立即运行。
|
||||
|
||||
### 6.2 何时不挂(教程写「不需要」)
|
||||
|
||||
- 一次性 / 课堂演示,对象当场指定
|
||||
- 强不可逆、须当场确认
|
||||
- 每次对象不同、须人指定链接或范围
|
||||
- 主路径依赖验证码 / 人工介入
|
||||
|
||||
决策三问(与 [`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md) §2.3 一致):
|
||||
|
||||
1. 明天人不盯着、同样参数再跑一遍,业务上合理吗?
|
||||
2. 失败后自动重跑可接受吗?
|
||||
3. 有没有须人当场确认或动态指定的输入?
|
||||
|
||||
①是 且 ②可接受 且 ③否 → 可挂 cron;否则不挂,§0.1 写理由,教程声明 N/A。
|
||||
|
||||
---
|
||||
|
||||
## 7. 复制后自检(发布前)
|
||||
|
||||
- [ ] README 无分步操作、无 Agent 话术块
|
||||
- [ ] TUTORIAL 有三入口结构(定时有或 N/A)
|
||||
- [ ] 对照表与 `actions.json` `label` 一致
|
||||
- [ ] 准备类按钮在数据管理表顶可见(若技能有数据管理主路径)
|
||||
- [ ] 教程未把 skill-detail 当唯一载入/主流程入口
|
||||
- [ ] §0.1「挂」的入口在 TUTORIAL 有专章;「不挂」有理由且教程一致
|
||||
|
||||
交活清单见 [`CHECKLIST.md`](CHECKLIST.md) A2-7~A2-11。
|
||||
|
||||
---
|
||||
|
||||
## 8. 参考样例
|
||||
|
||||
已验收参考(业务技能,非模板正文):`client-gdcm/review-sku-selection-default`
|
||||
(说明 Tab 瘦身 + 教程三入口 + 「载入课堂样例」挂 `selection_cases` toolbar。)
|
||||
|
||||
复制模板后**不要**照搬选品表名与样例编码;只复用**章节结构与职责划分**。
|
||||
|
||||
---
|
||||
|
||||
## 9. 相关文档
|
||||
|
||||
- 宿主入口何时挂:[`PRODUCT_SURFACES.md`](PRODUCT_SURFACES.md)
|
||||
- 入口定位表:[`REQUIREMENTS.md`](REQUIREMENTS.md) §0.1
|
||||
- Action placements / 可见性:[`../references/ACTIONS.md`](../references/ACTIONS.md)
|
||||
- 市场四 Tab 同步:[`DEVELOPMENT.md`](DEVELOPMENT.md) §6 附近「用户市场四 Tab」
|
||||
- 交活验收:[`CHECKLIST.md`](CHECKLIST.md) §2
|
||||
@@ -52,7 +52,7 @@ real_browser_rpa/
|
||||
8. **等待结果容器** — `#search-result-container`(示例 selector)
|
||||
9. **滚动采集循环** — 采集可见条目 → 判断 stop_reason → 滚动加载
|
||||
10. **返回结果** — `ScrapeRunResult` + `format_stop_reason_for_user()`
|
||||
11. **释放租约** — `finally` 中 `release_lease()`
|
||||
11. **释放租约** — `finally` 中**先** `release_lease()`,**再**关浏览器;进度 `emit` 不得用裸 `except Exception` 吞掉 `JobStopped`
|
||||
|
||||
## 开发新 skill 时如何复制
|
||||
|
||||
@@ -62,7 +62,7 @@ real_browser_rpa/
|
||||
4. 修改 `util/constants.py` 中的 `TARGET_PLATFORM`、`DEFAULT_START_URL`、selector 常量
|
||||
5. 替换 `task_rpa.py` 中的登录态 marker、搜索框/结果解析逻辑(**必须 F12 实测**)
|
||||
6. 替换 `human_verification.py` 中的验证码 selector(若目标站点 DOM 不同)
|
||||
7. 在 `task_service.py` 中接入真实 DB、任务日志、RpaVideoSession
|
||||
7. 在 `task_service.py` 中接入真实 DB、RpaVideoSession
|
||||
8. 在新技能根目录补齐 `scripts/main.py` CLI;`SKILL.md` 与根 `README.md` 按模板主目录规则编写,**不要**复制本 README
|
||||
|
||||
## Copy map(复制到新技能 `scripts/`)
|
||||
@@ -91,6 +91,7 @@ real_browser_rpa/
|
||||
- **不要**在技能内安装 Playwright;浏览器与 Python 包由宿主/runtime 提供
|
||||
- `launch_persistent_context` 的 `args` **只放 Chrome 参数**;**不要把 URL 放进 `args`**
|
||||
- 页面必须通过 `new_page()` + `goto()`,或通过真实地址栏/点击进入
|
||||
- **必须** `no_viewport=True` + `--start-maximized`;**禁止** `viewport=None` 与 CDP/JS 中途改窗(见 `development/RPA.md` §1.2.1、`POLICY-RPA-007`)
|
||||
|
||||
## 复制边界:哪些能复制,哪些必须替换
|
||||
|
||||
@@ -116,7 +117,6 @@ real_browser_rpa/
|
||||
- `_parse_item_locator()` 业务字段解析
|
||||
- 用户可见中文文案
|
||||
- 数据库写入逻辑
|
||||
- 任务日志逻辑
|
||||
- 录屏/截图业务标题
|
||||
|
||||
### 禁止照抄的
|
||||
@@ -151,7 +151,7 @@ real_browser_rpa/
|
||||
- **不自动破解验证码** — 滑块/短信只检测 + 等待人工完成
|
||||
- **不对抗平台风控** — 频控、账号风险、封禁或权限不足时停止并交由人工处理
|
||||
- **日志脱敏** — 不输出完整手机号/账号
|
||||
- **租约释放** — `ensure-web` / `pick-web --lease` 后必须在 `finally` 释放
|
||||
- **租约释放** — `ensure-web` / `pick-web --lease` 后必须在 `finally` **先**释放租约再关浏览器;任务中心中断靠 `JobStopped` 进 `finally`,禁止吞掉停止信号
|
||||
- **失败留痕** — 真实 skill 应在关键失败点截图(示例中已留注释位)
|
||||
|
||||
## 常见坑
|
||||
@@ -175,7 +175,7 @@ real_browser_rpa/
|
||||
| `human_verification.py` | `SLIDER_SELECTORS`、`SMS_SELECTORS`、`DIALOG_TEXT_CONTAINERS` |
|
||||
| `task_rpa.py` | `LOGGED_IN_SELECTORS`、`LOGGED_OUT_SELECTOR`、`_parse_item_locator()` |
|
||||
| `account_client.py` | `PLACEHOLDER_PLATFORM`、`LEASE_HOLDER`、账号 setup 文案 |
|
||||
| `task_service.py` | 接入 DB、任务日志、entitlement、视频留痕 |
|
||||
| `task_service.py` | 接入 DB、entitlement、视频留痕 |
|
||||
|
||||
## 依赖说明
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ async def start_browser_session(
|
||||
"""
|
||||
启动持久化 Chrome profile,new_page + goto 入口页,返回 (playwright, context, page)。
|
||||
|
||||
窗口标准(POLICY-RPA-007):no_viewport=True + --start-maximized;勿用固定 viewport 或 CDP 改窗。
|
||||
真实 skill 需要安装 playwright 与 jiangchang_skill_core。
|
||||
"""
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
@@ -12,6 +12,7 @@ from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
from jiangchang_skill_core import config
|
||||
from jiangchang_skill_core.activity import interruptible_sleep
|
||||
from jiangchang_skill_core.rpa import poll_until
|
||||
|
||||
from service.browser_session import close_browser_context, get_start_url, start_browser_session
|
||||
from service.human_verification import (
|
||||
@@ -236,17 +237,20 @@ async def _find_search_button(page):
|
||||
|
||||
|
||||
async def _wait_search_results(page, timeout_sec: float = 45.0) -> bool:
|
||||
deadline = time.monotonic() + timeout_sec
|
||||
while time.monotonic() < deadline:
|
||||
"""有界等待搜索结果容器出现(kit poll_until;区分加载中与超时未就绪)。"""
|
||||
|
||||
async def _ready() -> bool:
|
||||
try:
|
||||
url = page.url or ""
|
||||
if "/search" in url or "search" in url.lower():
|
||||
if await page.locator(RESULT_CONTAINER_SELECTOR).count() > 0:
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
await interruptible_sleep(0.8)
|
||||
return False
|
||||
return False
|
||||
return False
|
||||
|
||||
result = await poll_until(_ready, timeout_sec=timeout_sec, interval_sec=0.8)
|
||||
return result.ok
|
||||
|
||||
|
||||
async def _has_end_marker(page) -> bool:
|
||||
|
||||
@@ -74,7 +74,6 @@ async def run_keyword_search_task(
|
||||
|
||||
真实 skill 中还应:
|
||||
- 写 SQLite 入库(items 表)
|
||||
- 写 scrape_tasks / task_logs 任务日志
|
||||
- 使用 RpaVideoSession 录制步骤留痕
|
||||
- 失败时 capture_failure 截图
|
||||
"""
|
||||
|
||||
@@ -55,7 +55,7 @@ simulator_browser_rpa/
|
||||
| `browser_session.py` | async 系统 Chrome/Edge + launch args;**不在此 goto** |
|
||||
| `account_client.py` | **唯一** account-manager subprocess 封装(`pick_web_account`→ensure-web / `release_lease`) |
|
||||
| `simulator_playwright.py` | 门户轮询、登录、批量表单、PIN、解析 `batch_id`、截图 |
|
||||
| `adapter/simulator_rpa.py` | **薄** adapter:pick 账号、lease、委托 RPA、`finally release_lease` |
|
||||
| `adapter/simulator_rpa.py` | **薄** adapter:pick 账号、lease、委托 RPA、`finally` 先 `release_lease` 再关浏览器 |
|
||||
| `adapter/mock.py` | 不启浏览器,供 unit/mock/CI |
|
||||
| `task_service.py` | async 校验输入 → 选 adapter → `await submit_batch` |
|
||||
| `sandbox/demo_app.html` | 可控 DOM;含可选门户门闩 + 原批量提交流程 |
|
||||
|
||||
@@ -51,7 +51,7 @@ async def run_batch_submit(
|
||||
"""
|
||||
最小编排示例。
|
||||
|
||||
真实 skill 中还应:写 task_logs、entitlement、RpaVideoSession 等。
|
||||
真实 skill 中还应:写业务表、entitlement、RpaVideoSession 等。
|
||||
账号 pick / lease 在 adapter 内完成,编排层不直接碰 account-manager。
|
||||
"""
|
||||
del force # 示例占位;真实 skill 可用于跳过确认
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
343
references/CELL_PRESENTATION.md
Normal file
343
references/CELL_PRESENTATION.md
Normal file
@@ -0,0 +1,343 @@
|
||||
# 数据管理单元格呈现(Cell Presentation)契约(v1.0)
|
||||
|
||||
> **状态:正式契约 v1.0(`cells[]` 文档 `schemaVersion` 1)+ 加法 Grid Presentation。**
|
||||
> **目标:** 冻结「技能声明语义呈现 → 宿主映射主题样式」的通用能力:某一格、整行、整列、若干行×若干列,供任意业务场景复用。
|
||||
> **非目标:** 超时/SLA 专用字段;技能下发任意 CSS / 任意色值;宿主实现业务规则引擎。
|
||||
> **配套 Schema:** [`../assets/schemas/skill-cell-presentation.schema.json`](../assets/schemas/skill-cell-presentation.schema.json)(格子)、[`../assets/schemas/skill-grid-presentation.schema.json`](../assets/schemas/skill-grid-presentation.schema.json)(行/列/区域)。
|
||||
|
||||
相关正交轴见 [`ACTIONS.md`](ACTIONS.md)、[`POLICY.md`](POLICY.md)、[`SCHEMA.md`](SCHEMA.md)。
|
||||
|
||||
---
|
||||
|
||||
## 0. 一句话
|
||||
|
||||
技能可以声明:**哪张业务表**上,**哪些行 × 哪些列**需要特殊标色/强调(语义 `tone` + 可选说明)。
|
||||
|
||||
| 想涂什么 | 怎么声明 |
|
||||
|----------|----------|
|
||||
| 某一格 | 一行 × 一列(v1 `cells[]` 或一条格子规则) |
|
||||
| 整行 | 若干行 × 该轴全部列 |
|
||||
| 整列 | 该轴全部行 × 若干列 |
|
||||
| 区域 | 若干行 × 若干列 |
|
||||
|
||||
用途不限:时效超时未填、库存预警、状态异常、必填缺失、审核驳回等。
|
||||
**不是**「再加一列超时文字」;强调落在**真正要关注的业务格子或行**上。
|
||||
|
||||
---
|
||||
|
||||
## 0.1 能力档位
|
||||
|
||||
| 档位 | 技能行为 | 宿主行为 |
|
||||
|------|----------|----------|
|
||||
| **P0(默认)** | 不写呈现数据 / 未启用 | 数据管理表格与今日一致,无特殊标色 |
|
||||
| **P1** | 按规则写入呈现文档或推荐旁路表 | 渲染对应格子 / 行 / 列 / 区域的语义样式;可选汇总 `alert: true` 做进页提醒 |
|
||||
|
||||
多数技能保持 **P0**。需要视觉强调时再启用。模板 `actions.json` **默认不启用**。
|
||||
|
||||
---
|
||||
|
||||
## 0.2 宿主能力
|
||||
|
||||
| 能力 | 旁路表 | 宿主 |
|
||||
|------|--------|------|
|
||||
| 格子着色 | `_jiangchang_cell_presentation` | 既有宿主即可 |
|
||||
| 整行 / 整列 / 区域 | `_jiangchang_grid_presentation` | 需已读取该表(匠厂数据管理已支持)。**旧宿主忽略该表**,格子着色仍有效 |
|
||||
|
||||
同一开关 `cellPresentation.enabled` 同时打开两张旁路表。未建表 = 该通道为空,宿主忽略。
|
||||
|
||||
---
|
||||
|
||||
## 1. 职责边界
|
||||
|
||||
| 角色 | 负责 | 不负责 |
|
||||
|------|------|--------|
|
||||
| **技能** | 按**当前业务数据**算出哪些格子/行/列要强调;在数据变更后**重算**并覆盖旧结果 | 不写宿主 CSS;不把强调文案污染进业务真值列冒充「变红」 |
|
||||
| **宿主** | 读取呈现结果,按闭集 `tone` 映射主题样式;按几何涂表格;可选 Toast/提醒汇总 | 不理解「提前 3 天交单」等业务规则;不为单个技能写死样式 |
|
||||
|
||||
**派生状态(重要):** 呈现是计算结果,不是粘性开关。
|
||||
用户补填数据 / 同步刷新后技能重算 → 该项消失 → **样式与提醒自然关闭**。不要单独维护「已关闭提醒」状态机(除非产品另做「忽略此条」能力,本契约不冻结)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 格子文档形状(冻,`schemaVersion` 1)
|
||||
|
||||
技能产出一份 **Cell Presentation Document**(可落库、可 CLI 打印,形状相同),须符合格子 Schema。v1 **只描述某一格**;整行/整列用 §2.1。
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"table": "business_records",
|
||||
"rowKey": "id",
|
||||
"generatedAt": "2026-08-10T06:00:00Z",
|
||||
"cells": [
|
||||
{
|
||||
"rowId": "42",
|
||||
"column": "qty",
|
||||
"tone": "danger",
|
||||
"message": "库存低于安全线",
|
||||
"alert": true
|
||||
},
|
||||
{
|
||||
"rowId": "42",
|
||||
"column": "status",
|
||||
"tone": "warning",
|
||||
"message": "待审核"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `schemaVersion` | 是 | 固定 `1`(本形状不升版本) |
|
||||
| `skill` | 是 | 与 slug 一致 |
|
||||
| `table` | 是 | 当前文档作用的业务表 |
|
||||
| `rowKey` | 是 | 行匹配列(`id` / `feishu_record_id` 等) |
|
||||
| `generatedAt` | 否 | 排障用 |
|
||||
| `cells` | 是 | 数组;**空数组 = 该表清除全部格子呈现** |
|
||||
|
||||
### 2.0 `cells[]` 项
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `rowId` | 是 | `rowKey` 列值的字符串形式 |
|
||||
| `column` | 是 | 要强调的**业务列**(snake_case),须真实存在 |
|
||||
| `tone` | 是 | 见 §3 |
|
||||
| `message` | 否 | 短说明(≤500);悬停 / 无障碍 |
|
||||
| `alert` | 否 | 默认 `false`;`true` 可计入进页/同步提醒汇总 |
|
||||
|
||||
同一 `(rowId, column)` 多条:宿主可**后者覆盖前者**。
|
||||
`column` 对用户不可见(policy 打码 / 列隐藏)时:宿主可仍应用样式,或跳过——**推荐跳过不可见列的强调**,避免泄露「有问题」的存在感(技能仍可写,宿主按可见性过滤)。
|
||||
|
||||
### 2.1 Grid 文档形状(加法,独立 Schema)
|
||||
|
||||
整行 / 整列 / 区域用 **Grid Presentation Document**(`rules[]`)。与 `cells[]` **并存**,不替代、不升 `cells` 的 `schemaVersion`。
|
||||
|
||||
统一几何:**`rows` × `columns`**。某一侧为 `"all"`(或省略,落库时写成 JSON `"all"`)= 该轴全部。禁止两侧都是全部(见 §4.2)。
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"table": "business_records",
|
||||
"rowKey": "id",
|
||||
"generatedAt": "2026-08-10T06:00:00Z",
|
||||
"rules": [
|
||||
{
|
||||
"ruleKey": "qty-low-rows",
|
||||
"rows": ["42", "99"],
|
||||
"columns": "all",
|
||||
"tone": "danger",
|
||||
"message": "库存低于安全线",
|
||||
"alert": true
|
||||
},
|
||||
{
|
||||
"ruleKey": "status-column",
|
||||
"rows": "all",
|
||||
"columns": ["status"],
|
||||
"tone": "warning",
|
||||
"message": "待审核"
|
||||
},
|
||||
{
|
||||
"ruleKey": "overdue-print-region",
|
||||
"rows": ["7", "8"],
|
||||
"columns": ["print_done_at", "ship_done_at"],
|
||||
"tone": "danger",
|
||||
"message": "环节到期未填",
|
||||
"alert": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `schemaVersion` | 是 | 固定 `1` |
|
||||
| `skill` / `table` / `rowKey` | 是 | 同格子文档;`rowKey` 用于匹配 `rows` 里的 id |
|
||||
| `rules` | 是 | 数组;**空数组 = 该表清除全部 grid 规则** |
|
||||
|
||||
#### `rules[]` 项
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `ruleKey` | 是 | 本表内唯一(建议稳定业务键,便于重算覆盖) |
|
||||
| `rows` | 是 | `"all"` 或行 id 字符串数组(对应 `rowKey` 列值) |
|
||||
| `columns` | 是 | `"all"` 或业务列名数组 |
|
||||
| `tone` | 是 | 见 §3 |
|
||||
| `message` | 否 | ≤500 |
|
||||
| `alert` | 否 | 默认 `false`;按**规则条数**计提醒,不按命中格子数 |
|
||||
|
||||
| `rows` × `columns` | 宿主 scope |
|
||||
|--------------------|-----------|
|
||||
| 1 行 × 1 列 | `cell` |
|
||||
| 若干行 × 若干列(非单格) | `region` |
|
||||
| 若干行 × `"all"` | `row`(整行) |
|
||||
| `"all"` × 若干列 | `column`(整列) |
|
||||
| `"all"` × `"all"` | **禁止**,宿主丢弃该规则 |
|
||||
|
||||
完整 JSON 例见 [`../assets/examples/skill-grid-presentation.example.json`](../assets/examples/skill-grid-presentation.example.json)。
|
||||
|
||||
---
|
||||
|
||||
## 3. `tone` 闭集(冻)
|
||||
|
||||
| tone | 语义(技能怎么选) | 宿主怎么呈现(示例,主题可调) |
|
||||
|------|-------------------|-------------------------------|
|
||||
| `danger` | 必须立刻处理 / 已违规 / 严重超时 | 强强调(如浅红底) |
|
||||
| `warning` | 即将到期 / 需注意 | 中等强调(如浅黄) |
|
||||
| `info` | 提示性标记 | 轻强调(如浅蓝) |
|
||||
| `success` | 正向完成态(少用在「待填」场景) | 浅绿等 |
|
||||
| `muted` | 弱化 / 次要 | 灰底或灰字 |
|
||||
|
||||
**禁止:** 技能下发原始 `#RRGGBB`、任意 CSS class、内联 style。
|
||||
以后若扩展色板,只能**追加新 tone token** 并升契约,不能开放任意色值。
|
||||
|
||||
「技能配置什么就显示什么」在本契约中的含义是:技能选择 **哪一格/行/列 + 哪个 tone + 什么说明**;**像素级颜色由宿主主题决定**,以保证多技能 UI 一致与安全。
|
||||
|
||||
---
|
||||
|
||||
## 4. 推荐持久化(给宿主实现)
|
||||
|
||||
v1 **推荐**技能将「当前表」的**格子**呈现写入用户库旁路表(与业务表分离,**不是**多余业务超时列):
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS _jiangchang_cell_presentation (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
table_name TEXT NOT NULL,
|
||||
row_key TEXT NOT NULL,
|
||||
row_id TEXT NOT NULL,
|
||||
column_name TEXT NOT NULL,
|
||||
tone TEXT NOT NULL,
|
||||
message TEXT,
|
||||
alert INTEGER NOT NULL DEFAULT 0,
|
||||
generated_at TEXT,
|
||||
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
UNIQUE (table_name, row_id, column_name)
|
||||
);
|
||||
```
|
||||
|
||||
### 4.1 整行 / 整列 / 区域(`_jiangchang_grid_presentation`)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS _jiangchang_grid_presentation (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
table_name TEXT NOT NULL,
|
||||
rule_key TEXT NOT NULL,
|
||||
row_key TEXT NOT NULL DEFAULT '',
|
||||
rows_json TEXT NOT NULL,
|
||||
columns_json TEXT NOT NULL,
|
||||
tone TEXT NOT NULL,
|
||||
message TEXT,
|
||||
alert INTEGER NOT NULL DEFAULT 0,
|
||||
generated_at TEXT,
|
||||
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
UNIQUE (table_name, rule_key)
|
||||
);
|
||||
```
|
||||
|
||||
`rows_json` / `columns_json` 存 JSON:字符串 `"all"`,或 id / 列名数组(如 `["42","99"]`)。
|
||||
`row_key`:有限行集合时必填(与文档 `rowKey` 相同,如 `id`);整列(`rows_json = "all"`)可空字符串。
|
||||
|
||||
两张表都列入技能 `INTERNAL_METADATA_TABLES`(见 `scripts/db/display_metadata.py`),**不得**作为业务表出现在数据管理侧栏。模板 **不**在 `init_db()` 里默认建这两张表;技能用到时再 `CREATE`。
|
||||
|
||||
### 4.2 冲突与特异性
|
||||
|
||||
同一格被多条规则命中时,宿主按 **格子 > 区域 > 行 > 列** 取胜。同级后者覆盖前者。
|
||||
|
||||
宿主先加载 grid 规则,再加载 v1 `cells[]`(同级时 **v1 格子仍赢**),因此旧技能只写 `cells[]` 的行为不变。
|
||||
|
||||
**禁止 `all` × `all`**(会把整张表涂满)。技能不要写;宿主遇到则丢弃该规则。
|
||||
|
||||
### 4.3 表格 vs 详情
|
||||
|
||||
| 表面 | 整行(scope=`row`) | 格子 / 列 / 区域 |
|
||||
|------|-------------------|------------------|
|
||||
| 数据管理表格 | 给整行(`<tr>`)上色 | 给对应单元格上色 |
|
||||
| 行详情 | **不**把行色铺到每个字段 | 对应字段可保留 tone |
|
||||
|
||||
整行强调的是「这一行在表里扎眼」,不是「打开详情后每个值都变色」。
|
||||
|
||||
约定:
|
||||
|
||||
- 重算某表时:**先删该 `table_name` 在两张旁路表中的全部行,再插入新结果**(或等价事务替换),保证填完后旧 `danger` 不会残留。只维护其中一张时,至少清掉自己负责的那张,并删掉已迁走的旧行(例如从格子改成整行后,删掉对应 `_jiangchang_cell_presentation`)。
|
||||
- 宿主打开数据管理某表时:若该技能启用了呈现(见 §5)且存在旁路表,则按当前页行的 `rowKey` 值匹配并着色。
|
||||
- 也允许技能用 CLI 打印整份 Document 供调试;**运行时以库内旁路表为准**(除非后续宿主另定拉取入口)。
|
||||
|
||||
未创建该旁路表 = 该通道 P0,宿主忽略。
|
||||
|
||||
---
|
||||
|
||||
## 5. 在 `actions.json` 中启用(可选,推荐)
|
||||
|
||||
与 `policy` 类似,顶层可选。**一份开关同时启用格子与 grid:**
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"cellPresentation": {
|
||||
"enabled": true
|
||||
},
|
||||
"actions": []
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `cellPresentation` | 省略 = P0 |
|
||||
| `cellPresentation.enabled` | 仅 `true` 表示宿主应尝试加载呈现(两张旁路表);`false`/缺省等同 P0 |
|
||||
|
||||
v1 **不**要求单独 CLI entrypoint(与 policy 不同);技能在 `sync` / 写路径 / `init-db` 后自行维护旁路表即可。若未来需要「仅刷新呈现」Action,用普通 Skill Action 即可,不必扩展本启用块。
|
||||
|
||||
---
|
||||
|
||||
## 6. 何时重算(技能必须遵守)
|
||||
|
||||
至少在以下时机用**同一套 domain 规则**重算并覆盖该表呈现:
|
||||
|
||||
1. 全量/增量把外部数据写入本地投影之后(如同步成功)
|
||||
2. 会改变「是否仍需强调」的写路径成功之后(新建、更新、登记等)
|
||||
3. (可选)用户显式「刷新呈现」类 Action
|
||||
|
||||
进页 Toast:宿主可在加载表后统计当前页或全表 `alert=1` 的**规则条数**(grid 按规则计,不按命中格子数);**0 则不打扰**。
|
||||
提醒关闭 = 重算后没有对应 cell/rule,而不是用户点「我知道了」写死关闭(除非另做忽略列表,本契约不冻)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 与其它契约的关系
|
||||
|
||||
| 契约 | 关系 |
|
||||
|------|------|
|
||||
| **SCHEMA / `_jiangchang_columns`** | 管列是否可见、类型、中文名;**不管**条件标色。呈现旁路表须进内部隐藏表集合 |
|
||||
| **POLICY Snapshot** | 管谁能看哪些 Action/行列;呈现仍建议按可见列过滤 |
|
||||
| **hostEffects / steps** | 管 Action 跑完后是否 `reloadTable`;重载后宿主应重新读呈现 |
|
||||
| **业务列真值** | 保持干净;不要把 `【超时】` 写进业务字段冒充变色 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 反模式
|
||||
|
||||
- 为标色新增「超时环节」「时效状态」等**业务展示冗余列**当作终态方案
|
||||
- 技能下发 CSS / 内联颜色
|
||||
- 只在首次超时时打标、填完不重算导致红字残留
|
||||
- 宿主写死某个技能 slug 的特殊染色逻辑
|
||||
- 把 Cell Presentation 做成仅服务 SLA 的专用协议
|
||||
- 写 `rows: "all"` 且 `columns: "all"` 企图整表变色
|
||||
- 假定打开行详情后,整行色会铺到每一个字段
|
||||
- 把某一业务技能的检查逻辑(例如「检查列 → 整行」)复制进模板当通用运行时
|
||||
|
||||
---
|
||||
|
||||
## 9. 示例场景(说明用途不限)
|
||||
|
||||
| 场景 | 几何 | tone | message 例 |
|
||||
|------|------|------|------------|
|
||||
| 打单超时未填 | 格子 `print_done_at` | `danger` | 打单已超 1 天未填 |
|
||||
| 库存低于安全线 | **整行**(该行 × `all`) | `warning` | 低于安全库存 |
|
||||
| 审核驳回 | 格子或整列 `status` | `danger` | 已驳回,请修改 |
|
||||
| 选填备注缺失但不紧急 | 格子 `notes` | `muted` | 建议补充备注 |
|
||||
| 两个日期列同时逾期 | 区域:若干行 × `print_done_at`+`ship_done_at` | `danger` | 环节到期未填 |
|
||||
|
||||
格子 JSON 例见 [`../assets/examples/skill-cell-presentation.example.json`](../assets/examples/skill-cell-presentation.example.json)。
|
||||
Grid JSON 例见 [`../assets/examples/skill-grid-presentation.example.json`](../assets/examples/skill-grid-presentation.example.json)。
|
||||
@@ -24,10 +24,10 @@ python {baseDir}/scripts/main.py <command> …
|
||||
|
||||
| 类型 | 本技能命令(自填) | Agent |
|
||||
|------|--------------------|-------|
|
||||
| **短 CLI 白名单** | (例)`health` / `version` / `logs` / `<your-read-or-prep>` | 可用 bash/exec + 共享 `python` |
|
||||
| **短 CLI 白名单** | (例)`health` / `version` / `render-html` / `<your-read-or-prep>` | 可用 bash/exec + 共享 `python` |
|
||||
| **Action 强制黑名单** | (例)`<your-long-or-rpa-command>` | **禁止** bash + `process poll`;必须 `run_skill_action` |
|
||||
|
||||
细节与 Action 清单见 [`ACTIONS.md`](ACTIONS.md)。
|
||||
细节与 Action 清单见 [`ACTIONS.md`](ACTIONS.md)。快照「重新生成」须有 Action id **`render-html`**;壳子页与页面调用 `jiangchang.call` 见 ACTIONS.md「HTML 页面报告」。
|
||||
|
||||
## 最小命令
|
||||
|
||||
@@ -44,16 +44,16 @@ python {baseDir}/scripts/main.py init-db
|
||||
- **`health`**:只读 runtime 诊断,**不下载、不修复 media-assets,不执行业务动作**;不输出敏感值。
|
||||
- **`config-path`**:输出 JSON,包含 `skill`、`env_path`、`example_path`。
|
||||
- **`version`**:输出 JSON(`version`、`skill`)。
|
||||
- **`init-db`**:幂等创建/迁移本地 SQLite(含 `_jiangchang_*` 展示元数据)。成功输出一行 JSON(`ok` / `skill` / `db_path`),退出码 `0`。宿主可在技能安装或更新成功后静默调用;不要并入 `health`。
|
||||
- **`init-db`**:幂等创建/迁移当前 active 后端(默认本地 SQLite;配 `MYSQL_DSN` 时为 MySQL 云库,见 `references/DATA_SOURCES.md`),含 `_jiangchang_*` 展示元数据。成功输出一行 JSON(`ok` / `skill` / `db_path` 或等价字段),退出码 `0`。宿主可在技能安装或更新成功后静默调用;不要并入 `health`。
|
||||
- **`policy`(可选)**:仅当 `actions.json` 顶层启用 Data Policy 时需要;向 stdout 输出 Snapshot JSON(见 `references/POLICY.md`)。模板默认不实现;多数技能保持 L0 即可。
|
||||
- **`run` / RPA / 长任务**:CLI 仍是同一内核;**Agent 禁止**用 bash 干等长任务结束。须走宿主 Skill Action(`run_skill_action`),且该 Action 一般为 `executionProfile: "async"` → **任务中心**。长时间无 stdout **不代表卡死**;进度看任务中心 / Run Journal。
|
||||
- **排查顺序**(由快到慢):
|
||||
1. `python {baseDir}/scripts/main.py logs`
|
||||
2. `python {baseDir}/scripts/main.py log-get <log_id>`
|
||||
3. 统一日志文件 `{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/logs/jiangchang.log`(含 `trace_id`、`task_failed` 等)
|
||||
4. Run Journal `{JIANGCHANG_DATA_ROOT}/.jiangchang/runs/{job_id}.jsonl`
|
||||
5. RPA artifacts / `video_log` / 失败截图(开发规范见 `development/RPA.md`);未开启录屏时可能只有截图或日志
|
||||
1. 任务中心 / Run Journal `{JIANGCHANG_DATA_ROOT}/.jiangchang/runs/{job_id}.jsonl`
|
||||
2. 统一日志文件 `{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/logs/jiangchang.log`(含 `trace_id`、`task_failed` 等)
|
||||
3. 业务表(宿主数据管理;有才建)
|
||||
4. RPA artifacts / `video_log` / 失败截图(开发规范见 `development/RPA.md`);未开启录屏时可能只有截图或日志
|
||||
- **录屏默认关闭**:模板 `.env.example` 为 `OPENCLAW_RECORD_VIDEO=0`;若未开启录屏,任务**可能不会**打印录屏路径。
|
||||
- 需要录屏时在用户 `.env` 或进程环境变量中设置 **`OPENCLAW_RECORD_VIDEO=1`**;开启后任务完成时 CLI 应打印录屏路径、录屏日志、视频/音频诊断(见 `task_run_support._print_video_summary`),且 `result_summary` 含 video 字段。
|
||||
- 需要录屏时在用户 `.env` 或进程环境变量中设置 **`OPENCLAW_RECORD_VIDEO=1`**;开启后任务完成时 CLI 应打印录屏路径、录屏日志、视频/音频诊断(见 `task_run_support._print_video_summary`),且 `finish()` 含 video 字段。
|
||||
|
||||
## config-path(配置路径)
|
||||
|
||||
@@ -81,12 +81,8 @@ python {baseDir}/scripts/main.py init-db
|
||||
```bash
|
||||
python {baseDir}/scripts/main.py run
|
||||
python {baseDir}/scripts/main.py run -t <target> -i <input_id>
|
||||
python {baseDir}/scripts/main.py logs
|
||||
python {baseDir}/scripts/main.py logs --task-type demo --status failed
|
||||
python {baseDir}/scripts/main.py log-get <log_id>
|
||||
```
|
||||
|
||||
| 命令 | 用途 | Agent 注意 |
|
||||
|------|------|------------|
|
||||
| `run` | 执行一次任务(常为长任务 / RPA) | **走 `run_skill_action`**,不要 bash poll |
|
||||
| `logs` / `log-get` | 查任务日志 | 可用短 CLI |
|
||||
|
||||
139
references/DATA_SOURCES.md
Normal file
139
references/DATA_SOURCES.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# 多数据源声明(SQLite / MySQL 云库)
|
||||
|
||||
本文说明技能如何向匠厂宿主**声明**业务数据库,以及技能 CLI / Action 如何与宿主读写**同一物理库**。
|
||||
|
||||
**产品定位:** 默认仍是本地 SQLite(`{skill_slug}.db`)。需要多用户共享、集中运维或云库时,可显式声明 **MySQL**。UI / API 契约不变;`_jiangchang_*` 展示元数据仍写在同一逻辑库中。
|
||||
|
||||
**宿主版本:** 匠厂 Host **≥ 6.9.28** 才支持 `data-sources.json` 与 MySQL 连接器。
|
||||
|
||||
**完整参考实现:** [`process-work-order`](https://git.jc2009.com/client-commons/process-work-order) @ v1.0.2(工单管理 + MySQL 云库试点)。
|
||||
|
||||
---
|
||||
|
||||
## 何时用哪种后端
|
||||
|
||||
| 场景 | 推荐 |
|
||||
|------|------|
|
||||
| 单机、离线、小数据量、便携部署 | **SQLite**(默认,不配声明文件亦可) |
|
||||
| 多用户共享、运维集中管库、与外部系统共库 | **MySQL 云库**(显式 opt-in) |
|
||||
|
||||
**硬规则:一个 `sourceId` 对应一个物理库。** 禁止双写、禁止 SQLite 与 MySQL 同步镜像。
|
||||
|
||||
---
|
||||
|
||||
## 文件位置
|
||||
|
||||
均在**用户数据目录**(不是安装包目录):
|
||||
|
||||
```
|
||||
{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/{skill_slug}/
|
||||
├── data-sources.json # 声明式数据源(可选;见下)
|
||||
├── .env # MYSQL_DSN 等连接配置
|
||||
└── {skill_slug}.db # SQLite 模式下的本地库文件
|
||||
```
|
||||
|
||||
安装/升级时,若用户目录尚无 `data-sources.json` 且技能包内有 `assets/data-sources.example.json`,宿主会**复制一次**示例(不覆盖已有文件)。
|
||||
|
||||
模板默认示例为 **SQLite 显式声明**(与自动扫描 `{skill_slug}.db` 等价)。改走 MySQL 时,请参照 `assets/examples/data-sources.mysql.example.json` 手工替换用户目录中的 `data-sources.json`,并在 `.env` 配置 DSN。
|
||||
|
||||
若不需要声明式接入,可删除用户目录中的 `data-sources.json`,宿主恢复自动扫描 `.db` 文件。
|
||||
|
||||
---
|
||||
|
||||
## `data-sources.json` 格式
|
||||
|
||||
根对象:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `version` | `1` | 固定为 1 |
|
||||
| `sources` | 数组 | 至少一条数据源 |
|
||||
|
||||
每条 `sources[]`:
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `id` | 是 | 英文标识,如 `primary` |
|
||||
| `kind` | 是 | `sqlite` 或 `mysql` |
|
||||
| `displayName` | 否 | 侧栏展示名;多库时建议填写 |
|
||||
| `path` | sqlite 必填 | 相对技能数据目录的路径;可用 `{skillSlug}` 占位 |
|
||||
| `connectionEnv` | mysql 必填 | `.env` 中 DSN 的环境变量名,如 `MYSQL_DSN` |
|
||||
|
||||
### SQLite 示例(模板默认)
|
||||
|
||||
见 [`../assets/data-sources.example.json`](../assets/data-sources.example.json)。
|
||||
|
||||
### MySQL 示例
|
||||
|
||||
见 [`../assets/examples/data-sources.mysql.example.json`](../assets/examples/data-sources.mysql.example.json)。
|
||||
|
||||
---
|
||||
|
||||
## `.env` 与 DSN
|
||||
|
||||
MySQL 连接串写在用户 `.env`(或进程环境变量),**不要**写进 `data-sources.json`:
|
||||
|
||||
```env
|
||||
# MySQL 云库连接(不配则走本地 SQLite)
|
||||
MYSQL_DSN=mysql://user:password@192.168.0.20:3306/your_database
|
||||
```
|
||||
|
||||
格式:`mysql://` 或 `mysql2://`,须含库名。注释写法须符合 [`../development/CONFIG.md`](../development/CONFIG.md)(配置管理会展示给用户)。
|
||||
|
||||
技能 Python 侧:`scripts/db/backend.py` 读取同一 `MYSQL_DSN`;有值则 `resolve_backend()` 为 `mysql`,否则 `sqlite`。
|
||||
|
||||
---
|
||||
|
||||
## 技能代码职责
|
||||
|
||||
### 宿主 UI(数据管理)
|
||||
|
||||
宿主读取用户目录 `data-sources.json` + `.env`,经 Host 内置连接器访问库。**不依赖**技能 `connection.py`。
|
||||
|
||||
### CLI / Action / `init-db`
|
||||
|
||||
**必须**与宿主使用同一后端策略,否则会出现「UI 看云库、CLI 写本地 `.db`」的分裂。
|
||||
|
||||
模板已提供:
|
||||
|
||||
| 模块 | 作用 |
|
||||
|------|------|
|
||||
| `scripts/db/backend.py` | `resolve_backend()`、`parse_mysql_dsn()` |
|
||||
| `scripts/db/cursor_compat.py` | 统一 `?` 占位符与 `executescript` |
|
||||
| `scripts/db/connection.py` | `get_conn()` 双分支;`init_db()` 幂等建 `_jiangchang_*` |
|
||||
|
||||
业务表 DDL / 迁移仍在技能自己的 `schema` / `init_db()` 中按 `conn.backend` 分支处理(见工单参考实现)。
|
||||
|
||||
可选:`describe_active_store()` 用于 health / 排障,输出当前连的是 sqlite 还是 mysql。
|
||||
|
||||
---
|
||||
|
||||
## MySQL 当前限制(Host)
|
||||
|
||||
- 不支持 `tableAdmin`(建表/改表 UI)
|
||||
- 不支持 `cellPresentation`
|
||||
- 元数据与 CRUD 与 SQLite 路径一致
|
||||
|
||||
表结构变更仍由技能 `init-db` / 迁移脚本负责。
|
||||
|
||||
---
|
||||
|
||||
## 开发 Checklist(云库)
|
||||
|
||||
复制新业务技能并选择 MySQL 时:
|
||||
|
||||
1. Host ≥ 6.9.28
|
||||
2. 用户目录放置 `data-sources.json`(mysql 源 + `connectionEnv`)
|
||||
3. `.env` 配置 `MYSQL_DSN`(或你在 json 里声明的 env 名)
|
||||
4. `requirements.txt` 含 `PyMySQL>=1.1.0`(模板已声明)
|
||||
5. 业务 DDL 同时支持 sqlite / mysql 分支(或仅部署 mysql)
|
||||
6. **抽测**:数据管理 UI 与 CLI(如 list / create)读写同一库
|
||||
7. 见 [`../development/CHECKLIST.md`](../development/CHECKLIST.md) A6-16
|
||||
|
||||
---
|
||||
|
||||
## 与 SCHEMA / ACTIONS 的关系
|
||||
|
||||
- [`SCHEMA.md`](SCHEMA.md):`_jiangchang_*` 契约适用于**已注册数据源**上的表,不限于本地文件。
|
||||
- [`ACTIONS.md`](ACTIONS.md):无 `replaces` 时宿主直写**已注册数据源**(默认本地 SQLite)。
|
||||
- [`CLI.md`](CLI.md):`init-db` 对当前 active backend 幂等初始化元数据。
|
||||
385
references/POLICY.md
Normal file
385
references/POLICY.md
Normal file
@@ -0,0 +1,385 @@
|
||||
# Skill Data Policy 契约(v1.0)
|
||||
|
||||
> **状态:正式契约 v1.0(`schemaVersion` 1)。**
|
||||
> **目标:** 冻结「启用方式 + Snapshot 最小字段 + L2 scope 白名单」,使多数技能(L0)零配置,需要权限的技能(L1/L2)有稳定扩展面。
|
||||
> **非目标:** 宿主实现业务用户/角色库;规定飞书/HTTP/本地表等权限数据源;本期不强制 L3(菜单级藏表、`rows.mode=delegate` 等)。
|
||||
> **注意:**
|
||||
> - 行菜单按业务状态显隐请用 Action 契约 **`bind.visibleWhen`**(见 [`ACTIONS.md`](ACTIONS.md)「行菜单显隐」)——宿主已支持,**不属于**本 Policy L3。
|
||||
> - L3 预留的行态 `visibleWhen` **不是** Action 参数表单的 `inputSchema.properties.*.visibleWhen`,也**不是** `bind.visibleWhen`。三者同名不同义,勿混用。
|
||||
> **配套 Schema:** [`../assets/schemas/skill-policy-snapshot.schema.json`](../assets/schemas/skill-policy-snapshot.schema.json);启用块见 [`../assets/schemas/skill-actions.schema.json`](../assets/schemas/skill-actions.schema.json)。
|
||||
|
||||
相关正交轴见 [`ACTIONS.md`](ACTIONS.md)。本契约增加第六轴:**policy(当前身份允许什么)**。
|
||||
|
||||
---
|
||||
|
||||
## 0. 能力档位(必须按此理解)
|
||||
|
||||
| 档位 | 技能配置 | 宿主行为 |
|
||||
|------|----------|----------|
|
||||
| **L0** | 无 `policy`,或 `policy.enabled !== true` | **与今日完全一致**:不拉 Snapshot,不按身份过滤按钮/行/列 |
|
||||
| **L1** | `policy.enabled: true`,Snapshot 至少给出 Action 授权 | 数据管理展示 skill Action 前与 allowlist 求交;可选限制宿主内置能力 |
|
||||
| **L2** | 在 L1 基础上 Snapshot 带 `data.tables[table]` | 列表查询应用声明式 `rows.scope`;列按 `columns` 投影 |
|
||||
| **L3** | (预留,本契约不冻结行为) | 如菜单级藏表、`rows.mode=delegate`、更复杂的行态策略字段等(≠ `bind.visibleWhen` 行菜单,≠ 表单 `inputSchema.visibleWhen`) |
|
||||
|
||||
**80% 技能停留 L0。** 启用 policy 的技能必须实现约定 CLI,并在写路径自行再鉴权(见 §7)。
|
||||
|
||||
---
|
||||
|
||||
## 0.1 已冻结最佳实践(v1.0)
|
||||
|
||||
| 议题 | 决定 | 技能开发者怎么做 |
|
||||
|------|------|------------------|
|
||||
| Skill Action 授权模式 | **仅** `allowlist` / `open`;v1 **禁止** `denylist` | 有角色就算出允许的 action id 列表;只控行不控按钮时用 `open` |
|
||||
| `hostCapabilities` | **省略 = 不限制**宿主内置;写出则按 allowlist(`[]` = 全禁内置) | 只控业务按钮时**不要写**该字段;要限导出/删除时再显式列出 |
|
||||
| scope 运算符 | 保留小白名单(见 §3.4);**禁止** like/raw/嵌套 | 日常优先 `eq` / `in` / `is_null`;比较符按需 |
|
||||
| `valueFrom` | v1 **仅** `actor.userId` | 角色只在技能内用来算 Snapshot,**不要**用 `actor.roles` 做行过滤 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 启用字段(冻在 `assets/actions.json` 顶层)
|
||||
|
||||
`actions.json` 根对象增加**可选**字段 `policy`(与 `schemaVersion` / `skill` / `actions` 同级)。
|
||||
|
||||
### 1.1 形状(L0/L1 必冻)
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"policy": {
|
||||
"enabled": true,
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "policy",
|
||||
"args": ["--table", "{{table}}"]
|
||||
},
|
||||
"appliesTo": ["data-management"]
|
||||
},
|
||||
"actions": []
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 必填 | 冻结说明 |
|
||||
|------|------|----------|
|
||||
| `policy` | 否 | 缺省 = L0 |
|
||||
| `policy.enabled` | 若存在 `policy` 则**必填** | 仅 `true` 启用;`false` 等同 L0 |
|
||||
| `policy.entrypoint` | `enabled: true` 时必填 | 与 Action `entrypoint` 同构:`type` 目前仅 `"cli"` |
|
||||
| `policy.entrypoint.command` | 是 | 子命令名;**推荐**固定为 `policy` |
|
||||
| `policy.entrypoint.args` | 是 | JSON 数组;可含占位符,由宿主替换后再拼接 CLI |
|
||||
| `policy.appliesTo` | 否 | 缺省 `["data-management"]`。本期宿主**只实现** `data-management`;其它值忽略但不报错 |
|
||||
|
||||
### 1.2 占位符白名单(冻)
|
||||
|
||||
| 占位符 | 含义 | 例 |
|
||||
|--------|------|----|
|
||||
| `{{table}}` | 当前数据管理选中的**物理表名** | `customs_timelines` |
|
||||
| `{{skill}}` | 当前技能 slug | 与 manifest `skill` 一致 |
|
||||
| `{{userId}}` | 当前宿主用户 id | 与注入的 `JIANGCHANG_USER_ID` 一致 |
|
||||
|
||||
未列出的 `{{…}}`:**禁止**;宿主应视为契约错误(fail-closed,见 §6)。
|
||||
|
||||
### 1.3 显式不放进启用块的内容(防推翻)
|
||||
|
||||
以下**不得**出现在 `actions.json.policy` 中(属于技能内部或 Snapshot):
|
||||
|
||||
- 角色列表、人员表、飞书 app id、外部 IAM URL
|
||||
- 具体 allowlist / scope(那是 Snapshot 的事)
|
||||
- 按角色复制的 Action 定义
|
||||
|
||||
### 1.4 Action 级可选提示(不冻为运行时权威)
|
||||
|
||||
允许(文档/静态检查用),**宿主不得仅凭此显隐按钮**:
|
||||
|
||||
```json
|
||||
"authz": { "capability": "optional.skill.defined.string" }
|
||||
```
|
||||
|
||||
运行时权威仅为 Snapshot §2 的 `actions`。
|
||||
|
||||
---
|
||||
|
||||
## 2. Snapshot 最小字段集(`policy` CLI 标准输出)
|
||||
|
||||
- 编码:UTF-8 JSON 对象,写在 **stdout**(可先打日志到 stderr/文件,但 stdout 必须是单 JSON)。
|
||||
- 成功退出码 `0`;业务上「无权限看任何东西」仍是 `0` + 合法 Snapshot(例如空 allowlist),不是靠非 0 表示未授权。
|
||||
- 非 0 或非合法 JSON → §6 fail-closed。
|
||||
|
||||
### 2.1 根对象(L0 不出现;L1 必冻)
|
||||
|
||||
| 字段 | 必填 | 类型 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `schemaVersion` | 是 | `integer` | 本期冻结为 **`1`** |
|
||||
| `skill` | 是 | `string` | 必须等于 manifest `skill` |
|
||||
| `actor` | 是 | `object` | 见 §2.2 |
|
||||
| `actions` | 是 | `object` | 见 §2.3 |
|
||||
| `hostCapabilities` | 否 | `string[]` | 见 §2.4;**省略 = 不限制宿主内置按钮**(兼容) |
|
||||
| `data` | 否 | `object` | 见 §3;省略 = 不限制行/列(L1-only) |
|
||||
| `extensions` | 否 | `object` | 技能私有扩展;宿主**必须忽略**未知键 |
|
||||
|
||||
根级未知字段:宿主**忽略**;**不得**依赖未写入本契约的字段做安全决策。
|
||||
|
||||
### 2.2 `actor`(L1 必冻)
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `userId` | 是 | 非空;应与 `JIANGCHANG_USER_ID` / `{{userId}}` 对齐 |
|
||||
| `displayName` | 否 | 展示用 |
|
||||
| `roles` | 否 | `string[]`,角色**码**;仅供展示 / 技能自检。**不得**用于 v1 scope 的 `valueFrom` |
|
||||
| `attributes` | 否 | `object`,仅允许 JSON 标量值;本期 scope **不**解引用 |
|
||||
|
||||
### 2.3 `actions`(L1 必冻)— Skill Action 授权
|
||||
|
||||
**最佳实践:** 角色 → 技能内计算 → `allowlist` 的 id 列表。宿主不理解角色名。
|
||||
|
||||
```json
|
||||
"actions": {
|
||||
"mode": "allowlist",
|
||||
"ids": ["whoami", "create-record", "register-submit"]
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 必填 | 冻结取值 |
|
||||
|------|------|----------|
|
||||
| `mode` | 是 | **仅两值**:`"allowlist"` \| `"open"` |
|
||||
| `ids` | `mode=allowlist` 时必填 | `string[]`;元素为 **`actions.json` 里的 action.id** |
|
||||
| `ids` | `mode=open` 时 | 必须省略或 `[]`;宿主视为**不限制** skill Action |
|
||||
|
||||
规则:
|
||||
|
||||
1. 宿主先按现有规则得到候选集:`placements` ∩ `bind.tables`(及 batch 等)。
|
||||
2. 若 `mode=allowlist`:候选 ∩ `ids`。漏配 id = 少按钮(安全侧失败)。
|
||||
3. `ids` 中的未知 id:**忽略**(不报错)。
|
||||
4. `mode=open`:等价「L1 不限制按钮」,可与 L2 行列限制并存。
|
||||
5. **`denylist` 不是 v1 合法值**;出现则 §6 fail-closed。
|
||||
|
||||
`replaces` 接管的默认新增/编辑/删除/导入:视为对应 Action 的 id 是否在 allowlist 中;不在则宿主隐藏该接管按钮(导入按钮是否可用还受 `hostCapabilities` 含 `import` 影响)。`mode=open` 时不按 id 限制 replaces。
|
||||
|
||||
### 2.4 `hostCapabilities`(L1 可选冻)
|
||||
|
||||
控制数据管理**宿主内置**能力,与 skill `action.id` **命名空间分离**。
|
||||
|
||||
本期枚举冻结:`refresh` | `export` | `view` | `edit` | `delete` | `import`。
|
||||
|
||||
| 写法 | 含义 | 何时用 |
|
||||
|------|------|--------|
|
||||
| **省略字段** | **不限制**(与今日一致) | **默认推荐**:只控 skill 按钮 / 行数据时 |
|
||||
| 写出非空数组 | 仅允许列出的内置能力(allowlist) | 要限导出、删除等时 |
|
||||
| 写出 `[]` | 禁止全部上表内置能力 | 极少见;慎用 |
|
||||
|
||||
未知字符串:忽略。**禁止**把「省略」解释成全禁。
|
||||
|
||||
---
|
||||
|
||||
## 3. L2:`data` 与 scope 语法白名单
|
||||
|
||||
### 3.1 `data` 形状
|
||||
|
||||
```json
|
||||
"data": {
|
||||
"tables": {
|
||||
"customs_timelines": {
|
||||
"accessible": true,
|
||||
"columns": {
|
||||
"visible": ["id", "contract_no", "status"],
|
||||
"writable": ["status"]
|
||||
},
|
||||
"rows": {
|
||||
"mode": "scope",
|
||||
"scope": {
|
||||
"all": [
|
||||
{ "field": "owner_user_id", "op": "eq", "valueFrom": "actor.userId" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `data.tables` | 若存在 `data` 则建议有 | `key` = 物理表名 |
|
||||
| `accessible` | 否 | 缺省 `true`。`false`:宿主可拒绝打开该表 |
|
||||
| `columns.visible` / `writable` | 否 | 物理列名;省略 = 不额外限制 |
|
||||
| `rows` | 否 | 见下 |
|
||||
|
||||
`writable` 应 ⊆ `visible`;若违规,宿主按交集降级,不 fail-closed。
|
||||
|
||||
### 3.2 `rows.mode`(冻)
|
||||
|
||||
| 值 | 本期 | 含义 |
|
||||
|----|------|------|
|
||||
| `scope` | **实现目标** | 宿主将 `scope` 编译为安全查询约束 |
|
||||
| `open` | 允许 | 显式不限制行 |
|
||||
| `delegate` | **仅占位** | 本期未实现时 fail-closed / 提示;**禁止**当 `open` |
|
||||
|
||||
缺省:无 `rows` = `open`。
|
||||
|
||||
### 3.3 `scope` 布尔结构(冻)
|
||||
|
||||
```text
|
||||
Scope =
|
||||
| { "all": Predicate[] } // AND,长度 ≥ 1
|
||||
| { "any": Predicate[] } // OR,长度 ≥ 1
|
||||
| Predicate // 单个谓词的语法糖
|
||||
```
|
||||
|
||||
**禁止**:嵌套超过一层。
|
||||
|
||||
### 3.4 `Predicate` 白名单(冻)
|
||||
|
||||
**运算符(宿主必须实现):** `eq` | `ne` | `in` | `not_in` | `gt` | `gte` | `lt` | `lte` | `is_null` | `is_not_null`
|
||||
|
||||
**推荐日常只用:** `eq` / `in` / `is_null`(及 `is_not_null`)。
|
||||
|
||||
**明确排除:** `like` / `contains` / `sql` / `raw` / `expr`。
|
||||
|
||||
**`valueFrom` v1 仅:** `actor.userId`。
|
||||
`actor.roles` / `actor.attributes.*` **非法** → fail-closed。
|
||||
|
||||
`value` 与 `valueFrom` 互斥。标量:`string` | `number` | `boolean` | `null`(与 null 比较优先用 `is_null`)。
|
||||
|
||||
### 3.5 与宿主原有过滤的关系
|
||||
|
||||
最终行集 = **原有表查询**(搜索、排序、分页、用户 Filter) **AND** policy `scope`。宿主必须把 scope 下推到查询层。
|
||||
|
||||
---
|
||||
|
||||
## 4. 宿主消费算法
|
||||
|
||||
```text
|
||||
IF manifest.policy?.enabled !== true:
|
||||
→ L0;END
|
||||
|
||||
调用 entrypoint(替换占位符),解析 Snapshot
|
||||
IF 失败 / 非法 / skill 不匹配 / mode 非法:
|
||||
→ fail-closed(§6);END
|
||||
|
||||
候选 = filter(placements, bind.tables, …)
|
||||
IF mode == allowlist: 候选 ∩ ids
|
||||
IF mode == open: 候选不变
|
||||
|
||||
渲染 skill 按钮 ← 候选
|
||||
渲染宿主内置 ← hostCapabilities(省略则不限制)
|
||||
应用 data.tables[T](若有)
|
||||
```
|
||||
|
||||
拉取时机(建议):进入表、切换表、刷新。短缓存不得跨 `userId`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 身份与环境
|
||||
|
||||
| 项 | 约定 |
|
||||
|----|------|
|
||||
| 用户主键 | `JIANGCHANG_USER_ID` |
|
||||
| 技能解析 | 飞书 / 本地表 / HTTP IAM 等,**契约不规定** |
|
||||
|
||||
---
|
||||
|
||||
## 6. Fail-closed(启用后必冻)
|
||||
|
||||
当且仅当 `policy.enabled === true`:
|
||||
|
||||
| 情况 | 宿主行为 |
|
||||
|------|----------|
|
||||
| CLI 非 0 / 超时 / 无 JSON | 不渲染该表 skill 写操作与(建议)导出;明确错误;**禁止**回退 L0 全开 |
|
||||
| `schemaVersion` ≠ 1 / `skill` 不匹配 / `actions.mode` 非法 | 同上 |
|
||||
| `rows.mode=scope` 但 predicate 非法 | 不返回行或整表不可用;**禁止**忽略 scope |
|
||||
| `rows.mode=delegate`(未实现) | 提示不支持;**禁止**当 `open` |
|
||||
|
||||
---
|
||||
|
||||
## 7. 安全边界
|
||||
|
||||
1. Snapshot **只**服务宿主 UI 与查询约束。
|
||||
2. 写 Action / 敏感读的 **domain service 必须再次鉴权**。
|
||||
3. 禁止「只藏按钮」宣称为完整权限方案。
|
||||
4. Cron / Agent **不**自动套用本 Snapshot。
|
||||
|
||||
---
|
||||
|
||||
## 8. 最小示例
|
||||
|
||||
模板仓库另有可复制示例:
|
||||
|
||||
- [`../assets/examples/skill-policy-snapshot.allowlist.example.json`](../assets/examples/skill-policy-snapshot.allowlist.example.json)
|
||||
- [`../assets/examples/skill-policy-snapshot.scope.example.json`](../assets/examples/skill-policy-snapshot.scope.example.json)
|
||||
|
||||
### 8.1 L0
|
||||
|
||||
不写 `policy` 字段。
|
||||
|
||||
### 8.2 L1 allowlist(不碰宿主内置)
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actor": { "userId": "u1", "displayName": "张三", "roles": ["clerk"] },
|
||||
"actions": {
|
||||
"mode": "allowlist",
|
||||
"ids": ["whoami", "create-record", "register-submit"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 L1 + 限制导出
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actor": { "userId": "u1", "roles": ["clerk"] },
|
||||
"actions": { "mode": "allowlist", "ids": ["whoami"] },
|
||||
"hostCapabilities": ["refresh", "view"]
|
||||
}
|
||||
```
|
||||
|
||||
### 8.4 L1 open + L2 scope
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actor": { "userId": "u1" },
|
||||
"actions": { "mode": "open" },
|
||||
"data": {
|
||||
"tables": {
|
||||
"orders": {
|
||||
"rows": {
|
||||
"mode": "scope",
|
||||
"scope": {
|
||||
"all": [
|
||||
{ "field": "owner_user_id", "op": "eq", "valueFrom": "actor.userId" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 刻意延后
|
||||
|
||||
| 项 | 备注 |
|
||||
|----|------|
|
||||
| `actions.mode: denylist` | 另开 `schemaVersion` |
|
||||
| `valueFrom: actor.roles` | **明确不做** |
|
||||
| `visibleWhen` / 嵌套 scope / `like` | L3 或永不做(此处指**行态**策略字段;Action 表单 `inputSchema.visibleWhen` 见 ACTIONS.md,不属本表) |
|
||||
| `rows.mode: delegate` 宿主实现 | 字段名已占坑 |
|
||||
| 平台全局 Role UI / cron·agent 套用 Snapshot | 另议 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 冻结检查清单
|
||||
|
||||
- [x] L0 缺省无歧义
|
||||
- [x] 启用字段仅 `enabled` + `entrypoint` + 可选 `appliesTo`
|
||||
- [x] `actions.mode` 仅 `allowlist` / `open`
|
||||
- [x] `hostCapabilities` 省略 = 不限制
|
||||
- [x] scope 小白名单;`valueFrom` 仅 `actor.userId`
|
||||
- [x] fail-closed;写路径技能自检
|
||||
@@ -17,8 +17,12 @@
|
||||
| 根目录 [`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、任务中心、多入口(可选能力) |
|
||||
| [`SCHEMA.md`](SCHEMA.md) | 本地 SQLite 与数据管理展示契约 |
|
||||
| [`ACTIONS.md`](ACTIONS.md) | Skill Action、sync/async、任务中心、多入口、按钮中文名 `label` 参考(按入口定位表挂载;先读 `development/PRODUCT_SURFACES.md`) |
|
||||
| [`SCHEMA.md`](SCHEMA.md) | 业务表与数据管理展示契约(SQLite / MySQL) |
|
||||
| [`DATA_SOURCES.md`](DATA_SOURCES.md) | 多数据源声明:SQLite 默认、可选 MySQL 云库(Host ≥ 6.9.28) |
|
||||
| [`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`。
|
||||
按需阅读:短操作优先 `CLI.md`;Action / 任务中心读 `ACTIONS.md`;结构化字段读 `SCHEMA.md`;云库 / 声明式数据源读 `DATA_SOURCES.md`;权限 opt-in 读 `POLICY.md`;格子/行/列标色读 `CELL_PRESENTATION.md`;队列处理状态机读 `WORK_ITEM_STATUS.md`。
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
# 数据存储与数据管理展示规范
|
||||
|
||||
本文是 skill 本地 SQLite 与匠厂宿主「数据管理」页面展示契约的**权威说明**。
|
||||
本文是 skill **已注册数据源**(默认本地 SQLite 文件;可选 MySQL 云库,见 [`DATA_SOURCES.md`](DATA_SOURCES.md))与匠厂宿主「数据管理」页面展示契约的**权威说明**。
|
||||
|
||||
**产品定位:** 数据管理既是业务数据与过程的可见面,也是与对话**平级**的 **UI 执行面**(表上 Action 走同一 CLI 闭环)。何时挂执行入口见 [`../development/PRODUCT_SURFACES.md`](../development/PRODUCT_SURFACES.md);本文管表结构与展示元数据。
|
||||
|
||||
实现上的 DDL 与默认中文元数据种子见 `scripts/db/display_metadata.py`;`scripts/db/connection.py` 的 `init_db()` 会一并初始化。
|
||||
|
||||
## 职责边界
|
||||
|
||||
### 技能负责
|
||||
|
||||
- 定义真实 SQLite 表结构
|
||||
- 定义真实业务表结构(SQLite 或 MySQL,按技能所选后端)
|
||||
- 使用英文 `snake_case` 表名和字段名
|
||||
- 使用 `_jiangchang_tables` 提供表级展示元数据
|
||||
- 使用 `_jiangchang_columns` 提供字段级展示元数据
|
||||
- 使用 `_jiangchang_columns` 提供字段级展示元数据(含可选 `description` 表单说明、可选 `grid_help` 表格说明)
|
||||
- 需要「新增表单预填」时,在物理列上声明 SQLite `DEFAULT`(勿把默认值只写在文案里)
|
||||
- 定义哪些字段可见、可搜索、可创建、可编辑
|
||||
- 定义字段展示类型和枚举选项
|
||||
- (可选)按业务规则维护单元格 / 整行 / 整列 / 区域呈现旁路数据(语义 tone;见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md))
|
||||
- 保证元数据与真实数据库结构一致
|
||||
- 保证元数据初始化幂等
|
||||
|
||||
@@ -20,13 +25,19 @@
|
||||
|
||||
- 读取 `_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 则留空
|
||||
- 执行通用查询、新增、编辑、删除、导入、导出
|
||||
- 对字段名、表名、类型和权限进行最终校验
|
||||
- 不猜测技能业务含义
|
||||
- 不根据具体技能名称写特殊逻辑
|
||||
- 可在技能安装/更新成功后静默调用 `python scripts/main.py init-db` 做幂等 ensure(内部即 `init_db()`);失败不影响安装结果
|
||||
- 若技能启用了单元格呈现(见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)),读取旁路呈现数据并为格子 / 行 / 列 / 区域映射语义样式(`tone`);不猜测业务规则;整行色只涂表格行,不铺到详情字段
|
||||
- **导入模板下拉(宿主能力)**:下载「Excel 模板」时,宿主按本表可导入列的元数据自动加 Excel 数据有效性下拉——凡列已有可选集合(`options_json` 静态枚举、schema 已解析的 lookup 选项、未写 options 的 `boolean` 默认「是/否」等)即加下拉;无选项的列仍为自由文本。下拉展示 **label**;导入写库时宿主(或 `replaces: import` 技能)再 **label → value**。CSV 模板**不能**带下拉,可选值仍写在导入弹窗「可导入字段说明」与 Excel「填写说明」页。技能**无需**自备带下拉的模板文件;把字段选项声明在 `_jiangchang_columns` 即可让所有技能受益。宿主不按技能名写死「只支持某几种业务字段」。
|
||||
|
||||
## 初始化入口
|
||||
|
||||
@@ -35,7 +46,8 @@
|
||||
SQLite **没有**可靠的 `COMMENT ON TABLE/COLUMN`;SQL 文件里的 `-- 注释` 也不会成为可查询结构。
|
||||
因此中文展示名称必须写入元数据表;字段顺序必须写在 `CREATE TABLE` 的列定义顺序中(`PRAGMA table_info` 的 `cid`)。
|
||||
|
||||
> **禁止**新技能使用宿主的旧版 `_schema_meta` 表。仅 `_jiangchang_tables` / `_jiangchang_columns` 为正式契约。
|
||||
> **禁止**新技能使用宿主的旧版 `_schema_meta` 表。仅 `_jiangchang_tables` / `_jiangchang_columns` 为正式契约。
|
||||
> 可选呈现旁路表 `_jiangchang_cell_presentation` / `_jiangchang_grid_presentation` **不是**展示元数据,但须列入 `INTERNAL_METADATA_TABLES`(侧栏不可见)。见 [`CELL_PRESENTATION.md`](CELL_PRESENTATION.md)。
|
||||
|
||||
## 技能名称(SKILL.md)
|
||||
|
||||
@@ -61,7 +73,7 @@ metadata:
|
||||
```
|
||||
|
||||
- 数据库文件:英文 slug,例如 `your-skill-slug.db`(默认 `{skill-slug}.db`)。
|
||||
- 业务表、字段:**英文 snake_case** 物理名,例如 `demo_items`、`task_logs`、`created_at`。
|
||||
- 业务表、字段:**英文 snake_case** 物理名,例如 `demo_items`、`created_at`。
|
||||
- **禁止**用中文作为 SQLite 真实表名或字段名。
|
||||
|
||||
## 外部来源只读与本地数据可写
|
||||
@@ -78,6 +90,25 @@ metadata:
|
||||
|
||||
不要把「外部来源只读」错误理解成「技能本地数据库一定只读」。技能通过 `_jiangchang_tables.readonly` 与 `_jiangchang_columns.editable` 声明本地表的实际写权限。
|
||||
|
||||
若创建/更新/删除/导入带有领域副作用(自动建目录、加密、级联清理、归一化字段),不要依赖宿主默认直写:在 `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)。
|
||||
|
||||
## 标准数据同步语义(外源 → 本地库)
|
||||
|
||||
适用于「同步 / refresh / collect」类能力。使用通用名称表述,勿把具体平台表名写进模板实现。
|
||||
@@ -107,13 +138,14 @@ metadata:
|
||||
}
|
||||
```
|
||||
|
||||
### 同步数据 vs 导出当前数据表 vs 特殊业务报告
|
||||
### 同步数据 vs 导出当前数据表 vs 特殊业务报告 vs HTML 页面
|
||||
|
||||
| 能力 | 语义 | 负责方 |
|
||||
|------|------|--------|
|
||||
| **同步数据** | 外部来源 → 技能本地数据库 | 技能 Action / domain service |
|
||||
| **导出当前数据表** | 本地库 → CSV/Excel 等 | 数据管理页**宿主统一导出** |
|
||||
| **生成特殊业务报告** | 多表合并、特殊排版或附加计算 | 仅当系统导出无法满足时,才提供独立 `export-*` |
|
||||
| **生成特殊业务报告(文件)** | 多表合并、特殊排版或附加计算的文件 | 仅当系统导出无法满足时,才提供独立 `export-*`;落 `output/` |
|
||||
| **HTML 页面报告** | 侧栏可预览的整页 HTML | 壳子页 `assets/html/pages/`;快照写入 `html/` + Action id **`render-html`**(见 [`ACTIONS.md`](ACTIONS.md)) |
|
||||
|
||||
同步型 Action 默认只写库、不生成导出文件。兼容旧 export 命令需要刷新时,必须调用同一 sync 内核。可复用测试范式见 `tests/samples/test_sync_contract.py.sample`。
|
||||
|
||||
@@ -127,10 +159,142 @@ metadata:
|
||||
|------|------|------|
|
||||
| `table_name` | 是 | 真实英文表名 |
|
||||
| `display_name` | 是 | 用户可见的中文业务名称 |
|
||||
| `description` | 否 | 表说明 |
|
||||
| `description` | 否 | 表说明(表单/备注用途);**不**用于表格上方横幅 |
|
||||
| `grid_help` | 否 | 数据管理表格上方说明;空/未写则不出横幅。与 `description` 独立,禁止互拷(见下节) |
|
||||
| `sort_order` | 否 | 兼容保留字段,不用于替代物理字段顺序 |
|
||||
| `visible` | 是 | `1` 在宿主数据管理中展示 / `0` 隐藏 |
|
||||
| `readonly` | 是 | `1` 整张表只读;`0` 允许宿主根据字段权限执行写操作 |
|
||||
| `default_sort_json` | 否 | 数据管理**默认行排序**(见下节);空则宿主用内置默认 |
|
||||
| `default_visible_columns_json` | 否 | 数据管理**默认展示列**(见下节);空则全显 |
|
||||
|
||||
#### 表级默认排序 `default_sort_json`(宿主支持)
|
||||
|
||||
技能可声明该表在数据管理中的默认排序;宿主优先采用,未声明或声明无效时回退:
|
||||
|
||||
`updated_at DESC` → `created_at DESC` → 主键 DESC。
|
||||
|
||||
JSON 数组(可多字段,按顺序):
|
||||
|
||||
```json
|
||||
[{"column":"sent_at","direction":"desc"},{"column":"id","direction":"desc"}]
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- `column` 必须是该表**真实物理列名**(存在于 SQLite 表);不存在则宿主忽略该段或技能校验报错
|
||||
- **可以**引用 `visible=0` 的列(含隐藏的主键 `id`)。`visible=0` 只表示界面不展示 / 不进默认 SELECT·筛选·搜索;**不表示不能 ORDER BY**
|
||||
- 敏感列(宿主标为 sensitive)不得用于排序;技能侧勿把密钥类字段写进 `default_sort_json`
|
||||
- `direction` 仅 `asc` / `desc`(缺省按 `desc`)
|
||||
- 用户在 UI 上手动改排序后,以用户选择为准;声明只影响未选择时的默认
|
||||
- 通过 `upsert_table_metadata(..., default_sort_json=...)` 写入;旧库由 `init_display_metadata_tables` 幂等 `ALTER` 补列
|
||||
|
||||
与 `visible` 的分工(勿再踩坑):
|
||||
|
||||
| 能力 | `visible=0` 是否仍可用 |
|
||||
|------|------------------------|
|
||||
| 表格列展示 / 「列」面板 | 否(硬隐藏) |
|
||||
| 默认 SELECT、列筛选、顶栏搜索 | 否 |
|
||||
| **`default_sort_json` / ORDER BY** | **是**(非敏感列) |
|
||||
| 行主键定位(`$row.$pk` 等) | 是(主键即使隐藏仍应可排序、可定位;宿主查询层保留) |
|
||||
|
||||
示例(编号硬隐藏,但仍按编号排序——合法):
|
||||
|
||||
```python
|
||||
upsert_table_metadata(
|
||||
cursor,
|
||||
table_name="period_rule_overrides",
|
||||
display_name="本月默认金额",
|
||||
readonly=1,
|
||||
default_sort_json='[{"column":"period","direction":"desc"},{"column":"id","direction":"asc"}]',
|
||||
)
|
||||
# id 列元数据可见性:
|
||||
# upsert_column_metadata(..., column_name="id", visible=0, ...)
|
||||
```
|
||||
|
||||
示例(消息时间线):
|
||||
|
||||
```python
|
||||
upsert_table_metadata(
|
||||
cursor,
|
||||
table_name="messages",
|
||||
display_name="消息",
|
||||
readonly=0,
|
||||
default_sort_json='[{"column":"sent_at","direction":"desc"},{"column":"id","direction":"desc"}]',
|
||||
)
|
||||
```
|
||||
|
||||
#### 表级默认展示列 `default_visible_columns_json`(宿主支持)
|
||||
|
||||
技能可声明该表在数据管理表格中的**默认展示列**(软默认,不是硬隐藏):
|
||||
|
||||
- **已配置**:首次打开(用户尚未改过「列」)只显示名单中的列;其余列仍在「列」面板中,用户可勾回
|
||||
- **未配置 / 空 / 无效**:显示全部 schema 可见列(与今日行为一致)
|
||||
- **用户改过「列」或表头「隐藏此列」后**:以本机偏好为准;「恢复默认列布局」后重新回到技能默认(若有)
|
||||
|
||||
JSON 数组(物理列名;也可写 `{"column":"col"}` 对象,宿主同样接受):
|
||||
|
||||
```json
|
||||
["stat_type","period","stage","ops_count","timeout_ratio"]
|
||||
```
|
||||
|
||||
规则:
|
||||
|
||||
- 列名必须是该表真实列,且 `_jiangchang_columns.visible=1`(硬隐藏列本来就不进表格)
|
||||
- 与 `visible=0` **不同**:`visible=0` = 列从数据管理 schema /「列」面板移除;本字段只影响默认勾选状态
|
||||
- 名单为空或全部无效时,宿主 fail-open:全显
|
||||
- 通过 `upsert_table_metadata(..., default_visible_columns_json=...)` 写入;旧库由 `init_display_metadata_tables` 幂等 `ALTER` 补列
|
||||
|
||||
示例(时效统计:默认不展示编号与时间戳列):
|
||||
|
||||
```python
|
||||
upsert_table_metadata(
|
||||
cursor,
|
||||
table_name="timeliness_stats",
|
||||
display_name="时效统计",
|
||||
readonly=1,
|
||||
default_visible_columns_json=(
|
||||
'["stat_type","period","stage","ops_count",'
|
||||
'"timeout_open","timeout_done","timeout_total","timeout_ratio"]'
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
#### 表格说明 `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,
|
||||
)
|
||||
```
|
||||
|
||||
禁止:
|
||||
|
||||
@@ -138,6 +302,9 @@ metadata:
|
||||
- 使用 `_schema_meta` 作为新的正式契约
|
||||
- 只写英文物理名而不写中文 `display_name`
|
||||
- 让元数据引用不存在的表
|
||||
- 用 `visible=0` 假装「默认不展示但用户还能勾出来」(那是硬隐藏,勾不回来)
|
||||
- 误以为「列 `visible=0` 就不能写进 `default_sort_json`」——排序与展示已拆开;隐藏编号/主键仍可排序(须列真实存在且非敏感)
|
||||
- 把不存在的列名写进 `default_sort_json`(技能校验应报错;宿主也会忽略无效段)
|
||||
|
||||
### `_jiangchang_columns` 字段级标准
|
||||
|
||||
@@ -145,17 +312,178 @@ metadata:
|
||||
|------|------|------|
|
||||
| `table_name` | 是 | 所属物理表 |
|
||||
| `column_name` | 是 | 真实英文字段名 |
|
||||
| `display_name` | 是 | 用户可见的中文字段名 |
|
||||
| `description` | 否 | 字段说明 |
|
||||
| `display_name` | 是 | 用户可见的中文字段名(表头与表单标签);保持简短 |
|
||||
| `description` | 否 | 字段说明(给人看的短文案);**宿主在新增/编辑表单标签下方展示**(见下节);不要塞进 `display_name` |
|
||||
| `grid_help` | 否 | 数据管理表头「?」;空/未写则不出。与表单 `description` 独立,禁止互拷 |
|
||||
| `display_order` | 否 | 兼容字段,默认保持 `NULL` |
|
||||
| `visible` | 是 | 是否在数据管理表格中展示 |
|
||||
| `searchable` | 是 | 是否参与搜索 |
|
||||
| `visible` | 是 | 是否进入数据管理 schema /「列」面板(`0` = **硬隐藏**,用户无法勾回;默认不展示但可勾回请用表级 `default_visible_columns_json`) |
|
||||
| `searchable` | 是 | 是否参与**列筛选**与**顶栏全局搜索**(`0` = 两者均不参与);详见下节「数据管理:列筛选与全局搜索」 |
|
||||
| `editable` | 是 | `1` 业务字段允许宿主用于新增和编辑;`0` 系统/只读字段 |
|
||||
| `display_type` | 否 | 字段渲染类型(如 `text`、`textarea`、`datetime_unix_seconds`) |
|
||||
| `display_type` | 否 | 字段渲染类型;稳定取值见下节「表单与展示:稳定字段类型」 |
|
||||
| `options_json` | 否 | 枚举选项 JSON |
|
||||
|
||||
这两张表是**数据字典**,不是业务数据;宿主不会把它们当作普通业务表展示。
|
||||
|
||||
#### 表单与展示:稳定字段类型(宿主支持,须与 Action 对称)
|
||||
|
||||
数据管理「新增 / 编辑」表单与 Skill Action 参数表单应使用**同一套控件语义**(配置入口不同,见 [`ACTIONS.md`](ACTIONS.md))。新技能只使用下表;宿主须两边都实现,避免「Action 有、CRUD 没有」。
|
||||
|
||||
**本版稳定支持(须齐):**
|
||||
|
||||
| `display_type`(CRUD) | 物理类型建议 | 列表展示 | 新增/编辑控件 | 存库约定 |
|
||||
|------------------------|--------------|----------|---------------|----------|
|
||||
| `text`(或缺省) | `TEXT` / 一般标量 | 原文 | 单行文本 | 原文 |
|
||||
| `textarea` | `TEXT` | 原文(可截断) | 多行文本 | 原文 |
|
||||
| `json` | `TEXT` | 原文 | 多行文本(JSON 字符串) | 原文;提交时宿主可做基础 JSON 合法性校验 |
|
||||
| `number` | `INTEGER` / `REAL` / `NUMERIC` | 原文 | **数字输入**(`type=number`) | 数字;无 `display_type` 时,物理类型为 INT/REAL/NUMERIC 也应走数字控件 |
|
||||
| `boolean` | `INTEGER` 0/1 或 BOOL | 开关语义展示 | 开关 | `0` / `1` |
|
||||
| (枚举) | 任意 + `options_json` **数组** | 标签映射 | 下拉选择 | 选项 `value` |
|
||||
| `lookup` | 跨表引用 id;`options_json` **对象**;可选 `multiple` | 单选:标签映射;多选:多标签(如「豆包、元宝」) | 单选下拉;多选见下文 | 单选:业务 id 标量;多选:`TEXT` 逗号分隔 id(非 SQLite 外键) |
|
||||
| (敏感) | 任意;列 `sensitive=1` | 掩码/隐藏策略由宿主定 | 新建可用密码框;编辑策略与既有宿主一致 | 原文(勿日志明文) |
|
||||
| `year` | `TEXT` 推荐 `YYYY` | 原文 | **年份选择** | `YYYY` 字符串 |
|
||||
| `year_month` | `TEXT` 推荐 `YYYY-MM` | 原文 | **年月选择** | `YYYY-MM` 字符串;别名 `month` |
|
||||
| `date` | `TEXT` 推荐 `YYYY-MM-DD`,或可安全归一的值 | 日期 | **日期选择**(日) | `YYYY-MM-DD` |
|
||||
| `datetime_unix_seconds` | `INTEGER` | `YYYY-MM-DD HH:mm:ss` | **日期时间选择**;提交写回 **Unix 秒** | Unix **秒**整数;禁止改成 ISO 字符串 |
|
||||
| `url` | `TEXT` | 原文(可作链接) | 单行输入 `type=url`(薄层;校验从宽) | 原文 |
|
||||
| `email` | `TEXT` | 原文 | 单行输入 `type=email`(薄层;校验从宽) | 原文 |
|
||||
|
||||
说明灰字与创建默认值见下一小节;与 Action 的对照写在 [`ACTIONS.md`](ACTIONS.md)「表单字段类型对照」。
|
||||
|
||||
**明确不在本版稳定范围(勿在新技能宣传为已支持):**
|
||||
|
||||
- 文件 / 图片上传
|
||||
- 复杂 `oneOf` / nested object 表单(任意 JSON 数组当业务多选控件)
|
||||
- 把业务可编辑时间存成毫秒、ISO 文本或 SQLite `datetime` 文本(审计字段规范见「标准时间字段」)
|
||||
|
||||
> **Lookup 多选**已纳入本版契约(见下文 `multiple`),与「任意多选/标签数组」不是同一能力。
|
||||
|
||||
`datetime_unix_milliseconds`:仅兼容**列表展示**旧数据;**新技能不要用**;可编辑时间一律 `datetime_unix_seconds`。
|
||||
|
||||
#### 新增/编辑表单:字段说明与默认值(宿主支持)
|
||||
|
||||
数据管理「新增记录 / 编辑记录」与 Skill Action 弹窗是**两套表单**;CRUD 侧约定如下(交互对齐 Action 的 `inputSchema.description` / `default`,但**配置入口不同**)。
|
||||
|
||||
| 能力 | 技能写什么 | 宿主做什么 |
|
||||
|------|------------|------------|
|
||||
| 字段说明 | `_jiangchang_columns.description`(可选) | 新增/编辑表单中,在标签下方以灰字展示;空则不显示 |
|
||||
| 创建默认值 | 物理列 SQLite `DEFAULT`(如 `limit_n INTEGER NOT NULL DEFAULT 0`) | 打开「新增」时,对可创建字段预填该默认值;无 DEFAULT 则留空 |
|
||||
| 编辑已有行 | — | **不**用 DEFAULT 覆盖已有行数据 |
|
||||
| 表格列头标题 | `display_name` | 短名;不要把长说明塞进列头文字 |
|
||||
| 表格列头「?」 | `_jiangchang_columns.grid_help`(可选) | 有内容才显示「?」;**不**回退 `description` |
|
||||
| 表格上方说明 | `_jiangchang_tables.grid_help`(可选) | 有内容才显示横幅;**不**回退表级 `description` |
|
||||
|
||||
规则与禁止:
|
||||
|
||||
- **推荐**对易填错、语义特殊的可编辑字段写 `description`(示例链接、`0` 表示不限制等);非强制每个字段都有
|
||||
- **禁止**把说明硬塞进 `display_name`(如 `评论上限(0不限)`);表头会变脏,也无法与 Action 说明对齐
|
||||
- 业务语义(如「0 = 不限制」)写在 `description` 里;宿主只展示,**不**解读业务含义
|
||||
- 第一期**不**新增元数据「表单默认值」列;CRUD 默认值 = DDL `DEFAULT`,与 Action 的 `inputSchema.default` 分开配置
|
||||
- 同一业务字段若同时出现在 Action 与新增表单,**文案应对齐**(短标题 + 说明),但分别写在 `actions.json` 与 `_jiangchang_columns` / DDL
|
||||
- 数字类可编辑列推荐 `display_type: "number"`(或依赖 INT/REAL/NUMERIC 推断)并在 DDL 写清 `DEFAULT`(如条数上限默认 `0`)
|
||||
|
||||
示例(可编辑列种子 + DDL 默认):
|
||||
|
||||
```sql
|
||||
-- 物理列:创建默认 0
|
||||
comment_limit INTEGER NOT NULL DEFAULT 0
|
||||
```
|
||||
|
||||
```python
|
||||
seed_column_metadata_batch(
|
||||
cursor,
|
||||
table_name="videos",
|
||||
columns=(
|
||||
{
|
||||
"column_name": "video_url",
|
||||
"display_name": "视频链接",
|
||||
"editable": 1,
|
||||
"display_type": "url",
|
||||
"description": "填平台视频页链接;需包含可识别的视频路径片段。",
|
||||
},
|
||||
{
|
||||
"column_name": "comment_limit",
|
||||
"display_name": "采集评论条数",
|
||||
"editable": 1,
|
||||
"display_type": "number",
|
||||
"description": "最多采多少条。填 0 = 不限制;填正整数则按该数量封顶。",
|
||||
},
|
||||
),
|
||||
valid_column_names=physical,
|
||||
)
|
||||
```
|
||||
|
||||
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 |
|
||||
| `year` / `year_month` / `date` / `datetime_unix_seconds` | 年 / 年月 / 日期 / 日期时间 | 比较类 | 账期/月份列推荐 `year_month`;时间列通常 `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` 这一份字段权限声明:
|
||||
@@ -260,15 +588,27 @@ readonly = 0
|
||||
| 存储格式 | Unix 秒级整数 |
|
||||
| 展示元数据 `display_type` | `datetime_unix_seconds` |
|
||||
| 中文名 | `created_at` → **创建时间**;`updated_at` → **更新时间** |
|
||||
| 用户编辑 | `editable = 0`(系统字段,宿主只展示格式化结果) |
|
||||
| 用户编辑 | **审计字段** `editable = 0`(系统维护,列表只展示格式化结果,**不**进可编辑表单) |
|
||||
| 字段顺序 | 位于业务字段末尾:`… → created_at → updated_at` |
|
||||
| 排序 | **不使用** `display_order`;顺序仅来自 `PRAGMA table_info` |
|
||||
|
||||
### 业务可编辑时间 vs 审计时间
|
||||
|
||||
| 场景 | `editable` | 列表 | 新增/编辑表单 |
|
||||
|------|------------|------|----------------|
|
||||
| `created_at` / `updated_at` 等审计列 | `0` | 格式化为 `YYYY-MM-DD HH:mm:ss` | 不出现(或只读,由宿主既有规则) |
|
||||
| 业务时间列(预约、截止、活动开始等) | `1` | 同上格式化 | **日期时间控件**;提交写入 Unix **秒**整数 |
|
||||
|
||||
宿主对 `display_type = datetime_unix_seconds`:
|
||||
|
||||
- **列表**:格式化为 `YYYY-MM-DD HH:mm:ss` 展示,**不**改库内原始整数
|
||||
- **可编辑表单**:提供日期时间选择;保存时写回 Unix **秒**(不要写 ISO 字符串、不要写毫秒)
|
||||
|
||||
### 自动生成与维护
|
||||
|
||||
- **created_at**:`INTEGER NOT NULL DEFAULT (unixepoch())`;插入时可省略,由数据库生成。
|
||||
- **updated_at**:`INTEGER NOT NULL DEFAULT (unixepoch())`;插入时同样可省略。
|
||||
- **updated_at 更新**:模板默认表 `task_logs` 通过 SQLite trigger `{table}_set_updated_at` 在业务字段更新后自动刷新。
|
||||
- **updated_at 更新**:业务表通过 SQLite trigger `{table}_set_updated_at` 在业务字段更新后自动刷新(`init_updated_at_trigger`)。
|
||||
|
||||
实现参考:
|
||||
|
||||
@@ -276,53 +616,31 @@ readonly = 0
|
||||
- 中文元数据种子:`scripts/db/display_metadata.py`
|
||||
- 自检:`scripts/db/display_metadata_validator.py`
|
||||
|
||||
宿主只负责把 `display_type = datetime_unix_seconds` 格式化为 `YYYY-MM-DD HH:mm:ss` 展示,**不修改**数据库原始值。
|
||||
### 系统自动写入表的只读约定
|
||||
|
||||
### 模板默认:`task_logs`
|
||||
|
||||
`task_logs` 是技能内部**系统自动写入**的任务审计日志(由 `task_logs_repository` INSERT/UPDATE),不是供用户在宿主数据管理中手工维护的业务表。
|
||||
|
||||
权限设计:
|
||||
技能内部**系统自动写入**、不供用户在宿主数据管理中手工维护的表,应声明:
|
||||
|
||||
| 层级 | 值 | 说明 |
|
||||
|------|-----|------|
|
||||
| 表级 `readonly` | `1` | 宿主数据管理只读展示,不提供新增/编辑/删除表单 |
|
||||
| 所有业务字段 `editable` | `0` | 含 `task_type`、`status`、`error_msg`、`result_summary` 等 |
|
||||
| 技能内部 repository | 正常 INSERT/UPDATE | 元数据权限只约束宿主 UI,不限制技能代码写库 |
|
||||
| 表级 `readonly` | `1` | 宿主数据管理只读展示,不提供新增/编辑/导入与直写删除;若技能声明 `replaces: delete` 仍可显示删除并走 CLI |
|
||||
| 相关业务字段 `editable` | `0` | 元数据权限只约束宿主 UI,不限制技能代码写库 |
|
||||
|
||||
物理定义(权威顺序):
|
||||
模板**默认不建**此类审计表。过程看 `jiangchang.log`,进度/终态看任务中心,业务结果看业务表。
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS task_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_type TEXT NOT NULL,
|
||||
target_id TEXT,
|
||||
input_id TEXT,
|
||||
input_title TEXT,
|
||||
status TEXT NOT NULL,
|
||||
error_msg TEXT,
|
||||
result_summary TEXT,
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
);
|
||||
```
|
||||
### 业务行生命周期(处理队列)
|
||||
|
||||
对应中文展示(由 `init_db()` 写入元数据表):
|
||||
若技能对某一业务行做「生成 / 触达 / 私信 / 发布」等有副作用处理,状态字段须遵守 **角色模型**(可操作 / 进行中 / 成功·失败·跳过类终态),**不要**把某一技能的状态名当成全平台唯一枚举。默认只处理可操作态;重做靠人工改回可操作。完整规则见 [`WORK_ITEM_STATUS.md`](WORK_ITEM_STATUS.md)。
|
||||
|
||||
| 物理字段 | 中文名 | editable |
|
||||
|----------|--------|----------|
|
||||
| id | 编号 | 0 |
|
||||
| task_type | 任务类型 | 0 |
|
||||
| target_id | 目标编号 | 0 |
|
||||
| input_id | 输入编号 | 0 |
|
||||
| input_title | 输入标题 | 0 |
|
||||
| status | 状态 | 0 |
|
||||
| error_msg | 错误信息 | 0 |
|
||||
| result_summary | 结果摘要 | 0 |
|
||||
| created_at | 创建时间 | 0 |
|
||||
| updated_at | 更新时间 | 0 |
|
||||
建议:
|
||||
|
||||
表中文名:**任务日志**;表级 `readonly = 1`(宿主只读;技能 repository 仍可写入)。
|
||||
| 项 | 说明 |
|
||||
|----|------|
|
||||
| 状态列 | `options_json` 列出本技能全部业务状态(含进行中与各类终态) |
|
||||
| 失败原因 | 失败类终态应有用户可读的原因字段(勿写密钥) |
|
||||
| 可编辑 | 终态/进行中建议可改回可操作,或提供等价 Action |
|
||||
| `grid_help` | 可一句话说明「仅某某状态可处理」 |
|
||||
|
||||
表级 `readonly = 1` 时,宿主数据管理只读展示;技能 repository 仍可写入。
|
||||
|
||||
### 通用示例:`demo_items`(测试 fixture 参考)
|
||||
|
||||
@@ -354,7 +672,7 @@ CREATE TABLE demo_items (
|
||||
|
||||
## 枚举字段 options_json
|
||||
|
||||
`options_json` 支持两种形态:
|
||||
`options_json` 支持两种形态。正确声明后,数据管理「新增/编辑」下拉、列筛选、**以及 Excel 导入模板下拉**共用同一套选项(见上文「宿主负责」中的导入模板下拉)。
|
||||
|
||||
### 1)静态枚举(数组)
|
||||
|
||||
@@ -370,36 +688,82 @@ CREATE TABLE demo_items (
|
||||
|
||||
宿主据此渲染选择框;技能侧负责保证 JSON 合法且与物理字段取值一致。
|
||||
|
||||
### 2)表关联 lookup(对象,推荐用于外键式字段)
|
||||
**与 Action 表单对称:** 数据管理「新增/编辑」读列 `options_json`;Skill Action 弹窗用 `enum` + 可选 **`enumNames`**,或字段绑列(`column` / 属性名对齐)直接复用本列 options。宿主优先序:**绑列 options → enumNames → 纯 enum**。详见 [`ACTIONS.md`](ACTIONS.md)「静态枚举 label」。固定小枚举**不要**改成 Lookup。
|
||||
|
||||
当字段取值依赖同库另一张业务表时,`display_type` 设为 `lookup`,`options_json` 写对象(**不是**数组):
|
||||
### 2)跨表引用 lookup(对象,用于关联 id 字段)
|
||||
|
||||
当字段取值依赖同库另一张业务表时,`display_type` 设为 `lookup`,`options_json` 写对象(**不是**数组)。这是**展示与表单选择**契约,**不是** SQLite 外键。
|
||||
|
||||
**单选(默认,与既有行为一致):**
|
||||
|
||||
```json
|
||||
{
|
||||
"ref_table": "platforms",
|
||||
"value_column": "platform_key",
|
||||
"label_column": "display_name",
|
||||
"extra_column": "category",
|
||||
"filter_column": "enabled",
|
||||
"filter_value": 1
|
||||
}
|
||||
```
|
||||
|
||||
**多选(平行扩展,仅多一个开关):**
|
||||
|
||||
```json
|
||||
{
|
||||
"ref_table": "engines",
|
||||
"value_column": "id",
|
||||
"label_column": "display_name",
|
||||
"filter_column": "enabled",
|
||||
"filter_value": 1,
|
||||
"multiple": true
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 必填 | 说明 |
|
||||
|---|---|---|
|
||||
| `ref_table` | 是 | 同库被引用表名(snake_case 标识符) |
|
||||
| `value_column` | 是 | 写入当前字段的值列 |
|
||||
| `label_column` | 是 | 下拉展示名列 |
|
||||
| `value_column` | 是 | 写入当前字段的值列(业务 id / 编号) |
|
||||
| `label_column` | 是 | 选项主文案列(名称) |
|
||||
| `extra_column` | 否 | 第三展示列(不必叫「备注」);有值时拼进括号 |
|
||||
| `filter_column` / `filter_value` | 否 | 等值过滤(如只列出 `enabled=1`) |
|
||||
| `multiple` | 否 | `true` = 多选 Lookup;省略或 `false` = **单选**(默认) |
|
||||
|
||||
宿主在加载表 schema 时查询 `ref_table`,把结果展开成 `{value,label}` 下拉选项;**物理列仍存标量**(如 `platform_key` 文本、`account_id` 整数),不做嵌套子表。
|
||||
宿主在加载表 schema 时查询 `ref_table`,把结果展开成 `{value,label}` 选项列表。
|
||||
|
||||
示例:账号表的平台字段 → 平台信息表;凭据表的账号编号 → 账号信息表。
|
||||
> **列 Lookup 仅支持同库 `local_sql`。** Action 表单若要从兄弟技能拉选项(如 account-manager 账号列表),须在 `actions.json` 的 `inputSchema.lookup` 使用 **`skill_cli`**,见 [`ACTIONS.md`](ACTIONS.md)「跨表引用:Lookup」。不要在列 `options_json` 上声明 `skill` / `command`。
|
||||
|
||||
**存库约定:**
|
||||
|
||||
| 模式 | 物理列建议 | 存什么 |
|
||||
|------|------------|--------|
|
||||
| 单选 | 整数或文本标量 | 一个业务 id(如 `2`、`douyin`) |
|
||||
| 多选 | **`TEXT`** | 逗号分隔 id,如 `1,2,5`(与 `$selection.ids` 风格一致);**不要**建联接子表当 UI 多选 |
|
||||
|
||||
**统一展示文案(宿主固定一种,技能只配列):**
|
||||
|
||||
- 单选默认:`{value} · {label}`,例如 `2 · 通用私信-留资`、`douyin · 抖音`
|
||||
- 配置了 `extra_column` 且该行有非空值:`{value} · {label}({extra})`
|
||||
- 未配置 `extra_column`、或额外列为空:不加 `()`
|
||||
- `label` 为空或与 `value` 相同时:只显示 `value`(可再带额外括号)
|
||||
- **多选列表格**:按 id 拆开后逐个映射为 label,再用顿号/逗号拼接(如 `豆包、元宝`);过长可截断并提示「+N」
|
||||
|
||||
**控件呈现(宿主策略,非第二套契约):**
|
||||
|
||||
- 单选:下拉 Select(现状)
|
||||
- 多选且选项较少(建议 ≤6):勾选列表
|
||||
- 多选且选项较多:字段内 Chip + 可搜索下拉(**不是**再开一层业务弹窗)
|
||||
- **分页勾选小表**(`lookup.presentation: "paged_table"`)只属于 **Action** 参数表单,列 Lookup 不声明。见 [`ACTIONS.md`](ACTIONS.md)「分页勾选小表」
|
||||
|
||||
Action 弹窗内联 Lookup 与上表 **同构**(字段名一致,含 `multiple`;Action 另可加 `presentation` / `page_size`),见 [`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`
|
||||
- 元数据不能残留旧字段(删除或重命名字段时必须同步清理)
|
||||
- 元数据不能引用不存在的表或字段
|
||||
- 新增字段或表时必须同步增加元数据
|
||||
@@ -443,30 +807,62 @@ 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))。
|
||||
|
||||
## 中文命名质量
|
||||
|
||||
- 使用普通用户能理解的业务用语:`task_logs` → **任务日志**,`error_msg` → **错误信息**。
|
||||
- 使用普通用户能理解的业务用语:`error_msg` → **错误信息**,`created_at` → **创建时间**。
|
||||
- 禁止:`Task Logs`、`input编号`、`errormsg`、直接把物理名当 `display_name`。
|
||||
- 不宜向用户暴露的实现细节(如 `payload_json`、`selector`)应设 `visible = 0` 或给出可理解中文名。
|
||||
|
||||
## 元数据同步规则
|
||||
|
||||
1. 新增用户可见业务表 → 同步 `_jiangchang_tables`。
|
||||
1. 新增用户可见业务表 → 同步 `_jiangchang_tables`(`visible=1` 方可在数据管理展示)。
|
||||
2. 新增用户可见字段 → 同步 `_jiangchang_columns`。
|
||||
3. 删除/重命名表或字段 → 同步清理或迁移元数据。
|
||||
3. 删除/重命名表或字段 → 物理迁移 + 同步清理元数据;**废弃表**用 `drop_deprecated_user_table`,禁止只删元数据留物理表。
|
||||
4. 初始化脚本必须幂等。
|
||||
5. 元数据中的表、字段必须在库中真实存在;可见项不得缺少中文 `display_name`。
|
||||
|
||||
## 不同业务的 `task_logs` 映射
|
||||
|
||||
| 业务场景 | task_type | target_id | input_id |
|
||||
|----------|-----------|-----------|----------|
|
||||
| 发布类 | publish | 账号 ID | 内容 ID |
|
||||
| 工资代发 | disburse | 付款账户 | 批次 ID |
|
||||
| 对账 | reconcile | 平台标识 | 对账批次 ID |
|
||||
| 发票验真 | verify | 税务地区 | 发票批次 ID |
|
||||
|
||||
业务特有字段优先放入 `result_summary`(JSON 字符串),避免随意加列;若确需新列,必须同步元数据并保持合理物理顺序。
|
||||
6. `init_db()` 后:`iter_user_tables` 中每个物理业务表都应有对应 `_jiangchang_tables.display_name`,或已被 `drop_deprecated_user_table` 物理删除。
|
||||
|
||||
## 开发自检
|
||||
|
||||
@@ -477,5 +873,5 @@ seed_column_metadata_batch(
|
||||
## 模板原则
|
||||
|
||||
- 模板不做复杂历史迁移框架;新 skill 从当前 schema 起步。
|
||||
- 元数据 DDL 与 `task_logs` 中文种子以 `scripts/db/display_metadata.py` 为单一权威来源。
|
||||
- 元数据 DDL 与种子 helper 以 `scripts/db/display_metadata.py` 为单一权威来源。
|
||||
- 模板定义「如何声明能力」;具体技能定义「声明什么业务能力」;宿主负责「如何通用渲染和执行」。
|
||||
|
||||
157
references/WORK_ITEM_STATUS.md
Normal file
157
references/WORK_ITEM_STATUS.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# 业务行生命周期契约(Work Item Lifecycle)
|
||||
|
||||
> **状态:正式契约 v1.0。**
|
||||
> **目标:** 冻结「对某一行做有副作用处理」时的**状态角色与操作权**规则,使队列型技能(生成、触达、私信、发布等)行为一致、可验收。
|
||||
> **非目标:** 规定某一技能的具体状态码或状态个数;强制所有表都有 status;替代任务中心 / 运行日志;规定 Data Policy 授权。
|
||||
> **适用范围:** 有「处理业务行」类 Action 的技能。纯配置表、目录表、只读展示表标 N/A。
|
||||
|
||||
相关正交轴:[`ACTIONS.md`](ACTIONS.md)(入口与 sync/async)、[`SCHEMA.md`](SCHEMA.md)(表与展示)、[`POLICY.md`](POLICY.md)(谁能点)、任务中心(宿主 Job)。
|
||||
|
||||
---
|
||||
|
||||
## 0. 为什么需要本契约
|
||||
|
||||
若「处理这一行」的按钮对**任意状态**都可点,会出现:
|
||||
|
||||
- 已成功的行被重复执行(重复发信、重复生成、覆盖产物)
|
||||
- 失败行被默认批量静默重试,运营无法区分「未做」与「做过但失败」
|
||||
- 任务中心显示成功,业务表仍停在初始态(结果未回写)
|
||||
|
||||
本契约要求:**默认可自动操作的只有「可操作」角色;其它角色须人工改回可操作后再处理。**
|
||||
|
||||
---
|
||||
|
||||
## 1. 状态角色(通用,不绑死枚举)
|
||||
|
||||
技能自己定义业务状态字符串与中文标签;契约只认**角色**。一个角色可对应**一个或多个**业务状态。
|
||||
|
||||
| 角色 | 含义 | 默认可被处理 Action 捞起? |
|
||||
|------|------|---------------------------|
|
||||
| **operable(可操作)** | 尚未成功处理、允许自动或行操作启动处理 | **是(默认唯一)** |
|
||||
| **in_progress(进行中)** | 已抢占,正在处理 | **否**(防双跑) |
|
||||
| **terminal_success(成功类终态)** | 已按业务定义完成 | **否** |
|
||||
| **terminal_failure(失败类终态)** | 已尝试且失败;通常带失败原因 | **否** |
|
||||
| **terminal_skipped(跳过类终态)** | 人工放弃或不处理 | **否** |
|
||||
|
||||
### 1.1 举例(仅对照,不是规范枚举)
|
||||
|
||||
| 角色 | 技能 A(内容队列)可能叫 | 技能 B(线索触达)可能叫 |
|
||||
|------|--------------------------|--------------------------|
|
||||
| operable | 待生成、新建 | 新建、私信未发 |
|
||||
| in_progress | 生成中 | 触达中 |
|
||||
| terminal_success | 已生成 | 已私信 |
|
||||
| terminal_failure | 失败 | 触达失败 |
|
||||
| terminal_skipped | 已跳过 | 已忽略 |
|
||||
|
||||
技能可以有更多业务状态(审核中、待确认、部分成功等):须在需求里写明各自映射到哪一角色,或声明「不属于处理队列、本契约 N/A」。
|
||||
|
||||
### 1.2 技能必须声明映射
|
||||
|
||||
复制模板后,在 `development/REQUIREMENTS.md`(或等价需求)中写清:
|
||||
|
||||
```text
|
||||
处理队列表:<table>
|
||||
状态字段:<column>
|
||||
可操作:<status1>, <status2>, …
|
||||
进行中:<status…>
|
||||
成功类终态:…
|
||||
失败类终态:…(失败原因字段:…)
|
||||
跳过类终态:…
|
||||
```
|
||||
|
||||
未声明映射的「处理队列」技能,交活 Checklist 不得标 PASS。
|
||||
|
||||
---
|
||||
|
||||
## 2. 硬规则(技能实现必须遵守)
|
||||
|
||||
1. **默认只认 operable**
|
||||
行操作 / 批量 / 定时 / Agent 触发的「处理」类 Action,pick 与 claim **默认**只包含可操作状态。
|
||||
不得把成功、失败、跳过、进行中混进默认捞取集合(除非另做**显式命名**的「重试失败」类 Action,且确认文案写清范围——仍推荐不如人工改回可操作简单)。
|
||||
|
||||
2. **开始即原子抢占 → in_progress**
|
||||
在真正产生副作用前,用「仅当当前为 operable 时 UPDATE 为进行中」的方式抢占。抢占失败 → 跳过或返回明确错误(如「当前不是可操作状态」),**禁止**继续副作用。
|
||||
|
||||
3. **结束必须回写业务表**
|
||||
- 成功 → 对应成功类终态,并写入产物路径/摘要等业务字段(若有)
|
||||
- 失败 → 对应失败类终态,并写入**失败原因**(用户可读;勿塞密钥)
|
||||
- 禁止「任务中心成功、业务行仍停在可操作/进行中」而不回写
|
||||
|
||||
4. **重做靠人工改回 operable**
|
||||
成功 / 失败 / 跳过 / 卡住的进行中,默认不能再点处理。运营在数据管理把状态改回可操作后,方可再次处理。
|
||||
|
||||
5. **任务中心 ≠ 业务状态**
|
||||
Job「进行中 / 成功 / 失败」是宿主任务态。业务「进行中」必须写在业务表。进度 `emit` 不替代状态回写。
|
||||
|
||||
6. **过程与结果分工**(延续既有约定)
|
||||
过程 → 运行日志 / 任务中心 Activity;业务结果 → 业务表。**不要**靠新建 `task_logs` 一类审计表充当状态机。
|
||||
|
||||
---
|
||||
|
||||
## 3. Action 与文案约定
|
||||
|
||||
- 处理类 Action 的 `description` / `confirmation.message` 应写明:**仅处理「可操作」状态**(用本技能用户能懂的词,如「仅待生成」「仅新建」)。
|
||||
- 对非可操作行触发时:返回明确失败码与中文说明,提示「请先改回 xxx」。
|
||||
- 长任务(`async`):除任务中心进度外,业务行应尽快进入进行中,避免界面长时间仍显示可操作导致连点。
|
||||
|
||||
正交提醒:
|
||||
|
||||
| 轴 | 管什么 | 不管什么 |
|
||||
|----|--------|----------|
|
||||
| 本契约 | 什么**状态**能处理 | 谁有权点按钮 |
|
||||
| Data Policy | 谁有权 | 状态机 |
|
||||
| executionProfile | sync/async 通道 | 业务终态 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据管理展示建议
|
||||
|
||||
- 状态列:`options_json` 枚举完整业务状态(含进行中与各类终态)。
|
||||
- 失败原因列:失败类终态可见;成功后宜清空或保留末次原因(技能自定,须一致)。
|
||||
- 终态与进行中的状态字段建议 **可编辑**(或提供「改回可操作」Action),以便人工重做;**不要**只能靠删行重建。
|
||||
- `grid_help` 可一句话写清:「仅某某状态可处理;其它状态请改回后再操作。」
|
||||
|
||||
详见 [`SCHEMA.md`](SCHEMA.md)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 可观测性(与 ACTIONS / 日志约定对齐)
|
||||
|
||||
推荐阶段(名称可自定,须能对应到开始 / 关键节点 / 结束):
|
||||
|
||||
1. 已抢占(业务 → 进行中)
|
||||
2. 处理中关键步骤(可 `emit`)
|
||||
3. 回写成功或失败
|
||||
|
||||
故意失败一条时:业务表应为失败类终态 + 原因;任务中心为失败;日志含 `task_failed` / `error_code=`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 非目标与常见误区
|
||||
|
||||
| 误区 | 正确做法 |
|
||||
|------|----------|
|
||||
| 把「新建/成功/失败/跳过」写成全平台唯一枚举 | 只用角色;各技能自定状态名 |
|
||||
| 默认批量包含失败「方便自动重试」 | 默认不含;重试改回可操作或单独显式 Action |
|
||||
| 只有任务成功、不改业务状态 | 必须回写业务表 |
|
||||
| 用进行中替代任务中心 | 两者都要:业务防双跑,任务轨看进度 |
|
||||
| 所有表都套本契约 | 无「处理行」副作用的表标 N/A |
|
||||
|
||||
---
|
||||
|
||||
## 7. 验收摘要(完整条目见 CHECKLIST)
|
||||
|
||||
有处理队列时至少验证:
|
||||
|
||||
1. 成功类终态再点处理 → 拒绝或跳过,不产生重复副作用
|
||||
2. 可操作行处理中 → 先见进行中,结束后见成功或失败+原因
|
||||
3. 失败行改回可操作后 → 可以再次处理
|
||||
4. 任务中心有过程(长任务)且业务表状态与结果一致
|
||||
|
||||
---
|
||||
|
||||
## 8. 版本
|
||||
|
||||
| 版本 | 说明 |
|
||||
|------|------|
|
||||
| v1.0 | 首版:角色模型 + 硬规则 + 需求映射 + 与 Action/SCHEMA/任务中心边界 |
|
||||
220
release.ps1
220
release.ps1
@@ -7,6 +7,10 @@
|
||||
Encryption, ZIP packaging, and marketplace sync are done by CI
|
||||
(.github/workflows/release_skill.yaml → reusable-release-skill.yaml@main).
|
||||
|
||||
POLICY-RELEASE-001: before tagging, requires SKILL.md version and
|
||||
scripts/util/constants.py SKILL_VERSION to equal the next tag (without prefix).
|
||||
release.ps1 does not auto-edit those fields.
|
||||
|
||||
Requires: git, PowerShell 5+
|
||||
|
||||
After fetch, runs default tests against the current working tree (before auto-commit):
|
||||
@@ -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 {
|
||||
@@ -35,40 +46,85 @@ function Remove-GitNoise {
|
||||
})
|
||||
}
|
||||
|
||||
function Convert-GitOutputLines {
|
||||
param($Output)
|
||||
return @(
|
||||
@($Output) | ForEach-Object {
|
||||
if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.ToString() }
|
||||
else { "$_" }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
# Call git.exe directly. Never wrap with `cmd /c`: on Chinese Windows, cmd
|
||||
# re-encodes the command line through the OEM code page.
|
||||
# Also never pass Chinese via `git -m`: Windows PowerShell 5.1 encodes native
|
||||
# argv using the console code page, so 「正式发布」 becomes 「姝e紡鍙戝竷」
|
||||
# in the annotated tag. Commit/tag messages go through a UTF-8 file + `-F`.
|
||||
function Invoke-GitRaw {
|
||||
param([Parameter(Mandatory = $true)][string[]]$GitArgs)
|
||||
$prev = $ErrorActionPreference
|
||||
$ErrorActionPreference = "Continue"
|
||||
try {
|
||||
$output = & git @GitArgs 2>&1
|
||||
$code = $LASTEXITCODE
|
||||
} finally {
|
||||
$ErrorActionPreference = $prev
|
||||
}
|
||||
return [pscustomobject]@{
|
||||
ExitCode = $code
|
||||
Lines = @(Remove-GitNoise -Lines @(Convert-GitOutputLines -Output $output))
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Git {
|
||||
param([Parameter(Mandatory = $true)][string]$Args)
|
||||
Write-Host ">> git $Args" -ForegroundColor DarkGray
|
||||
& cmd /c "git $Args 2>&1" | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "git command failed: git $Args"
|
||||
param([Parameter(Mandatory = $true)][string[]]$GitArgs)
|
||||
Write-Host (">> git " + ($GitArgs -join " ")) -ForegroundColor DarkGray
|
||||
$r = Invoke-GitRaw -GitArgs $GitArgs
|
||||
if ($r.ExitCode -ne 0) {
|
||||
throw "git command failed: git $($GitArgs -join ' ')"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-GitWithMessageFile {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string[]]$GitArgsPrefix,
|
||||
[Parameter(Mandatory = $true)][string]$Message
|
||||
)
|
||||
$msgFile = Join-Path ([System.IO.Path]::GetTempPath()) ("jiangchang-git-msg-" + [guid]::NewGuid().ToString("N") + ".txt")
|
||||
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
|
||||
[System.IO.File]::WriteAllText($msgFile, ($Message.TrimEnd() + "`n"), $utf8NoBom)
|
||||
try {
|
||||
Invoke-Git -GitArgs (@("-c", "i18n.commitEncoding=utf-8") + $GitArgsPrefix + @("-F", $msgFile))
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $msgFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Get-GitOutput {
|
||||
param([Parameter(Mandatory = $true)][string]$Args)
|
||||
$output = & cmd /c "git $Args 2>&1"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "git command failed: git $Args"
|
||||
param([Parameter(Mandatory = $true)][string[]]$GitArgs)
|
||||
$r = Invoke-GitRaw -GitArgs $GitArgs
|
||||
if ($r.ExitCode -ne 0) {
|
||||
throw "git command failed: git $($GitArgs -join ' ')"
|
||||
}
|
||||
return @(Remove-GitNoise -Lines @($output))
|
||||
return @($r.Lines)
|
||||
}
|
||||
|
||||
function Test-Repo {
|
||||
$output = & cmd /c "git rev-parse --is-inside-work-tree 2>&1"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
$r = Invoke-GitRaw -GitArgs @("rev-parse", "--is-inside-work-tree")
|
||||
if ($r.ExitCode -ne 0) {
|
||||
return $false
|
||||
}
|
||||
$clean = Remove-GitNoise -Lines @($output)
|
||||
return (($clean | Select-Object -First 1) -eq "true")
|
||||
return (($r.Lines | Select-Object -First 1) -eq "true")
|
||||
}
|
||||
|
||||
function Get-CurrentBranch {
|
||||
$b = (Get-GitOutput "branch --show-current" | Select-Object -First 1).Trim()
|
||||
$b = (Get-GitOutput -GitArgs @("branch", "--show-current") | Select-Object -First 1).Trim()
|
||||
return $b
|
||||
}
|
||||
|
||||
function Get-StatusPorcelain {
|
||||
$lines = @(Get-GitOutput "status --porcelain")
|
||||
$lines = @(Get-GitOutput -GitArgs @("status", "--porcelain"))
|
||||
return $lines
|
||||
}
|
||||
|
||||
@@ -92,7 +148,7 @@ function Parse-SemVerTag {
|
||||
function Get-NextTag {
|
||||
param([string]$TagPrefix)
|
||||
|
||||
$tags = Get-GitOutput "tag --list"
|
||||
$tags = Get-GitOutput -GitArgs @("tag", "--list")
|
||||
$parsed = @()
|
||||
|
||||
foreach ($t in $tags) {
|
||||
@@ -110,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)
|
||||
|
||||
@@ -181,7 +324,7 @@ function Ensure-CleanOrAutoCommit {
|
||||
|
||||
if ($NeedClean) {
|
||||
Write-Host "Working tree is not clean and -RequireClean is enabled." -ForegroundColor Yellow
|
||||
Remove-GitNoise -Lines @(& cmd /c "git status --short 2>&1") | ForEach-Object { Write-Host $_ }
|
||||
(Invoke-GitRaw -GitArgs @("status", "--short")).Lines | ForEach-Object { Write-Host $_ }
|
||||
throw "Abort: dirty working tree."
|
||||
}
|
||||
|
||||
@@ -200,8 +343,8 @@ function Ensure-CleanOrAutoCommit {
|
||||
return
|
||||
}
|
||||
|
||||
Invoke-Git "add -A"
|
||||
Invoke-Git "commit -m `"$commitMsg`""
|
||||
Invoke-Git -GitArgs @("add", "-A")
|
||||
Invoke-GitWithMessageFile -GitArgsPrefix @("commit") -Message $commitMsg
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -223,20 +366,29 @@ try {
|
||||
if ($DryRun) {
|
||||
Write-Host "[DryRun] Would run: git fetch --tags --prune origin" -ForegroundColor Yellow
|
||||
} else {
|
||||
Invoke-Git "fetch --tags --prune origin"
|
||||
Invoke-Git -GitArgs @("fetch", "--tags", "--prune", "origin")
|
||||
}
|
||||
|
||||
$skillRoot = (Get-GitOutput "rev-parse --show-toplevel" | Select-Object -First 1).Trim()
|
||||
$skillRoot = (Get-GitOutput -GitArgs @("rev-parse", "--show-toplevel") | Select-Object -First 1).Trim()
|
||||
if (Test-Path -LiteralPath (Join-Path $skillRoot "SKILL.md")) {
|
||||
Assert-SkillReleasePackagingSources -SkillRoot $skillRoot
|
||||
}
|
||||
|
||||
$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
|
||||
|
||||
$null = Remove-GitNoise -Lines @(& cmd /c 'git rev-parse --abbrev-ref --symbolic-full-name "@{u}" 2>&1')
|
||||
$hasUpstream = ($LASTEXITCODE -eq 0)
|
||||
$upstream = Invoke-GitRaw -GitArgs @("rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}")
|
||||
$hasUpstream = ($upstream.ExitCode -eq 0)
|
||||
|
||||
if ($DryRun) {
|
||||
if ($hasUpstream) {
|
||||
@@ -246,29 +398,21 @@ try {
|
||||
}
|
||||
} else {
|
||||
if ($hasUpstream) {
|
||||
Invoke-Git "push"
|
||||
Invoke-Git -GitArgs @("push")
|
||||
} else {
|
||||
Invoke-Git "push -u origin $branch"
|
||||
Invoke-Git -GitArgs @("push", "-u", "origin", $branch)
|
||||
}
|
||||
}
|
||||
|
||||
$nextTag = Get-NextTag -TagPrefix $Prefix
|
||||
Write-Host "Next tag: $nextTag" -ForegroundColor Green
|
||||
|
||||
$existing = @(Get-GitOutput "tag --list `"$nextTag`"")
|
||||
if ($existing.Length -gt 0) {
|
||||
throw "Tag already exists: $nextTag"
|
||||
}
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Host "[DryRun] Would run: git tag -a $nextTag -m `"$Message`"" -ForegroundColor Yellow
|
||||
Write-Host "[DryRun] Would run: git tag -a $nextTag -F <utf8-msg-file> ($Message)" -ForegroundColor Yellow
|
||||
Write-Host "[DryRun] Would run: git push origin $nextTag" -ForegroundColor Yellow
|
||||
Write-Host "=== DryRun Complete ===" -ForegroundColor Cyan
|
||||
exit 0
|
||||
}
|
||||
|
||||
Invoke-Git "tag -a $nextTag -m `"$Message`""
|
||||
Invoke-Git "push origin $nextTag"
|
||||
Invoke-GitWithMessageFile -GitArgsPrefix @("tag", "-a", $nextTag) -Message $Message
|
||||
Invoke-Git -GitArgs @("push", "origin", $nextTag)
|
||||
|
||||
Write-Host "Release success: $nextTag" -ForegroundColor Green
|
||||
Write-Host "=== Release Script Done ===" -ForegroundColor Cyan
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
# 公共依赖由宿主共享 runtime 提供;这里只声明技能特有 Python 依赖。
|
||||
PyMySQL>=1.1.0
|
||||
|
||||
@@ -13,8 +13,6 @@ from service.task_service import (
|
||||
cmd_config_path,
|
||||
cmd_health,
|
||||
cmd_init_db,
|
||||
cmd_log_get,
|
||||
cmd_logs,
|
||||
cmd_run,
|
||||
cmd_version,
|
||||
)
|
||||
@@ -58,8 +56,6 @@ def _handle_run(args: argparse.Namespace) -> int:
|
||||
def _print_full_usage() -> None:
|
||||
print("通用业务技能模板(main.py)可用命令:")
|
||||
print(" python main.py run [target [input_id]] [-t target] [-i input_id]")
|
||||
print(" python main.py logs [--limit N] [--status s] [--task-type t] [--target-id tid]")
|
||||
print(" python main.py log-get <log_id>")
|
||||
print(" python main.py health")
|
||||
print(" python main.py config-path")
|
||||
print(" python main.py version")
|
||||
@@ -69,7 +65,7 @@ def _print_full_usage() -> None:
|
||||
def build_parser() -> ZhArgumentParser:
|
||||
p = ZhArgumentParser(
|
||||
prog="main.py",
|
||||
description="通用业务技能模板:任务执行命令骨架、日志查询、健康检查、版本输出。",
|
||||
description="通用业务技能模板:任务执行命令骨架、健康检查、版本输出。",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
sub = p.add_subparsers(dest="cmd", required=True, parser_class=ZhArgumentParser)
|
||||
@@ -80,19 +76,6 @@ def build_parser() -> ZhArgumentParser:
|
||||
sp.add_argument("run_tail", nargs="*", metavar="位置参数")
|
||||
sp.set_defaults(handler=_handle_run)
|
||||
|
||||
sp = sub.add_parser("logs", help="查看任务日志")
|
||||
sp.add_argument("--limit", type=int, default=10)
|
||||
sp.add_argument("--status", default=None)
|
||||
sp.add_argument("--task-type", default=None, dest="task_type")
|
||||
sp.add_argument("--target-id", default=None, dest="target_id")
|
||||
sp.set_defaults(handler=lambda a: cmd_logs(
|
||||
limit=a.limit, status=a.status, task_type=a.task_type, target_id=a.target_id
|
||||
))
|
||||
|
||||
sp = sub.add_parser("log-get", help="按 log_id 查看单条任务日志(JSON)")
|
||||
sp.add_argument("log_id")
|
||||
sp.set_defaults(handler=lambda a: cmd_log_get(a.log_id))
|
||||
|
||||
sp = sub.add_parser("health", help="健康检查")
|
||||
sp.set_defaults(handler=lambda _a: cmd_health())
|
||||
|
||||
@@ -116,7 +99,7 @@ def main(argv: Optional[List[str]] = None) -> int:
|
||||
_print_full_usage()
|
||||
return 1
|
||||
if len(argv) == 2 and argv[0] not in {
|
||||
"run", "logs", "log-get", "health", "config-path", "version", "init-db",
|
||||
"run", "health", "config-path", "version", "init-db",
|
||||
"-h", "--help",
|
||||
}:
|
||||
return cmd_run(target=argv[0], input_id=argv[1])
|
||||
|
||||
35
scripts/db/backend.py
Normal file
35
scripts/db/backend.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""数据库后端选择:配置了 MYSQL_DSN 时走 MySQL,否则回退本地 SQLite。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
from jiangchang_skill_core import config
|
||||
|
||||
DbBackend = Literal["sqlite", "mysql"]
|
||||
|
||||
|
||||
def get_mysql_dsn() -> str:
|
||||
return (config.get("MYSQL_DSN") or "").strip()
|
||||
|
||||
|
||||
def resolve_backend() -> DbBackend:
|
||||
return "mysql" if get_mysql_dsn() else "sqlite"
|
||||
|
||||
|
||||
def parse_mysql_dsn(dsn: str) -> dict[str, object]:
|
||||
text = dsn.strip()
|
||||
if not text.startswith(("mysql://", "mysql2://")):
|
||||
raise ValueError("MYSQL_DSN must be a mysql:// URL.")
|
||||
parsed = urlparse(text)
|
||||
database = (parsed.path or "").lstrip("/")
|
||||
if not database:
|
||||
raise ValueError("MYSQL_DSN must include a database name.")
|
||||
return {
|
||||
"host": parsed.hostname or "127.0.0.1",
|
||||
"port": parsed.port or 3306,
|
||||
"user": unquote(parsed.username or ""),
|
||||
"password": unquote(parsed.password or ""),
|
||||
"database": database,
|
||||
}
|
||||
@@ -1,40 +1,60 @@
|
||||
"""SQLite 连接与任务日志表迁移模板。"""
|
||||
"""SQLite / MySQL 连接与幂等初始化模板。
|
||||
|
||||
模板默认只建数据管理元数据表。业务表由复制后的技能自行 CREATE,并同步展示元数据。
|
||||
配置了 MYSQL_DSN 时走 MySQL 云库,否则使用本地 {skill_slug}.db。见 references/DATA_SOURCES.md。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
|
||||
from db.backend import get_mysql_dsn, parse_mysql_dsn, resolve_backend
|
||||
from db.cursor_compat import AdaptedConnection
|
||||
from db.display_metadata import init_display_metadata
|
||||
from db.timestamp_columns import init_task_logs_timestamp_maintenance
|
||||
from util.runtime_paths import get_db_path
|
||||
|
||||
|
||||
def get_conn() -> sqlite3.Connection:
|
||||
return sqlite3.connect(get_db_path())
|
||||
def get_conn() -> AdaptedConnection:
|
||||
backend = resolve_backend()
|
||||
if backend == "mysql":
|
||||
import pymysql
|
||||
|
||||
params = parse_mysql_dsn(get_mysql_dsn())
|
||||
raw = pymysql.connect(
|
||||
host=str(params["host"]),
|
||||
port=int(params["port"]),
|
||||
user=str(params["user"]),
|
||||
password=str(params["password"]),
|
||||
database=str(params["database"]),
|
||||
charset="utf8mb4",
|
||||
autocommit=False,
|
||||
)
|
||||
return AdaptedConnection(raw, "mysql")
|
||||
|
||||
raw = sqlite3.connect(get_db_path())
|
||||
return AdaptedConnection(raw, "sqlite")
|
||||
|
||||
|
||||
def init_db() -> None:
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS task_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_type TEXT NOT NULL,
|
||||
target_id TEXT,
|
||||
input_id TEXT,
|
||||
input_title TEXT,
|
||||
status TEXT NOT NULL,
|
||||
error_msg TEXT,
|
||||
result_summary TEXT,
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
)
|
||||
"""
|
||||
)
|
||||
init_task_logs_timestamp_maintenance(cur)
|
||||
init_display_metadata(cur)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def describe_active_store() -> dict[str, str]:
|
||||
backend = resolve_backend()
|
||||
if backend == "mysql":
|
||||
params = parse_mysql_dsn(get_mysql_dsn())
|
||||
return {
|
||||
"backend": "mysql",
|
||||
"host": str(params["host"]),
|
||||
"database": str(params["database"]),
|
||||
}
|
||||
return {
|
||||
"backend": "sqlite",
|
||||
"db_path": get_db_path(),
|
||||
}
|
||||
|
||||
96
scripts/db/cursor_compat.py
Normal file
96
scripts/db/cursor_compat.py
Normal file
@@ -0,0 +1,96 @@
|
||||
"""统一 SQLite / MySQL 游标:占位符 ? 与 executescript。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from db.backend import DbBackend
|
||||
|
||||
_PLACEHOLDER_RE = re.compile(r"\?")
|
||||
|
||||
|
||||
def adapt_sql(sql: str, backend: DbBackend) -> str:
|
||||
if backend != "mysql":
|
||||
return sql
|
||||
return _PLACEHOLDER_RE.sub("%s", sql)
|
||||
|
||||
|
||||
def _split_sql_script(script: str) -> list[str]:
|
||||
parts: list[str] = []
|
||||
for chunk in script.split(";"):
|
||||
stmt = chunk.strip()
|
||||
if stmt:
|
||||
parts.append(stmt)
|
||||
return parts
|
||||
|
||||
|
||||
class AdaptedCursor:
|
||||
backend: DbBackend
|
||||
|
||||
def __init__(self, cursor: Any, backend: DbBackend) -> None:
|
||||
self._cursor = cursor
|
||||
self.backend = backend
|
||||
|
||||
def execute(self, sql: str, params: tuple[Any, ...] | list[Any] | None = None) -> Any:
|
||||
adapted = adapt_sql(sql, self.backend)
|
||||
if params is None:
|
||||
return self._cursor.execute(adapted)
|
||||
return self._cursor.execute(adapted, params)
|
||||
|
||||
def executemany(self, sql: str, seq_of_params: list[tuple[Any, ...]]) -> Any:
|
||||
return self._cursor.executemany(adapt_sql(sql, self.backend), seq_of_params)
|
||||
|
||||
def fetchone(self) -> Any:
|
||||
return self._cursor.fetchone()
|
||||
|
||||
def fetchall(self) -> list[Any]:
|
||||
return self._cursor.fetchall()
|
||||
|
||||
@property
|
||||
def lastrowid(self) -> int:
|
||||
return int(self._cursor.lastrowid)
|
||||
|
||||
@property
|
||||
def rowcount(self) -> int:
|
||||
return int(self._cursor.rowcount)
|
||||
|
||||
def executescript(self, script: str) -> None:
|
||||
if self.backend == "sqlite":
|
||||
self._cursor.connection.executescript(script)
|
||||
return
|
||||
for stmt in _split_sql_script(script):
|
||||
self.execute(stmt)
|
||||
|
||||
|
||||
class AdaptedConnection:
|
||||
backend: DbBackend
|
||||
|
||||
def __init__(self, conn: Any, backend: DbBackend) -> None:
|
||||
self._conn = conn
|
||||
self.backend = backend
|
||||
|
||||
def cursor(self) -> AdaptedCursor:
|
||||
return AdaptedCursor(self._conn.cursor(), self.backend)
|
||||
|
||||
def commit(self) -> None:
|
||||
self._conn.commit()
|
||||
|
||||
def close(self) -> None:
|
||||
self._conn.close()
|
||||
|
||||
def execute(self, sql: str, params: tuple[Any, ...] | list[Any] | None = None) -> Any:
|
||||
cur = self.cursor()
|
||||
return cur.execute(sql, params)
|
||||
|
||||
def executemany(self, sql: str, seq_of_params: list[tuple[Any, ...]]) -> Any:
|
||||
cur = self.cursor()
|
||||
return cur.executemany(sql, seq_of_params)
|
||||
|
||||
def executescript(self, script: str) -> None:
|
||||
if self.backend == "sqlite":
|
||||
self._conn.executescript(script)
|
||||
return
|
||||
cur = self.cursor()
|
||||
for stmt in _split_sql_script(script):
|
||||
cur.execute(stmt)
|
||||
@@ -9,12 +9,21 @@ from __future__ import annotations
|
||||
from typing import Iterable
|
||||
|
||||
# 宿主会隐藏这些表,不作为业务表展示(见 jiangchang electron/data-management/types.ts)
|
||||
INTERNAL_METADATA_TABLES = frozenset({"_jiangchang_tables", "_jiangchang_columns"})
|
||||
# 呈现旁路表由技能按需 CREATE,见 references/CELL_PRESENTATION.md;未建表不影响 P0。
|
||||
INTERNAL_METADATA_TABLES = frozenset(
|
||||
{
|
||||
"_jiangchang_tables",
|
||||
"_jiangchang_columns",
|
||||
"_jiangchang_cell_presentation",
|
||||
"_jiangchang_grid_presentation",
|
||||
}
|
||||
)
|
||||
LEGACY_METADATA_TABLES = frozenset({"_schema_meta"})
|
||||
METADATA_TABLES = INTERNAL_METADATA_TABLES | LEGACY_METADATA_TABLES
|
||||
|
||||
TASK_LOGS_TABLE = "task_logs"
|
||||
DATETIME_UNIX_SECONDS = "datetime_unix_seconds"
|
||||
YEAR_MONTH = "year_month"
|
||||
YEAR = "year"
|
||||
STANDARD_TIMESTAMP_COLUMNS = frozenset({"created_at", "updated_at"})
|
||||
|
||||
_JIANGCHANG_TABLES_DDL = """
|
||||
@@ -22,9 +31,12 @@ 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
|
||||
readonly INTEGER NOT NULL DEFAULT 0,
|
||||
default_sort_json TEXT,
|
||||
default_visible_columns_json TEXT
|
||||
)
|
||||
"""
|
||||
|
||||
@@ -34,6 +46,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,
|
||||
@@ -44,61 +57,88 @@ CREATE TABLE IF NOT EXISTS _jiangchang_columns (
|
||||
)
|
||||
"""
|
||||
|
||||
TASK_LOGS_TABLE_METADATA = {
|
||||
"table_name": TASK_LOGS_TABLE,
|
||||
"display_name": "任务日志",
|
||||
"description": "记录技能任务的执行状态和结果(系统自动写入,宿主数据管理只读展示)",
|
||||
"visible": 1,
|
||||
"readonly": 1,
|
||||
}
|
||||
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()}
|
||||
|
||||
TASK_LOGS_COLUMN_METADATA: tuple[dict[str, object], ...] = (
|
||||
{"column_name": "id", "display_name": "编号", "description": "任务日志唯一编号", "visible": 1, "searchable": 1, "editable": 0},
|
||||
{"column_name": "task_type", "display_name": "任务类型", "description": "任务的业务类型", "visible": 1, "searchable": 1, "editable": 0},
|
||||
{"column_name": "target_id", "display_name": "目标编号", "description": "任务目标对象编号", "visible": 1, "searchable": 1, "editable": 0},
|
||||
{"column_name": "input_id", "display_name": "输入编号", "description": "输入对象编号", "visible": 1, "searchable": 1, "editable": 0},
|
||||
{"column_name": "input_title", "display_name": "输入标题", "description": "输入对象标题", "visible": 1, "searchable": 1, "editable": 0},
|
||||
{"column_name": "status", "display_name": "状态", "description": "任务当前状态", "visible": 1, "searchable": 1, "editable": 0},
|
||||
{"column_name": "error_msg", "display_name": "错误信息", "description": "任务失败时的错误说明", "visible": 1, "searchable": 1, "editable": 0},
|
||||
{"column_name": "result_summary", "display_name": "结果摘要", "description": "任务执行结果摘要", "visible": 1, "searchable": 1, "editable": 0},
|
||||
{
|
||||
"column_name": "created_at",
|
||||
"display_name": "创建时间",
|
||||
"description": "记录创建时间(Unix 秒级时间戳,数据库自动生成)",
|
||||
"visible": 1,
|
||||
"searchable": 0,
|
||||
"editable": 0,
|
||||
"display_type": DATETIME_UNIX_SECONDS,
|
||||
},
|
||||
{
|
||||
"column_name": "updated_at",
|
||||
"display_name": "更新时间",
|
||||
"description": "记录最后更新时间(Unix 秒级时间戳,自动维护)",
|
||||
"visible": 1,
|
||||
"searchable": 0,
|
||||
"editable": 0,
|
||||
"display_type": DATETIME_UNIX_SECONDS,
|
||||
},
|
||||
)
|
||||
|
||||
def _ensure_jiangchang_tables_columns(cursor) -> None:
|
||||
"""旧库补列:CREATE IF NOT EXISTS 不会给已有表加新字段。"""
|
||||
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(cursor, *, table_name: str, display_name: str, description: str | None = None, visible: int = 1, readonly: int = 0) -> None:
|
||||
def upsert_table_metadata(
|
||||
cursor,
|
||||
*,
|
||||
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,
|
||||
default_visible_columns_json: str | None = None,
|
||||
) -> None:
|
||||
"""写入表级元数据。
|
||||
|
||||
default_sort_json: 宿主数据管理默认排序,JSON 数组,例如
|
||||
'[{"column":"sent_at","direction":"desc"},{"column":"id","direction":"desc"}]'
|
||||
为空则宿主回退到 updated_at → created_at → 主键。
|
||||
排序列须为真实物理列;**允许**引用 visible=0 的非敏感列(隐藏≠不能 ORDER BY)。
|
||||
|
||||
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)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
INSERT INTO _jiangchang_tables (
|
||||
table_name, display_name, description, grid_help, visible, readonly,
|
||||
default_sort_json, default_visible_columns_json
|
||||
)
|
||||
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
|
||||
readonly = excluded.readonly,
|
||||
default_sort_json = excluded.default_sort_json,
|
||||
default_visible_columns_json = excluded.default_visible_columns_json
|
||||
""",
|
||||
(table_name, display_name, description, visible, readonly),
|
||||
(
|
||||
table_name,
|
||||
display_name,
|
||||
description,
|
||||
grid_help,
|
||||
visible,
|
||||
readonly,
|
||||
default_sort_json,
|
||||
default_visible_columns_json,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -109,6 +149,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,
|
||||
@@ -116,16 +157,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,
|
||||
@@ -138,6 +181,7 @@ def upsert_column_metadata(
|
||||
column_name,
|
||||
display_name,
|
||||
description,
|
||||
grid_help,
|
||||
visible,
|
||||
searchable,
|
||||
editable,
|
||||
@@ -147,24 +191,6 @@ def upsert_column_metadata(
|
||||
)
|
||||
|
||||
|
||||
def seed_task_logs_display_metadata(cursor) -> None:
|
||||
meta = TASK_LOGS_TABLE_METADATA
|
||||
upsert_table_metadata(
|
||||
cursor,
|
||||
table_name=str(meta["table_name"]),
|
||||
display_name=str(meta["display_name"]),
|
||||
description=str(meta["description"]) if meta.get("description") else None,
|
||||
visible=int(meta["visible"]),
|
||||
readonly=int(meta["readonly"]),
|
||||
)
|
||||
seed_column_metadata_batch(
|
||||
cursor,
|
||||
table_name=TASK_LOGS_TABLE,
|
||||
columns=TASK_LOGS_COLUMN_METADATA,
|
||||
valid_column_names=[str(column["column_name"]) for column in TASK_LOGS_COLUMN_METADATA],
|
||||
)
|
||||
|
||||
|
||||
def seed_column_metadata_batch(
|
||||
cursor,
|
||||
*,
|
||||
@@ -180,6 +206,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)),
|
||||
@@ -259,7 +286,32 @@ def table_allows_manual_create(cursor, table_name: str) -> bool:
|
||||
|
||||
def init_display_metadata(cursor) -> None:
|
||||
init_display_metadata_tables(cursor)
|
||||
seed_task_logs_display_metadata(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]:
|
||||
|
||||
@@ -12,8 +12,6 @@ from db.display_metadata import (
|
||||
DATETIME_UNIX_SECONDS,
|
||||
METADATA_TABLES,
|
||||
STANDARD_TIMESTAMP_COLUMNS,
|
||||
TASK_LOGS_COLUMN_METADATA,
|
||||
TASK_LOGS_TABLE,
|
||||
iter_user_tables,
|
||||
list_editable_business_columns,
|
||||
list_physical_column_names,
|
||||
@@ -32,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
|
||||
@@ -220,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):
|
||||
@@ -234,6 +235,139 @@ 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 parse_default_sort_json(raw: object) -> tuple[list[dict[str, str]], list[str]]:
|
||||
"""解析 default_sort_json;返回 (specs, errors)。specs 项含 column / direction。"""
|
||||
errors: list[str] = []
|
||||
if raw is None or str(raw).strip() == "":
|
||||
return [], []
|
||||
text = str(raw).strip()
|
||||
try:
|
||||
parsed = json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
return [], ["default_sort_json 不是合法 JSON"]
|
||||
if not isinstance(parsed, list):
|
||||
return [], ["default_sort_json 必须是 JSON 数组"]
|
||||
out: list[dict[str, str]] = []
|
||||
for index, item in enumerate(parsed):
|
||||
if not isinstance(item, dict):
|
||||
errors.append(f"default_sort_json[{index}] 必须是对象")
|
||||
continue
|
||||
column = str(
|
||||
item.get("column")
|
||||
or item.get("columnName")
|
||||
or item.get("column_name")
|
||||
or ""
|
||||
).strip()
|
||||
direction_raw = str(item.get("direction") or "desc").strip().lower()
|
||||
if not column:
|
||||
errors.append(f"default_sort_json[{index}] 缺少 column")
|
||||
continue
|
||||
if direction_raw not in {"asc", "desc"}:
|
||||
errors.append(f"default_sort_json[{index}].direction 只能是 asc 或 desc")
|
||||
continue
|
||||
out.append({"column": column, "direction": direction_raw})
|
||||
return out, errors
|
||||
|
||||
|
||||
def validate_default_sort_metadata(
|
||||
cursor,
|
||||
table_name: str,
|
||||
*,
|
||||
physical_columns: list[str] | None = None,
|
||||
) -> tuple[list[str], list[str]]:
|
||||
"""校验表级 default_sort_json:列须真实存在;允许 visible=0;不存在则报错。
|
||||
|
||||
契约:visible=0 只藏界面,ORDER BY 仍可用非敏感物理列(含隐藏主键)。
|
||||
"""
|
||||
errors: list[str] = []
|
||||
warnings: list[str] = []
|
||||
table_cols = {
|
||||
str(row[1]) for row in cursor.execute("PRAGMA table_info(_jiangchang_tables)").fetchall()
|
||||
}
|
||||
if "default_sort_json" not in table_cols:
|
||||
return [], []
|
||||
cursor.execute(
|
||||
"SELECT default_sort_json FROM _jiangchang_tables WHERE table_name = ?",
|
||||
(table_name,),
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
return [], []
|
||||
specs, parse_errors = parse_default_sort_json(row[0])
|
||||
for msg in parse_errors:
|
||||
errors.append(f"{table_name}: {msg}")
|
||||
if parse_errors:
|
||||
return errors, warnings
|
||||
|
||||
physical = set(physical_columns or list_physical_column_names(cursor, table_name))
|
||||
for spec in specs:
|
||||
column = spec["column"]
|
||||
if column not in physical:
|
||||
errors.append(
|
||||
f"{table_name}: default_sort_json 引用了不存在的列 {column!r}"
|
||||
)
|
||||
continue
|
||||
# visible=0 合法:不报错。仅提示契约,避免作者误删排序列。
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT visible FROM _jiangchang_columns
|
||||
WHERE table_name = ? AND column_name = ?
|
||||
""",
|
||||
(table_name, column),
|
||||
)
|
||||
meta = cursor.fetchone()
|
||||
if meta is not None and meta[0] is not None and int(meta[0]) == 0:
|
||||
warnings.append(
|
||||
f"{table_name}.{column}: default_sort_json 引用了 visible=0 列"
|
||||
"(合法:隐藏列仍可 ORDER BY;见 references/SCHEMA.md)"
|
||||
)
|
||||
return errors, warnings
|
||||
|
||||
|
||||
def validate_column_permissions(
|
||||
cursor,
|
||||
table_name: str,
|
||||
@@ -414,6 +548,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()
|
||||
@@ -427,6 +582,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 = ?",
|
||||
@@ -486,6 +644,17 @@ 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)
|
||||
|
||||
sort_errors, sort_warnings = validate_default_sort_metadata(
|
||||
cur,
|
||||
table_name,
|
||||
physical_columns=physical_columns,
|
||||
)
|
||||
report.errors.extend(sort_errors)
|
||||
report.warnings.extend(sort_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:
|
||||
@@ -516,11 +685,5 @@ def validate_display_metadata(conn: sqlite3.Connection, *, check_column_order: b
|
||||
|
||||
|
||||
def validate_template_database(conn: sqlite3.Connection) -> ValidationReport:
|
||||
report = validate_display_metadata(conn)
|
||||
physical = list_physical_column_names(conn.cursor(), TASK_LOGS_TABLE)
|
||||
expected = [str(item["column_name"]) for item in TASK_LOGS_COLUMN_METADATA]
|
||||
if physical != expected:
|
||||
report.errors.append(
|
||||
f"task_logs 物理字段顺序应为 {expected},当前为 {physical}"
|
||||
)
|
||||
return report
|
||||
"""模板自检入口:校验展示元数据完整性,并扫描业务表 DDL 是否含 SQLite 外键。"""
|
||||
return validate_display_metadata(conn)
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
"""task_logs 表读写模板。
|
||||
|
||||
通用业务日志仓储:记录每次任务执行的 task_type / target / input / 状态 / 结果摘要。
|
||||
不假设任何特定业务(发布、对账、审核等都可复用)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
from db.connection import get_conn, init_db
|
||||
|
||||
|
||||
def save_task_log(
|
||||
task_type: str,
|
||||
target_id: Optional[str] = None,
|
||||
input_id: Optional[str] = None,
|
||||
input_title: Optional[str] = None,
|
||||
status: str = "success",
|
||||
error_msg: Optional[str] = None,
|
||||
result_summary: Optional[str] = None,
|
||||
) -> int:
|
||||
init_db()
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO task_logs
|
||||
(task_type, target_id, input_id, input_title, status, error_msg, result_summary)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(task_type, target_id, input_id, input_title or "", status, error_msg, result_summary),
|
||||
)
|
||||
new_id = int(cur.lastrowid)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
return new_id
|
||||
|
||||
|
||||
def list_task_logs(
|
||||
limit: int,
|
||||
status: Optional[str] = None,
|
||||
task_type: Optional[str] = None,
|
||||
target_id: Optional[str] = None,
|
||||
) -> List[Tuple[Any, ...]]:
|
||||
init_db()
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
sql = (
|
||||
"SELECT id, task_type, target_id, input_id, input_title, status, error_msg, result_summary, "
|
||||
"created_at, updated_at FROM task_logs WHERE 1=1 "
|
||||
)
|
||||
params: List[Any] = []
|
||||
if status:
|
||||
sql += "AND status = ? "
|
||||
params.append(status)
|
||||
if task_type:
|
||||
sql += "AND task_type = ? "
|
||||
params.append(task_type)
|
||||
if target_id:
|
||||
sql += "AND target_id = ? "
|
||||
params.append(target_id)
|
||||
sql += "ORDER BY created_at DESC, id DESC LIMIT ?"
|
||||
params.append(int(limit))
|
||||
cur.execute(sql, tuple(params))
|
||||
return list(cur.fetchall())
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_task_log_by_id(log_id: int) -> Optional[Tuple[Any, ...]]:
|
||||
init_db()
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"SELECT id, task_type, target_id, input_id, input_title, status, error_msg, result_summary, "
|
||||
"created_at, updated_at FROM task_logs WHERE id = ?",
|
||||
(int(log_id),),
|
||||
)
|
||||
return cur.fetchone()
|
||||
finally:
|
||||
conn.close()
|
||||
@@ -1,30 +1,44 @@
|
||||
"""标准时间字段:Unix 秒级时间戳、默认值与 updated_at 自动维护。"""
|
||||
"""标准时间字段:Unix 秒级时间戳、默认值与 updated_at 自动维护。
|
||||
|
||||
业务表若包含 created_at / updated_at,在 CREATE TABLE 后调用
|
||||
``init_updated_at_trigger(cursor, table_name)``。模板默认不建业务表。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
STANDARD_TIMESTAMP_COLUMNS = frozenset({"created_at", "updated_at"})
|
||||
DATETIME_UNIX_SECONDS = "datetime_unix_seconds"
|
||||
TASK_LOGS_UPDATED_AT_TRIGGER = "task_logs_set_updated_at"
|
||||
|
||||
_TASK_LOGS_UPDATED_AT_TRIGGER_DDL = f"""
|
||||
CREATE TRIGGER IF NOT EXISTS {TASK_LOGS_UPDATED_AT_TRIGGER}
|
||||
AFTER UPDATE ON task_logs
|
||||
|
||||
def updated_at_trigger_name(table_name: str) -> str:
|
||||
return f"{table_name}_set_updated_at"
|
||||
|
||||
|
||||
def _quote_identifier(name: str) -> str:
|
||||
return '"' + name.replace('"', '""') + '"'
|
||||
|
||||
|
||||
def init_updated_at_trigger(cursor, table_name: str) -> None:
|
||||
"""幂等创建 {table}_set_updated_at:业务字段 UPDATE 后刷新 updated_at。"""
|
||||
trigger = updated_at_trigger_name(table_name)
|
||||
quoted = _quote_identifier(table_name)
|
||||
quoted_trigger = _quote_identifier(trigger)
|
||||
cursor.execute(
|
||||
f"""
|
||||
CREATE TRIGGER IF NOT EXISTS {quoted_trigger}
|
||||
AFTER UPDATE ON {quoted}
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE task_logs
|
||||
UPDATE {quoted}
|
||||
SET updated_at = unixepoch()
|
||||
WHERE id = NEW.id AND updated_at = OLD.updated_at;
|
||||
END
|
||||
"""
|
||||
|
||||
|
||||
def init_task_logs_timestamp_maintenance(cursor) -> None:
|
||||
"""幂等创建 task_logs.updated_at 自动维护 trigger。"""
|
||||
cursor.execute(_TASK_LOGS_UPDATED_AT_TRIGGER_DDL)
|
||||
)
|
||||
|
||||
|
||||
def has_updated_at_trigger(cursor, table_name: str) -> bool:
|
||||
trigger_name = f"{table_name}_set_updated_at"
|
||||
trigger_name = updated_at_trigger_name(table_name)
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT 1 FROM sqlite_master
|
||||
|
||||
79
scripts/service/html_page_service.py
Normal file
79
scripts/service/html_page_service.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""技能 HTML 快照:写入 `{skill_data}/html/*.html`(宿主扫描预览)。
|
||||
|
||||
契约要点(权威说明见 `references/ACTIONS.md`「HTML 页面报告」):
|
||||
|
||||
- 本模块只写快照。装完就有的壳子页放安装包 `assets/html/pages/*.html`,不要从这里拷过去
|
||||
- 快照目录:仅一层 `html/*.html`(非递归)
|
||||
- 重新生成 Action id 必须为 `render-html`(宿主硬编码;只针对快照)
|
||||
- 页面布局底线见 `references/ACTIONS.md`「宿主预览布局」;模板壳 `assets/html/example-report.html`
|
||||
- 成功后建议 `steps: refreshCatalog` 以重扫侧栏
|
||||
- 本模块只提供路径/模板/原子写;业务 KPI/表格由各技能自行填充
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
from typing import Mapping
|
||||
|
||||
from util.logging_config import get_skill_logger
|
||||
from util.runtime_paths import get_html_dir, get_skill_root
|
||||
|
||||
# Host Data Management looks up this exact Skill Action id for「重新生成」.
|
||||
HOST_HTML_RENDER_ACTION_ID = "render-html"
|
||||
|
||||
_UNSAFE_NAME = re.compile(r"[\\/]|^\.\.|\.\.$")
|
||||
|
||||
|
||||
def _log():
|
||||
try:
|
||||
return get_skill_logger()
|
||||
except RuntimeError:
|
||||
return None
|
||||
|
||||
|
||||
def assert_html_filename(filename: str) -> str:
|
||||
name = (filename or "").strip()
|
||||
if not name.lower().endswith(".html"):
|
||||
raise ValueError(f"html filename must end with .html: {filename!r}")
|
||||
if _UNSAFE_NAME.search(name) or "/" in name or "\\" in name:
|
||||
raise ValueError(f"html filename must be a basename only: {filename!r}")
|
||||
if len(name) > 200:
|
||||
raise ValueError(f"html filename too long: {filename!r}")
|
||||
return name
|
||||
|
||||
|
||||
def html_page_path(filename: str, *, create_dir: bool = True) -> str:
|
||||
return os.path.join(get_html_dir(create=create_dir), assert_html_filename(filename))
|
||||
|
||||
|
||||
def load_html_template(relative_under_skill_root: str) -> str:
|
||||
"""Load UTF-8 template from skill root (e.g. ``assets/html/example-report.html``)."""
|
||||
rel = (relative_under_skill_root or "").strip().replace("\\", "/")
|
||||
if not rel or rel.startswith("/") or ".." in rel.split("/"):
|
||||
raise ValueError(f"invalid template path: {relative_under_skill_root!r}")
|
||||
path = os.path.join(get_skill_root(), *rel.split("/"))
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def render_html_placeholders(template: str, mapping: Mapping[str, str]) -> str:
|
||||
"""Replace ``{{KEY}}`` tokens. Unknown keys are left as-is."""
|
||||
page = template
|
||||
for key, value in mapping.items():
|
||||
page = page.replace("{{" + str(key) + "}}", str(value))
|
||||
return page
|
||||
|
||||
|
||||
def write_html_page(filename: str, html: str) -> str:
|
||||
"""Atomically write UTF-8 HTML under ``html/``. Returns absolute path."""
|
||||
dest = html_page_path(filename, create_dir=True)
|
||||
tmp = dest + ".tmp"
|
||||
text = html if isinstance(html, str) else str(html)
|
||||
with open(tmp, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(text)
|
||||
os.replace(tmp, dest)
|
||||
log = _log()
|
||||
if log:
|
||||
log.info("html_page_written path=%s bytes=%s", dest, len(text.encode("utf-8")))
|
||||
return dest
|
||||
@@ -10,7 +10,7 @@ def build_video_info(
|
||||
compose_context_before: Optional[dict[str, Any]] = None,
|
||||
compose_context_after: Optional[dict[str, Any]] = None,
|
||||
) -> dict[str, Any]:
|
||||
"""将 RpaVideoSession.summary() 转为 task log / CLI 可用的 video 块。"""
|
||||
"""将 RpaVideoSession.summary() 转为 finish() / CLI 可用的 video 块。"""
|
||||
return {
|
||||
"enabled": video_summary.get("enabled", False),
|
||||
"path": video_summary.get("path"),
|
||||
@@ -31,7 +31,7 @@ def merge_video_into_result_summary(
|
||||
payload: dict[str, Any],
|
||||
video_info: Optional[dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
"""把 video artifact 字段写入 result_summary JSON 对象。"""
|
||||
"""把 video artifact 字段写入 finish() 结果对象(video / video_path 等)。"""
|
||||
if not video_info:
|
||||
return payload
|
||||
payload = dict(payload)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""任务编排、日志查询模板。
|
||||
"""任务编排模板。
|
||||
|
||||
编排层:job_context / emit / checkpoint / finish、鉴权、任务日志与错误转换。
|
||||
编排层:job_context / emit / checkpoint / finish、鉴权与错误转换。
|
||||
过程看统一文件日志;进度/终态看任务中心;业务结果写入业务表(有才建)。
|
||||
领域业务请放在 ``service/<domain>_service.py``,由本模块调用;多入口(CLI/Action/cron)复用同一内核。
|
||||
复制后在 cmd_run(或 cmd_sync_*)中接线到真实 domain service,勿按 toolbar/cron/agent 分叉业务。
|
||||
"""
|
||||
@@ -11,12 +12,11 @@ import asyncio
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Optional
|
||||
|
||||
from jiangchang_skill_core import collect_runtime_diagnostics, config, format_runtime_health_lines
|
||||
from jiangchang_skill_core.activity import JobStopped, emit, finish, job_context
|
||||
|
||||
from db import task_logs_repository as tlr
|
||||
from jiangchang_skill_core.rpa.video_session import RpaVideoSession
|
||||
from service.entitlement_service import check_entitlement
|
||||
from service.task_run_support import (
|
||||
@@ -27,7 +27,6 @@ from service.task_run_support import (
|
||||
from util.constants import LOG_LOGGER_NAME, PLATFORM_KIT_MIN_VERSION, SKILL_SLUG, SKILL_VERSION
|
||||
from util.logging_config import get_skill_logger, setup_skill_logging
|
||||
from util.runtime_paths import get_db_path, get_skill_data_dir, get_skill_root
|
||||
from util.timeutil import unix_to_iso
|
||||
from db.connection import init_db
|
||||
|
||||
|
||||
@@ -84,21 +83,6 @@ def cmd_run(target: Optional[str] = None, input_id: Optional[str] = None) -> int
|
||||
task_type,
|
||||
)
|
||||
emit("鉴权失败,已记录诊断信息", type="warn", skill=SKILL_SLUG, stage="auth")
|
||||
tlr.save_task_log(
|
||||
task_type=task_type,
|
||||
target_id=target,
|
||||
input_id=input_id,
|
||||
input_title="模板示例任务",
|
||||
status="failed",
|
||||
error_msg=reason,
|
||||
result_summary=json.dumps({"stage": "auth", "error": reason}, ensure_ascii=False),
|
||||
)
|
||||
log.info(
|
||||
"task_log_saved task_type=%s target_id=%s input_id=%s stage=auth status=failed",
|
||||
task_type,
|
||||
target,
|
||||
input_id,
|
||||
)
|
||||
finish(
|
||||
status="failed",
|
||||
message=reason,
|
||||
@@ -129,21 +113,6 @@ def cmd_run(target: Optional[str] = None, input_id: Optional[str] = None) -> int
|
||||
},
|
||||
video_info,
|
||||
)
|
||||
tlr.save_task_log(
|
||||
task_type=task_type,
|
||||
target_id=target,
|
||||
input_id=input_id,
|
||||
input_title="模板示例任务",
|
||||
status="failed",
|
||||
error_msg="模板仓库未实现真实业务",
|
||||
result_summary=json.dumps(summary_payload, ensure_ascii=False),
|
||||
)
|
||||
log.info(
|
||||
"task_log_saved task_type=%s target_id=%s input_id=%s status=failed",
|
||||
task_type,
|
||||
target,
|
||||
input_id,
|
||||
)
|
||||
|
||||
log.warning(
|
||||
"task_failed task_type=%s stage=demo_finish error_code=TEMPLATE_STUB",
|
||||
@@ -153,10 +122,8 @@ def cmd_run(target: Optional[str] = None, input_id: Optional[str] = None) -> int
|
||||
status="failed",
|
||||
message="模板仓库未实现真实业务",
|
||||
skill=SKILL_SLUG,
|
||||
template_demo=True,
|
||||
target=target,
|
||||
input_id=input_id,
|
||||
error_code="TEMPLATE_STUB",
|
||||
**summary_payload,
|
||||
)
|
||||
print("ERROR:TEMPLATE_STUB 这是模板仓库,请复制后在 scripts/service/task_service.py 中实现 cmd_run 的真实业务逻辑。")
|
||||
return rc
|
||||
@@ -170,32 +137,6 @@ def cmd_run(target: Optional[str] = None, input_id: Optional[str] = None) -> int
|
||||
input_id,
|
||||
)
|
||||
emit("任务失败,已记录诊断信息", type="warn", skill=SKILL_SLUG, stage="run")
|
||||
try:
|
||||
tlr.save_task_log(
|
||||
task_type=task_type,
|
||||
target_id=target,
|
||||
input_id=input_id,
|
||||
input_title="模板示例任务",
|
||||
status="failed",
|
||||
error_msg="任务执行异常,详见统一日志",
|
||||
result_summary=json.dumps(
|
||||
{"stage": "run", "error": "unexpected_exception"},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
)
|
||||
log.info(
|
||||
"task_log_saved task_type=%s target_id=%s input_id=%s stage=run status=failed",
|
||||
task_type,
|
||||
target,
|
||||
input_id,
|
||||
)
|
||||
except Exception:
|
||||
log.exception(
|
||||
"task_log_save_failed task_type=%s target_id=%s input_id=%s",
|
||||
task_type,
|
||||
target,
|
||||
input_id,
|
||||
)
|
||||
finish(
|
||||
status="failed",
|
||||
message="任务执行异常,详见统一日志",
|
||||
@@ -205,67 +146,6 @@ def cmd_run(target: Optional[str] = None, input_id: Optional[str] = None) -> int
|
||||
return 1
|
||||
|
||||
|
||||
def cmd_logs(
|
||||
limit: int = 10,
|
||||
status: Optional[str] = None,
|
||||
task_type: Optional[str] = None,
|
||||
target_id: Optional[str] = None,
|
||||
) -> int:
|
||||
if limit <= 0:
|
||||
limit = 10
|
||||
rows = tlr.list_task_logs(limit, status, task_type, target_id)
|
||||
if not rows:
|
||||
print("暂无任务日志")
|
||||
return 0
|
||||
|
||||
sep_line = "_" * 39
|
||||
for idx, r in enumerate(rows):
|
||||
rid, ttype, tid, iid, ititle, st, err, rsum, cat, uat = r
|
||||
print(f"id:{rid}")
|
||||
print(f"task_type:{ttype or ''}")
|
||||
print(f"target_id:{tid or ''}")
|
||||
print(f"input_id:{iid or ''}")
|
||||
print(f"input_title:{ititle or ''}")
|
||||
print(f"status:{st or ''}")
|
||||
print(f"error_msg:{err or ''}")
|
||||
print(f"result_summary:{rsum or ''}")
|
||||
print(f"created_at:{unix_to_iso(cat) or str(cat or '')}")
|
||||
print(f"updated_at:{unix_to_iso(uat) or str(uat or '')}")
|
||||
if idx != len(rows) - 1:
|
||||
print(sep_line)
|
||||
print()
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_log_get(log_id: str) -> int:
|
||||
if not str(log_id).isdigit():
|
||||
print("❌ log_id 必须是数字")
|
||||
return 1
|
||||
row = tlr.get_task_log_by_id(int(log_id))
|
||||
if not row:
|
||||
print("❌ 没有这条任务日志")
|
||||
return 1
|
||||
rid, ttype, tid, iid, ititle, st, err, rsum, cat, uat = row
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"id": int(rid),
|
||||
"task_type": ttype,
|
||||
"target_id": tid,
|
||||
"input_id": iid,
|
||||
"input_title": ititle,
|
||||
"status": st,
|
||||
"error_msg": err,
|
||||
"result_summary": rsum,
|
||||
"created_at": unix_to_iso(cat),
|
||||
"updated_at": unix_to_iso(uat),
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_config_path() -> int:
|
||||
example_path = os.path.join(get_skill_root(), ".env.example")
|
||||
env_path = config.get_env_file_path() or ""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。"""
|
||||
|
||||
SKILL_SLUG = "your-skill-slug"
|
||||
SKILL_VERSION = "1.0.61"
|
||||
SKILL_VERSION = "1.0.124"
|
||||
LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug"
|
||||
PLATFORM_KIT_MIN_VERSION = "1.2.2"
|
||||
PLATFORM_KIT_MIN_VERSION = "1.2.11"
|
||||
|
||||
@@ -10,18 +10,22 @@ from util.constants import SKILL_SLUG
|
||||
|
||||
_SCRIPTS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
STANDARD_SUBDIR_DOWNLOADS = "downloads"
|
||||
STANDARD_SUBDIR_IMPORTS = "imports"
|
||||
STANDARD_SUBDIR_EXPORTS = "exports"
|
||||
# 用户可见的上行/下行目录(数据管理「输入/输出」文件浏览器)
|
||||
STANDARD_SUBDIR_INPUT = "input"
|
||||
STANDARD_SUBDIR_OUTPUT = "output"
|
||||
|
||||
# 宿主扫描的 HTML 报告页(数据管理侧栏 htmlPages,非 input/output 文件浏览器)
|
||||
STANDARD_SUBDIR_HTML = "html"
|
||||
|
||||
# 内部/可选目录(不上侧栏)
|
||||
STANDARD_SUBDIR_UPLOADS = "uploads"
|
||||
STANDARD_SUBDIR_CACHE = "cache"
|
||||
STANDARD_SUBDIR_TEMP = "temp"
|
||||
STANDARD_SUBDIR_RPA_ARTIFACTS = "rpa-artifacts"
|
||||
STANDARD_SUBDIR_VIDEOS = "videos"
|
||||
|
||||
CONFIG_KEY_DOWNLOAD_DIR = "SKILL_DOWNLOAD_DIR"
|
||||
CONFIG_KEY_IMPORT_DIR = "SKILL_IMPORT_DIR"
|
||||
CONFIG_KEY_EXPORT_DIR = "SKILL_EXPORT_DIR"
|
||||
CONFIG_KEY_INPUT_DIR = "SKILL_INPUT_DIR"
|
||||
CONFIG_KEY_OUTPUT_DIR = "SKILL_OUTPUT_DIR"
|
||||
CONFIG_KEY_UPLOAD_DIR = "SKILL_UPLOAD_DIR"
|
||||
CONFIG_KEY_CACHE_DIR = "SKILL_CACHE_DIR"
|
||||
CONFIG_KEY_TEMP_DIR = "SKILL_TEMP_DIR"
|
||||
@@ -81,27 +85,25 @@ def resolve_data_path(
|
||||
|
||||
|
||||
def resolve_input_path(raw_path: str, *, create_parent: bool = False) -> str:
|
||||
"""解析 CLI 输入路径:绝对路径原样;相对路径相对 {skill_data_dir}/imports/。"""
|
||||
"""解析 CLI 输入路径:绝对路径原样;相对路径相对 {skill_data_dir}/input/。"""
|
||||
text = (raw_path or "").strip()
|
||||
if not text:
|
||||
return text
|
||||
expanded = os.path.expanduser(text)
|
||||
if os.path.isabs(expanded):
|
||||
return os.path.abspath(expanded)
|
||||
imports_dir = get_imports_dir(create=create_parent)
|
||||
return os.path.abspath(os.path.join(imports_dir, os.path.normpath(expanded)))
|
||||
input_dir = get_input_dir(create=create_parent)
|
||||
return os.path.abspath(os.path.join(input_dir, os.path.normpath(expanded)))
|
||||
|
||||
|
||||
def get_downloads_dir(*, create: bool = True) -> str:
|
||||
return resolve_data_path(CONFIG_KEY_DOWNLOAD_DIR, STANDARD_SUBDIR_DOWNLOADS, create=create)
|
||||
def get_input_dir(*, create: bool = True) -> str:
|
||||
"""上行:用户/Agent 放入的待处理文件(清单、CSV、产品表等)。"""
|
||||
return resolve_data_path(CONFIG_KEY_INPUT_DIR, STANDARD_SUBDIR_INPUT, create=create)
|
||||
|
||||
|
||||
def get_imports_dir(*, create: bool = True) -> str:
|
||||
return resolve_data_path(CONFIG_KEY_IMPORT_DIR, STANDARD_SUBDIR_IMPORTS, create=create)
|
||||
|
||||
|
||||
def get_exports_dir(*, create: bool = True) -> str:
|
||||
return resolve_data_path(CONFIG_KEY_EXPORT_DIR, STANDARD_SUBDIR_EXPORTS, create=create)
|
||||
def get_output_dir(*, create: bool = True) -> str:
|
||||
"""下行:技能/宿主交给用户拿走的文件(视频、报表、导出 CSV 等)。"""
|
||||
return resolve_data_path(CONFIG_KEY_OUTPUT_DIR, STANDARD_SUBDIR_OUTPUT, create=create)
|
||||
|
||||
|
||||
def get_uploads_dir(*, create: bool = True) -> str:
|
||||
@@ -131,13 +133,21 @@ def get_videos_dir(*, create: bool = True) -> str:
|
||||
return path
|
||||
|
||||
|
||||
def get_html_dir(*, create: bool = True) -> str:
|
||||
"""HTML 快照目录:宿主扫描 `{skill_data}/html/*.html`(一层、非递归)。壳子页不在这里。"""
|
||||
path = os.path.join(get_skill_data_dir(), STANDARD_SUBDIR_HTML)
|
||||
if create:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def list_resolved_data_paths() -> dict[str, str]:
|
||||
"""供 health / config-path 输出已解析的数据子目录(不创建目录)。"""
|
||||
return {
|
||||
"skill_data_dir": get_skill_data_dir(),
|
||||
"downloads_dir": get_downloads_dir(create=False),
|
||||
"imports_dir": get_imports_dir(create=False),
|
||||
"exports_dir": get_exports_dir(create=False),
|
||||
"input_dir": get_input_dir(create=False),
|
||||
"output_dir": get_output_dir(create=False),
|
||||
"html_dir": get_html_dir(create=False),
|
||||
"uploads_dir": get_uploads_dir(create=False),
|
||||
"cache_dir": get_cache_dir(create=False),
|
||||
"temp_dir": get_temp_dir(create=False),
|
||||
|
||||
@@ -7,7 +7,7 @@ import re
|
||||
|
||||
_KEBAB_SLUG = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
||||
_MAX_SLUG_LENGTH = 48
|
||||
_MIN_SEGMENTS = 3
|
||||
_MIN_SEGMENTS = 2
|
||||
_MAX_SEGMENTS = 5
|
||||
_MAX_NOUN_SEGMENTS = 3
|
||||
|
||||
@@ -60,7 +60,9 @@ _BUILTIN_PLATFORMS = frozenset(
|
||||
"etax",
|
||||
}
|
||||
)
|
||||
_BUILTIN_SCOPES = frozenset({"all", "batch", "multi", "default"})
|
||||
_BUILTIN_SCOPES = frozenset({"all", "batch", "multi"})
|
||||
# 旧技能用 default 占位末段。不再当成合法 scope;校验只警告,避免同步后挡住已发布仓库。
|
||||
_DEPRECATED_PLATFORM_PLACEHOLDER = "default"
|
||||
|
||||
LEGACY_EXEMPT_SLUGS: frozenset[str] = frozenset(
|
||||
{
|
||||
@@ -117,11 +119,12 @@ def _match_platform_suffix(
|
||||
|
||||
def classify_slug_form(segments: list[str], *, slug: str = "") -> str:
|
||||
"""
|
||||
返回: 'legacy_exempt' | 'standard' | 'scope' | 'invalid'
|
||||
返回: 'legacy_exempt' | 'standard' | 'scope' | 'plain' | 'invalid'
|
||||
|
||||
- legacy_exempt: slug in LEGACY_EXEMPT_SLUGS
|
||||
- standard: 末段匹配 platforms, 首段 in verbs, 3<=len<=5
|
||||
- scope: 末段 in scopes, 首段 in verbs, 3<=len<=5
|
||||
- standard: 末段匹配 platforms,首段 in verbs,2<=len<=5,且动词后至少还有一段名词
|
||||
- scope: 末段 in scopes(all/batch/multi),首段 in verbs,至少 verb-noun-scope
|
||||
- plain: 动词 + 名词,无平台、无 scope
|
||||
- invalid: 其他
|
||||
"""
|
||||
if slug in LEGACY_EXEMPT_SLUGS:
|
||||
@@ -138,12 +141,14 @@ def classify_slug_form(segments: list[str], *, slug: str = "") -> str:
|
||||
return "invalid"
|
||||
|
||||
if segments[-1] in scopes:
|
||||
if len(segments) < 3:
|
||||
return "invalid"
|
||||
return "scope"
|
||||
|
||||
if _match_platform_suffix(segments, platforms) is not None:
|
||||
return "standard"
|
||||
|
||||
return "invalid"
|
||||
return "plain"
|
||||
|
||||
|
||||
def validate_slug_semantics(
|
||||
@@ -173,8 +178,8 @@ def validate_slug_semantics(
|
||||
|
||||
if segment_count < _MIN_SEGMENTS:
|
||||
msg = (
|
||||
f"slug 段数应为 {_MIN_SEGMENTS}~{_MAX_SEGMENTS}(verb-noun-platform),"
|
||||
f"当前为 {segment_count} 段:{slug!r};新技能禁止 2 段无平台形态(如 reconcile-finance)"
|
||||
f"slug 段数应为 {_MIN_SEGMENTS}~{_MAX_SEGMENTS}(动词-名词,平台可选),"
|
||||
f"当前为 {segment_count} 段:{slug!r}"
|
||||
)
|
||||
(errors if strict else warnings).append(msg)
|
||||
return errors, warnings
|
||||
@@ -197,9 +202,6 @@ def validate_slug_semantics(
|
||||
|
||||
form = classify_slug_form(segments, slug=slug)
|
||||
if form == "scope":
|
||||
if segments[-1] not in scopes:
|
||||
msg = f"slug 末段应为 scope 白名单中的词:{segments[-1]!r}(slug={slug!r})"
|
||||
(errors if strict else warnings).append(msg)
|
||||
noun_count = segment_count - 2
|
||||
elif form == "standard":
|
||||
platform_match = _match_platform_suffix(segments, platforms)
|
||||
@@ -210,13 +212,17 @@ def validate_slug_semantics(
|
||||
else:
|
||||
_platform_name, platform_part_count = platform_match
|
||||
noun_count = segment_count - 1 - platform_part_count
|
||||
elif form == "plain":
|
||||
noun_count = segment_count - 1
|
||||
if segments[-1] == _DEPRECATED_PLATFORM_PLACEHOLDER:
|
||||
warnings.append(
|
||||
"slug 末段 default 已不再作为平台占位;没有外部平台时直接省略。"
|
||||
f"已发布的技能不用改名:{slug!r}"
|
||||
)
|
||||
else:
|
||||
if segment_count >= _MIN_SEGMENTS:
|
||||
if segments[-1] not in scopes:
|
||||
platform_match = _match_platform_suffix(segments, platforms)
|
||||
if platform_match is None:
|
||||
msg = f"slug 末段应匹配平台或 scope 白名单:{slug!r}"
|
||||
(errors if strict else warnings).append(msg)
|
||||
if segments and segments[-1] in scopes and segment_count < 3:
|
||||
msg = f"scope 型 slug 至少为动词-名词-scope:{slug!r}"
|
||||
(errors if strict else warnings).append(msg)
|
||||
noun_count = 0
|
||||
|
||||
if noun_count > _MAX_NOUN_SEGMENTS:
|
||||
|
||||
668
sync.ps1
Normal file
668
sync.ps1
Normal file
@@ -0,0 +1,668 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Sync this skill's plaintext source into the local OpenClaw skills directory.
|
||||
|
||||
.DESCRIPTION
|
||||
Local development only. Does not commit, tag, push, or trigger Gitea CI.
|
||||
release.ps1 remains the formal publish path.
|
||||
Ships in skill-template beside release.ps1. Scaffold copies this file into each new skill.
|
||||
|
||||
Mirrors this repo onto ~/.openclaw/skills/<slug> and removes marketplace
|
||||
pyarmor_runtime_* leftovers. Stop any running job for this skill first;
|
||||
otherwise the copy can fail because files are in use.
|
||||
|
||||
Dependencies follow the host skill:direct-install path:
|
||||
uv pip install --python <JIANGCHANG_DATA_ROOT>/python-runtime/.venv/Scripts/python.exe -r requirements.txt
|
||||
Packages are not installed into system Python. Unchanged requirements that
|
||||
are already present in the shared venv are skipped.
|
||||
|
||||
platform_kit_min_version is provided by jiangchang-platform-kit in the shared
|
||||
runtime. This script only checks it. It does not upgrade the kit. If the
|
||||
installed kit is too old, install or update the skill once from the market,
|
||||
or upgrade the host.
|
||||
|
||||
After the copy, this script runs scripts\main.py init-db the same way the
|
||||
host does after a market install. The user id is read from
|
||||
%APPDATA%\jiangchang\settings.json (jiangchangUserId), which the host writes
|
||||
on login and logout. It is never hardcoded. An empty id or _anon stops the
|
||||
script so the anonymous data folder is not initialized by mistake.
|
||||
-SkipInitDb skips that step. -SkipDeps still runs init-db.
|
||||
|
||||
No Chinese literals in this file: the skill repo requires UTF-8 without BOM,
|
||||
and Windows PowerShell 5.1 would mis-parse a no-BOM UTF-8 script.
|
||||
|
||||
.EXAMPLE
|
||||
.\sync.ps1
|
||||
.\sync.ps1 -DryRun
|
||||
.\sync.ps1 -ForceDeps
|
||||
.\sync.ps1 -SkipDeps
|
||||
.\sync.ps1 -SkipInitDb
|
||||
.\sync.ps1 -DataRoot D:\jiangchang-data
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Destination,
|
||||
[string]$DataRoot,
|
||||
[switch]$SkipDeps,
|
||||
[switch]$ForceDeps,
|
||||
[switch]$SkipInitDb,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$SourceRoot = $PSScriptRoot
|
||||
$PypiMirrors = @(
|
||||
@{ Name = "aliyun"; Url = "https://mirrors.aliyun.com/pypi/simple/" },
|
||||
@{ Name = "tuna"; Url = "https://pypi.tuna.tsinghua.edu.cn/simple/" },
|
||||
@{ Name = "tencent"; Url = "https://mirrors.cloud.tencent.com/pypi/simple/" },
|
||||
@{ Name = "huawei"; Url = "https://repo.huaweicloud.com/repository/pypi/simple/" },
|
||||
@{ Name = "pypi"; Url = "https://pypi.org/simple/" }
|
||||
)
|
||||
|
||||
function Read-Utf8Text {
|
||||
param([Parameter(Mandatory = $true)][string]$Path)
|
||||
return [System.IO.File]::ReadAllText($Path)
|
||||
}
|
||||
|
||||
function Get-SkillFrontmatter {
|
||||
param([Parameter(Mandatory = $true)][string]$SkillMdPath)
|
||||
$raw = Read-Utf8Text -Path $SkillMdPath
|
||||
if ($raw -notmatch '(?s)\A---\s*\r?\n(.*?)\r?\n---') {
|
||||
throw "SKILL.md is missing frontmatter: $SkillMdPath"
|
||||
}
|
||||
return $Matches[1]
|
||||
}
|
||||
|
||||
function Get-FrontmatterScalar {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Body,
|
||||
[Parameter(Mandatory = $true)][string]$Key
|
||||
)
|
||||
$pattern = '(?m)^\s*' + [regex]::Escape($Key) + ':\s*["'']?([^\s"''#]+)["'']?\s*$'
|
||||
$match = [regex]::Match($Body, $pattern)
|
||||
if (-not $match.Success) {
|
||||
return $null
|
||||
}
|
||||
return $match.Groups[1].Value.Trim()
|
||||
}
|
||||
|
||||
function Get-RequirementsLines {
|
||||
param([Parameter(Mandatory = $true)][string]$Content)
|
||||
$lines = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($raw in ($Content -split '\r?\n')) {
|
||||
$trimmed = $raw.Trim()
|
||||
if (-not $trimmed -or $trimmed.StartsWith('#')) {
|
||||
continue
|
||||
}
|
||||
$withoutInline = ($trimmed -split '\s+#', 2)[0].Trim()
|
||||
if ($withoutInline) {
|
||||
$lines.Add($withoutInline)
|
||||
}
|
||||
}
|
||||
return @($lines)
|
||||
}
|
||||
|
||||
function Get-RequirementsHash {
|
||||
param([Parameter(Mandatory = $true)][string]$Content)
|
||||
$normalized = (@(Get-RequirementsLines -Content $Content)) -join "`n"
|
||||
$sha = [System.Security.Cryptography.SHA256]::Create()
|
||||
try {
|
||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes($normalized)
|
||||
$hash = $sha.ComputeHash($bytes)
|
||||
} finally {
|
||||
$sha.Dispose()
|
||||
}
|
||||
return ([System.BitConverter]::ToString($hash) -replace '-', '').ToLowerInvariant()
|
||||
}
|
||||
|
||||
function Get-DistributionNames {
|
||||
param([Parameter(Mandatory = $true)][string[]]$Lines)
|
||||
$names = New-Object System.Collections.Generic.List[string]
|
||||
$seen = New-Object 'System.Collections.Generic.HashSet[string]'
|
||||
foreach ($line in $Lines) {
|
||||
$trimmed = $line.Trim()
|
||||
if (-not $trimmed -or $trimmed.StartsWith('#') -or $trimmed.StartsWith('-')) {
|
||||
continue
|
||||
}
|
||||
if ($trimmed -match '^(https?:|git\+|svn\+|hg\+|file:)') {
|
||||
continue
|
||||
}
|
||||
$match = [regex]::Match($trimmed, '^([A-Za-z0-9][A-Za-z0-9._-]*)')
|
||||
if (-not $match.Success) {
|
||||
continue
|
||||
}
|
||||
$name = $match.Groups[1].Value
|
||||
$key = $name.ToLowerInvariant().Replace('_', '-')
|
||||
if ($seen.Add($key)) {
|
||||
$names.Add($name)
|
||||
}
|
||||
}
|
||||
return @($names)
|
||||
}
|
||||
|
||||
function Resolve-SkillDataRoot {
|
||||
param([string]$Explicit)
|
||||
if ($Explicit -and $Explicit.Trim()) {
|
||||
return $Explicit.Trim().TrimEnd('\')
|
||||
}
|
||||
|
||||
$settingsPath = Join-Path $env:APPDATA "jiangchang\settings.json"
|
||||
if (Test-Path -LiteralPath $settingsPath) {
|
||||
try {
|
||||
$settings = Read-Utf8Text -Path $settingsPath | ConvertFrom-Json
|
||||
$fromSettings = [string]$settings.jiangchangSkillDataRoot
|
||||
if ($fromSettings -and $fromSettings.Trim()) {
|
||||
return $fromSettings.Trim().TrimEnd('\')
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Could not read $settingsPath. Falling back to the default data root. $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($candidate in @("D:\jiangchang-data", (Join-Path $env:USERPROFILE ".jiangchang-data"))) {
|
||||
$python = Join-Path $candidate "python-runtime\.venv\Scripts\python.exe"
|
||||
if (Test-Path -LiteralPath $python) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
|
||||
return "D:\jiangchang-data"
|
||||
}
|
||||
|
||||
function Resolve-JiangchangUserId {
|
||||
$settingsPath = Join-Path $env:APPDATA "jiangchang\settings.json"
|
||||
if (-not (Test-Path -LiteralPath $settingsPath)) {
|
||||
throw "Jiangchang settings were not found: $settingsPath`nOpen Jiangchang and log in, then run this script again."
|
||||
}
|
||||
try {
|
||||
$settings = Read-Utf8Text -Path $settingsPath | ConvertFrom-Json
|
||||
} catch {
|
||||
throw "Could not read the logged-in user from $settingsPath. $($_.Exception.Message)"
|
||||
}
|
||||
$userId = ""
|
||||
if ($null -ne $settings -and $settings.PSObject.Properties.Name -contains "jiangchangUserId" -and $null -ne $settings.jiangchangUserId) {
|
||||
$userId = ([string]$settings.jiangchangUserId).Trim()
|
||||
}
|
||||
if (-not $userId -or $userId -eq "_anon") {
|
||||
throw "Jiangchang has no logged-in user. Open Jiangchang and log in, then run this script again. This script does not guess a user id."
|
||||
}
|
||||
$normalized = $userId.Replace("\", "/")
|
||||
if ($normalized.Contains("..") -or $normalized.Contains("/") -or $userId.StartsWith("\") -or $userId -match '^[A-Za-z]:') {
|
||||
throw "jiangchangUserId is not a single folder name: $userId"
|
||||
}
|
||||
return $userId
|
||||
}
|
||||
|
||||
function Test-InitDbJsonOk {
|
||||
param([Parameter(Mandatory = $true)][string]$Text)
|
||||
foreach ($line in ($Text -split "`r?`n")) {
|
||||
$trimmed = $line.Trim()
|
||||
if (-not $trimmed.StartsWith("{")) {
|
||||
continue
|
||||
}
|
||||
try {
|
||||
$parsed = $trimmed | ConvertFrom-Json
|
||||
if ($parsed.ok -eq $true) {
|
||||
return $true
|
||||
}
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Test-InitDbUnknownCommand {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Text,
|
||||
[Parameter(Mandatory = $true)][int]$ExitCode
|
||||
)
|
||||
if ($ExitCode -eq 2) {
|
||||
return $true
|
||||
}
|
||||
$lower = $Text.ToLowerInvariant()
|
||||
if ($lower.Contains("invalid choice") -or $lower.Contains("unrecognized arguments") -or $lower.Contains("unknown command")) {
|
||||
return $true
|
||||
}
|
||||
if ($lower.Contains("invalid") -and $lower.Contains("init-db")) {
|
||||
return $true
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Invoke-SkillInitDb {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$PythonExe,
|
||||
[Parameter(Mandatory = $true)][string]$SkillDir,
|
||||
[Parameter(Mandatory = $true)][string]$DataRoot,
|
||||
[Parameter(Mandatory = $true)][string]$UserId,
|
||||
[switch]$DryRun
|
||||
)
|
||||
$mainPy = Join-Path $SkillDir "scripts\main.py"
|
||||
if (-not (Test-Path -LiteralPath $mainPy)) {
|
||||
Write-Host "No scripts\main.py. Skipping init-db."
|
||||
return
|
||||
}
|
||||
if ($DryRun) {
|
||||
Write-Host "DryRun: would run init-db" -ForegroundColor Yellow
|
||||
Write-Host " python $PythonExe"
|
||||
Write-Host " script $mainPy init-db"
|
||||
Write-Host " data $DataRoot"
|
||||
Write-Host " user $UserId"
|
||||
return
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $PythonExe)) {
|
||||
throw "Shared Python was not found: $PythonExe`nOpen Jiangchang and wait for the shared runtime, or pass -DataRoot."
|
||||
}
|
||||
|
||||
Write-Host ">> Initialize the logged-in user's database (init-db)" -ForegroundColor DarkGray
|
||||
Write-Host " user=$UserId"
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = $PythonExe
|
||||
$psi.WorkingDirectory = $SkillDir
|
||||
$psi.UseShellExecute = $false
|
||||
$psi.RedirectStandardOutput = $true
|
||||
$psi.RedirectStandardError = $true
|
||||
$psi.CreateNoWindow = $true
|
||||
$psi.Arguments = '"' + $mainPy + '" init-db'
|
||||
$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
|
||||
$psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8
|
||||
$psi.EnvironmentVariables["JIANGCHANG_DATA_ROOT"] = $DataRoot
|
||||
$psi.EnvironmentVariables["JIANGCHANG_USER_ID"] = $UserId
|
||||
$psi.EnvironmentVariables["JIANGCHANG_RUN_MODE"] = "cli"
|
||||
$psi.EnvironmentVariables["PYTHONIOENCODING"] = "utf-8"
|
||||
|
||||
$proc = New-Object System.Diagnostics.Process
|
||||
$proc.StartInfo = $psi
|
||||
[void]$proc.Start()
|
||||
$stdoutTask = $proc.StandardOutput.ReadToEndAsync()
|
||||
$stderrTask = $proc.StandardError.ReadToEndAsync()
|
||||
if (-not $proc.WaitForExit(60000)) {
|
||||
try { $proc.Kill() } catch { }
|
||||
throw "init-db timed out after 60 seconds."
|
||||
}
|
||||
$proc.WaitForExit()
|
||||
$stdout = $stdoutTask.Result
|
||||
$stderr = $stderrTask.Result
|
||||
$combined = ($stdout + "`n" + $stderr).Trim()
|
||||
if ($stdout.Trim()) {
|
||||
Write-Host $stdout.Trim()
|
||||
}
|
||||
if ($stderr.Trim()) {
|
||||
Write-Host $stderr.Trim()
|
||||
}
|
||||
|
||||
if (Test-InitDbJsonOk -Text $combined) {
|
||||
Write-Host "init-db finished for user $UserId." -ForegroundColor Green
|
||||
return
|
||||
}
|
||||
if ($proc.ExitCode -eq 0 -and $combined -match '(?m)^OK:\s*INIT_DB\b') {
|
||||
Write-Host "init-db finished for user $UserId." -ForegroundColor Green
|
||||
return
|
||||
}
|
||||
if (Test-InitDbUnknownCommand -Text $combined -ExitCode $proc.ExitCode) {
|
||||
Write-Warning "This skill has no init-db command. Database initialization was skipped."
|
||||
return
|
||||
}
|
||||
|
||||
$detail = $stderr.Trim()
|
||||
if (-not $detail) {
|
||||
$detail = $stdout.Trim()
|
||||
}
|
||||
if ($detail.Length -gt 500) {
|
||||
$detail = $detail.Substring($detail.Length - 500)
|
||||
}
|
||||
if (-not $detail) {
|
||||
$detail = "init-db exited with code $($proc.ExitCode)"
|
||||
}
|
||||
throw "init-db failed for user ${UserId}: $detail"
|
||||
}
|
||||
|
||||
function Resolve-UvExecutable {
|
||||
$cmd = Get-Command uv -ErrorAction SilentlyContinue
|
||||
if ($cmd -and $cmd.Source) {
|
||||
return $cmd.Source
|
||||
}
|
||||
throw "uv was not found on PATH. The host installs skill requirements with uv pip, not pip."
|
||||
}
|
||||
|
||||
function Test-DistributionsPresent {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$PythonExe,
|
||||
[Parameter(Mandatory = $true)][string[]]$Names
|
||||
)
|
||||
if ($Names.Count -eq 0) {
|
||||
return $false
|
||||
}
|
||||
$probe = @'
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
import sys
|
||||
missing = []
|
||||
for n in sys.argv[1:]:
|
||||
try:
|
||||
version(n)
|
||||
except PackageNotFoundError:
|
||||
missing.append(n)
|
||||
sys.stderr.write("missing:" + ",".join(missing) if missing else "")
|
||||
raise SystemExit(1 if missing else 0)
|
||||
'@
|
||||
$prev = $ErrorActionPreference
|
||||
$ErrorActionPreference = "Continue"
|
||||
try {
|
||||
& $PythonExe -c $probe @Names 2>&1 | Out-Null
|
||||
return ($LASTEXITCODE -eq 0)
|
||||
} finally {
|
||||
$ErrorActionPreference = $prev
|
||||
}
|
||||
}
|
||||
|
||||
function Get-InstalledDistributionVersion {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$PythonExe,
|
||||
[Parameter(Mandatory = $true)][string]$Name
|
||||
)
|
||||
$prev = $ErrorActionPreference
|
||||
$ErrorActionPreference = "Continue"
|
||||
try {
|
||||
$output = & $PythonExe -c "from importlib.metadata import version, PackageNotFoundError`nimport sys`nname=sys.argv[1]`ntry:`n print(version(name))`nexcept PackageNotFoundError:`n print('')" $Name 2>$null
|
||||
$code = $LASTEXITCODE
|
||||
} finally {
|
||||
$ErrorActionPreference = $prev
|
||||
}
|
||||
if ($code -ne 0) {
|
||||
return $null
|
||||
}
|
||||
return ([string]$output).Trim()
|
||||
}
|
||||
|
||||
function Compare-VersionPrefix {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Installed,
|
||||
[Parameter(Mandatory = $true)][string]$Required
|
||||
)
|
||||
function Convert-ToVersion([string]$Text) {
|
||||
$core = ($Text -split '[^0-9.]', 2)[0].Trim('.')
|
||||
$parts = @($core -split '\.' | Where-Object { $_ -ne '' })
|
||||
while ($parts.Count -lt 3) {
|
||||
$parts += '0'
|
||||
}
|
||||
$take = @($parts | Select-Object -First 3)
|
||||
return [version]($take -join '.')
|
||||
}
|
||||
try {
|
||||
return (Convert-ToVersion $Installed) -ge (Convert-ToVersion $Required)
|
||||
} catch {
|
||||
return $true
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-RobocopyMirror {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Source,
|
||||
[Parameter(Mandatory = $true)][string]$Dest,
|
||||
[switch]$ListOnly
|
||||
)
|
||||
$excludeDirs = @(
|
||||
".git", ".github", "__pycache__", ".pytest_cache", "htmlcov",
|
||||
"dist", "build", ".venv", "_fix_workspace", ".skill-requirements"
|
||||
)
|
||||
$excludeFiles = @(".env", "*.env.local", "*.pyc", "*.pyo")
|
||||
$robocopyArgs = @($Source, $Dest, "/MIR", "/R:2", "/W:1", "/XD") + $excludeDirs + @("/XF") + $excludeFiles
|
||||
if ($ListOnly) {
|
||||
$robocopyArgs += "/L"
|
||||
}
|
||||
& robocopy.exe @robocopyArgs
|
||||
$code = $LASTEXITCODE
|
||||
if ($code -ge 8) {
|
||||
throw "Copy failed (robocopy exit $code). Stop the running skill job in Task Center, then retry."
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-PyarmorRuntimes {
|
||||
param([Parameter(Mandatory = $true)][string]$Dest)
|
||||
$removed = @()
|
||||
$dirs = @(Get-ChildItem -LiteralPath $Dest -Recurse -Directory -Force -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -like "pyarmor_runtime_*" })
|
||||
foreach ($dir in $dirs) {
|
||||
Remove-Item -LiteralPath $dir.FullName -Recurse -Force
|
||||
$removed += $dir.FullName
|
||||
}
|
||||
return $removed
|
||||
}
|
||||
|
||||
function Assert-PlainMainPy {
|
||||
param([Parameter(Mandatory = $true)][string]$MainPy)
|
||||
if (-not (Test-Path -LiteralPath $MainPy)) {
|
||||
throw "scripts\main.py is missing after sync: $MainPy"
|
||||
}
|
||||
$head = Read-Utf8Text -Path $MainPy
|
||||
if ($head -match 'pyarmor_runtime_|__pyarmor__') {
|
||||
throw "scripts\main.py is still PyArmor-encrypted. Re-run .\sync.ps1 so the whole scripts tree is replaced."
|
||||
}
|
||||
}
|
||||
|
||||
function Write-RequirementsState {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$RuntimeDir,
|
||||
[Parameter(Mandatory = $true)][string]$Slug,
|
||||
[Parameter(Mandatory = $true)][string]$RequirementsPath,
|
||||
[Parameter(Mandatory = $true)][string]$RequirementsHash,
|
||||
[string]$RuntimeFingerprint
|
||||
)
|
||||
$stateDir = Join-Path $RuntimeDir ".skill-requirements"
|
||||
New-Item -ItemType Directory -Force -Path $stateDir | Out-Null
|
||||
$state = [ordered]@{
|
||||
skill = $Slug
|
||||
requirementsPath = $RequirementsPath
|
||||
requirementsHash = $RequirementsHash
|
||||
runtimeFingerprint = $RuntimeFingerprint
|
||||
installedAt = [DateTime]::UtcNow.ToString("o")
|
||||
status = "ok"
|
||||
}
|
||||
$json = ($state | ConvertTo-Json -Depth 4) + "`n"
|
||||
$statePath = Join-Path $stateDir "$Slug.json"
|
||||
$utf8 = New-Object System.Text.UTF8Encoding $false
|
||||
[System.IO.File]::WriteAllText($statePath, $json, $utf8)
|
||||
}
|
||||
|
||||
$skillMd = Join-Path $SourceRoot "SKILL.md"
|
||||
$sourceMain = Join-Path $SourceRoot "scripts\main.py"
|
||||
$sourceReq = Join-Path $SourceRoot "requirements.txt"
|
||||
if (-not (Test-Path -LiteralPath $skillMd)) {
|
||||
throw "SKILL.md not found: $skillMd"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $sourceMain)) {
|
||||
throw "scripts\main.py not found: $sourceMain"
|
||||
}
|
||||
|
||||
$frontmatter = Get-SkillFrontmatter -SkillMdPath $skillMd
|
||||
$slug = Get-FrontmatterScalar -Body $frontmatter -Key "slug"
|
||||
$version = Get-FrontmatterScalar -Body $frontmatter -Key "version"
|
||||
$platformKitMin = Get-FrontmatterScalar -Body $frontmatter -Key "platform_kit_min_version"
|
||||
if (-not $slug) {
|
||||
throw "metadata.openclaw.slug in SKILL.md is empty, so the install directory cannot be determined."
|
||||
}
|
||||
|
||||
if (-not $Destination -or -not $Destination.Trim()) {
|
||||
$Destination = Join-Path $env:USERPROFILE ".openclaw\skills\$slug"
|
||||
}
|
||||
$Destination = $Destination.Trim().TrimEnd('\')
|
||||
|
||||
$sourceFull = [System.IO.Path]::GetFullPath($SourceRoot)
|
||||
$destFull = [System.IO.Path]::GetFullPath($Destination)
|
||||
if ($sourceFull.TrimEnd('\') -eq $destFull.TrimEnd('\')) {
|
||||
throw "Source and destination are the same directory: $sourceFull"
|
||||
}
|
||||
|
||||
$resolvedDataRoot = Resolve-SkillDataRoot -Explicit $DataRoot
|
||||
$runtimeDir = Join-Path $resolvedDataRoot "python-runtime"
|
||||
$pythonExe = Join-Path $runtimeDir ".venv\Scripts\python.exe"
|
||||
$resolvedUserId = $null
|
||||
if (-not $SkipInitDb) {
|
||||
$resolvedUserId = Resolve-JiangchangUserId
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Local skill sync (does not publish to the skill market)" -ForegroundColor Cyan
|
||||
Write-Host " source $sourceFull"
|
||||
Write-Host " dest $destFull"
|
||||
Write-Host " slug $slug"
|
||||
Write-Host " version $(if ($version) { $version } else { '(none)' })"
|
||||
Write-Host " data $resolvedDataRoot"
|
||||
Write-Host " python $pythonExe"
|
||||
Write-Host " user $(if ($SkipInitDb) { '(init-db skipped)' } else { $resolvedUserId })"
|
||||
if ($DryRun) {
|
||||
Write-Host " mode DryRun (no writes)" -ForegroundColor Yellow
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
if (-not (Test-Path -LiteralPath $destFull)) {
|
||||
Write-Warning "Destination does not exist yet. Files can be created, but if this skill was never installed from the market the host may not have enabled it. Install it once, then use this script to overwrite."
|
||||
}
|
||||
|
||||
$configPath = Join-Path $env:USERPROFILE ".openclaw\openclaw.json"
|
||||
if (Test-Path -LiteralPath $configPath) {
|
||||
try {
|
||||
$config = Read-Utf8Text -Path $configPath | ConvertFrom-Json
|
||||
$entry = $null
|
||||
if ($config.PSObject.Properties.Name -contains "skills" -and $null -ne $config.skills -and $config.skills.PSObject.Properties.Name -contains "entries" -and $null -ne $config.skills.entries) {
|
||||
$entryProp = $config.skills.entries.PSObject.Properties[$slug]
|
||||
if ($entryProp) {
|
||||
$entry = $entryProp.Value
|
||||
}
|
||||
}
|
||||
if ($null -eq $entry) {
|
||||
Write-Warning "openclaw.json has no skills.entries.$slug. Install the skill once from the market, or chat and data management may not see it."
|
||||
} elseif ($entry.PSObject.Properties.Name -contains "enabled" -and $entry.enabled -eq $false) {
|
||||
Write-Warning "skills.entries.$slug is disabled. Syncing files does not re-enable it."
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Could not check openclaw.json enablement: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ">> Mirror source onto the install directory (exclude .git, drop encrypted runtime)" -ForegroundColor DarkGray
|
||||
Invoke-RobocopyMirror -Source $sourceFull -Dest $destFull -ListOnly:$DryRun
|
||||
if (-not $DryRun) {
|
||||
$removed = @(Remove-PyarmorRuntimes -Dest $destFull)
|
||||
foreach ($path in $removed) {
|
||||
Write-Host " removed $path"
|
||||
}
|
||||
Assert-PlainMainPy -MainPy (Join-Path $destFull "scripts\main.py")
|
||||
Write-Host "scripts\main.py is plaintext." -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "DryRun: pyarmor_runtime_* was not deleted, and no destination file was changed." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
$skipInstall = $false
|
||||
if ($SkipDeps) {
|
||||
Write-Host "Skipped dependency install (-SkipDeps)." -ForegroundColor Yellow
|
||||
$skipInstall = $true
|
||||
} elseif (-not (Test-Path -LiteralPath $pythonExe)) {
|
||||
throw "Shared Python was not found: $pythonExe`nOpen Jiangchang and wait for the shared runtime, or pass -DataRoot. If the copy already finished, install dependencies before running the skill."
|
||||
} else {
|
||||
$reqPath = if ($DryRun) { $sourceReq } else { Join-Path $destFull "requirements.txt" }
|
||||
if (-not (Test-Path -LiteralPath $reqPath)) {
|
||||
Write-Host "No requirements.txt. Skipping dependency install."
|
||||
$skipInstall = $true
|
||||
} else {
|
||||
$reqContent = Read-Utf8Text -Path $reqPath
|
||||
$reqLines = @(Get-RequirementsLines -Content $reqContent)
|
||||
if ($reqLines.Count -eq 0) {
|
||||
Write-Host "requirements.txt has no effective lines. Skipping install."
|
||||
$skipInstall = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $skipInstall) {
|
||||
|
||||
$reqHash = Get-RequirementsHash -Content $reqContent
|
||||
$stampPath = Join-Path $runtimeDir ".jiangchang-runtime-stamp.json"
|
||||
$runtimeFingerprint = ""
|
||||
if (Test-Path -LiteralPath $stampPath) {
|
||||
try {
|
||||
$stamp = Read-Utf8Text -Path $stampPath | ConvertFrom-Json
|
||||
$runtimeFingerprint = [string]$stamp.fingerprint
|
||||
} catch {
|
||||
$runtimeFingerprint = ""
|
||||
}
|
||||
}
|
||||
|
||||
$statePath = Join-Path $runtimeDir ".skill-requirements\$slug.json"
|
||||
$previousHash = ""
|
||||
$previousFingerprint = ""
|
||||
$previousStatus = ""
|
||||
if (Test-Path -LiteralPath $statePath) {
|
||||
try {
|
||||
$previous = Read-Utf8Text -Path $statePath | ConvertFrom-Json
|
||||
$previousHash = [string]$previous.requirementsHash
|
||||
$previousFingerprint = [string]$previous.runtimeFingerprint
|
||||
$previousStatus = [string]$previous.status
|
||||
} catch {
|
||||
$previousStatus = ""
|
||||
}
|
||||
}
|
||||
|
||||
$distributionNames = @(Get-DistributionNames -Lines $reqLines)
|
||||
$unchanged = (
|
||||
-not $ForceDeps -and
|
||||
$previousStatus -eq "ok" -and
|
||||
$previousHash -eq $reqHash -and
|
||||
$runtimeFingerprint -and
|
||||
$previousFingerprint -eq $runtimeFingerprint
|
||||
)
|
||||
if ($unchanged -and $distributionNames.Count -gt 0 -and (Test-DistributionsPresent -PythonExe $pythonExe -Names $distributionNames)) {
|
||||
Write-Host "Requirements unchanged and already present in the shared environment. Skipping uv pip install." -ForegroundColor Green
|
||||
} elseif ($DryRun) {
|
||||
Write-Host "DryRun: would run uv pip install -r requirements.txt" -ForegroundColor Yellow
|
||||
$distributionNames | ForEach-Object { Write-Host " - $_" }
|
||||
} else {
|
||||
$uv = Resolve-UvExecutable
|
||||
Write-Host ">> Install skill requirements into the shared environment" -ForegroundColor DarkGray
|
||||
Write-Host " uv=$uv"
|
||||
$installed = $false
|
||||
foreach ($mirror in $PypiMirrors) {
|
||||
Write-Host ">> uv pip install --index-url $($mirror.Url)" -ForegroundColor DarkGray
|
||||
& $uv pip install --index-url $mirror.Url --python $pythonExe -r $reqPath
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
$installed = $true
|
||||
break
|
||||
}
|
||||
Write-Warning "Mirror $($mirror.Name) failed (exit $LASTEXITCODE). Trying the next one."
|
||||
}
|
||||
if (-not $installed) {
|
||||
throw "requirements.txt install failed. Code is already at $destFull, but the shared environment is missing dependencies."
|
||||
}
|
||||
Write-RequirementsState -RuntimeDir $runtimeDir -Slug $slug -RequirementsPath $reqPath -RequirementsHash $reqHash -RuntimeFingerprint $runtimeFingerprint
|
||||
Write-Host "Dependencies are installed in the shared environment." -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
if ($platformKitMin -and (Test-Path -LiteralPath $pythonExe)) {
|
||||
$kitVersion = Get-InstalledDistributionVersion -PythonExe $pythonExe -Name "jiangchang-platform-kit"
|
||||
if (-not $kitVersion) {
|
||||
Write-Warning "jiangchang-platform-kit is not installed in the shared environment, but this skill requires >= $platformKitMin."
|
||||
} elseif (-not (Compare-VersionPrefix -Installed $kitVersion -Required $platformKitMin)) {
|
||||
Write-Warning "jiangchang-platform-kit $kitVersion is below this skill's requirement >= $platformKitMin. This script does not upgrade the platform kit."
|
||||
} else {
|
||||
Write-Host "platform kit $kitVersion satisfies >= $platformKitMin." -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
if ($SkipInitDb) {
|
||||
Write-Host "Skipped init-db (-SkipInitDb). The database was not updated." -ForegroundColor Yellow
|
||||
} else {
|
||||
$initSkillDir = $destFull
|
||||
if ($DryRun -and -not (Test-Path -LiteralPath (Join-Path $destFull "scripts\main.py"))) {
|
||||
$initSkillDir = $sourceFull
|
||||
}
|
||||
Invoke-SkillInitDb -PythonExe $pythonExe -SkillDir $initSkillDir -DataRoot $resolvedDataRoot -UserId $resolvedUserId -DryRun:$DryRun
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Done. Stop any running job, leave Data Management, then open it again." -ForegroundColor Cyan
|
||||
Write-Host "Do not click Update in the skill market. That replaces this source with the encrypted package." -ForegroundColor Cyan
|
||||
@@ -75,7 +75,7 @@ python tests/run_tests.py test_cli_smoke
|
||||
| **subprocess 真实入口** ``python scripts/main.py`` | ``test_entrypoint_subprocess.py`` |
|
||||
| 运行路径与 **JIANGCHANG_*** 隔离 | ``test_runtime_paths.py`` |
|
||||
| ``SKILL.md`` slug 与 ``constants.SKILL_SLUG`` | ``test_skill_metadata.py`` |
|
||||
| DB / ``task_logs`` 骨架冒烟 | ``test_db_smoke.py`` |
|
||||
| DB / 元数据骨架冒烟 | ``test_db_smoke.py`` |
|
||||
| 数据管理元数据 / 字段顺序 | ``test_display_metadata.py`` |
|
||||
| 标准时间字段(Unix 秒 / trigger / 元数据) | ``test_timestamp_columns.py`` |
|
||||
| **adapter profile / 外呼授权策略** | ``test_adapter_profile_policy.py`` + ``adapter_test_utils.py``(非 ``test_`` 前缀,不自动当用例收集) |
|
||||
|
||||
@@ -35,7 +35,7 @@ def get_skill_root() -> str:
|
||||
return _SKILL_ROOT
|
||||
|
||||
|
||||
def platform_kit_version_patch(version: str = "1.2.2"):
|
||||
def platform_kit_version_patch(version: str = "1.2.11"):
|
||||
"""Mock installed jiangchang-platform-kit version for health/diagnostics tests."""
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -79,3 +79,63 @@ class IsolatedDataRoot:
|
||||
if self._tmp and os.path.isdir(self._tmp):
|
||||
shutil.rmtree(self._tmp, ignore_errors=True)
|
||||
self._tmp = None
|
||||
|
||||
|
||||
DEMO_ITEMS_TABLE = "demo_items"
|
||||
|
||||
DEMO_ITEMS_DDL = """
|
||||
CREATE TABLE IF NOT EXISTS demo_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'draft',
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
)
|
||||
"""
|
||||
|
||||
DEMO_ITEMS_COLUMN_METADATA: tuple[dict[str, object], ...] = (
|
||||
{"column_name": "id", "display_name": "编号", "description": "主键", "visible": 1, "searchable": 1, "editable": 0},
|
||||
{"column_name": "title", "display_name": "标题", "description": "标题", "visible": 1, "searchable": 1, "editable": 1},
|
||||
{"column_name": "status", "display_name": "状态", "description": "状态", "visible": 1, "searchable": 1, "editable": 1},
|
||||
{
|
||||
"column_name": "created_at",
|
||||
"display_name": "创建时间",
|
||||
"description": "记录创建时间(Unix 秒级时间戳,数据库自动生成)",
|
||||
"visible": 1,
|
||||
"searchable": 0,
|
||||
"editable": 0,
|
||||
"display_type": "datetime_unix_seconds",
|
||||
},
|
||||
{
|
||||
"column_name": "updated_at",
|
||||
"display_name": "更新时间",
|
||||
"description": "记录最后更新时间(Unix 秒级时间戳,自动维护)",
|
||||
"visible": 1,
|
||||
"searchable": 0,
|
||||
"editable": 0,
|
||||
"display_type": "datetime_unix_seconds",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def seed_demo_items(cursor, *, readonly: int = 0) -> None:
|
||||
"""测试夹具:创建 demo_items 物理表、时间戳 trigger 与展示元数据。"""
|
||||
from db.display_metadata import seed_column_metadata_batch, upsert_table_metadata
|
||||
from db.timestamp_columns import init_updated_at_trigger
|
||||
|
||||
cursor.execute(DEMO_ITEMS_DDL)
|
||||
init_updated_at_trigger(cursor, DEMO_ITEMS_TABLE)
|
||||
upsert_table_metadata(
|
||||
cursor,
|
||||
table_name=DEMO_ITEMS_TABLE,
|
||||
display_name="示例记录",
|
||||
description="模板测试用业务表示例,不是默认交付表",
|
||||
visible=1,
|
||||
readonly=readonly,
|
||||
)
|
||||
seed_column_metadata_batch(
|
||||
cursor,
|
||||
table_name=DEMO_ITEMS_TABLE,
|
||||
columns=DEMO_ITEMS_COLUMN_METADATA,
|
||||
valid_column_names=[str(column["column_name"]) for column in DEMO_ITEMS_COLUMN_METADATA],
|
||||
)
|
||||
|
||||
66
tests/samples/test_table_deprecation_migration.py.sample
Normal file
66
tests/samples/test_table_deprecation_migration.py.sample
Normal file
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""旧库废弃表迁移样例(复制到 tests/ 并按技能改写)。
|
||||
|
||||
验收:给定旧库含废弃物理表 + 元数据 + 历史行 → init_db() 后表与元数据均清除。
|
||||
见 references/SCHEMA.md「表增删改与退役」。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from _support import IsolatedDataRoot
|
||||
|
||||
|
||||
class TestTableDeprecationMigration(unittest.TestCase):
|
||||
def test_init_db_drops_legacy_task_logs(self) -> None:
|
||||
with IsolatedDataRoot():
|
||||
from db.connection import get_conn, init_db
|
||||
from db.display_metadata import iter_user_tables
|
||||
|
||||
# 1) 模拟升级前的旧库
|
||||
init_db()
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE task_logs (
|
||||
id INTEGER PRIMARY KEY,
|
||||
task_type TEXT,
|
||||
status TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO _jiangchang_tables
|
||||
(table_name, display_name, visible, readonly)
|
||||
VALUES ('task_logs', '任务日志', 1, 0)
|
||||
"""
|
||||
)
|
||||
cur.execute("INSERT INTO task_logs (task_type, status) VALUES ('run', 'ok')")
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
# 2) 新代码 init_db() 应幂等清除(在 connection.init_db 中调用 drop_deprecated_user_table)
|
||||
init_db()
|
||||
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"SELECT 1 FROM sqlite_master WHERE type='table' AND name='task_logs'"
|
||||
)
|
||||
self.assertIsNone(cur.fetchone())
|
||||
cur.execute(
|
||||
"SELECT COUNT(*) FROM _jiangchang_tables WHERE table_name='task_logs'"
|
||||
)
|
||||
self.assertEqual(int(cur.fetchone()[0]), 0)
|
||||
self.assertNotIn("task_logs", list(iter_user_tables(cur)))
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -74,8 +74,17 @@ def _validate_bind_tables(action_id: str, bind: object) -> list[str]:
|
||||
errors: list[str] = []
|
||||
if not isinstance(bind, dict):
|
||||
return [f"{action_id}: bind must be object"]
|
||||
if set(bind.keys()) - {"tables"}:
|
||||
errors.append(f"{action_id}: bind only allows tables, got {sorted(bind.keys())}")
|
||||
allowed = {
|
||||
"tables",
|
||||
"inputMapping",
|
||||
"columnMapping",
|
||||
"labelColumn",
|
||||
"selectionMode",
|
||||
"visibleWhen",
|
||||
}
|
||||
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 +99,20 @@ def _validate_bind_tables(action_id: str, bind: object) -> list[str]:
|
||||
if table in seen:
|
||||
errors.append(f"{action_id}: bind.tables duplicate {table!r}")
|
||||
seen.add(table)
|
||||
label_column = bind.get("labelColumn")
|
||||
if label_column is not None:
|
||||
if not isinstance(label_column, str) or not label_column.strip():
|
||||
errors.append(f"{action_id}: bind.labelColumn must be non-empty string")
|
||||
elif not _TABLE_NAME_PATTERN.fullmatch(label_column):
|
||||
errors.append(f"{action_id}: bind.labelColumn {label_column!r} must be snake_case")
|
||||
selection_mode = bind.get("selectionMode")
|
||||
if selection_mode is not None and selection_mode not in ("required", "none", "optional"):
|
||||
errors.append(
|
||||
f"{action_id}: bind.selectionMode must be required|none|optional, got {selection_mode!r}"
|
||||
)
|
||||
visible_when = bind.get("visibleWhen")
|
||||
if visible_when is not None and (not isinstance(visible_when, dict) or not visible_when):
|
||||
errors.append(f"{action_id}: bind.visibleWhen must be a non-empty object")
|
||||
return errors
|
||||
|
||||
|
||||
@@ -100,7 +123,7 @@ def _load_bindable_business_tables() -> set[str]:
|
||||
- SQLite 物理表存在(排除 sqlite_* 与 _jiangchang_* 元数据表)
|
||||
- ``_jiangchang_tables`` 中有对应行且 ``visible=1``
|
||||
|
||||
失败时抛出 RuntimeError(带诊断),不得静默返回空集供测试 skip。
|
||||
模板默认无业务表,允许返回空集。失败时抛出 RuntimeError(带诊断)。
|
||||
"""
|
||||
from db.connection import init_db
|
||||
from db.display_metadata import METADATA_TABLES
|
||||
@@ -136,12 +159,6 @@ def _load_bindable_business_tables() -> set[str]:
|
||||
finally:
|
||||
conn.close()
|
||||
bindable = physical & visible_meta
|
||||
if not bindable:
|
||||
raise RuntimeError(
|
||||
"isolated init_db produced no bindable tables "
|
||||
f"(physical={sorted(physical)}, visible_meta={sorted(visible_meta)}, "
|
||||
f"db_path={db_path!r})"
|
||||
)
|
||||
return bindable
|
||||
except RuntimeError:
|
||||
raise
|
||||
@@ -190,7 +207,21 @@ class TestActionsManifest(unittest.TestCase):
|
||||
|
||||
placements = action.get("placements")
|
||||
self.assertIsInstance(placements, list, msg=f"{action_id} placements must be array")
|
||||
self.assertGreater(len(placements), 0, msg=f"{action_id} placements empty")
|
||||
replaces = action.get("replaces")
|
||||
if replaces is not None:
|
||||
self.assertIn(
|
||||
replaces,
|
||||
{"create", "update", "delete", "import"},
|
||||
msg=f"{action_id} invalid replaces {replaces!r}",
|
||||
)
|
||||
bind = action.get("bind") or {}
|
||||
tables = bind.get("tables") or []
|
||||
self.assertTrue(
|
||||
isinstance(tables, list) and len(tables) > 0,
|
||||
msg=f"{action_id} replaces requires non-empty bind.tables",
|
||||
)
|
||||
else:
|
||||
self.assertGreater(len(placements), 0, msg=f"{action_id} placements empty")
|
||||
for placement in placements:
|
||||
self.assertIn(placement, _ALLOWED_PLACEMENTS, msg=f"{action_id} invalid placement {placement!r}")
|
||||
|
||||
@@ -228,12 +259,113 @@ 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"):
|
||||
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 [])),
|
||||
{"direct", "confirm", "form"},
|
||||
)
|
||||
self.assertIn("replaces", props)
|
||||
self.assertEqual(props["replaces"].get("$ref"), "#/$defs/mutationKind")
|
||||
self.assertEqual(
|
||||
set(schema["$defs"]["mutationKind"].get("enum") or []),
|
||||
{"create", "update", "delete", "import"},
|
||||
)
|
||||
|
||||
def test_schema_defines_hover_hint(self) -> None:
|
||||
schema = _load_actions_schema()
|
||||
action_def = schema["$defs"]["action"]
|
||||
props = action_def["properties"]
|
||||
self.assertIn("hoverHint", props)
|
||||
self.assertNotIn("hoverHint", action_def.get("required") or [])
|
||||
self.assertEqual(props["hoverHint"].get("type"), "string")
|
||||
|
||||
def test_schema_defines_visible_when_on_input_property(self) -> None:
|
||||
schema = _load_actions_schema()
|
||||
prop_def = schema["$defs"]["inputProperty"]
|
||||
props = prop_def["properties"]
|
||||
self.assertIn("visibleWhen", props)
|
||||
self.assertNotIn("visibleWhen", prop_def.get("required") or [])
|
||||
# Must live on input property, not action top-level (≠ Policy L3).
|
||||
action_props = schema["$defs"]["action"]["properties"]
|
||||
self.assertNotIn("visibleWhen", action_props)
|
||||
|
||||
def test_schema_defines_enum_names_on_input_property(self) -> None:
|
||||
schema = _load_actions_schema()
|
||||
props = schema["$defs"]["inputProperty"]["properties"]
|
||||
self.assertIn("enum", props)
|
||||
self.assertIn("enumNames", props)
|
||||
self.assertEqual(props["enumNames"].get("type"), "array")
|
||||
self.assertEqual(props["enumNames"].get("minItems"), 1)
|
||||
self.assertIn("column", props)
|
||||
|
||||
def test_enum_names_example_exists_and_aligns_labels(self) -> None:
|
||||
path = os.path.join(
|
||||
get_skill_root(), "assets", "examples", "enum-names-action.example.json"
|
||||
)
|
||||
self.assertTrue(os.path.isfile(path), msg="enum-names example must exist")
|
||||
with open(path, encoding="utf-8") as f:
|
||||
example = json.load(f)
|
||||
props = example["actions"][0]["inputSchema"]["properties"]
|
||||
category = props["category"]
|
||||
self.assertEqual(len(category["enum"]), len(category["enumNames"]))
|
||||
self.assertEqual(category.get("column"), "category")
|
||||
priority = props["priority"]
|
||||
self.assertEqual(len(priority["enum"]), len(priority["enumNames"]))
|
||||
|
||||
def test_default_manifest_omits_visible_when(self) -> None:
|
||||
for action in _load_actions_manifest()["actions"]:
|
||||
properties = ((action.get("inputSchema") or {}).get("properties")) or {}
|
||||
for name, prop in properties.items():
|
||||
self.assertNotIn(
|
||||
"visibleWhen",
|
||||
prop if isinstance(prop, dict) else {},
|
||||
msg=f"{action['id']}.{name} default template should omit visibleWhen",
|
||||
)
|
||||
|
||||
def test_visible_when_example_exists_and_uses_field_rules(self) -> None:
|
||||
path = os.path.join(
|
||||
get_skill_root(), "assets", "examples", "visible-when-action.example.json"
|
||||
)
|
||||
self.assertTrue(os.path.isfile(path), msg="visible-when example must exist")
|
||||
with open(path, encoding="utf-8") as f:
|
||||
example = json.load(f)
|
||||
props = example["actions"][0]["inputSchema"]["properties"]
|
||||
self.assertIn("visibleWhen", props["keyword"])
|
||||
self.assertIn("visibleWhen", props["authorUrl"])
|
||||
self.assertNotIn("visibleWhen", props["collectMode"])
|
||||
|
||||
def test_default_manifest_omits_hover_hint(self) -> None:
|
||||
for action in _load_actions_manifest()["actions"]:
|
||||
self.assertNotIn(
|
||||
"hoverHint",
|
||||
action,
|
||||
msg=f"{action['id']} default template action should omit hoverHint",
|
||||
)
|
||||
|
||||
def test_schema_defines_host_effects_and_steps(self) -> None:
|
||||
schema = _load_actions_schema()
|
||||
action_def = schema["$defs"]["action"]
|
||||
props = action_def["properties"]
|
||||
self.assertIn("hostEffects", props)
|
||||
self.assertEqual(props["hostEffects"].get("$ref"), "#/$defs/hostEffects")
|
||||
self.assertIn("steps", props)
|
||||
effects = schema["$defs"]["hostEffects"]
|
||||
self.assertIs(effects.get("additionalProperties"), False)
|
||||
for key in ("onSuccess", "onFailure"):
|
||||
self.assertIn(key, effects.get("properties") or {})
|
||||
effect_ids = set(schema["$defs"]["hostEffectId"].get("enum") or [])
|
||||
self.assertEqual(effect_ids, {"reloadTable", "refreshCatalog", "clearSelection"})
|
||||
step = schema["$defs"]["actionStep"]
|
||||
self.assertEqual(
|
||||
set(step["properties"]["when"].get("enum") or []),
|
||||
{"before", "afterSuccess", "afterFailure"},
|
||||
)
|
||||
self.assertEqual(
|
||||
set(step["properties"]["do"].get("enum") or []),
|
||||
{"deleteByFk", "deleteRows", "reloadTable", "refreshCatalog", "clearSelection"},
|
||||
)
|
||||
self.assertEqual(props["steps"].get("maxItems"), 8)
|
||||
|
||||
def test_manifest_validates_against_json_schema(self) -> None:
|
||||
try:
|
||||
@@ -268,15 +400,14 @@ class TestActionsManifest(unittest.TestCase):
|
||||
self.assertEqual(errors, [], msg="\n".join(errors))
|
||||
|
||||
def test_bindable_business_tables_helper_loads_template_db(self) -> None:
|
||||
"""helper 本身必须可用:即便默认 manifest 无 toolbar bind,也要验证隔离库可读。"""
|
||||
"""helper 本身必须可用:模板默认无业务表,隔离库仍应可读且不含元数据表。"""
|
||||
bindable = _load_bindable_business_tables()
|
||||
self.assertIn("task_logs", bindable)
|
||||
self.assertEqual(bindable, set())
|
||||
self.assertNotIn("_jiangchang_tables", bindable)
|
||||
self.assertNotIn("_jiangchang_columns", bindable)
|
||||
|
||||
def test_bind_tables_exist_in_physical_and_visible_metadata(self) -> None:
|
||||
bindable = _load_bindable_business_tables()
|
||||
self.assertTrue(bindable, msg="bindable table set must be non-empty after init_db")
|
||||
manifest = _load_actions_manifest()
|
||||
missing: list[str] = []
|
||||
for action in manifest["actions"]:
|
||||
@@ -312,7 +443,7 @@ class TestActionsManifest(unittest.TestCase):
|
||||
"entrypoint": {"type": "cli", "command": "health", "args": []},
|
||||
}
|
||||
if placement == "toolbar":
|
||||
action["bind"] = {"tables": ["task_logs"]}
|
||||
action["bind"] = {"tables": ["demo_items"]}
|
||||
manifest = {
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
@@ -347,6 +478,181 @@ class TestActionsManifest(unittest.TestCase):
|
||||
with self.assertRaises(ValidationError):
|
||||
jsonschema.validate(instance=manifest, schema=schema)
|
||||
|
||||
def test_schema_allows_bind_label_column(self) -> None:
|
||||
try:
|
||||
import jsonschema
|
||||
except ImportError:
|
||||
self.fail("jsonschema is required")
|
||||
schema = _load_actions_schema()
|
||||
bind_props = schema["$defs"]["bind"]["properties"]
|
||||
self.assertIn("labelColumn", bind_props)
|
||||
manifest = {
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actions": [
|
||||
{
|
||||
"id": "probe-label-column",
|
||||
"label": "处理勾选",
|
||||
"description": "验证 bind.labelColumn 可通过 Schema",
|
||||
"placements": ["toolbar", "batch"],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "confirm",
|
||||
"bind": {
|
||||
"tables": ["demo_items"],
|
||||
"inputMapping": {"ids": "$selection.ids"},
|
||||
"labelColumn": "display_name",
|
||||
},
|
||||
"entrypoint": {"type": "cli", "command": "health", "args": []},
|
||||
}
|
||||
],
|
||||
}
|
||||
jsonschema.validate(instance=manifest, schema=schema)
|
||||
|
||||
def test_schema_allows_bind_selection_mode(self) -> None:
|
||||
try:
|
||||
import jsonschema
|
||||
except ImportError:
|
||||
self.fail("jsonschema is required")
|
||||
schema = _load_actions_schema()
|
||||
bind_props = schema["$defs"]["bind"]["properties"]
|
||||
self.assertIn("selectionMode", bind_props)
|
||||
self.assertEqual(
|
||||
bind_props["selectionMode"].get("enum"),
|
||||
["required", "none", "optional"],
|
||||
)
|
||||
manifest = {
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actions": [
|
||||
{
|
||||
"id": "probe-selection-mode",
|
||||
"label": "处理队列",
|
||||
"description": "验证 bind.selectionMode 可通过 Schema",
|
||||
"placements": ["toolbar", "batch"],
|
||||
"executionProfile": "async",
|
||||
"interaction": "form",
|
||||
"bind": {
|
||||
"tables": ["demo_items"],
|
||||
"selectionMode": "optional",
|
||||
"inputMapping": {"itemIds": "$selection.ids"},
|
||||
"labelColumn": "display_name",
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["itemIds", "pickCount"],
|
||||
"properties": {
|
||||
"itemIds": {"type": "string", "readOnly": True},
|
||||
"pickCount": {"type": "number", "default": 1, "minimum": 1},
|
||||
},
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "process",
|
||||
"args": ["--ids", "{{itemIds}}", "--pick", "{{pickCount}}"],
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
jsonschema.validate(instance=manifest, schema=schema)
|
||||
|
||||
def test_schema_allows_bind_visible_when(self) -> None:
|
||||
try:
|
||||
import jsonschema
|
||||
except ImportError:
|
||||
self.fail("jsonschema is required")
|
||||
schema = _load_actions_schema()
|
||||
bind_props = schema["$defs"]["bind"]["properties"]
|
||||
self.assertIn("visibleWhen", bind_props)
|
||||
self.assertIn("visibleWhenRule", schema["$defs"])
|
||||
manifest = {
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"actions": [
|
||||
{
|
||||
"id": "probe-bind-visible-when",
|
||||
"label": "采用",
|
||||
"description": "验证 bind.visibleWhen 可通过 Schema",
|
||||
"placements": ["row"],
|
||||
"executionProfile": "sync",
|
||||
"interaction": "direct",
|
||||
"bind": {
|
||||
"tables": ["demo_items"],
|
||||
"visibleWhen": {
|
||||
"field": "status",
|
||||
"in": ["pending_review", "skipped"],
|
||||
},
|
||||
"inputMapping": {"itemId": "$row.$pk"},
|
||||
},
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["itemId"],
|
||||
"properties": {
|
||||
"itemId": {"type": "number", "readOnly": True},
|
||||
},
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "approve",
|
||||
"args": ["--id", "{{itemId}}"],
|
||||
},
|
||||
}
|
||||
],
|
||||
}
|
||||
jsonschema.validate(instance=manifest, schema=schema)
|
||||
|
||||
def test_action_examples_user_facing_copy_avoids_jargon(self) -> None:
|
||||
"""示例 Action 的用户可见字符串不得含常见开发黑话(属性名本身除外)。"""
|
||||
examples_dir = os.path.join(get_skill_root(), "assets", "examples")
|
||||
# 故意拆开,避免本测试源码触发自身扫描。
|
||||
forbidden = (
|
||||
"pickCount",
|
||||
"itemIds",
|
||||
"status=",
|
||||
"import-urls",
|
||||
"download-enqueue",
|
||||
"bind.selectionMode",
|
||||
"selectionMode=",
|
||||
)
|
||||
# async/pending 作为整词出现在 description 才禁止;executionProfile 值除外。
|
||||
word_forbidden = re.compile(r"\b(async|pending|sync)\b", re.I)
|
||||
|
||||
for name in os.listdir(examples_dir):
|
||||
if not name.endswith(".action.example.json"):
|
||||
continue
|
||||
path = os.path.join(examples_dir, name)
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
actions = data["actions"] if isinstance(data, dict) and "actions" in data else [data]
|
||||
for action in actions:
|
||||
if not isinstance(action, dict):
|
||||
continue
|
||||
texts: list[str] = []
|
||||
for key in ("description",):
|
||||
if action.get(key):
|
||||
texts.append(str(action[key]))
|
||||
conf = action.get("confirmation") or {}
|
||||
if isinstance(conf, dict) and conf.get("message"):
|
||||
texts.append(str(conf["message"]))
|
||||
props = ((action.get("inputSchema") or {}).get("properties")) or {}
|
||||
for prop in props.values():
|
||||
if not isinstance(prop, dict):
|
||||
continue
|
||||
for key in ("title", "description"):
|
||||
if prop.get(key):
|
||||
texts.append(str(prop[key]))
|
||||
blob = "\n".join(texts)
|
||||
for token in forbidden:
|
||||
self.assertNotIn(
|
||||
token,
|
||||
blob,
|
||||
msg=f"{name} user-facing copy must not contain {token!r}",
|
||||
)
|
||||
match = word_forbidden.search(blob)
|
||||
if match is not None:
|
||||
self.fail(
|
||||
f"{name} user-facing copy must not contain {match.group(0)!r}"
|
||||
)
|
||||
|
||||
def test_no_execution_profile_placement_coupling_in_tests(self) -> None:
|
||||
"""守护:本文件不得引入入口位置与 sync/async 的错误硬耦合断言。"""
|
||||
path = os.path.join(get_skill_root(), "tests", "test_actions_manifest.py")
|
||||
@@ -490,8 +796,56 @@ class TestActionsManifest(unittest.TestCase):
|
||||
"executionProfile",
|
||||
"interaction",
|
||||
"confirmation",
|
||||
"数据管理技能按钮顺序",
|
||||
"数组相对顺序",
|
||||
"全部直接展示",
|
||||
"左右分栏",
|
||||
"行级入口",
|
||||
"行右键",
|
||||
"六职责正交",
|
||||
"POLICY.md",
|
||||
"hostEffects",
|
||||
"steps",
|
||||
"hoverHint",
|
||||
"visibleWhen",
|
||||
"字段条件显隐",
|
||||
"reloadTable",
|
||||
"afterSuccess",
|
||||
"afterFailure",
|
||||
"cellPresentation",
|
||||
"CELL_PRESENTATION.md",
|
||||
"整行",
|
||||
"labelColumn",
|
||||
"selectionMode",
|
||||
"勾选驱动",
|
||||
"行菜单显隐",
|
||||
"bind.visibleWhen",
|
||||
"用户可见文案",
|
||||
"双受众",
|
||||
"已选择",
|
||||
"revealInForm",
|
||||
"映射只读",
|
||||
"enumNames",
|
||||
"静态枚举 label",
|
||||
"绑列 options",
|
||||
):
|
||||
self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}")
|
||||
self.assertNotIn(
|
||||
"其余进「更多操作」",
|
||||
text,
|
||||
msg="ACTIONS.md must not document removed overflow more-menu truncation",
|
||||
)
|
||||
self.assertIn(
|
||||
"新增 `row-context-menu` 等 placement",
|
||||
text,
|
||||
msg="ACTIONS.md must forbid inventing a separate row-context-menu placement",
|
||||
)
|
||||
placement_enum = _load_actions_schema().get("$defs", {}).get("placement", {}).get("enum") or []
|
||||
self.assertNotIn(
|
||||
"row-context-menu",
|
||||
placement_enum,
|
||||
msg="skill-actions.schema must not add row-context-menu to placements enum",
|
||||
)
|
||||
for forbidden in (
|
||||
"数据管理 toolbar " + "只挂",
|
||||
"toolbar " + "只放 async",
|
||||
|
||||
164
tests/test_cell_presentation_contract.py
Normal file
164
tests/test_cell_presentation_contract.py
Normal file
@@ -0,0 +1,164 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Cell / Grid Presentation 契约:schema + 文档 + 示例。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from util.runtime_paths import get_skill_root
|
||||
|
||||
|
||||
def _skill_path(*parts: str) -> str:
|
||||
return os.path.join(get_skill_root(), *parts)
|
||||
|
||||
|
||||
def _load_json(*parts: str) -> dict:
|
||||
with open(_skill_path(*parts), encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def _load_cell_schema() -> dict:
|
||||
return _load_json("assets", "schemas", "skill-cell-presentation.schema.json")
|
||||
|
||||
|
||||
def _load_cell_example() -> dict:
|
||||
return _load_json("assets", "examples", "skill-cell-presentation.example.json")
|
||||
|
||||
|
||||
def _load_grid_schema() -> dict:
|
||||
return _load_json("assets", "schemas", "skill-grid-presentation.schema.json")
|
||||
|
||||
|
||||
def _load_grid_example() -> dict:
|
||||
return _load_json("assets", "examples", "skill-grid-presentation.example.json")
|
||||
|
||||
|
||||
class TestCellPresentationContract(unittest.TestCase):
|
||||
def test_schema_file_exists_and_parses(self) -> None:
|
||||
schema = _load_cell_schema()
|
||||
self.assertEqual(schema.get("title"), "SkillCellPresentationDocument")
|
||||
self.assertIs(schema.get("additionalProperties"), False)
|
||||
self.assertEqual(schema["properties"]["schemaVersion"].get("const"), 1)
|
||||
tones = set(schema["$defs"]["tone"].get("enum") or [])
|
||||
self.assertEqual(tones, {"danger", "warning", "info", "success", "muted"})
|
||||
|
||||
def test_example_validates_against_schema(self) -> None:
|
||||
try:
|
||||
import jsonschema
|
||||
except ImportError:
|
||||
self.skipTest("jsonschema not installed")
|
||||
jsonschema.validate(instance=_load_cell_example(), schema=_load_cell_schema())
|
||||
|
||||
def test_actions_schema_defines_cell_presentation_enable_block(self) -> None:
|
||||
actions_schema = _load_json("assets", "schemas", "skill-actions.schema.json")
|
||||
self.assertIn("cellPresentation", actions_schema["properties"])
|
||||
self.assertIn("cellPresentationDeclaration", actions_schema["$defs"])
|
||||
|
||||
def test_docs_and_example_exist(self) -> None:
|
||||
self.assertTrue(os.path.isfile(_skill_path("references", "CELL_PRESENTATION.md")))
|
||||
self.assertTrue(
|
||||
os.path.isfile(
|
||||
_skill_path("assets", "schemas", "skill-cell-presentation.schema.json")
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
os.path.isfile(
|
||||
_skill_path("assets", "examples", "skill-cell-presentation.example.json")
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
os.path.isfile(
|
||||
_skill_path("assets", "schemas", "skill-grid-presentation.schema.json")
|
||||
)
|
||||
)
|
||||
self.assertTrue(
|
||||
os.path.isfile(
|
||||
_skill_path("assets", "examples", "skill-grid-presentation.example.json")
|
||||
)
|
||||
)
|
||||
|
||||
def test_cell_presentation_md_covers_contract(self) -> None:
|
||||
path = _skill_path("references", "CELL_PRESENTATION.md")
|
||||
with open(path, encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
for marker in (
|
||||
"tone",
|
||||
"danger",
|
||||
"warning",
|
||||
"alert",
|
||||
"_jiangchang_cell_presentation",
|
||||
"_jiangchang_grid_presentation",
|
||||
"cellPresentation",
|
||||
"派生",
|
||||
"任意 CSS",
|
||||
"用途不限",
|
||||
"schemaVersion",
|
||||
"整行",
|
||||
"整列",
|
||||
"all",
|
||||
"格子 > 区域 > 行 > 列",
|
||||
"详情",
|
||||
):
|
||||
self.assertIn(marker, text, msg=f"CELL_PRESENTATION.md missing {marker!r}")
|
||||
|
||||
def test_grid_schema_file_exists_and_parses(self) -> None:
|
||||
schema = _load_grid_schema()
|
||||
self.assertEqual(schema.get("title"), "SkillGridPresentationDocument")
|
||||
self.assertIs(schema.get("additionalProperties"), False)
|
||||
self.assertEqual(schema["properties"]["schemaVersion"].get("const"), 1)
|
||||
tones = set(schema["$defs"]["tone"].get("enum") or [])
|
||||
self.assertEqual(tones, {"danger", "warning", "info", "success", "muted"})
|
||||
self.assertIn("not", schema["$defs"]["ruleItem"])
|
||||
|
||||
def test_grid_example_validates_against_schema(self) -> None:
|
||||
try:
|
||||
import jsonschema
|
||||
except ImportError:
|
||||
self.skipTest("jsonschema not installed")
|
||||
jsonschema.validate(instance=_load_grid_example(), schema=_load_grid_schema())
|
||||
|
||||
def test_grid_schema_rejects_all_by_all(self) -> None:
|
||||
try:
|
||||
import jsonschema
|
||||
except ImportError:
|
||||
self.skipTest("jsonschema not installed")
|
||||
payload = {
|
||||
"schemaVersion": 1,
|
||||
"skill": "your-skill-slug",
|
||||
"table": "business_records",
|
||||
"rowKey": "id",
|
||||
"rules": [
|
||||
{
|
||||
"ruleKey": "paint-everything",
|
||||
"rows": "all",
|
||||
"columns": "all",
|
||||
"tone": "danger",
|
||||
}
|
||||
],
|
||||
}
|
||||
with self.assertRaises(jsonschema.ValidationError):
|
||||
jsonschema.validate(instance=payload, schema=_load_grid_schema())
|
||||
|
||||
def test_internal_metadata_hides_presentation_tables(self) -> None:
|
||||
from db.display_metadata import INTERNAL_METADATA_TABLES, METADATA_TABLES
|
||||
|
||||
self.assertIn("_jiangchang_cell_presentation", INTERNAL_METADATA_TABLES)
|
||||
self.assertIn("_jiangchang_grid_presentation", INTERNAL_METADATA_TABLES)
|
||||
self.assertTrue(INTERNAL_METADATA_TABLES <= METADATA_TABLES)
|
||||
|
||||
def test_template_actions_do_not_enable_presentation_by_default(self) -> None:
|
||||
manifest = _load_json("assets", "actions.json")
|
||||
self.assertNotEqual(manifest.get("cellPresentation", {}).get("enabled"), True)
|
||||
|
||||
def test_checklist_covers_grid_presentation(self) -> None:
|
||||
path = _skill_path("development", "CHECKLIST.md")
|
||||
with open(path, encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
self.assertIn("A6-15", text)
|
||||
self.assertIn("cellPresentation", text)
|
||||
self.assertIn("CELL_PRESENTATION.md", text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""通用 CLI 冒烟:用法、health、version、logs、log-get(不触网、不深测 run 占位)。"""
|
||||
"""通用 CLI 冒烟:用法、health、version、init-db(不触网、不深测 run 占位)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
@@ -14,7 +14,7 @@ from jiangchang_skill_core import config
|
||||
|
||||
# scripts/ 已由 _support 注入 sys.path
|
||||
from cli.app import main
|
||||
from util.constants import SKILL_SLUG
|
||||
from util.constants import SKILL_SLUG, SKILL_VERSION
|
||||
|
||||
|
||||
class TestCliSmoke(unittest.TestCase):
|
||||
@@ -57,6 +57,7 @@ class TestCliSmoke(unittest.TestCase):
|
||||
self.assertIn("version", payload)
|
||||
self.assertIn("skill", payload)
|
||||
self.assertEqual(payload["skill"], SKILL_SLUG)
|
||||
self.assertEqual(payload["version"], SKILL_VERSION)
|
||||
|
||||
def test_init_db_creates_database_and_is_idempotent(self) -> None:
|
||||
with IsolatedDataRoot(user_id="_cli_init_db"):
|
||||
@@ -78,20 +79,13 @@ class TestCliSmoke(unittest.TestCase):
|
||||
self.assertTrue(payload2["ok"])
|
||||
self.assertEqual(payload2["db_path"], payload["db_path"])
|
||||
|
||||
def test_logs_empty_returns_zero(self) -> None:
|
||||
with IsolatedDataRoot():
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf), redirect_stderr(io.StringIO()):
|
||||
rc = main(["logs"])
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertIn("暂无", buf.getvalue())
|
||||
|
||||
def test_log_get_non_numeric_returns_nonzero(self) -> None:
|
||||
def test_unknown_logs_subcommand_is_rejected(self) -> None:
|
||||
buf = io.StringIO()
|
||||
with redirect_stdout(buf), redirect_stderr(io.StringIO()):
|
||||
rc = main(["log-get", "not-a-number"])
|
||||
self.assertNotEqual(rc, 0)
|
||||
self.assertIn("数字", buf.getvalue())
|
||||
err = io.StringIO()
|
||||
with redirect_stdout(buf), redirect_stderr(err):
|
||||
with self.assertRaises(SystemExit) as caught:
|
||||
main(["logs"])
|
||||
self.assertNotEqual(caught.exception.code, 0)
|
||||
|
||||
def test_run_placeholder_returns_nonzero_without_network(self) -> None:
|
||||
"""占位 run:RpaVideoSession 示范 + 模板提示;OPENCLAW_RECORD_VIDEO=0 时不启 ffmpeg。"""
|
||||
|
||||
49
tests/test_db_backend.py
Normal file
49
tests/test_db_backend.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据库后端选择:默认 SQLite,DSN 解析。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from _support import IsolatedDataRoot
|
||||
|
||||
|
||||
class TestDbBackend(unittest.TestCase):
|
||||
def test_resolve_backend_defaults_to_sqlite(self) -> None:
|
||||
with IsolatedDataRoot():
|
||||
from db.backend import resolve_backend
|
||||
|
||||
self.assertEqual(resolve_backend(), "sqlite")
|
||||
|
||||
def test_parse_mysql_dsn(self) -> None:
|
||||
with IsolatedDataRoot():
|
||||
from db.backend import parse_mysql_dsn
|
||||
|
||||
params = parse_mysql_dsn("mysql://root:secret@192.168.0.20:3306/demo_db")
|
||||
self.assertEqual(params["host"], "192.168.0.20")
|
||||
self.assertEqual(params["port"], 3306)
|
||||
self.assertEqual(params["user"], "root")
|
||||
self.assertEqual(params["password"], "secret")
|
||||
self.assertEqual(params["database"], "demo_db")
|
||||
|
||||
def test_resolve_backend_uses_mysql_dsn_from_env(self) -> None:
|
||||
with IsolatedDataRoot():
|
||||
from db.backend import resolve_backend
|
||||
|
||||
os.environ["MYSQL_DSN"] = "mysql://u:p@127.0.0.1:3306/t"
|
||||
try:
|
||||
self.assertEqual(resolve_backend(), "mysql")
|
||||
finally:
|
||||
os.environ.pop("MYSQL_DSN", None)
|
||||
|
||||
def test_describe_active_store_sqlite(self) -> None:
|
||||
with IsolatedDataRoot():
|
||||
from db.connection import describe_active_store
|
||||
|
||||
info = describe_active_store()
|
||||
self.assertEqual(info["backend"], "sqlite")
|
||||
self.assertIn("db_path", info)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""task_logs 表:init 幂等、写入与查询(全程隔离数据目录)。"""
|
||||
"""init_db:幂等创建元数据表,默认不建业务表。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
@@ -8,44 +8,30 @@ from _support import IsolatedDataRoot
|
||||
|
||||
|
||||
class TestDbSmoke(unittest.TestCase):
|
||||
def test_init_db_idempotent_and_crud_smoke(self) -> None:
|
||||
def test_init_db_idempotent_metadata_only(self) -> None:
|
||||
with IsolatedDataRoot():
|
||||
from db import task_logs_repository as tlr
|
||||
from db.connection import get_conn, init_db
|
||||
from db.display_metadata import METADATA_TABLES, iter_user_tables
|
||||
|
||||
init_db()
|
||||
init_db() # 幂等
|
||||
init_db()
|
||||
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT display_name FROM _jiangchang_tables WHERE table_name = 'task_logs'")
|
||||
self.assertEqual(cur.fetchone()[0], "任务日志")
|
||||
cur.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
||||
)
|
||||
tables = {str(row[0]) for row in cur.fetchall()}
|
||||
self.assertIn("_jiangchang_tables", tables)
|
||||
self.assertIn("_jiangchang_columns", tables)
|
||||
self.assertNotIn("task_logs", tables)
|
||||
self.assertEqual(list(iter_user_tables(cur)), [])
|
||||
leftover = tables - METADATA_TABLES
|
||||
self.assertEqual(leftover, set())
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
new_id = tlr.save_task_log(
|
||||
task_type="demo_task",
|
||||
target_id="tgt-1",
|
||||
input_id="42",
|
||||
input_title="t",
|
||||
status="success",
|
||||
error_msg=None,
|
||||
result_summary=None,
|
||||
)
|
||||
self.assertIsInstance(new_id, int)
|
||||
self.assertGreater(new_id, 0)
|
||||
|
||||
rows = tlr.list_task_logs(limit=10)
|
||||
self.assertTrue(any(r[0] == new_id for r in rows))
|
||||
|
||||
row = tlr.get_task_log_by_id(new_id)
|
||||
self.assertIsNotNone(row)
|
||||
self.assertEqual(int(row[0]), new_id)
|
||||
|
||||
missing = tlr.get_task_log_by_id(999_999)
|
||||
self.assertIsNone(missing)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user