Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff75aefcdd | |||
| 49e4d6e9a6 | |||
| b27dd5b5ba |
@@ -31,6 +31,9 @@ STEP_DELAY_MAX=3.0 # 单位:秒;默认与最短等待组
|
|||||||
# 人工处理超时:遇到滑块、验证码、二次验证时,等待人工处理的最长时间
|
# 人工处理超时:遇到滑块、验证码、二次验证时,等待人工处理的最长时间
|
||||||
HUMAN_WAIT_TIMEOUT=180 # 单位:秒;来不及处理时可调大,如 300
|
HUMAN_WAIT_TIMEOUT=180 # 单位:秒;来不及处理时可调大,如 300
|
||||||
|
|
||||||
|
# 浏览器启动超时(可选):限制 launch_persistent_browser 最长时间;空=不限制(默认)
|
||||||
|
# OPENCLAW_BROWSER_LAUNCH_TIMEOUT_MS=60000 # 单位:毫秒;慢机器/限频时可开启并调大
|
||||||
|
|
||||||
# 以下为可选目录配置。一般不用改;取消注释并填写后,才覆盖默认子文件夹。
|
# 以下为可选目录配置。一般不用改;取消注释并填写后,才覆盖默认子文件夹。
|
||||||
# 填写相对路径时,相对于本技能的数据目录(不是当前工作目录)。
|
# 填写相对路径时,相对于本技能的数据目录(不是当前工作目录)。
|
||||||
# 用户可见只有 input(上行)与 output(下行);见 development/DATA_PATHS.md。
|
# 用户可见只有 input(上行)与 output(下行);见 development/DATA_PATHS.md。
|
||||||
|
|||||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -9,6 +9,26 @@
|
|||||||
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
|
- 小节正文写用户能看懂的变化(新能力、修复、注意事项),避免堆砌内部实现细节
|
||||||
- 打 tag 前**必须**为即将发布的版本新增一节;找不到匹配小节时,本次发布不会更新 changelog 字段
|
- 打 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
|
## 1.0.77
|
||||||
|
|
||||||
- 固化 Action / CRUD **对称**表单字段类型契约:text、textarea/json、number、boolean、enum/lookup、sensitive、date、datetime_unix_seconds、url/email(薄层)
|
- 固化 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: 技能开发模板(通用业务版)
|
name: 技能开发模板(通用业务版)
|
||||||
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
|
description: "OpenClaw 通用业务技能开发模板,供复制后定制新业务 skill。定制步骤见 development/DEVELOPMENT.md。"
|
||||||
version: 1.0.77
|
version: 1.0.80
|
||||||
author: 深圳匠厂科技有限公司
|
author: 深圳匠厂科技有限公司
|
||||||
metadata:
|
metadata:
|
||||||
openclaw:
|
openclaw:
|
||||||
slug: your-skill-slug
|
slug: your-skill-slug
|
||||||
platform_kit_min_version: "1.2.2"
|
platform_kit_min_version: "1.2.11"
|
||||||
emoji: "📦"
|
emoji: "📦"
|
||||||
category: "通用"
|
category: "通用"
|
||||||
developer_ids:
|
developer_ids:
|
||||||
@@ -37,7 +37,7 @@ allowed-tools:
|
|||||||
- **`executionProfile`**:只决定执行方式——`sync` 当场返回结果,`async` 进**任务中心**。与 placements / 入口**无关**。心智上多数为 sync,仅长任务 / RPA 标 async;宿主省略该字段时按 sync。每个 Action **仍必须**显式写 `sync` 或 `async`。
|
- **`executionProfile`**:只决定执行方式——`sync` 当场返回结果,`async` 进**任务中心**。与 placements / 入口**无关**。心智上多数为 sync,仅长任务 / RPA 标 async;宿主省略该字段时按 sync。每个 Action **仍必须**显式写 `sync` 或 `async`。
|
||||||
- **`policy`(可选)**:需要按用户/角色控制数据管理按钮或行列时,在 `actions.json` 顶层启用 Data Policy,由技能 `policy` CLI 输出 Snapshot;**未启用 = 与今日一致(L0)**。详见 `references/POLICY.md`。用户/角色数据源(飞书/库/API)属技能内部,宿主不托管。
|
- **`policy`(可选)**:需要按用户/角色控制数据管理按钮或行列时,在 `actions.json` 顶层启用 Data Policy,由技能 `policy` CLI 输出 Snapshot;**未启用 = 与今日一致(L0)**。详见 `references/POLICY.md`。用户/角色数据源(飞书/库/API)属技能内部,宿主不托管。
|
||||||
- **`cellPresentation`(可选)**:需要数据管理「某格特殊标色/强调」时启用;技能按当前数据写入呈现(语义 tone),宿主映射主题样式;**未启用 = P0 无标色**。用途不限(超时、预警、驳回等)。详见 `references/CELL_PRESENTATION.md`。
|
- **`cellPresentation`(可选)**:需要数据管理「某格特殊标色/强调」时启用;技能按当前数据写入呈现(语义 tone),宿主映射主题样式;**未启用 = P0 无标色**。用途不限(超时、预警、驳回等)。详见 `references/CELL_PRESENTATION.md`。
|
||||||
- **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;特殊业务报告才另做 `export-*`。
|
- **同步数据**写技能本地库、不生成导出文件;**导出当前表**由宿主数据管理负责;文件型特殊报告才另做 `export-*`(落 `output/`);**HTML 整页报告**写 `html/` 并用固定 Action id `render-html`(见 `references/ACTIONS.md`)。
|
||||||
- **准备 vs 执行(适用则拆)**:若技能存在「先写入/准备本地数据」与「再对外产生副作用」两段,Agent 必须能分开触发;禁止默认绑成不可拆的一步。一键无准备态的技能可跳过本条。
|
- **准备 vs 执行(适用则拆)**:若技能存在「先写入/准备本地数据」与「再对外产生副作用」两段,Agent 必须能分开触发;禁止默认绑成不可拆的一步。一键无准备态的技能可跳过本条。
|
||||||
|
|
||||||
### 如何触发(能力类型)
|
### 如何触发(能力类型)
|
||||||
@@ -83,7 +83,7 @@ python {baseDir}/scripts/main.py config-path
|
|||||||
python {baseDir}/scripts/main.py init-db
|
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`。
|
- 根目录 `requirements.txt` **只声明技能特有**依赖;不要写 `jiangchang-platform-kit` / `playwright`。
|
||||||
- `metadata.openclaw.platform_kit_min_version` 是宿主兼容声明,**不是** pip 依赖。
|
- `metadata.openclaw.platform_kit_min_version` 是宿主兼容声明,**不是** pip 依赖。
|
||||||
- `health` 用 `collect_runtime_diagnostics` 做只读诊断。
|
- `health` 用 `collect_runtime_diagnostics` 做只读诊断。
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
# assets
|
# assets
|
||||||
|
|
||||||
- `actions.json`:Skill Action manifest 最小示例(**可选**;不需要宿主入口时可删除)。每个 action **必须**显式声明 `executionProfile`(`sync` \| `async`)。默认仅 `health` / `version` / `config-path`,不放 toolbar,故无 `bind` 示例。
|
- `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` 等)。
|
- `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)。
|
`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` 成功时单条对象的形状参考。
|
- `log-get-response.json`:若技能有日志表,`log-get` 成功时单条对象的形状参考。
|
||||||
- `skill-policy-snapshot.*.example.json`:Data Policy Snapshot 形状参考(见 `references/POLICY.md`)。
|
- `skill-policy-snapshot.*.example.json`:Data Policy Snapshot 形状参考(见 `references/POLICY.md`)。
|
||||||
- `skill-cell-presentation.example.json`:数据管理单元格呈现文档形状参考(见 `references/CELL_PRESENTATION.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 秒**:
|
真实业务环境中,两个正常业务动作之间默认随机等待 **1~3 秒**:
|
||||||
|
|||||||
@@ -104,6 +104,9 @@ STEP_DELAY_MAX=3.0 # 单位:秒;默认与最短等待组
|
|||||||
|
|
||||||
# 人工处理超时:遇到滑块、验证码、二次验证时,等待人工处理的最长时间
|
# 人工处理超时:遇到滑块、验证码、二次验证时,等待人工处理的最长时间
|
||||||
HUMAN_WAIT_TIMEOUT=180 # 单位:秒;来不及处理时可调大,如 300
|
HUMAN_WAIT_TIMEOUT=180 # 单位:秒;来不及处理时可调大,如 300
|
||||||
|
|
||||||
|
# 浏览器启动超时(可选):限制 launch 最长时间;注释掉或不设=不限制
|
||||||
|
# OPENCLAW_BROWSER_LAUNCH_TIMEOUT_MS=60000 # 单位:毫秒;慢机器时可开启
|
||||||
```
|
```
|
||||||
|
|
||||||
四档运行模式的**实现与测试约定**见 [`ADAPTER.md`](ADAPTER.md);录屏管线见 [`RPA.md`](RPA.md)。这些内容**不要**写回 `.env.example` 注释。
|
四档运行模式的**实现与测试约定**见 [`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)
|
- 权威说明(开发):[`DATA_PATHS.md`](DATA_PATHS.md)
|
||||||
- `.env.example` 中路径类配置**留空或保持注释即用默认**;若写相对路径,必须相对 **skill 数据目录**,**禁止** `./outputs/...` 这类相对 workspace 的写法
|
- `.env.example` 中路径类配置**留空或保持注释即用默认**;若写相对路径,必须相对 **skill 数据目录**,**禁止** `./outputs/...` 这类相对 workspace 的写法
|
||||||
@@ -167,7 +170,8 @@ config.get_float("STEP_DELAY_MIN", 1.0)
|
|||||||
### 空值、行尾注释与假默认(硬规则)
|
### 空值、行尾注释与假默认(硬规则)
|
||||||
|
|
||||||
- **留空即未配置**:`KEY=` 或 `KEY= # 说明` 必须读成空字符串,再走技能自己的「自动 / ensure」逻辑;**禁止**把示例说明、`# 可填…` 当成真实配置值。
|
- **留空即未配置**:`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_ACCOUNT_ID`:仅**纯数字**视为账号主键;校验指定记录后仍必须取得该账号租约,不能只 `account get`
|
||||||
- `DEFAULT_LOGIN_ID`:手机号 / 用户名等;留空则按平台 `ensure-web`;非数字 `DEFAULT_ACCOUNT_ID` 可兼容为 login_id,但不推荐
|
- `DEFAULT_LOGIN_ID`:手机号 / 用户名等;留空则按平台 `ensure-web`;非数字 `DEFAULT_ACCOUNT_ID` 可兼容为 login_id,但不推荐
|
||||||
|
|||||||
@@ -30,13 +30,19 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
|||||||
|
|
||||||
## 2. 标准子目录树
|
## 2. 标准子目录树
|
||||||
|
|
||||||
用户可见目录(数据管理侧栏只挂这两个)按 **上行 / 下行** 划分:
|
用户可见 · **文件目录**(数据管理「输入 / 输出」文件浏览器只挂这两个):
|
||||||
|
|
||||||
| 子目录 | 方向 | 用途 | 默认 helper |
|
| 子目录 | 方向 | 用途 | 默认 helper |
|
||||||
|--------|------|------|-------------|
|
|--------|------|------|-------------|
|
||||||
| `input/` | 上行 | 用户/Agent 放入的待处理材料(urls.txt、batch.json、CSV、产品表…) | `get_input_dir()` |
|
| `input/` | 上行 | 用户/Agent 放入的待处理材料(urls.txt、batch.json、CSV、产品表…) | `get_input_dir()` |
|
||||||
| `output/` | 下行 | 技能/宿主交给用户拿走的结果(视频、回单、xlsx/csv/json…) | `get_output_dir()` |
|
| `output/` | 下行 | 技能/宿主交给用户拿走的结果(视频、回单、xlsx/csv/json…) | `get_output_dir()` |
|
||||||
|
|
||||||
|
用户可见 · **HTML 报告页**(宿主扫描挂到技能下,**不是** input/output 文件浏览器):
|
||||||
|
|
||||||
|
| 子目录 | 用途 | 默认 helper |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| `html/` | 技能写出的静态 `.html` 报告;宿主一层扫描 `*.html`,侧栏预览 / 右键重新生成 | `get_html_dir()` |
|
||||||
|
|
||||||
内部 / 可选目录(**不上侧栏**):
|
内部 / 可选目录(**不上侧栏**):
|
||||||
|
|
||||||
| 子目录 | 用途 | 默认 helper |
|
| 子目录 | 用途 | 默认 helper |
|
||||||
@@ -55,10 +61,14 @@ D:\jiangchang-data\12500\download-video-baidu-haokan\
|
|||||||
|
|
||||||
- 用户/Agent **放进来**的文件 → `input/`(`get_input_dir()`)
|
- 用户/Agent **放进来**的文件 → `input/`(`get_input_dir()`)
|
||||||
- 技能/宿主 **交给用户拿走**的文件 → `output/`(`get_output_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`,不要当成用户可见的「输出目录」
|
- 缓存、临时、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
|
`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
|
## 3.2 开发 RPA 类 skill
|
||||||
|
|
||||||
@@ -170,7 +170,7 @@ scripts/
|
|||||||
技能根目录的 `requirements.txt` 是**标准文件**,用于声明本技能**特有** Python 三方依赖。
|
技能根目录的 `requirements.txt` 是**标准文件**,用于声明本技能**特有** Python 三方依赖。
|
||||||
|
|
||||||
- **公共依赖**(`jiangchang-platform-kit`、`playwright`、config、runtime diagnostics、RPA 公共能力等)由**宿主共享 runtime** 提供,**不要**写入技能 `requirements.txt`。
|
- **公共依赖**(`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`。
|
- 匠厂宿主安装/更新技能后,会将技能 `requirements.txt` 安装到共享 venv:`{JIANGCHANG_DATA_ROOT}/python-runtime/.venv`。
|
||||||
- **不要**在业务代码中 `subprocess` / `pip install`;缺依赖由 `health` 报错,由宿主负责安装。
|
- **不要**在业务代码中 `subprocess` / `pip install`;缺依赖由 `health` 报错,由宿主负责安装。
|
||||||
- **版本约束尽量收窄**,降低多技能共享 venv 时的冲突风险。推荐范围写法:
|
- **版本约束尽量收窄**,降低多技能共享 venv 时的冲突风险。推荐范围写法:
|
||||||
@@ -586,6 +586,7 @@ cmd_stats() # 只读 sync:JSON,无 job_context
|
|||||||
- 批量数量用 **`pickCount` / `--pick N` 参数**(任意正整数),不写死条数。
|
- 批量数量用 **`pickCount` / `--pick N` 参数**(任意正整数),不写死条数。
|
||||||
- **顺序执行、不并发**同一浏览器 Profile / account lease。
|
- **顺序执行、不并发**同一浏览器 Profile / account lease。
|
||||||
- **同步数据**与**宿主表导出**职责分离:同步不写 CSV/Excel;表导出交给数据管理。
|
- **同步数据**与**宿主表导出**职责分离:同步不写 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`。
|
- 详情见 [`SKILL_ACTION_RUNTIME.md`](SKILL_ACTION_RUNTIME.md);契约样例见 `tests/samples/test_sync_contract.py.sample`、`tests/samples/test_action_core_contract.py.sample`。
|
||||||
|
|
||||||
### 队列 + pick(可选)
|
### 队列 + 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 干等 |
|
| **新建任务**(对话 Agent) | `placements` 含 `agent`;或短查询走共享 Python CLI | 自然语言能触发主流程;长任务 / RPA 须走 `run_skill_action`,禁止 bash 干等 |
|
||||||
| **数据管理 · 表顶栏** | `placements` 含 `toolbar`(须合法非空 `bind.tables`) | 左侧能看到本技能库表;表顶栏出现按钮;点按行为符合预期 |
|
| **数据管理 · 表顶栏** | `placements` 含 `toolbar`(须合法非空 `bind.tables`) | 左侧能看到本技能库表;表顶栏出现按钮;点按行为符合预期 |
|
||||||
|
| **数据管理 · HTML 报告** | 写出 `html/*.html`;提供 Action id `render-html` | 侧栏出现报告项;预览正常;右键「重新生成」成功且时间/内容更新 |
|
||||||
| **数据管理 · 行级** | `placements` 含 `row`(建议 `bind.tables` + `bind.inputMapping` 预填主键) | 操作列和/或行右键(及可选「⋯」)出现本技能行级 Action(需有主键列);点按后入参/行为与壳无关、均正确 |
|
| **数据管理 · 行级** | `placements` 含 `row`(建议 `bind.tables` + `bind.inputMapping` 预填主键) | 操作列和/或行右键(及可选「⋯」)出现本技能行级 Action(需有主键列);点按后入参/行为与壳无关、均正确 |
|
||||||
| **数据管理 · 批量** | `placements` 含 `batch` | 勾选行后批量按钮可用;无勾选时禁用;批量行为正确 |
|
| **数据管理 · 批量** | `placements` 含 `batch` | 勾选行后批量按钮可用;无勾选时禁用;批量行为正确 |
|
||||||
| **定时任务** | `placements` 含 `cron`(创建时选「技能直调」) | 能选到本技能 Action、保存并触发;到点或「立即运行」行为正确 |
|
| **定时任务** | `placements` 含 `cron`(创建时选「技能直调」) | 能选到本技能 Action、保存并触发;到点或「立即运行」行为正确 |
|
||||||
|
|||||||
@@ -65,7 +65,7 @@
|
|||||||
| RPA 阶段日志是否「够细」(每个控件) | development/LOGGING.md §3.2 | hard 已要求里程碑事件词与域模块有日志;逐步粒度靠 sample 契约与验收 |
|
| RPA 阶段日志是否「够细」(每个控件) | development/LOGGING.md §3.2 | hard 已要求里程碑事件词与域模块有日志;逐步粒度靠 sample 契约与验收 |
|
||||||
| adapter 四档契约测试覆盖 timeout/unauthorized 等 | development/ADAPTER.md §contract tests | 需业务实现后人工补测 |
|
| adapter 四档契约测试覆盖 timeout/unauthorized 等 | development/ADAPTER.md §contract tests | 需业务实现后人工补测 |
|
||||||
| `SKILL.md` / `constants.SKILL_SLUG` 一致性 | development/DEVELOPMENT.md §16 | 已有 `tests/test_skill_metadata.py` |
|
| `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 | 待宿主升级后完整生效;模板侧已强制显式绑定 |
|
| 宿主按 `bind.tables` 过滤 toolbar 按钮 | references/ACTIONS.md | 待宿主升级后完整生效;模板侧已强制显式绑定 |
|
||||||
| Data Policy 未启用(L0)时宿主不得拉 Snapshot / 不得改按钮与行列行为 | references/POLICY.md | soft;宿主实现契约 |
|
| Data Policy 未启用(L0)时宿主不得拉 Snapshot / 不得改按钮与行列行为 | references/POLICY.md | soft;宿主实现契约 |
|
||||||
| Data Policy 启用后 fail-closed(禁止回退 L0 全开);写路径 domain 必须再鉴权 | references/POLICY.md §6–§7 | soft;`POLICY-SKILL-AUTHZ-*` 硬门禁可后续加 |
|
| 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)` 假设成功。
|
**状态等待优先:**页面加载、控件可用、弹窗消失、保存成功、下载完成和业务结果出现,必须等待真实条件并设置超时;禁止用固定 `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` 秒。
|
- 两个正常业务动作之间默认随机等待 `1~3` 秒。
|
||||||
@@ -359,11 +369,20 @@ skill 退出/抛错统一用 `ERROR:` 前缀 + 稳定码,方便宿主与上层
|
|||||||
| `ERROR:AMBIGUOUS_ACCOUNT` | `--login-id` / `--label` 匹配到多条 | 改用数字 id 或更精确标识 |
|
| `ERROR:AMBIGUOUS_ACCOUNT` | `--login-id` / `--label` 匹配到多条 | 改用数字 id 或更精确标识 |
|
||||||
| `ERROR:RATE_LIMITED` | 触发频控 | 退避后重试 |
|
| `ERROR:RATE_LIMITED` | 触发频控 | 退避后重试 |
|
||||||
| `ERROR:MISSING_BROWSER` | 未检测到 Chrome/Edge | 提示安装 |
|
| `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:DEVICE_NOT_READY` | 手机未连接/未授权 | 检查 USB/ADB |
|
||||||
| `ERROR:WINDOW_NOT_FOUND` | 桌面目标窗口未找到 | 检查程序是否启动 |
|
| `ERROR:WINDOW_NOT_FOUND` | 桌面目标窗口未找到 | 检查程序是否启动 |
|
||||||
|
|
||||||
业务空结果(如「本页无评论」)用业务码或成功空列表表达,**不要**复用上表账号/登录错误码。
|
业务空结果(如「本页无评论」)用业务码或成功空列表表达,**不要**复用上表账号/登录错误码。
|
||||||
|
|
||||||
|
**归因纪律(防误诊):**
|
||||||
|
|
||||||
|
- 「未开 `ALLOW_REAL_RPA` / 档位门禁」必须用技能自己的配置类错误码,**禁止**与真实启动失败、启动超时共用一个 hint。
|
||||||
|
- 页面慢导致的超时优先 `UI_NOT_READY` / 业务就绪码;仅当有明确频控 DOM/文案时才用 `RATE_LIMITED`。
|
||||||
|
- 是否列入技能 `SESSION_FATAL` / `RETRYABLE` 由技能自定,但 **UI 未就绪 / 启动超时默认不应整批不可重试终止**(除非会话已明确不可恢复,如未登录、验证码人工超时)。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 存证与录屏规范
|
## 5. 存证与录屏规范
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 共享 Python Runtime
|
## 共享 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 依赖声明。
|
技能根目录 `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/私有源刚发了更新,但本技能行为未依赖 | **不必**仅为「追新」抬版本 |
|
| PyPI/私有源刚发了更新,但本技能行为未依赖 | **不必**仅为「追新」抬版本 |
|
||||||
| 宿主升级时机 | 宿主在技能/宿主声明的最低版本**未满足**时按需升级共享 venv;**不会**仅因源上有更新就后台拉 latest |
|
| 宿主升级时机 | 宿主在技能/宿主声明的最低版本**未满足**时按需升级共享 venv;**不会**仅因源上有更新就后台拉 latest |
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ Unix(未注入时):
|
|||||||
|
|
||||||
共享解释器通常位于 `{JIANGCHANG_DATA_ROOT}/python-runtime/.venv`。数据根由宿主注入(见 `jiangchang_skill_core.runtime_env`)。
|
共享解释器通常位于 `{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 诊断,**不在技能内重复实现**。典型字段:
|
`health` 命令通过 **`jiangchang_skill_core.collect_runtime_diagnostics`** 输出共享 runtime 诊断,**不在技能内重复实现**。典型字段:
|
||||||
|
|
||||||
@@ -48,6 +48,7 @@ Unix(未注入时):
|
|||||||
- `JIANGCHANG_DATA_ROOT` / `resolved_data_root`
|
- `JIANGCHANG_DATA_ROOT` / `resolved_data_root`
|
||||||
- `media_assets_root`、`ffmpeg_path`、`background_music_*`
|
- `media_assets_root`、`ffmpeg_path`、`background_music_*`
|
||||||
- `record_video_enabled`、`runtime_issue[warning|error]`
|
- `record_video_enabled`、`runtime_issue[warning|error]`
|
||||||
|
- `machine_power` — 只读供电/限频探测(探不到则 `probed=false`);**不得**单独作为 hard-fail 依据
|
||||||
|
|
||||||
`health` 是**只读诊断**,**不会**触发 media-assets 下载或修复。并补充 `env_path` / `env_exists` / `example_path` 配置路径字段。
|
`health` 是**只读诊断**,**不会**触发 media-assets 下载或修复。并补充 `env_path` / `env_exists` / `example_path` 配置路径字段。
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ Unix(未注入时):
|
|||||||
- 用户实际 `.env`:`{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/{skill_slug}/.env`。
|
- 用户实际 `.env`:`{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/{skill_slug}/.env`。
|
||||||
- `scripts/main.py` 与 `cli.app.main()` 启动时调用 `util.config_bootstrap.bootstrap_skill_config()`。
|
- `scripts/main.py` 与 `cli.app.main()` 启动时调用 `util.config_bootstrap.bootstrap_skill_config()`。
|
||||||
- 配置优先级:**进程环境变量** > **用户 `.env`** > **`.env.example` 默认值**。
|
- 配置优先级:**进程环境变量** > **用户 `.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 / 背景音乐
|
## media-assets / ffmpeg / 背景音乐
|
||||||
|
|
||||||
|
|||||||
@@ -164,9 +164,10 @@ cmd_stats() # 只读 sync:JSON stdout,无 job_context
|
|||||||
|------|------|--------|
|
|------|------|--------|
|
||||||
| 同步 / refresh / collect | 外部来源 → 技能本地 SQLite | 技能 domain service |
|
| 同步 / refresh / collect | 外部来源 → 技能本地 SQLite | 技能 domain service |
|
||||||
| 导出当前数据表 | 本地库 → CSV/Excel | **宿主**数据管理统一导出 |
|
| 导出当前数据表 | 本地库 → 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`
|
- [ ] `requirements.txt` **不含** `jiangchang-platform-kit` / `playwright`
|
||||||
- [ ] 无 `scripts/jiangchang_skill_core/` vendored 副本
|
- [ ] 无 `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`(或等价诊断行)
|
- [ ] `health` 能输出 `platform_kit_version_ok`(或等价诊断行)
|
||||||
- [ ] `config-path` 可输出用户 `.env` 路径 JSON
|
- [ ] `config-path` 可输出用户 `.env` 路径 JSON
|
||||||
- [ ] `pytest.ini` 存在且 `python_files` 只收集 `test_*.py` / `*_test.py`
|
- [ ] `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 import config
|
||||||
from jiangchang_skill_core.activity import interruptible_sleep
|
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.browser_session import close_browser_context, get_start_url, start_browser_session
|
||||||
from service.human_verification import (
|
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:
|
async def _wait_search_results(page, timeout_sec: float = 45.0) -> bool:
|
||||||
deadline = time.monotonic() + timeout_sec
|
"""有界等待搜索结果容器出现(kit poll_until;区分加载中与超时未就绪)。"""
|
||||||
while time.monotonic() < deadline:
|
|
||||||
|
async def _ready() -> bool:
|
||||||
try:
|
try:
|
||||||
url = page.url or ""
|
url = page.url or ""
|
||||||
if "/search" in url or "search" in url.lower():
|
if "/search" in url or "search" in url.lower():
|
||||||
if await page.locator(RESULT_CONTAINER_SELECTOR).count() > 0:
|
if await page.locator(RESULT_CONTAINER_SELECTOR).count() > 0:
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
return False
|
||||||
await interruptible_sleep(0.8)
|
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:
|
async def _has_end_marker(page) -> bool:
|
||||||
|
|||||||
@@ -473,6 +473,7 @@ HINT: <next step>
|
|||||||
- 臆造 `sortOrder` / `order` / `priority` 等字段指望宿主排序(模板 Schema `additionalProperties: false` 会拒收)。
|
- 臆造 `sortOrder` / `order` / `priority` 等字段指望宿主排序(模板 Schema `additionalProperties: false` 会拒收)。
|
||||||
- 依赖或宣传表顶栏「更多操作」溢出菜单(宿主已取消该能力)。
|
- 依赖或宣传表顶栏「更多操作」溢出菜单(宿主已取消该能力)。
|
||||||
- 把「表格行数据默认排序」与按钮顺序混淆——行排序见 [`SCHEMA.md`](SCHEMA.md) 的 `default_sort_json`。
|
- 把「表格行数据默认排序」与按钮顺序混淆——行排序见 [`SCHEMA.md`](SCHEMA.md) 的 `default_sort_json`。
|
||||||
|
- 把「默认展示列」与 `visible=0` 硬隐藏混淆——默认展示列见 [`SCHEMA.md`](SCHEMA.md) 的 `default_visible_columns_json`。
|
||||||
- 新增 `row-context-menu` 等 placement,或要求技能再维护一份与 `row` 平行的菜单清单。
|
- 新增 `row-context-menu` 等 placement,或要求技能再维护一份与 `row` 平行的菜单清单。
|
||||||
|
|
||||||
### Action 类型 × placements(常见示例,不是限制)
|
### Action 类型 × placements(常见示例,不是限制)
|
||||||
@@ -489,7 +490,7 @@ HINT: <next step>
|
|||||||
| 导入 / 重置 | sync 或 async | skill-detail, agent | 可选 |
|
| 导入 / 重置 | sync 或 async | skill-detail, agent | 可选 |
|
||||||
| 单条副作用 RPA | async(建议) | agent, skill-detail | **建议** |
|
| 单条副作用 RPA | async(建议) | agent, skill-detail | **建议** |
|
||||||
| 批量顺序 pick RPA | async(建议) | toolbar, cron, agent | **建议** |
|
| 批量顺序 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 |
|
| **同步数据**(sync / refresh / collect) | 外部来源 → 技能本地数据库 | 技能 Action / domain service |
|
||||||
| **导出当前数据表** | 技能本地数据库 → CSV/Excel 等 | 数据管理页**宿主统一导出** |
|
| **导出当前数据表** | 技能本地数据库 → 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)。
|
同步型 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 对称)
|
### 表单字段类型对照(须与 CRUD 对称)
|
||||||
|
|
||||||
Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件语义**。CRUD 侧配置见 [`SCHEMA.md`](SCHEMA.md)「表单与展示:稳定字段类型」;本表只写 Action 怎么配。
|
Skill Action 弹窗与数据管理「新增 / 编辑」使用**同一套控件语义**。CRUD 侧配置见 [`SCHEMA.md`](SCHEMA.md)「表单与展示:稳定字段类型」;本表只写 Action 怎么配。
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ python {baseDir}/scripts/main.py <command> …
|
|||||||
|
|
||||||
| 类型 | 本技能命令(自填) | Agent |
|
| 类型 | 本技能命令(自填) | 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 强制黑名单** | (例)`<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 |
|
| **同步数据** | 外部来源 → 技能本地数据库 | 技能 Action / domain service |
|
||||||
| **导出当前数据表** | 本地库 → CSV/Excel 等 | 数据管理页**宿主统一导出** |
|
| **导出当前数据表** | 本地库 → 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`。
|
同步型 Action 默认只写库、不生成导出文件。兼容旧 export 命令需要刷新时,必须调用同一 sync 内核。可复用测试范式见 `tests/samples/test_sync_contract.py.sample`。
|
||||||
|
|
||||||
@@ -139,6 +140,7 @@ metadata:
|
|||||||
| `visible` | 是 | `1` 在宿主数据管理中展示 / `0` 隐藏 |
|
| `visible` | 是 | `1` 在宿主数据管理中展示 / `0` 隐藏 |
|
||||||
| `readonly` | 是 | `1` 整张表只读;`0` 允许宿主根据字段权限执行写操作 |
|
| `readonly` | 是 | `1` 整张表只读;`0` 允许宿主根据字段权限执行写操作 |
|
||||||
| `default_sort_json` | 否 | 数据管理**默认行排序**(见下节);空则宿主用内置默认 |
|
| `default_sort_json` | 否 | 数据管理**默认行排序**(见下节);空则宿主用内置默认 |
|
||||||
|
| `default_visible_columns_json` | 否 | 数据管理**默认展示列**(见下节);空则全显 |
|
||||||
|
|
||||||
#### 表级默认排序 `default_sort_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` 作为新的正式契约
|
- 使用 `_schema_meta` 作为新的正式契约
|
||||||
- 只写英文物理名而不写中文 `display_name`
|
- 只写英文物理名而不写中文 `display_name`
|
||||||
- 让元数据引用不存在的表
|
- 让元数据引用不存在的表
|
||||||
|
- 用 `visible=0` 假装「默认不展示但用户还能勾出来」(那是硬隐藏,勾不回来)
|
||||||
|
|
||||||
### `_jiangchang_columns` 字段级标准
|
### `_jiangchang_columns` 字段级标准
|
||||||
|
|
||||||
@@ -187,7 +226,7 @@ upsert_table_metadata(
|
|||||||
| `display_name` | 是 | 用户可见的中文字段名(表头与表单标签);保持简短 |
|
| `display_name` | 是 | 用户可见的中文字段名(表头与表单标签);保持简短 |
|
||||||
| `description` | 否 | 字段说明(给人看的短文案);**宿主在新增/编辑表单标签下方展示**(见下节);不要塞进 `display_name` |
|
| `description` | 否 | 字段说明(给人看的短文案);**宿主在新增/编辑表单标签下方展示**(见下节);不要塞进 `display_name` |
|
||||||
| `display_order` | 否 | 兼容字段,默认保持 `NULL` |
|
| `display_order` | 否 | 兼容字段,默认保持 `NULL` |
|
||||||
| `visible` | 是 | 是否在数据管理表格中展示 |
|
| `visible` | 是 | 是否进入数据管理 schema /「列」面板(`0` = **硬隐藏**,用户无法勾回;默认不展示但可勾回请用表级 `default_visible_columns_json`) |
|
||||||
| `searchable` | 是 | 是否参与搜索 |
|
| `searchable` | 是 | 是否参与搜索 |
|
||||||
| `editable` | 是 | `1` 业务字段允许宿主用于新增和编辑;`0` 系统/只读字段 |
|
| `editable` | 是 | `1` 业务字段允许宿主用于新增和编辑;`0` 系统/只读字段 |
|
||||||
| `display_type` | 否 | 字段渲染类型;稳定取值见下节「表单与展示:稳定字段类型」 |
|
| `display_type` | 否 | 字段渲染类型;稳定取值见下节「表单与展示:稳定字段类型」 |
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ CREATE TABLE IF NOT EXISTS _jiangchang_tables (
|
|||||||
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
|
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()}
|
cols = {str(row[1]) for row in cursor.fetchall()}
|
||||||
if "default_sort_json" not in cols:
|
if "default_sort_json" not in cols:
|
||||||
cursor.execute("ALTER TABLE _jiangchang_tables ADD COLUMN default_sort_json TEXT")
|
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:
|
def init_display_metadata_tables(cursor) -> None:
|
||||||
@@ -106,27 +111,42 @@ def upsert_table_metadata(
|
|||||||
visible: int = 1,
|
visible: int = 1,
|
||||||
readonly: int = 0,
|
readonly: int = 0,
|
||||||
default_sort_json: str | None = None,
|
default_sort_json: str | None = None,
|
||||||
|
default_visible_columns_json: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""写入表级元数据。
|
"""写入表级元数据。
|
||||||
|
|
||||||
default_sort_json: 宿主数据管理默认排序,JSON 数组,例如
|
default_sort_json: 宿主数据管理默认排序,JSON 数组,例如
|
||||||
'[{"column":"sent_at","direction":"desc"},{"column":"id","direction":"desc"}]'
|
'[{"column":"sent_at","direction":"desc"},{"column":"id","direction":"desc"}]'
|
||||||
为空则宿主回退到 updated_at → created_at → 主键。
|
为空则宿主回退到 updated_at → created_at → 主键。
|
||||||
|
|
||||||
|
default_visible_columns_json: 宿主数据管理默认展示列,JSON 字符串数组,例如
|
||||||
|
'["stat_type","period","stage"]'
|
||||||
|
为空则全显;用户改过「列」后以本机偏好为准(软默认,区别于 visible=0 硬隐藏)。
|
||||||
"""
|
"""
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO _jiangchang_tables (
|
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
|
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
|
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)。"""
|
"""技能标识、版本与平台公共库约束(复制后请修改 slug/version/logger)。"""
|
||||||
|
|
||||||
SKILL_SLUG = "your-skill-slug"
|
SKILL_SLUG = "your-skill-slug"
|
||||||
SKILL_VERSION = "1.0.77"
|
SKILL_VERSION = "1.0.80"
|
||||||
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.11"
|
||||||
|
|||||||
@@ -10,10 +10,13 @@ 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_INPUT = "input"
|
STANDARD_SUBDIR_INPUT = "input"
|
||||||
STANDARD_SUBDIR_OUTPUT = "output"
|
STANDARD_SUBDIR_OUTPUT = "output"
|
||||||
|
|
||||||
|
# 宿主扫描的 HTML 报告页(数据管理侧栏 htmlPages,非 input/output 文件浏览器)
|
||||||
|
STANDARD_SUBDIR_HTML = "html"
|
||||||
|
|
||||||
# 内部/可选目录(不上侧栏)
|
# 内部/可选目录(不上侧栏)
|
||||||
STANDARD_SUBDIR_UPLOADS = "uploads"
|
STANDARD_SUBDIR_UPLOADS = "uploads"
|
||||||
STANDARD_SUBDIR_CACHE = "cache"
|
STANDARD_SUBDIR_CACHE = "cache"
|
||||||
@@ -130,12 +133,21 @@ def get_videos_dir(*, create: bool = True) -> str:
|
|||||||
return path
|
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]:
|
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(),
|
||||||
"input_dir": get_input_dir(create=False),
|
"input_dir": get_input_dir(create=False),
|
||||||
"output_dir": get_output_dir(create=False),
|
"output_dir": get_output_dir(create=False),
|
||||||
|
"html_dir": get_html_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),
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ def get_skill_root() -> str:
|
|||||||
return _SKILL_ROOT
|
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."""
|
"""Mock installed jiangchang-platform-kit version for health/diagnostics tests."""
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|||||||
@@ -507,6 +507,54 @@ metadata:
|
|||||||
self.assertIn("seed_column_metadata_batch", text)
|
self.assertIn("seed_column_metadata_batch", text)
|
||||||
self.assertIn("valid_column_names", 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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -344,6 +344,8 @@ class TestDocsStandards(unittest.TestCase):
|
|||||||
"DEFAULT",
|
"DEFAULT",
|
||||||
"description",
|
"description",
|
||||||
"文件 / 图片上传",
|
"文件 / 图片上传",
|
||||||
|
"default_visible_columns_json",
|
||||||
|
"默认展示列",
|
||||||
):
|
):
|
||||||
self.assertIn(marker, text, msg=f"SCHEMA.md missing {marker!r}")
|
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 jiangchang_skill_core import version_ge
|
||||||
from util.constants import PLATFORM_KIT_MIN_VERSION
|
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")
|
md_path = os.path.join(get_skill_root(), "SKILL.md")
|
||||||
with open(md_path, encoding="utf-8") as f:
|
with open(md_path, encoding="utf-8") as f:
|
||||||
md = f.read()
|
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")
|
req_path = os.path.join(get_skill_root(), "requirements.txt")
|
||||||
with open(req_path, encoding="utf-8") as f:
|
with open(req_path, encoding="utf-8") as f:
|
||||||
|
|||||||
@@ -136,11 +136,14 @@ class TestRuntimePaths(unittest.TestCase):
|
|||||||
importlib.reload(rp)
|
importlib.reload(rp)
|
||||||
input_dir = rp.get_input_dir(create=True)
|
input_dir = rp.get_input_dir(create=True)
|
||||||
output_dir = rp.get_output_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))
|
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(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(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(input_dir.replace("\\", "/").endswith("/input"))
|
||||||
self.assertTrue(output_dir.replace("\\", "/").endswith("/output"))
|
self.assertTrue(output_dir.replace("\\", "/").endswith("/output"))
|
||||||
|
self.assertTrue(html_dir.replace("\\", "/").endswith("/html"))
|
||||||
|
|
||||||
def test_list_resolved_data_paths_keys(self) -> None:
|
def test_list_resolved_data_paths_keys(self) -> None:
|
||||||
with IsolatedDataRoot():
|
with IsolatedDataRoot():
|
||||||
@@ -154,6 +157,7 @@ class TestRuntimePaths(unittest.TestCase):
|
|||||||
"skill_data_dir",
|
"skill_data_dir",
|
||||||
"input_dir",
|
"input_dir",
|
||||||
"output_dir",
|
"output_dir",
|
||||||
|
"html_dir",
|
||||||
):
|
):
|
||||||
self.assertIn(key, paths)
|
self.assertIn(key, paths)
|
||||||
self.assertTrue(paths[key])
|
self.assertTrue(paths[key])
|
||||||
@@ -161,6 +165,5 @@ class TestRuntimePaths(unittest.TestCase):
|
|||||||
self.assertNotIn("imports_dir", paths)
|
self.assertNotIn("imports_dir", paths)
|
||||||
self.assertNotIn("exports_dir", paths)
|
self.assertNotIn("exports_dir", paths)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ FORBIDDEN_PHRASES = (
|
|||||||
|
|
||||||
POSITIVE_MARKERS = (
|
POSITIVE_MARKERS = (
|
||||||
"jiangchang-platform-kit",
|
"jiangchang-platform-kit",
|
||||||
"1.2.2",
|
"1.2.11",
|
||||||
"共享 runtime",
|
"共享 runtime",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user