Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff75aefcdd | |||
| 49e4d6e9a6 | |||
| b27dd5b5ba |
@@ -31,6 +31,9 @@ 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。
|
||||
|
||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -9,6 +9,26 @@
|
||||
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
|
||||
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
|
||||
|
||||
## 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(薄层)
|
||||
|
||||
8
SKILL.md
8
SKILL.md
@@ -1,12 +1,12 @@
|
||||
---
|
||||
name: 技能开发模板(通用业务版)
|
||||
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
|
||||
version: 1.0.77
|
||||
version: 1.0.80
|
||||
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:
|
||||
@@ -37,7 +37,7 @@ allowed-tools:
|
||||
- **`executionProfile`**:只决定执行方式——`sync` 当场返回结果,`async` 进**任务中心**。与 placements / 入口**无关**。心智上多数为 sync,仅长任务 / RPA 标 async;宿主省略该字段时按 sync。每个 Action **仍必须**显式写 `sync` 或 `async`。
|
||||
- **`policy`(可选)**:需要按用户/角色控制数据管理按钮或行列时,在 `actions.json` 顶层启用 Data Policy,由技能 `policy` CLI 输出 Snapshot;**未启用 = 与今日一致(L0)**。详见 `references/POLICY.md`。用户/角色数据源(飞书/库/API)属技能内部,宿主不托管。
|
||||
- **`cellPresentation`(可选)**:需要数据管理「某格特殊标色/强调」时启用;技能按当前数据写入呈现(语义 tone),宿主映射主题样式;**未启用 = P0 无标色**。用途不限(超时、预警、驳回等)。详见 `references/CELL_PRESENTATION.md`。
|
||||
- **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;特殊业务报告才另做 `export-*`。
|
||||
- **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;文件型特殊报告才另做 `export-*`(落 `output/`);**HTML 整页报告**写 `html/` 并用固定 Action id `render-html`(见 `references/ACTIONS.md`)。
|
||||
- **准备 vs 执行(适用则拆)**:若技能存在「先写入/准备本地数据」与「再对外产生副作用」两段,Agent 必须能分开触发;禁止默认绑成不可拆的一步。一键无准备态的技能可跳过本条。
|
||||
|
||||
### 如何触发(能力类型)
|
||||
@@ -83,7 +83,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` 做只读诊断。
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# assets
|
||||
|
||||
- `actions.json`:Skill Action manifest 最小示例(**可选**;不需要宿主入口时可删除)。每个 action **必须**显式声明 `executionProfile`(`sync` \| `async`)。默认仅 `health` / `version` / `config-path`,不放 toolbar,故无 `bind` 示例。
|
||||
- `examples/`:CLI 成功输出形状示例(虚构路径与数据)。
|
||||
- `html/example-report.html`:HTML 报告页最小壳(`{{TITLE}}` / `{{GENERATED_AT}}` / `{{BODY}}`);渲染后须写入数据目录 `html/`,见 `references/ACTIONS.md`「HTML 页面报告」。
|
||||
- `examples/`:CLI 成功输出形状示例、以及 `render-html.action.example.json`(虚构路径与数据)。
|
||||
- `schemas/`:轻量 JSON Schema(`skill-actions.schema.json` 为**新技能严格规范**;`task-log-record.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)。
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
- `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`)。
|
||||
- `render-html.action.example.json`:HTML 报告「重新生成」Action 形状参考(固定 id `render-html`;见 `references/ACTIONS.md`)。
|
||||
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
60
assets/html/example-report.html
Normal file
60
assets/html/example-report.html
Normal file
@@ -0,0 +1,60 @@
|
||||
<!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;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html { overflow-y: auto; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
}
|
||||
header {
|
||||
padding: 20px 24px 8px;
|
||||
}
|
||||
header h1 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
}
|
||||
header p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
main {
|
||||
padding: 8px 24px 28px;
|
||||
}
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px;
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>{{TITLE}}</h1>
|
||||
<p>报告生成于 · {{GENERATED_AT}}</p>
|
||||
</header>
|
||||
<main>
|
||||
<div class="card">
|
||||
{{BODY}}
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 秒**:
|
||||
|
||||
@@ -104,6 +104,9 @@ 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` 注释。
|
||||
@@ -112,7 +115,7 @@ HUMAN_WAIT_TIMEOUT=180 # 单位:秒;来不及处理时可调
|
||||
|
||||
### 数据目录子路径(输入 / 输出)
|
||||
|
||||
用户可见目录只有 **`input/`(上行)** 与 **`output/`(下行)**。技能写入的业务产出默认落在 `output/`;用户放入的待处理文件落在 `input/`。须通过 `util.runtime_paths.resolve_data_path()` 或 `get_input_dir()` / `get_output_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 的写法
|
||||
@@ -167,7 +170,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,但不推荐
|
||||
|
||||
@@ -30,13 +30,19 @@ 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 |
|
||||
@@ -55,10 +61,14 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
||||
|
||||
- 用户/Agent **放进来**的文件 → `input/`(`get_input_dir()`)
|
||||
- 技能/宿主 **交给用户拿走**的文件 → `output/`(`get_output_dir()`)
|
||||
- 禁止再引入业务侧栏第三目录(如旧的 `downloads` / `imports` / `exports` 三分法)
|
||||
- 可浏览的 **HTML 报告页** → `html/`(`get_html_dir()` / `service.html_page_service`);**不要**把整页报告塞进 `output/` 指望侧栏预览
|
||||
- 禁止再引入业务「文件浏览器」第三目录(如旧的 `downloads` / `imports` / `exports` 三分法);`html/` 是报告页扫描面,不是第三种文件浏览器
|
||||
- 缓存、临时、RPA 录屏用 `cache` / `temp` / `videos`,不要当成用户可见的「输出目录」
|
||||
|
||||
宿主约定(与本模板对齐):工具栏「导入」默认从 `input/` 取文件;「导出」默认写到 `output/`。
|
||||
宿主约定(与本模板对齐):
|
||||
|
||||
- 工具栏「导入」默认从 `input/` 取文件;「导出」默认写到 `output/`
|
||||
- 数据管理扫描 `{skill_data}/html/*.html`(一层、非递归);`<title>` → 侧栏显示名;「重新生成」调用 Action id **`render-html`**(见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「HTML 页面报告」)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -127,7 +127,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,7 +170,7 @@ 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` 报错,由宿主负责安装。
|
||||
- **版本约束尽量收窄**,降低多技能共享 venv 时的冲突风险。推荐范围写法:
|
||||
@@ -586,6 +586,7 @@ cmd_stats() # 只读 sync:JSON,无 job_context
|
||||
- 批量数量用 **`pickCount` / `--pick N` 参数**(任意正整数),不写死条数。
|
||||
- **顺序执行、不并发**同一浏览器 Profile / account lease。
|
||||
- **同步数据**与**宿主表导出**职责分离:同步不写 CSV/Excel;表导出交给数据管理。
|
||||
- **HTML 页面报告**(可选):需要侧栏预览整页时,写入 `html/` + Action `render-html`;契约见 [`../references/ACTIONS.md`](../references/ACTIONS.md)「HTML 页面报告」、[`DATA_PATHS.md`](DATA_PATHS.md);薄写入 helper 见 `scripts/service/html_page_service.py`。
|
||||
- 详情见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md);契约样例见 `tests/samples/test_sync_contract.py.sample`、`tests/samples/test_action_core_contract.py.sample`。
|
||||
|
||||
### 队列 + pick(可选)
|
||||
@@ -841,6 +842,7 @@ uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-relea
|
||||
|-------------------------|----------------|--------------|
|
||||
| **新建任务**(对话 Agent) | `placements` 含 `agent`;或短查询走共享 Python CLI | 自然语言能触发主流程;长任务 / RPA 须走 `run_skill_action`,禁止 bash 干等 |
|
||||
| **数据管理 · 表顶栏** | `placements` 含 `toolbar`(须合法非空 `bind.tables`) | 左侧能看到本技能库表;表顶栏出现按钮;点按行为符合预期 |
|
||||
| **数据管理 · HTML 报告** | 写出 `html/*.html`;提供 Action id `render-html` | 侧栏出现报告项;预览正常;右键「重新生成」成功且时间/内容更新 |
|
||||
| **数据管理 · 行级** | `placements` 含 `row`(建议 `bind.tables` + `bind.inputMapping` 预填主键) | 操作列和/或行右键(及可选「⋯」)出现本技能行级 Action(需有主键列);点按后入参/行为与壳无关、均正确 |
|
||||
| **数据管理 · 批量** | `placements` 含 `batch` | 勾选行后批量按钮可用;无勾选时禁用;批量行为正确 |
|
||||
| **定时任务** | `placements` 含 `cron`(创建时选「技能直调」) | 能选到本技能 Action、保存并触发;到点或「立即运行」行为正确 |
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
| 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 | 待宿主升级后完整生效;模板侧已强制显式绑定 |
|
||||
| Data Policy 未启用(L0)时宿主不得拉 Snapshot / 不得改按钮与行列行为 | references/POLICY.md | soft;宿主实现契约 |
|
||||
| Data Policy 启用后 fail-closed(禁止回退 L0 全开);写路径 domain 必须再鉴权 | references/POLICY.md §6–§7 | soft;`POLICY-SKILL-AUTHZ-*` 硬门禁可后续加 |
|
||||
|
||||
@@ -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` 秒。
|
||||
@@ -359,11 +369,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. 存证与录屏规范
|
||||
|
||||
@@ -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 / 背景音乐
|
||||
|
||||
|
||||
@@ -164,9 +164,10 @@ cmd_stats() # 只读 sync:JSON stdout,无 job_context
|
||||
|------|------|--------|
|
||||
| 同步 / refresh / collect | 外部来源 → 技能本地 SQLite | 技能 domain service |
|
||||
| 导出当前数据表 | 本地库 → CSV/Excel | **宿主**数据管理统一导出 |
|
||||
| 特殊业务报告 | 多表合并 / 特殊格式 / 附加计算 | 仅系统导出不足时提供 `export-*` |
|
||||
| 特殊业务报告(文件) | 多表合并 / 特殊格式 → `output/` | 仅系统导出不足时提供 `export-*` |
|
||||
| HTML 页面报告 | 写入 `{skill_data}/html/*.html` | 技能生成;宿主扫描预览;重新生成 Action id 固定为 **`render-html`** |
|
||||
|
||||
同步 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)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -265,7 +265,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`
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -473,6 +473,7 @@ HINT: <next step>
|
||||
- 臆造 `sortOrder` / `order` / `priority` 等字段指望宿主排序(模板 Schema `additionalProperties: false` 会拒收)。
|
||||
- 依赖或宣传表顶栏「更多操作」溢出菜单(宿主已取消该能力)。
|
||||
- 把「表格行数据默认排序」与按钮顺序混淆——行排序见 [`SCHEMA.md`](SCHEMA.md) 的 `default_sort_json`。
|
||||
- 把「默认展示列」与 `visible=0` 硬隐藏混淆——默认展示列见 [`SCHEMA.md`](SCHEMA.md) 的 `default_visible_columns_json`。
|
||||
- 新增 `row-context-menu` 等 placement,或要求技能再维护一份与 `row` 平行的菜单清单。
|
||||
|
||||
### Action 类型 × placements(常见示例,不是限制)
|
||||
@@ -489,7 +490,7 @@ HINT: <next step>
|
||||
| 导入 / 重置 | sync 或 async | skill-detail, agent | 可选 |
|
||||
| 单条副作用 RPA | async(建议) | agent, skill-detail | **建议** |
|
||||
| 批量顺序 pick RPA | async(建议) | toolbar, cron, agent | **建议** |
|
||||
## 同步数据 vs 导出当前表 vs 特殊业务报告
|
||||
## 同步数据 vs 导出当前表 vs 特殊业务报告 vs HTML 页面
|
||||
|
||||
不要混用「导出」一词:
|
||||
|
||||
@@ -497,12 +498,64 @@ HINT: <next step>
|
||||
|------|------|--------|
|
||||
| **同步数据**(sync / refresh / collect) | 外部来源 → 技能本地数据库 | 技能 Action / domain service |
|
||||
| **导出当前数据表** | 技能本地数据库 → CSV/Excel 等 | 数据管理页**宿主统一导出** |
|
||||
| **生成特殊业务报告** | 多表合并、特殊排版或附加计算 | 仅当系统导出无法满足时,才提供独立 `export-*` Action |
|
||||
| **生成特殊业务报告(文件)** | 多表合并、特殊排版或附加计算的 **xlsx/csv/json 等文件** | 仅当系统导出无法满足时,才提供独立 `export-*` Action;文件落 `output/` |
|
||||
| **HTML 页面报告** | 可在数据管理侧栏预览的整页 HTML | 技能写入 `{skill_data}/html/*.html` + Action id **`render-html`**(见下节) |
|
||||
|
||||
同步型 Action 默认:读外部数据、写技能自己的库、**不**生成 CSV/Excel/JSON 文件、不重复实现宿主表导出。兼容旧 export 命令若需要刷新数据,必须调用同一个 sync 业务内核。通用同步语义见 [`SCHEMA.md`](SCHEMA.md) 与 [`../development/SKILL_ACTION_RUNTIME.md`](../development/SKILL_ACTION_RUNTIME.md)。
|
||||
|
||||
## inputSchema 与参数表单
|
||||
## HTML 页面报告(数据管理预览)
|
||||
|
||||
> 一句话:**技能负责生成并覆盖 `html/*.html`;宿主负责发现、预览、用 `render-html` 触发重新生成。**
|
||||
|
||||
### 何时用
|
||||
|
||||
- 需要**可浏览的整页报告**(KPI + 表格 + 自带样式),而不是再导出一张表
|
||||
- 与 `export-*` / 宿主表导出**并列**:HTML 走 `html/`;文件型特殊报告仍走 `output/` + `export-*`
|
||||
|
||||
### 目录与发现(宿主契约)
|
||||
|
||||
| 项 | 约定 |
|
||||
|----|------|
|
||||
| 路径 | `{skill_data}/html/*.html`,**只扫一层**(子目录忽略) |
|
||||
| helper | `util.runtime_paths.get_html_dir()`;写入用 `service.html_page_service.write_html_page` |
|
||||
| 侧栏名 | 取文件内 `<title>`(缺省则用文件名) |
|
||||
| 预览 | 宿主 iframe 预览;右键:重新生成 / 浏览器打开 / 刷新 |
|
||||
| 模板资源 | 建议放 `assets/html/*.html`(安装包内),渲染后**写出**到数据目录 `html/` |
|
||||
|
||||
### Action:`render-html`(固定 id)
|
||||
|
||||
宿主「重新生成」**硬编码**查找 Action id `render-html`(见 `service.html_page_service.HOST_HTML_RENDER_ACTION_ID`)。技能未提供该 id → 提示「该技能未提供生成 HTML」。
|
||||
|
||||
推荐形态:
|
||||
|
||||
| 字段 | 建议 |
|
||||
|------|------|
|
||||
| `id` | **必须** `render-html` |
|
||||
| `executionProfile` | `sync` |
|
||||
| `interaction` | `direct` |
|
||||
| `placements` | `toolbar` + `skill-detail`(配合表顶栏与 HTML 右键);可选 `agent` |
|
||||
| `bind.tables` | 数据**来源表**(含 `toolbar` 时必填);宿主从 HTML 页重新生成时会用 bind 补齐跑表上下文 |
|
||||
| `steps` | `afterSuccess` → **`refreshCatalog`**(重扫 `htmlPages`;不要只 `reloadTable`) |
|
||||
| `entrypoint` | CLI 子命令(如 `render-html`)调用同一写入内核 |
|
||||
|
||||
完整 JSON 形状见 [`../assets/examples/render-html.action.example.json`](../assets/examples/render-html.action.example.json)。最小页面壳见 [`../assets/html/example-report.html`](../assets/html/example-report.html)。
|
||||
|
||||
### 实现要点
|
||||
|
||||
1. 用 `load_html_template` + `render_html_placeholders` + `write_html_page`(原子 `*.tmp` + `os.replace`)覆盖同名文件;**不要**每次另存带时间戳的副本(除非产品明确要求历史版本)。
|
||||
2. 页头时间建议用**写出时刻**(「报告生成于」),不要误用业务表里过期的 `generated_at` 冒充「刚重新生成」。
|
||||
3. 可选:数据重算成功后调用静默写入(吞掉错误),保持侧栏报告与库一致。
|
||||
4. 权限:按业务决定谁能生成(管理员 / 角色);在 CLI/`cmd_render_html` 内强制校验。
|
||||
5. CHANGELOG / 用户说明:模板或样式升级后,提醒「请在数据管理对报告执行一次重新生成」。
|
||||
|
||||
### 验收(复制后自测)
|
||||
|
||||
- [ ] `html/` 下出现 `.html`,数据管理侧栏能看到报告项,预览正常
|
||||
- [ ] 表顶栏(若声明 toolbar)或报告右键「重新生成」能跑通;成功后预览刷新
|
||||
- [ ] 未登录/无权限用户被拒绝,且有可读错误
|
||||
- [ ] 成功路径带 `refreshCatalog`(或等价让侧栏/预览看到更新)
|
||||
|
||||
## inputSchema 与参数表单
|
||||
### 表单字段类型对照(须与 CRUD 对称)
|
||||
|
||||
Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件语义**。CRUD 侧配置见 [`SCHEMA.md`](SCHEMA.md)「表单与展示:稳定字段类型」;本表只写 Action 怎么配。
|
||||
|
||||
@@ -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` / `logs` / `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)。若提供 HTML 报告,「重新生成」须有 Action id **`render-html`**(可同时进短 CLI 白名单);见 ACTIONS.md「HTML 页面报告」。
|
||||
|
||||
## 最小命令
|
||||
|
||||
|
||||
@@ -114,13 +114,14 @@ metadata:
|
||||
}
|
||||
```
|
||||
|
||||
### 同步数据 vs 导出当前数据表 vs 特殊业务报告
|
||||
### 同步数据 vs 导出当前数据表 vs 特殊业务报告 vs HTML 页面
|
||||
|
||||
| 能力 | 语义 | 负责方 |
|
||||
|------|------|--------|
|
||||
| **同步数据** | 外部来源 → 技能本地数据库 | 技能 Action / domain service |
|
||||
| **导出当前数据表** | 本地库 → CSV/Excel 等 | 数据管理页**宿主统一导出** |
|
||||
| **生成特殊业务报告** | 多表合并、特殊排版或附加计算 | 仅当系统导出无法满足时,才提供独立 `export-*` |
|
||||
| **生成特殊业务报告(文件)** | 多表合并、特殊排版或附加计算的文件 | 仅当系统导出无法满足时,才提供独立 `export-*`;落 `output/` |
|
||||
| **HTML 页面报告** | 侧栏可预览的整页 HTML | 写入 `html/` + Action id **`render-html`**(见 [`ACTIONS.md`](ACTIONS.md)) |
|
||||
|
||||
同步型 Action 默认只写库、不生成导出文件。兼容旧 export 命令需要刷新时,必须调用同一 sync 内核。可复用测试范式见 `tests/samples/test_sync_contract.py.sample`。
|
||||
|
||||
@@ -139,6 +140,7 @@ metadata:
|
||||
| `visible` | 是 | `1` 在宿主数据管理中展示 / `0` 隐藏 |
|
||||
| `readonly` | 是 | `1` 整张表只读;`0` 允许宿主根据字段权限执行写操作 |
|
||||
| `default_sort_json` | 否 | 数据管理**默认行排序**(见下节);空则宿主用内置默认 |
|
||||
| `default_visible_columns_json` | 否 | 数据管理**默认展示列**(见下节);空则全显 |
|
||||
|
||||
#### 表级默认排序 `default_sort_json`(宿主支持)
|
||||
|
||||
@@ -171,12 +173,49 @@ upsert_table_metadata(
|
||||
)
|
||||
```
|
||||
|
||||
#### 表级默认展示列 `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"]'
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
禁止:
|
||||
|
||||
- 使用中文真实表名
|
||||
- 使用 `_schema_meta` 作为新的正式契约
|
||||
- 只写英文物理名而不写中文 `display_name`
|
||||
- 让元数据引用不存在的表
|
||||
- 用 `visible=0` 假装「默认不展示但用户还能勾出来」(那是硬隐藏,勾不回来)
|
||||
|
||||
### `_jiangchang_columns` 字段级标准
|
||||
|
||||
@@ -187,7 +226,7 @@ upsert_table_metadata(
|
||||
| `display_name` | 是 | 用户可见的中文字段名(表头与表单标签);保持简短 |
|
||||
| `description` | 否 | 字段说明(给人看的短文案);**宿主在新增/编辑表单标签下方展示**(见下节);不要塞进 `display_name` |
|
||||
| `display_order` | 否 | 兼容字段,默认保持 `NULL` |
|
||||
| `visible` | 是 | 是否在数据管理表格中展示 |
|
||||
| `visible` | 是 | 是否进入数据管理 schema /「列」面板(`0` = **硬隐藏**,用户无法勾回;默认不展示但可勾回请用表级 `default_visible_columns_json`) |
|
||||
| `searchable` | 是 | 是否参与搜索 |
|
||||
| `editable` | 是 | `1` 业务字段允许宿主用于新增和编辑;`0` 系统/只读字段 |
|
||||
| `display_type` | 否 | 字段渲染类型;稳定取值见下节「表单与展示:稳定字段类型」 |
|
||||
|
||||
@@ -25,7 +25,8 @@ CREATE TABLE IF NOT EXISTS _jiangchang_tables (
|
||||
sort_order INTEGER,
|
||||
visible INTEGER NOT NULL DEFAULT 1,
|
||||
readonly INTEGER NOT NULL DEFAULT 0,
|
||||
default_sort_json TEXT
|
||||
default_sort_json TEXT,
|
||||
default_visible_columns_json TEXT
|
||||
)
|
||||
"""
|
||||
|
||||
@@ -89,6 +90,10 @@ def _ensure_jiangchang_tables_columns(cursor) -> None:
|
||||
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")
|
||||
if "default_visible_columns_json" not in cols:
|
||||
cursor.execute(
|
||||
"ALTER TABLE _jiangchang_tables ADD COLUMN default_visible_columns_json TEXT"
|
||||
)
|
||||
|
||||
|
||||
def init_display_metadata_tables(cursor) -> None:
|
||||
@@ -106,27 +111,42 @@ def upsert_table_metadata(
|
||||
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 → 主键。
|
||||
|
||||
default_visible_columns_json: 宿主数据管理默认展示列,JSON 字符串数组,例如
|
||||
'["stat_type","period","stage"]'
|
||||
为空则全显;用户改过「列」后以本机偏好为准(软默认,区别于 visible=0 硬隐藏)。
|
||||
"""
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO _jiangchang_tables (
|
||||
table_name, display_name, description, visible, readonly, default_sort_json
|
||||
table_name, display_name, description, visible, readonly,
|
||||
default_sort_json, default_visible_columns_json
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(table_name) DO UPDATE SET
|
||||
display_name = excluded.display_name,
|
||||
description = excluded.description,
|
||||
visible = excluded.visible,
|
||||
readonly = excluded.readonly,
|
||||
default_sort_json = excluded.default_sort_json
|
||||
default_sort_json = excluded.default_sort_json,
|
||||
default_visible_columns_json = excluded.default_visible_columns_json
|
||||
""",
|
||||
(table_name, display_name, description, visible, readonly, default_sort_json),
|
||||
(
|
||||
table_name,
|
||||
display_name,
|
||||
description,
|
||||
visible,
|
||||
readonly,
|
||||
default_sort_json,
|
||||
default_visible_columns_json,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
77
scripts/service/html_page_service.py
Normal file
77
scripts/service/html_page_service.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""技能 HTML 报告页:写入 `{skill_data}/html/*.html`(宿主扫描预览)。
|
||||
|
||||
契约要点(权威说明见 `references/ACTIONS.md`「HTML 页面报告」):
|
||||
|
||||
- 目录:仅一层 `html/*.html`(非递归)
|
||||
- 重新生成 Action id 必须为 `render-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
|
||||
@@ -1,6 +1,6 @@
|
||||
"""技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。"""
|
||||
|
||||
SKILL_SLUG = "your-skill-slug"
|
||||
SKILL_VERSION = "1.0.77"
|
||||
SKILL_VERSION = "1.0.80"
|
||||
LOG_LOGGER_NAME = "openclaw.skill.your_skill_slug"
|
||||
PLATFORM_KIT_MIN_VERSION = "1.2.2"
|
||||
PLATFORM_KIT_MIN_VERSION = "1.2.11"
|
||||
|
||||
@@ -10,10 +10,13 @@ from util.constants import SKILL_SLUG
|
||||
|
||||
_SCRIPTS_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# 用户可见的上行/下行目录(数据管理侧栏只挂这两个)
|
||||
# 用户可见的上行/下行目录(数据管理「输入/输出」文件浏览器)
|
||||
STANDARD_SUBDIR_INPUT = "input"
|
||||
STANDARD_SUBDIR_OUTPUT = "output"
|
||||
|
||||
# 宿主扫描的 HTML 报告页(数据管理侧栏 htmlPages,非 input/output 文件浏览器)
|
||||
STANDARD_SUBDIR_HTML = "html"
|
||||
|
||||
# 内部/可选目录(不上侧栏)
|
||||
STANDARD_SUBDIR_UPLOADS = "uploads"
|
||||
STANDARD_SUBDIR_CACHE = "cache"
|
||||
@@ -130,12 +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(),
|
||||
"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),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -507,6 +507,54 @@ metadata:
|
||||
self.assertIn("seed_column_metadata_batch", text)
|
||||
self.assertIn("valid_column_names", text)
|
||||
|
||||
def test_default_visible_columns_json_upsert_and_migrate(self) -> None:
|
||||
conn = sqlite3.connect(":memory:")
|
||||
try:
|
||||
from db.display_metadata import (
|
||||
init_display_metadata_tables,
|
||||
upsert_table_metadata,
|
||||
)
|
||||
|
||||
cur = conn.cursor()
|
||||
# Simulate pre-1.0.79 table without the new column.
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE _jiangchang_tables (
|
||||
table_name TEXT PRIMARY KEY,
|
||||
display_name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
sort_order INTEGER,
|
||||
visible INTEGER NOT NULL DEFAULT 1,
|
||||
readonly INTEGER NOT NULL DEFAULT 0,
|
||||
default_sort_json TEXT
|
||||
)
|
||||
"""
|
||||
)
|
||||
init_display_metadata_tables(cur)
|
||||
cur.execute("PRAGMA table_info('_jiangchang_tables')")
|
||||
cols = {str(row[1]) for row in cur.fetchall()}
|
||||
self.assertIn("default_visible_columns_json", cols)
|
||||
|
||||
payload = '["stat_type","period","stage"]'
|
||||
upsert_table_metadata(
|
||||
cur,
|
||||
table_name="timeliness_stats",
|
||||
display_name="时效统计",
|
||||
readonly=1,
|
||||
default_visible_columns_json=payload,
|
||||
)
|
||||
conn.commit()
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT default_visible_columns_json
|
||||
FROM _jiangchang_tables
|
||||
WHERE table_name = 'timeliness_stats'
|
||||
"""
|
||||
)
|
||||
self.assertEqual(cur.fetchone()[0], payload)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -344,6 +344,8 @@ class TestDocsStandards(unittest.TestCase):
|
||||
"DEFAULT",
|
||||
"description",
|
||||
"文件 / 图片上传",
|
||||
"default_visible_columns_json",
|
||||
"默认展示列",
|
||||
):
|
||||
self.assertIn(marker, text, msg=f"SCHEMA.md missing {marker!r}")
|
||||
|
||||
|
||||
66
tests/test_html_page_service.py
Normal file
66
tests/test_html_page_service.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""HTML 报告页薄写入 helper。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from _support import IsolatedDataRoot
|
||||
|
||||
from service.html_page_service import (
|
||||
HOST_HTML_RENDER_ACTION_ID,
|
||||
load_html_template,
|
||||
render_html_placeholders,
|
||||
write_html_page,
|
||||
)
|
||||
|
||||
|
||||
class TestHtmlPageService(unittest.TestCase):
|
||||
def test_host_action_id_is_stable(self) -> None:
|
||||
self.assertEqual(HOST_HTML_RENDER_ACTION_ID, "render-html")
|
||||
|
||||
def test_write_html_page_atomic_under_html_dir(self) -> None:
|
||||
with IsolatedDataRoot() as tmp:
|
||||
path = write_html_page(
|
||||
"example-report.html",
|
||||
"<!DOCTYPE html><html><head><title>示例</title></head><body>ok</body></html>",
|
||||
)
|
||||
self.assertTrue(os.path.isfile(path))
|
||||
norm = path.replace("\\", "/")
|
||||
self.assertIn("/html/example-report.html", norm)
|
||||
self.assertTrue(os.path.normcase(os.path.normpath(path)).startswith(
|
||||
os.path.normcase(os.path.normpath(tmp)),
|
||||
))
|
||||
self.assertFalse(os.path.isfile(path + ".tmp"))
|
||||
with open(path, encoding="utf-8") as f:
|
||||
self.assertIn("<title>示例</title>", f.read())
|
||||
|
||||
def test_load_and_fill_example_template(self) -> None:
|
||||
with IsolatedDataRoot():
|
||||
template = load_html_template("assets/html/example-report.html")
|
||||
page = render_html_placeholders(
|
||||
template,
|
||||
{
|
||||
"TITLE": "示例报告",
|
||||
"GENERATED_AT": "2026-08-14 15:49:00",
|
||||
"BODY": "<p>hello</p>",
|
||||
},
|
||||
)
|
||||
self.assertIn("<title>示例报告</title>", page)
|
||||
self.assertIn("报告生成于 · 2026-08-14 15:49:00", page)
|
||||
self.assertIn("<p>hello</p>", page)
|
||||
self.assertNotIn("{{TITLE}}", page)
|
||||
dest = write_html_page("example-report.html", page)
|
||||
self.assertTrue(os.path.isfile(dest))
|
||||
|
||||
def test_rejects_unsafe_filename(self) -> None:
|
||||
with IsolatedDataRoot():
|
||||
with self.assertRaises(ValueError):
|
||||
write_html_page("../x.html", "<html></html>")
|
||||
with self.assertRaises(ValueError):
|
||||
write_html_page("note.txt", "<html></html>")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -87,12 +87,12 @@ class TestPlatformImportSource(unittest.TestCase):
|
||||
from jiangchang_skill_core import version_ge
|
||||
from util.constants import PLATFORM_KIT_MIN_VERSION
|
||||
|
||||
self.assertEqual(PLATFORM_KIT_MIN_VERSION, "1.2.2")
|
||||
self.assertEqual(PLATFORM_KIT_MIN_VERSION, "1.2.11")
|
||||
|
||||
md_path = os.path.join(get_skill_root(), "SKILL.md")
|
||||
with open(md_path, encoding="utf-8") as f:
|
||||
md = f.read()
|
||||
self.assertEqual(_parse_platform_kit_min_version(md), "1.2.2")
|
||||
self.assertEqual(_parse_platform_kit_min_version(md), "1.2.11")
|
||||
|
||||
req_path = os.path.join(get_skill_root(), "requirements.txt")
|
||||
with open(req_path, encoding="utf-8") as f:
|
||||
|
||||
@@ -136,11 +136,14 @@ class TestRuntimePaths(unittest.TestCase):
|
||||
importlib.reload(rp)
|
||||
input_dir = rp.get_input_dir(create=True)
|
||||
output_dir = rp.get_output_dir(create=True)
|
||||
html_dir = rp.get_html_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(os.path.normcase(os.path.normpath(html_dir)).startswith(norm_tmp))
|
||||
self.assertTrue(input_dir.replace("\\", "/").endswith("/input"))
|
||||
self.assertTrue(output_dir.replace("\\", "/").endswith("/output"))
|
||||
self.assertTrue(html_dir.replace("\\", "/").endswith("/html"))
|
||||
|
||||
def test_list_resolved_data_paths_keys(self) -> None:
|
||||
with IsolatedDataRoot():
|
||||
@@ -154,6 +157,7 @@ class TestRuntimePaths(unittest.TestCase):
|
||||
"skill_data_dir",
|
||||
"input_dir",
|
||||
"output_dir",
|
||||
"html_dir",
|
||||
):
|
||||
self.assertIn(key, paths)
|
||||
self.assertTrue(paths[key])
|
||||
@@ -161,6 +165,5 @@ class TestRuntimePaths(unittest.TestCase):
|
||||
self.assertNotIn("imports_dir", paths)
|
||||
self.assertNotIn("exports_dir", paths)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -34,7 +34,7 @@ FORBIDDEN_PHRASES = (
|
||||
|
||||
POSITIVE_MARKERS = (
|
||||
"jiangchang-platform-kit",
|
||||
"1.2.2",
|
||||
"1.2.11",
|
||||
"共享 runtime",
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user