Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 98f057e26b | |||
| 8878a90315 | |||
| 2ecb1172f4 | |||
| d2c8c3240c | |||
| 2c9f610a8e | |||
| 2c9bf56054 | |||
| 633ecff4e2 | |||
| 46bc573777 |
18
.env.example
18
.env.example
@@ -33,21 +33,19 @@ HUMAN_WAIT_TIMEOUT=180 # 单位:秒;来不及处理时可调
|
|||||||
|
|
||||||
# 以下为可选目录配置。一般不用改;取消注释并填写后,才覆盖默认子文件夹。
|
# 以下为可选目录配置。一般不用改;取消注释并填写后,才覆盖默认子文件夹。
|
||||||
# 填写相对路径时,相对于本技能的数据目录(不是当前工作目录)。
|
# 填写相对路径时,相对于本技能的数据目录(不是当前工作目录)。
|
||||||
|
# 用户可见只有 input(上行)与 output(下行);见 development/DATA_PATHS.md。
|
||||||
|
|
||||||
# 下载目录:技能保存下载文件的位置
|
# 输入目录:用户/Agent 放入的待处理文件(清单、CSV、产品表等)
|
||||||
# SKILL_DOWNLOAD_DIR=downloads # 相对本技能数据目录;留空或保持注释则用默认 downloads
|
# SKILL_INPUT_DIR=input # 相对本技能数据目录;留空或保持注释则用默认 input
|
||||||
|
|
||||||
# 导入目录:待导入文件放置的位置
|
# 输出目录:技能/宿主交给用户拿走的结果(视频、报表、导出文件等)
|
||||||
# SKILL_IMPORT_DIR=imports # 相对本技能数据目录;留空或保持注释则用默认 imports
|
# SKILL_OUTPUT_DIR=output # 相对本技能数据目录;留空或保持注释则用默认 output
|
||||||
|
|
||||||
# 导出目录:技能导出文件的保存位置
|
# 上传目录:待发布到外部平台的 staging(可选,不上侧栏)
|
||||||
# SKILL_EXPORT_DIR=exports # 相对本技能数据目录;留空或保持注释则用默认 exports
|
|
||||||
|
|
||||||
# 上传目录:待上传文件放置的位置
|
|
||||||
# SKILL_UPLOAD_DIR=uploads # 相对本技能数据目录;留空或保持注释则用默认 uploads
|
# SKILL_UPLOAD_DIR=uploads # 相对本技能数据目录;留空或保持注释则用默认 uploads
|
||||||
|
|
||||||
# 缓存目录:临时缓存文件的位置
|
# 缓存目录:临时缓存文件的位置(不上侧栏)
|
||||||
# SKILL_CACHE_DIR=cache # 相对本技能数据目录;留空或保持注释则用默认 cache
|
# SKILL_CACHE_DIR=cache # 相对本技能数据目录;留空或保持注释则用默认 cache
|
||||||
|
|
||||||
# 临时目录:运行中临时文件的位置
|
# 临时目录:运行中临时文件的位置(不上侧栏)
|
||||||
# SKILL_TEMP_DIR=temp # 相对本技能数据目录;留空或保持注释则用默认 temp
|
# SKILL_TEMP_DIR=temp # 相对本技能数据目录;留空或保持注释则用默认 temp
|
||||||
|
|||||||
33
CHANGELOG.md
33
CHANGELOG.md
@@ -9,6 +9,39 @@
|
|||||||
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
|
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
|
||||||
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
|
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
|
||||||
|
|
||||||
|
## 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
|
## 1.0.65
|
||||||
|
|
||||||
- `replaces` create/update 可复用表列 lookup:字段名对齐列名,或使用 `bind.columnMapping` / 字段 `column`
|
- `replaces` create/update 可复用表列 lookup:字段名对齐列名,或使用 `bind.columnMapping` / 字段 `column`
|
||||||
|
|||||||
4
SKILL.md
4
SKILL.md
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: 技能开发模板(通用业务版)
|
name: 技能开发模板(通用业务版)
|
||||||
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
|
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
|
||||||
version: 1.0.65
|
version: 1.0.71
|
||||||
author: 深圳匠厂科技有限公司
|
author: 深圳匠厂科技有限公司
|
||||||
metadata:
|
metadata:
|
||||||
openclaw:
|
openclaw:
|
||||||
@@ -32,7 +32,7 @@ allowed-tools:
|
|||||||
### 架构铁律(先读)
|
### 架构铁律(先读)
|
||||||
|
|
||||||
- **单业务内核、多入口**:数据管理 / 定时任务 / Agent / 技能详情 / 直接 CLI 必须复用同一 domain service;禁止按入口复制业务逻辑,禁止按 `source` 分叉业务行为。
|
- **单业务内核、多入口**:数据管理 / 定时任务 / Agent / 技能详情 / 直接 CLI 必须复用同一 domain service;禁止按入口复制业务逻辑,禁止按 `source` 分叉业务行为。
|
||||||
- **`placements`**:只决定 Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail)。
|
- **`placements`**:只决定 Action **出现在哪里**(toolbar / row / batch / cron / agent / skill-detail)。数据管理技能按钮顺序见 `actions` **数组相对顺序**(详见 `references/ACTIONS.md`),无独立 sortOrder。
|
||||||
- **`bind.tables`**:只决定数据管理出现在哪些表;含 `toolbar` 时**必须**显式非空绑定。
|
- **`bind.tables`**:只决定数据管理出现在哪些表;含 `toolbar` 时**必须**显式非空绑定。
|
||||||
- **`executionProfile`**:只决定执行方式——`sync` 当场返回结果,`async` 进**任务中心**。与 placements / 入口**无关**。心智上多数为 sync,仅长任务 / RPA 标 async;宿主省略该字段时按 sync。每个 Action **仍必须**显式写 `sync` 或 `async`。
|
- **`executionProfile`**:只决定执行方式——`sync` 当场返回结果,`async` 进**任务中心**。与 placements / 入口**无关**。心智上多数为 sync,仅长任务 / RPA 标 async;宿主省略该字段时按 sync。每个 Action **仍必须**显式写 `sync` 或 `async`。
|
||||||
- **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;特殊业务报告才另做 `export-*`。
|
- **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;特殊业务报告才另做 `export-*`。
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"minItems": 1,
|
"minItems": 1,
|
||||||
"items": { "$ref": "#/$defs/action" }
|
"items": { "$ref": "#/$defs/action" },
|
||||||
|
"description": "Action 列表。同一 placement(toolbar/row/batch)下,宿主按本数组中匹配项的相对顺序全部直接渲染技能按钮(无「更多操作」溢出);调整顺序即调整数组先后。无独立 sortOrder 字段。宿主内置刷新/导出/查看等不参与本顺序。表顶栏宽时左右横排,窄时左右分栏且栏内竖排。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"$defs": {
|
"$defs": {
|
||||||
@@ -166,7 +167,7 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"minItems": 0,
|
"minItems": 0,
|
||||||
"items": { "$ref": "#/$defs/placement" },
|
"items": { "$ref": "#/$defs/placement" },
|
||||||
"description": "决定 Action 出现在哪里;与 executionProfile 正交。仅 replaces 接管默认 CRUD 时可为空数组。"
|
"description": "决定 Action 出现在哪里;与 executionProfile 正交。仅 replaces 接管默认 CRUD 时可为空数组。数据管理 toolbar/row/batch 的按钮顺序由顶层 actions 数组相对顺序决定;表顶栏全部直接展示(无溢出菜单),不在本字段配置排序或截断。"
|
||||||
},
|
},
|
||||||
"replaces": { "$ref": "#/$defs/mutationKind" },
|
"replaces": { "$ref": "#/$defs/mutationKind" },
|
||||||
"entrypoint": { "$ref": "#/$defs/entrypoint" },
|
"entrypoint": { "$ref": "#/$defs/entrypoint" },
|
||||||
|
|||||||
@@ -108,15 +108,15 @@ HUMAN_WAIT_TIMEOUT=180 # 单位:秒;来不及处理时可调
|
|||||||
|
|
||||||
四档运行模式的**实现与测试约定**见 [`ADAPTER.md`](ADAPTER.md);录屏管线见 [`RPA.md`](RPA.md)。这些内容**不要**写回 `.env.example` 注释。
|
四档运行模式的**实现与测试约定**见 [`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/`(下行)**。技能写入的业务产出默认落在 `output/`;用户放入的待处理文件落在 `input/`。须通过 `util.runtime_paths.resolve_data_path()` 或 `get_input_dir()` / `get_output_dir()` 解析,**禁止** `os.path.abspath(config.get(...))` 相对 CWD。
|
||||||
|
|
||||||
- 权威说明(开发):[`DATA_PATHS.md`](DATA_PATHS.md)
|
- 权威说明(开发):[`DATA_PATHS.md`](DATA_PATHS.md)
|
||||||
- `.env.example` 中路径类配置**留空或保持注释即用默认**;若写相对路径,必须相对 **skill 数据目录**,**禁止** `./outputs/...` 这类相对 workspace 的写法
|
- `.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`)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 用户数据目录与文件路径黄金标准
|
# 用户数据目录与文件路径黄金标准
|
||||||
|
|
||||||
> 本文是 skill **读写本地文件** 的权威约定。凡涉及下载、导入、导出、缓存、RPA 存证等路径,复制模板后须遵守本文;Agent 实现业务 skill 时也应先读本文。
|
> 本文是 skill **读写本地文件** 的权威约定。凡涉及输入、输出、缓存、RPA 存证等路径,复制模板后须遵守本文;Agent 实现业务 skill 时也应先读本文。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -30,11 +30,17 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
|||||||
|
|
||||||
## 2. 标准子目录树
|
## 2. 标准子目录树
|
||||||
|
|
||||||
|
用户可见目录(数据管理侧栏只挂这两个)按 **上行 / 下行** 划分:
|
||||||
|
|
||||||
|
| 子目录 | 方向 | 用途 | 默认 helper |
|
||||||
|
|--------|------|------|-------------|
|
||||||
|
| `input/` | 上行 | 用户/Agent 放入的待处理材料(urls.txt、batch.json、CSV、产品表…) | `get_input_dir()` |
|
||||||
|
| `output/` | 下行 | 技能/宿主交给用户拿走的结果(视频、回单、xlsx/csv/json…) | `get_output_dir()` |
|
||||||
|
|
||||||
|
内部 / 可选目录(**不上侧栏**):
|
||||||
|
|
||||||
| 子目录 | 用途 | 默认 helper |
|
| 子目录 | 用途 | 默认 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()` |
|
| `uploads/` | 待发布到外部平台的 staging(可选) | `get_uploads_dir()` |
|
||||||
| `cache/` | 可再生缓存(页面快照、解析中间结果) | `get_cache_dir()` |
|
| `cache/` | 可再生缓存(页面快照、解析中间结果) | `get_cache_dir()` |
|
||||||
| `temp/` | 单次任务 scratch,任务结束可删 | `get_temp_dir()` |
|
| `temp/` | 单次任务 scratch,任务结束可删 | `get_temp_dir()` |
|
||||||
@@ -45,6 +51,15 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
|||||||
|
|
||||||
复制后若业务不需要某目录,可不创建;**但一旦写入该类文件,必须走对应 helper**。
|
复制后若业务不需要某目录,可不创建;**但一旦写入该类文件,必须走对应 helper**。
|
||||||
|
|
||||||
|
**开发者硬规则:**
|
||||||
|
|
||||||
|
- 用户/Agent **放进来**的文件 → `input/`(`get_input_dir()`)
|
||||||
|
- 技能/宿主 **交给用户拿走**的文件 → `output/`(`get_output_dir()`)
|
||||||
|
- 禁止再引入业务侧栏第三目录(如旧的 `downloads` / `imports` / `exports` 三分法)
|
||||||
|
- 缓存、临时、RPA 录屏用 `cache` / `temp` / `videos`,不要当成用户可见的「输出目录」
|
||||||
|
|
||||||
|
宿主约定(与本模板对齐):工具栏「导入」默认从 `input/` 取文件;「导出」默认写到 `output/`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 两类路径
|
## 3. 两类路径
|
||||||
@@ -54,6 +69,7 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
|||||||
技能**创建或更新**的文件:下载结果、导出报表、失败截图、队列 sidecar 等。
|
技能**创建或更新**的文件:下载结果、导出报表、失败截图、队列 sidecar 等。
|
||||||
|
|
||||||
- **默认**必须在 `{skill_data_dir}/<子目录>/` 下。
|
- **默认**必须在 `{skill_data_dir}/<子目录>/` 下。
|
||||||
|
- 用户可拿走的业务产出 → **`output/`**。
|
||||||
- **必须**通过 `resolve_data_path()` 或 `get_*_dir()` 解析,**禁止** `os.path.abspath(config.get(...))` 相对 CWD。
|
- **必须**通过 `resolve_data_path()` 或 `get_*_dir()` 解析,**禁止** `os.path.abspath(config.get(...))` 相对 CWD。
|
||||||
|
|
||||||
### B. 用户显式输入(user-owned)
|
### B. 用户显式输入(user-owned)
|
||||||
@@ -61,7 +77,7 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
|||||||
用户在 CLI / Agent 对话中给出的**已有文件**绝对路径,例如「发布这个视频 `D:\素材\x.mp4`」。
|
用户在 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 +88,8 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
|||||||
```ini
|
```ini
|
||||||
# ── 数据目录子路径(见 development/DATA_PATHS.md)──
|
# ── 数据目录子路径(见 development/DATA_PATHS.md)──
|
||||||
# 相对路径相对于 {JIANGCHANG_DATA_ROOT}/{USER_ID}/{slug}/,不是 workspace
|
# 相对路径相对于 {JIANGCHANG_DATA_ROOT}/{USER_ID}/{slug}/,不是 workspace
|
||||||
# SKILL_DOWNLOAD_DIR=downloads/videos
|
# SKILL_INPUT_DIR=input
|
||||||
# SKILL_IMPORT_DIR=imports
|
# SKILL_OUTPUT_DIR=output/videos
|
||||||
# SKILL_EXPORT_DIR=exports
|
|
||||||
# SKILL_UPLOAD_DIR=uploads
|
# SKILL_UPLOAD_DIR=uploads
|
||||||
# SKILL_CACHE_DIR=cache
|
# SKILL_CACHE_DIR=cache
|
||||||
# SKILL_TEMP_DIR=temp
|
# SKILL_TEMP_DIR=temp
|
||||||
@@ -83,8 +98,8 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
|||||||
| 规则 | 说明 |
|
| 规则 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 未配置 | `{skill_data_dir}/{default_subdir}` |
|
| 未配置 | `{skill_data_dir}/{default_subdir}` |
|
||||||
| 相对值 `downloads/videos` | `{skill_data_dir}/downloads/videos` |
|
| 相对值 `output/videos` | `{skill_data_dir}/output/videos` |
|
||||||
| 绝对值 `D:\nas\exports` | 高级用户/NAS 挂载,原样使用 |
|
| 绝对值 `D:\nas\output` | 高级用户/NAS 挂载,原样使用 |
|
||||||
| **禁止** `./outputs/...` | 会随 CWD 漂移,不得作为 `.env.example` 活跃配置行 |
|
| **禁止** `./outputs/...` | 会随 CWD 漂移,不得作为 `.env.example` 活跃配置行 |
|
||||||
|
|
||||||
业务专属路径 env key 须带 **`SKILL_` 前缀**(或技能 slug 前缀),避免污染全局命名空间。
|
业务专属路径 env key 须带 **`SKILL_` 前缀**(或技能 slug 前缀),避免污染全局命名空间。
|
||||||
@@ -95,20 +110,20 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
from util.runtime_paths import (
|
from util.runtime_paths import (
|
||||||
get_downloads_dir,
|
get_input_dir,
|
||||||
get_imports_dir,
|
get_output_dir,
|
||||||
resolve_data_path,
|
resolve_data_path,
|
||||||
resolve_input_path,
|
resolve_input_path,
|
||||||
list_resolved_data_paths,
|
list_resolved_data_paths,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 下载保存(A 类)
|
# 下行产出(A 类)——视频、报表等
|
||||||
dest = os.path.join(get_downloads_dir(), f"{vid}.mp4")
|
dest = os.path.join(get_output_dir(), f"{vid}.mp4")
|
||||||
|
|
||||||
# 或带 env 覆盖
|
# 或带 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)
|
manifest = resolve_input_path(args.input_path)
|
||||||
|
|
||||||
# health 诊断
|
# health 诊断
|
||||||
@@ -123,6 +138,8 @@ os.path.abspath(config.get("DOWNLOAD_OUTPUT_DIR"))
|
|||||||
|
|
||||||
# ❌ .env.example 里写 ./outputs/videos
|
# ❌ .env.example 里写 ./outputs/videos
|
||||||
DOWNLOAD_OUTPUT_DIR=./outputs/videos
|
DOWNLOAD_OUTPUT_DIR=./outputs/videos
|
||||||
|
|
||||||
|
# ❌ 再拆 downloads / imports / exports 三个用户目录
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -138,7 +155,7 @@ DOWNLOAD_OUTPUT_DIR=./outputs/videos
|
|||||||
- 失败截图、录屏中间文件:`rpa-artifacts/{batch_id}/`(见 [`RPA.md`](RPA.md) §5.1)
|
- 失败截图、录屏中间文件:`rpa-artifacts/{batch_id}/`(见 [`RPA.md`](RPA.md) §5.1)
|
||||||
- 录屏成片:`videos/`(见 [`RPA.md`](RPA.md) §5.3)
|
- 录屏成片:`videos/`(见 [`RPA.md`](RPA.md) §5.3)
|
||||||
|
|
||||||
业务 `downloads/` 与 RPA `videos/` **分工不同**:前者是业务产出文件,后者是操作过程录屏。
|
业务 `output/` 与 RPA `videos/` **分工不同**:前者是业务产出文件,后者是操作过程录屏。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,8 @@
|
|||||||
|
|
||||||
未完成第 2~6 步时,代码只能保留明确的 `TODO: NEED_REAL_UI_INSPECTION`,不得写“看起来可能正确”的 selector。
|
未完成第 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. 元素清单
|
## 3. 元素清单
|
||||||
|
|
||||||
每个关键输入、按钮、列表、弹窗、成功状态和错误状态都必须登记。推荐 YAML:
|
每个关键输入、按钮、列表、弹窗、成功状态和错误状态都必须登记。推荐 YAML:
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ RPA 录屏成片(`RpaVideoSession`)、ffmpeg 路径、背景音乐探测均
|
|||||||
{...}/{skill_slug}.db
|
{...}/{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`)。
|
- 业务表使用英文 snake_case;中文表名/字段名写入 `_jiangchang_tables` / `_jiangchang_columns`(见 `references/SCHEMA.md`)。
|
||||||
- 界面字段顺序由 `CREATE TABLE` 列定义顺序决定,**不要**维护 `display_order` 来调整顺序。
|
- 界面字段顺序由 `CREATE TABLE` 列定义顺序决定,**不要**维护 `display_order` 来调整顺序。
|
||||||
|
|||||||
@@ -38,7 +38,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 本身无默认墙钟上限 | ✅ 进入 |
|
| **`async`**(须显式声明) | 后台 spawn Job,立即返回 `jobId`;`emit` / `checkpoint` / `finish`;长任务应支持暂停/停止/取消检查 | Job 本身无默认墙钟上限 | ✅ 进入 |
|
||||||
|
|
||||||
直接执行 CLI:终端等待进程结束。宿主经 Action 调用同一 CLI:再按 `executionProfile` 分流。技能不得自建任务中心。
|
直接执行 CLI:终端等待进程结束。宿主经 Action 调用同一 CLI:再按 `executionProfile` 分流。技能不得自建任务中心。
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
| 场景 | 建议 |
|
| 场景 | 建议 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 浏览器 / Playwright / RPA 主路径 | `async` + 建议 `confirmation` |
|
| 浏览器 / Playwright / RPA 主路径 | `async` + 建议 `confirmation` |
|
||||||
| 单次可能超过 ~20s 的网络/下载/同步 | `async` |
|
| 单次可能接近或超过约 60s 的网络/下载/同步 | `async` |
|
||||||
| `health` / `version` / `config-path` / 纯统计 / 秒级查询 | `sync` |
|
| `health` / `version` / `config-path` / 纯统计 / 秒级查询 | `sync` |
|
||||||
| 任意 placement(含 toolbar) | 允许 sync 或 async;以 manifest 为准 |
|
| 任意 placement(含 toolbar) | 允许 sync 或 async;以 manifest 为准 |
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ cmd_stats() # 只读 sync:JSON stdout,无 job_context
|
|||||||
| 能力 | 约定 |
|
| 能力 | 约定 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| 队列表 | `status`: `pending` → `processing` → `success` / `failed` |
|
| 队列表 | `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` |
|
| 消费 | `run --pick N`,默认仅 `pending` |
|
||||||
| 重试 | `reset` 或数据管理改 `pending`;**failed 不要自动重试** |
|
| 重试 | `reset` 或数据管理改 `pending`;**failed 不要自动重试** |
|
||||||
| 批次 | 可选 `batch_id`,便于筛选与任务中心对照 |
|
| 批次 | 可选 `batch_id`,便于筛选与任务中心对照 |
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ Action 是**可选能力**:
|
|||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `schemaVersion` | 是 | 当前固定为 `1` |
|
| `schemaVersion` | 是 | 当前固定为 `1` |
|
||||||
| `skill` | 是 | 必须与 `SKILL.md` 的 `metadata.openclaw.slug` 一致 |
|
| `skill` | 是 | 必须与 `SKILL.md` 的 `metadata.openclaw.slug` 一致 |
|
||||||
| `actions` | 是 | Action 数组,每项 `id` 唯一 |
|
| `actions` | 是 | Action 数组,每项 `id` 唯一;**数组相对顺序**决定数据管理 toolbar / row / batch 技能按钮的展示顺序(见下节) |
|
||||||
|
|
||||||
模板最小示例见 [`assets/actions.json`](../assets/actions.json);JSON Schema 见 [`assets/schemas/skill-actions.schema.json`](../assets/schemas/skill-actions.schema.json)。
|
模板最小示例见 [`assets/actions.json`](../assets/actions.json);JSON Schema 见 [`assets/schemas/skill-actions.schema.json`](../assets/schemas/skill-actions.schema.json)。
|
||||||
|
|
||||||
@@ -358,6 +358,29 @@ HINT: <next step>
|
|||||||
| `agent` | Agent 可直接调用(`run_skill_action`) |
|
| `agent` | Agent 可直接调用(`run_skill_action`) |
|
||||||
| `cron` | 定时任务「技能直调」 |
|
| `cron` | 定时任务「技能直调」 |
|
||||||
|
|
||||||
|
### 数据管理技能按钮顺序(宿主正式契约)
|
||||||
|
|
||||||
|
宿主**没有**单独的 `sortOrder` / `uiOrder` 字段。同一 `placement`(`toolbar` / `row` / `batch`)下,技能按钮的展示顺序 = `actions` 数组中**匹配该项 placement(且通过 `bind.tables` 过滤)的相对先后**。
|
||||||
|
|
||||||
|
规则:
|
||||||
|
|
||||||
|
1. 想调整顺序 → **调整 `actions` 数组先后**(不必改 `id` / `label`)。
|
||||||
|
2. 无关 placement 的 Action(如仅 `skill-detail` 的 `health`)夹在中间**不影响** toolbar/row 的相对顺序;宿主按匹配子集保序渲染。
|
||||||
|
3. 同一 Action 可同时挂多个 placement;它在各入口的位置,分别是该入口匹配子集里的相对位置。
|
||||||
|
4. **宿主内置按钮不参与本契约、位置固定**:
|
||||||
|
- 表顶栏右侧:删除(有勾选时)/ 刷新 / 新增 / 导入 / 导出 —— 技能 toolbar/batch 按钮在左侧,不能与右侧内置按钮混排。
|
||||||
|
- 行内「操作」列:技能 `row` 按钮在前;其后固定为宿主「查看」→(非只读)「编辑」→「删除」。
|
||||||
|
5. **表顶栏全部直接展示,无「更多操作」溢出菜单**:匹配到的 toolbar/batch 技能按钮一律渲染(顺序仍按本契约)。
|
||||||
|
6. **表顶栏宽窄布局(宿主行为,技能无需配置)**:
|
||||||
|
- 够宽:左右各横排一行(左技能按钮 + 右宿主工具)。
|
||||||
|
- 不够宽:保持左右分栏,栏内按钮上下竖排(左栏技能、右栏刷新/导出等)。
|
||||||
|
|
||||||
|
禁止:
|
||||||
|
|
||||||
|
- 臆造 `sortOrder` / `order` / `priority` 等字段指望宿主排序(模板 Schema `additionalProperties: false` 会拒收)。
|
||||||
|
- 依赖或宣传「更多操作」溢出菜单(宿主已取消该能力)。
|
||||||
|
- 把「表格行数据默认排序」与按钮顺序混淆——行排序见 [`SCHEMA.md`](SCHEMA.md) 的 `default_sort_json`。
|
||||||
|
|
||||||
### Action 类型 × placements(常见示例,不是限制)
|
### Action 类型 × placements(常见示例,不是限制)
|
||||||
|
|
||||||
下表只表示**常见用法示例**。实际 `placements` 以 manifest 配置为准;**`sync` / `async` 不影响展示入口**。已声明的入口都要在宿主自测,不要只测其中一条。
|
下表只表示**常见用法示例**。实际 `placements` 以 manifest 配置为准;**`sync` / `async` 不影响展示入口**。已声明的入口都要在宿主自测,不要只测其中一条。
|
||||||
|
|||||||
@@ -133,6 +133,38 @@ metadata:
|
|||||||
| `sort_order` | 否 | 兼容保留字段,不用于替代物理字段顺序 |
|
| `sort_order` | 否 | 兼容保留字段,不用于替代物理字段顺序 |
|
||||||
| `visible` | 是 | `1` 在宿主数据管理中展示 / `0` 隐藏 |
|
| `visible` | 是 | `1` 在宿主数据管理中展示 / `0` 隐藏 |
|
||||||
| `readonly` | 是 | `1` 整张表只读;`0` 允许宿主根据字段权限执行写操作 |
|
| `readonly` | 是 | `1` 整张表只读;`0` 允许宿主根据字段权限执行写操作 |
|
||||||
|
| `default_sort_json` | 否 | 数据管理**默认行排序**(见下节);空则宿主用内置默认 |
|
||||||
|
|
||||||
|
#### 表级默认排序 `default_sort_json`(宿主支持)
|
||||||
|
|
||||||
|
技能可声明该表在数据管理中的默认排序;宿主优先采用,未声明或声明无效时回退:
|
||||||
|
|
||||||
|
`updated_at DESC` → `created_at DESC` → 主键 DESC。
|
||||||
|
|
||||||
|
JSON 数组(可多字段,按顺序):
|
||||||
|
|
||||||
|
```json
|
||||||
|
[{"column":"sent_at","direction":"desc"},{"column":"id","direction":"desc"}]
|
||||||
|
```
|
||||||
|
|
||||||
|
规则:
|
||||||
|
|
||||||
|
- `column` 必须是该表真实列名;敏感列会被宿主忽略
|
||||||
|
- `direction` 仅 `asc` / `desc`(缺省按 `desc`)
|
||||||
|
- 用户在 UI 上手动改排序后,以用户选择为准;声明只影响未选择时的默认
|
||||||
|
- 通过 `upsert_table_metadata(..., default_sort_json=...)` 写入;旧库由 `init_display_metadata_tables` 幂等 `ALTER` 补列
|
||||||
|
|
||||||
|
示例(消息时间线):
|
||||||
|
|
||||||
|
```python
|
||||||
|
upsert_table_metadata(
|
||||||
|
cursor,
|
||||||
|
table_name="messages",
|
||||||
|
display_name="消息",
|
||||||
|
readonly=0,
|
||||||
|
default_sort_json='[{"column":"sent_at","direction":"desc"},{"column":"id","direction":"desc"}]',
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
禁止:
|
禁止:
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ CREATE TABLE IF NOT EXISTS _jiangchang_tables (
|
|||||||
description TEXT,
|
description TEXT,
|
||||||
sort_order INTEGER,
|
sort_order INTEGER,
|
||||||
visible INTEGER NOT NULL DEFAULT 1,
|
visible INTEGER NOT NULL DEFAULT 1,
|
||||||
readonly INTEGER NOT NULL DEFAULT 0
|
readonly INTEGER NOT NULL DEFAULT 0,
|
||||||
|
default_sort_json TEXT
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -82,23 +83,50 @@ TASK_LOGS_COLUMN_METADATA: tuple[dict[str, object], ...] = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_jiangchang_tables_columns(cursor) -> None:
|
||||||
|
"""旧库补列:CREATE IF NOT EXISTS 不会给已有表加新字段。"""
|
||||||
|
cursor.execute("PRAGMA table_info('_jiangchang_tables')")
|
||||||
|
cols = {str(row[1]) for row in cursor.fetchall()}
|
||||||
|
if "default_sort_json" not in cols:
|
||||||
|
cursor.execute("ALTER TABLE _jiangchang_tables ADD COLUMN default_sort_json TEXT")
|
||||||
|
|
||||||
|
|
||||||
def init_display_metadata_tables(cursor) -> None:
|
def init_display_metadata_tables(cursor) -> None:
|
||||||
cursor.execute(_JIANGCHANG_TABLES_DDL)
|
cursor.execute(_JIANGCHANG_TABLES_DDL)
|
||||||
cursor.execute(_JIANGCHANG_COLUMNS_DDL)
|
cursor.execute(_JIANGCHANG_COLUMNS_DDL)
|
||||||
|
_ensure_jiangchang_tables_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,
|
||||||
|
visible: int = 1,
|
||||||
|
readonly: int = 0,
|
||||||
|
default_sort_json: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""写入表级元数据。
|
||||||
|
|
||||||
|
default_sort_json: 宿主数据管理默认排序,JSON 数组,例如
|
||||||
|
'[{"column":"sent_at","direction":"desc"},{"column":"id","direction":"desc"}]'
|
||||||
|
为空则宿主回退到 updated_at → created_at → 主键。
|
||||||
|
"""
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO _jiangchang_tables (table_name, display_name, description, visible, readonly)
|
INSERT INTO _jiangchang_tables (
|
||||||
VALUES (?, ?, ?, ?, ?)
|
table_name, display_name, description, visible, readonly, default_sort_json
|
||||||
|
)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(table_name) DO UPDATE SET
|
ON CONFLICT(table_name) DO UPDATE SET
|
||||||
display_name = excluded.display_name,
|
display_name = excluded.display_name,
|
||||||
description = excluded.description,
|
description = excluded.description,
|
||||||
visible = excluded.visible,
|
visible = excluded.visible,
|
||||||
readonly = excluded.readonly
|
readonly = excluded.readonly,
|
||||||
|
default_sort_json = excluded.default_sort_json
|
||||||
""",
|
""",
|
||||||
(table_name, display_name, description, visible, readonly),
|
(table_name, display_name, description, visible, readonly, default_sort_json),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。"""
|
"""技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。"""
|
||||||
|
|
||||||
SKILL_SLUG = "your-skill-slug"
|
SKILL_SLUG = "your-skill-slug"
|
||||||
SKILL_VERSION = "1.0.61"
|
SKILL_VERSION = "1.0.71"
|
||||||
LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug"
|
LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug"
|
||||||
PLATFORM_KIT_MIN_VERSION = "1.2.2"
|
PLATFORM_KIT_MIN_VERSION = "1.2.2"
|
||||||
|
|||||||
@@ -10,18 +10,19 @@ from util.constants import SKILL_SLUG
|
|||||||
|
|
||||||
_SCRIPTS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
_SCRIPTS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
STANDARD_SUBDIR_DOWNLOADS = "downloads"
|
# 用户可见的上行/下行目录(数据管理侧栏只挂这两个)
|
||||||
STANDARD_SUBDIR_IMPORTS = "imports"
|
STANDARD_SUBDIR_INPUT = "input"
|
||||||
STANDARD_SUBDIR_EXPORTS = "exports"
|
STANDARD_SUBDIR_OUTPUT = "output"
|
||||||
|
|
||||||
|
# 内部/可选目录(不上侧栏)
|
||||||
STANDARD_SUBDIR_UPLOADS = "uploads"
|
STANDARD_SUBDIR_UPLOADS = "uploads"
|
||||||
STANDARD_SUBDIR_CACHE = "cache"
|
STANDARD_SUBDIR_CACHE = "cache"
|
||||||
STANDARD_SUBDIR_TEMP = "temp"
|
STANDARD_SUBDIR_TEMP = "temp"
|
||||||
STANDARD_SUBDIR_RPA_ARTIFACTS = "rpa-artifacts"
|
STANDARD_SUBDIR_RPA_ARTIFACTS = "rpa-artifacts"
|
||||||
STANDARD_SUBDIR_VIDEOS = "videos"
|
STANDARD_SUBDIR_VIDEOS = "videos"
|
||||||
|
|
||||||
CONFIG_KEY_DOWNLOAD_DIR = "SKILL_DOWNLOAD_DIR"
|
CONFIG_KEY_INPUT_DIR = "SKILL_INPUT_DIR"
|
||||||
CONFIG_KEY_IMPORT_DIR = "SKILL_IMPORT_DIR"
|
CONFIG_KEY_OUTPUT_DIR = "SKILL_OUTPUT_DIR"
|
||||||
CONFIG_KEY_EXPORT_DIR = "SKILL_EXPORT_DIR"
|
|
||||||
CONFIG_KEY_UPLOAD_DIR = "SKILL_UPLOAD_DIR"
|
CONFIG_KEY_UPLOAD_DIR = "SKILL_UPLOAD_DIR"
|
||||||
CONFIG_KEY_CACHE_DIR = "SKILL_CACHE_DIR"
|
CONFIG_KEY_CACHE_DIR = "SKILL_CACHE_DIR"
|
||||||
CONFIG_KEY_TEMP_DIR = "SKILL_TEMP_DIR"
|
CONFIG_KEY_TEMP_DIR = "SKILL_TEMP_DIR"
|
||||||
@@ -81,27 +82,25 @@ def resolve_data_path(
|
|||||||
|
|
||||||
|
|
||||||
def resolve_input_path(raw_path: str, *, create_parent: bool = False) -> str:
|
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()
|
text = (raw_path or "").strip()
|
||||||
if not text:
|
if not text:
|
||||||
return text
|
return text
|
||||||
expanded = os.path.expanduser(text)
|
expanded = os.path.expanduser(text)
|
||||||
if os.path.isabs(expanded):
|
if os.path.isabs(expanded):
|
||||||
return os.path.abspath(expanded)
|
return os.path.abspath(expanded)
|
||||||
imports_dir = get_imports_dir(create=create_parent)
|
input_dir = get_input_dir(create=create_parent)
|
||||||
return os.path.abspath(os.path.join(imports_dir, os.path.normpath(expanded)))
|
return os.path.abspath(os.path.join(input_dir, os.path.normpath(expanded)))
|
||||||
|
|
||||||
|
|
||||||
def get_downloads_dir(*, create: bool = True) -> str:
|
def get_input_dir(*, create: bool = True) -> str:
|
||||||
return resolve_data_path(CONFIG_KEY_DOWNLOAD_DIR, STANDARD_SUBDIR_DOWNLOADS, create=create)
|
"""上行:用户/Agent 放入的待处理文件(清单、CSV、产品表等)。"""
|
||||||
|
return resolve_data_path(CONFIG_KEY_INPUT_DIR, STANDARD_SUBDIR_INPUT, create=create)
|
||||||
|
|
||||||
|
|
||||||
def get_imports_dir(*, create: bool = True) -> str:
|
def get_output_dir(*, create: bool = True) -> str:
|
||||||
return resolve_data_path(CONFIG_KEY_IMPORT_DIR, STANDARD_SUBDIR_IMPORTS, create=create)
|
"""下行:技能/宿主交给用户拿走的文件(视频、报表、导出 CSV 等)。"""
|
||||||
|
return resolve_data_path(CONFIG_KEY_OUTPUT_DIR, STANDARD_SUBDIR_OUTPUT, 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_uploads_dir(*, create: bool = True) -> str:
|
def get_uploads_dir(*, create: bool = True) -> str:
|
||||||
@@ -135,9 +134,8 @@ def list_resolved_data_paths() -> dict[str, str]:
|
|||||||
"""供 health / config-path 输出已解析的数据子目录(不创建目录)。"""
|
"""供 health / config-path 输出已解析的数据子目录(不创建目录)。"""
|
||||||
return {
|
return {
|
||||||
"skill_data_dir": get_skill_data_dir(),
|
"skill_data_dir": get_skill_data_dir(),
|
||||||
"downloads_dir": get_downloads_dir(create=False),
|
"input_dir": get_input_dir(create=False),
|
||||||
"imports_dir": get_imports_dir(create=False),
|
"output_dir": get_output_dir(create=False),
|
||||||
"exports_dir": get_exports_dir(create=False),
|
|
||||||
"uploads_dir": get_uploads_dir(create=False),
|
"uploads_dir": get_uploads_dir(create=False),
|
||||||
"cache_dir": get_cache_dir(create=False),
|
"cache_dir": get_cache_dir(create=False),
|
||||||
"temp_dir": get_temp_dir(create=False),
|
"temp_dir": get_temp_dir(create=False),
|
||||||
|
|||||||
@@ -510,8 +510,17 @@ class TestActionsManifest(unittest.TestCase):
|
|||||||
"executionProfile",
|
"executionProfile",
|
||||||
"interaction",
|
"interaction",
|
||||||
"confirmation",
|
"confirmation",
|
||||||
|
"数据管理技能按钮顺序",
|
||||||
|
"数组相对顺序",
|
||||||
|
"全部直接展示",
|
||||||
|
"左右分栏",
|
||||||
):
|
):
|
||||||
self.assertIn(marker, text, msg=f"ACTIONS.md missing {marker!r}")
|
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",
|
||||||
|
)
|
||||||
for forbidden in (
|
for forbidden in (
|
||||||
"数据管理 toolbar " + "只挂",
|
"数据管理 toolbar " + "只挂",
|
||||||
"toolbar " + "只放 async",
|
"toolbar " + "只放 async",
|
||||||
|
|||||||
@@ -89,11 +89,11 @@ class TestRuntimePaths(unittest.TestCase):
|
|||||||
import util.runtime_paths as rp
|
import util.runtime_paths as rp
|
||||||
|
|
||||||
importlib.reload(rp)
|
importlib.reload(rp)
|
||||||
path = rp.resolve_data_path(None, "downloads", create=True)
|
path = rp.resolve_data_path(None, "output", create=True)
|
||||||
norm = os.path.normcase(os.path.normpath(path))
|
norm = os.path.normcase(os.path.normpath(path))
|
||||||
norm_tmp = os.path.normcase(os.path.normpath(tmp))
|
norm_tmp = os.path.normcase(os.path.normpath(tmp))
|
||||||
self.assertTrue(norm.startswith(norm_tmp))
|
self.assertTrue(norm.startswith(norm_tmp))
|
||||||
self.assertTrue(norm.endswith(os.path.normcase(os.path.join("downloads"))))
|
self.assertTrue(norm.endswith(os.path.normcase(os.path.join("output"))))
|
||||||
|
|
||||||
def test_resolve_data_path_relative_uses_data_dir_not_cwd(self) -> None:
|
def test_resolve_data_path_relative_uses_data_dir_not_cwd(self) -> None:
|
||||||
with IsolatedDataRoot() as tmp:
|
with IsolatedDataRoot() as tmp:
|
||||||
@@ -102,20 +102,20 @@ class TestRuntimePaths(unittest.TestCase):
|
|||||||
import util.runtime_paths as rp
|
import util.runtime_paths as rp
|
||||||
|
|
||||||
importlib.reload(rp)
|
importlib.reload(rp)
|
||||||
os.environ["SKILL_DOWNLOAD_DIR"] = "downloads/videos"
|
os.environ["SKILL_OUTPUT_DIR"] = "output/videos"
|
||||||
from jiangchang_skill_core import config
|
from jiangchang_skill_core import config
|
||||||
|
|
||||||
config.reset_cache()
|
config.reset_cache()
|
||||||
importlib.reload(rp)
|
importlib.reload(rp)
|
||||||
path = rp.resolve_data_path("SKILL_DOWNLOAD_DIR", "downloads", create=False)
|
path = rp.resolve_data_path("SKILL_OUTPUT_DIR", "output", create=False)
|
||||||
norm = os.path.normcase(os.path.normpath(path))
|
norm = os.path.normcase(os.path.normpath(path))
|
||||||
self.assertIn(os.path.normcase("downloads"), norm)
|
self.assertIn(os.path.normcase("output"), norm)
|
||||||
self.assertIn(os.path.normcase("videos"), norm)
|
self.assertIn(os.path.normcase("videos"), norm)
|
||||||
self.assertTrue(norm.startswith(os.path.normcase(os.path.normpath(tmp))))
|
self.assertTrue(norm.startswith(os.path.normcase(os.path.normpath(tmp))))
|
||||||
os.environ.pop("SKILL_DOWNLOAD_DIR", None)
|
os.environ.pop("SKILL_OUTPUT_DIR", None)
|
||||||
config.reset_cache()
|
config.reset_cache()
|
||||||
|
|
||||||
def test_resolve_input_path_relative_under_imports(self) -> None:
|
def test_resolve_input_path_relative_under_input(self) -> None:
|
||||||
with IsolatedDataRoot() as tmp:
|
with IsolatedDataRoot() as tmp:
|
||||||
import importlib
|
import importlib
|
||||||
|
|
||||||
@@ -124,9 +124,24 @@ class TestRuntimePaths(unittest.TestCase):
|
|||||||
importlib.reload(rp)
|
importlib.reload(rp)
|
||||||
resolved = rp.resolve_input_path("urls.txt", create_parent=True)
|
resolved = rp.resolve_input_path("urls.txt", create_parent=True)
|
||||||
norm = os.path.normcase(os.path.normpath(resolved))
|
norm = os.path.normcase(os.path.normpath(resolved))
|
||||||
self.assertIn(os.path.normcase("imports"), norm)
|
self.assertIn(os.path.normcase("input"), norm)
|
||||||
self.assertTrue(norm.startswith(os.path.normcase(os.path.normpath(tmp))))
|
self.assertTrue(norm.startswith(os.path.normcase(os.path.normpath(tmp))))
|
||||||
|
|
||||||
|
def test_get_input_and_output_dirs(self) -> None:
|
||||||
|
with IsolatedDataRoot() as tmp:
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
import util.runtime_paths as rp
|
||||||
|
|
||||||
|
importlib.reload(rp)
|
||||||
|
input_dir = rp.get_input_dir(create=True)
|
||||||
|
output_dir = rp.get_output_dir(create=True)
|
||||||
|
norm_tmp = os.path.normcase(os.path.normpath(tmp))
|
||||||
|
self.assertTrue(os.path.normcase(os.path.normpath(input_dir)).startswith(norm_tmp))
|
||||||
|
self.assertTrue(os.path.normcase(os.path.normpath(output_dir)).startswith(norm_tmp))
|
||||||
|
self.assertTrue(input_dir.replace("\\", "/").endswith("/input"))
|
||||||
|
self.assertTrue(output_dir.replace("\\", "/").endswith("/output"))
|
||||||
|
|
||||||
def test_list_resolved_data_paths_keys(self) -> None:
|
def test_list_resolved_data_paths_keys(self) -> None:
|
||||||
with IsolatedDataRoot():
|
with IsolatedDataRoot():
|
||||||
import importlib
|
import importlib
|
||||||
@@ -137,12 +152,14 @@ class TestRuntimePaths(unittest.TestCase):
|
|||||||
paths = rp.list_resolved_data_paths()
|
paths = rp.list_resolved_data_paths()
|
||||||
for key in (
|
for key in (
|
||||||
"skill_data_dir",
|
"skill_data_dir",
|
||||||
"downloads_dir",
|
"input_dir",
|
||||||
"imports_dir",
|
"output_dir",
|
||||||
"exports_dir",
|
|
||||||
):
|
):
|
||||||
self.assertIn(key, paths)
|
self.assertIn(key, paths)
|
||||||
self.assertTrue(paths[key])
|
self.assertTrue(paths[key])
|
||||||
|
self.assertNotIn("downloads_dir", paths)
|
||||||
|
self.assertNotIn("imports_dir", paths)
|
||||||
|
self.assertNotIn("exports_dir", paths)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
# skill-template 工具
|
# skill-template 工具
|
||||||
|
|
||||||
|
## UI 拾取(开发期)
|
||||||
|
|
||||||
|
`tools/ui_inspector` 已迁出本模板仓。请使用独立产品仓:
|
||||||
|
|
||||||
|
- Gitea:https://git.jc2009.com/client-jiangchang/jiangchang-ui-recorder
|
||||||
|
- 本机常见路径:`D:\OpenClaw\client-jiangchang\jiangchang-ui-recorder`
|
||||||
|
- 启动:`python run.py`
|
||||||
|
|
||||||
|
模板仓历史中仍保留删除前的归档提交(可按需从 Git 恢复旧副本);日常开发与运行以独立仓为准。
|
||||||
|
|
||||||
## 业务仓也可以直接 clone(来源 A)
|
## 业务仓也可以直接 clone(来源 A)
|
||||||
|
|
||||||
若项目经理已在**公司自建 Gitea**([https://git.jc2009.com/](https://git.jc2009.com/),基于开源 Gitea 自建,**不是** Gitea 官网)开好业务技能仓库并授权,技术人员应直接:
|
若项目经理已在**公司自建 Gitea**([https://git.jc2009.com/](https://git.jc2009.com/),基于开源 Gitea 自建,**不是** Gitea 官网)开好业务技能仓库并授权,技术人员应直接:
|
||||||
|
|||||||
Reference in New Issue
Block a user