Publish dataease via gitea-publish skill

This commit is contained in:
qwenpaw-skills
2026-08-09 00:36:02 +00:00
commit ac82f67e09
24 changed files with 7145 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
# DataEase V2 全能技能
一站式 DataEase 自动化解决方案,融合图表部署与资源管理能力。
## 🎯 功能特性
| 功能 | 命令 | 说明 |
|------|------|------|
| 📊 数据探索 | `inspect_data.py` | 查询数据集、字段信息 |
| 📈 图表部署 | `deploy.py` | 创建图表并自动截图 |
| 📋 多图看板 | `multi_deploy.py` | 创建多图表仪表板 |
| 🏢 组织管理 | `capture_dashboard.py` | 查询/切换组织 |
| 📑 资源列表 | `capture_dashboard.py` | 列出仪表板/大屏 |
| 📸 截图导出 | `capture_dashboard.py` | 导出截图或PDF |
## 🚀 快速开始
### 1. 安装依赖
```bash
npm install
npx playwright install chromium
```
### 2. 配置环境
```bash
cp .env.example .env
```
编辑 `.env` 文件:
```bash
DATAEASE_BASE_URL=https://your-dataease.example.com
DATAEASE_API_PREFIX=/de2api
DATAEASE_ACCESS_KEY=your_access_key
DATAEASE_SECRET_KEY=your_secret_key
DATAEASE_USERNAME=admin
DATAEASE_PASSWORD=your_password
DATAEASE_LOGIN_ORIGIN=0
```
**认证方式(二选一):**
- AK/SK:配置 `ACCESS_KEY` + `SECRET_KEY`
- 密码登录:配置 `USERNAME` + `PASSWORD`
### 3. 使用示例
```bash
# 查询数据集
python3 scripts/inspect_data.py --list-datasets
# 查询字段
python3 scripts/inspect_data.py --dataset "销售数据"
# 创建图表(自动截图)
python3 scripts/deploy.py bar '各产品销售额' '销售数据' '产品' '实际销售'
# 创建多图表看板
python3 scripts/multi_deploy.py '销售分析' '[{"type":"bar","title":"销售","dataset_name":"销售数据","x_axis":["产品"],"y_axis":["销售额"]}]'
# 查询组织
python3 scripts/capture_dashboard.py list-orgs
# 导出截图
python3 scripts/capture_dashboard.py capture --resource-id <ID> --busi-type dashboard --output-dir ./output
# 导出 PDF
python3 scripts/capture_dashboard.py capture --resource-id <ID> --busi-type dashboard --result-format 1 --output-dir ./output
```
## 📁 目录结构
```
dataease-v2-chart-skill/
├── SKILL.md # 技能文档(Agent 读取)
├── README.md # 本文档
├── .env.example # 环境变量模板
├── package.json # Node 依赖
├── scripts/
│ ├── inspect_data.py # 数据探索
│ ├── deploy.py # 图表部署
│ ├── multi_deploy.py # 多图表部署
│ ├── engine.py # 图表引擎
│ ├── client.py # API 客户端
│ ├── capture_dashboard.py # 截图/资源管理
│ └── browser_capture.mjs # 浏览器截图
├── templates/ # 图表模板
├── references/ # API 参考
└── agents/ # Agent 配置
```
## 📊 支持的图表类型
- `bar` - 柱状图
- `line` - 折线图
- `pie` - 饼图
- `table_info` - 明细表
## 📸 截图参数
- `--pixel`: 分辨率,默认 `1920*1080`,可设 `2560*1440`
- `--ext-wait-time`: 额外等待秒数
- `--result-format`: `0`=JPEG, `1`=PDF
## 📝 许可证
MIT
+363
View File
@@ -0,0 +1,363 @@
---
name: dataease
description: DataEase V2 全能技能 - 创建图表看板、查询数据集、管理组织、导出截图/PDF。支持柱状图、折线图、饼图、明细表,部署后自动截图展示。
environment:
required:
- DATAEASE_ACCESS_KEY
- DATAEASE_SECRET_KEY
- DATAEASE_BASE_URL
security:
requiresSecrets: true
sensitiveEnvironment: true
externalNetworkAccess: true
---
# DataEase V2 全能技能
一站式 DataEase 自动化解决方案,融合图表部署与资源管理能力。
## 🎯 核心功能
| 功能模块 | 命令 | 说明 |
|---------|------|------|
| 📊 数据探索 | `inspect_data.py` | 查询数据集列表、字段信息 |
| 📈 图表部署 | `deploy.py` | 创建单图表并自动截图 |
| 📋 多图看板 | `multi_deploy.py` | 创建多图表仪表板并自动截图 |
| 🏢 组织管理 | `capture_dashboard.py list-orgs/switch-org` | 查询/切换组织 |
| 📑 资源列表 | `capture_dashboard.py list-resources` | 列出仪表板/大屏 |
| 📸 截图导出 | `capture_dashboard.py capture` | 导出仪表板截图或PDF |
## 🎭 沟通风格 (Persona)
作为 DataEase 自动化专家:
1. **执行优先**: 直接运行工具,不预先展示代码
2. **结果导向**: 优先展示截图,再说明逻辑
3. **单次往复**: 一次完成从探索到部署的全过程
## 🚀 环境配置
### 1. 配置凭据
复制 `.env.example``.env`,填写:
```bash
DATAEASE_BASE_URL=https://your-dataease.example.com
DATAEASE_ACCESS_KEY=your_access_key
DATAEASE_SECRET_KEY=your_secret_key
DATAEASE_USERNAME=admin # 可选,用于密码登录
DATAEASE_PASSWORD=your_password # 可选
DATAEASE_LOGIN_ORIGIN=0 # 登录源
# 截图输出目录(必须在 OpenClaw workspace 内才能用 MEDIA: 展示,不输入默认直接在 OpenClaw workspace 内创建)
DATAEASE_OUTPUT_DIR=/Users/username/.openclaw/workspace/dataease-output
```
### 2. 安装截图依赖
首次使用需安装 Playwright 浏览器:
```bash
cd {baseDir}
npm install
npx playwright install chromium
```
### 3. 验证连接
```bash
python3 scripts/inspect_data.py --list-datasets
```
---
## 📊 数据探索
### 查询所有数据集
```bash
python3 scripts/inspect_data.py --list-datasets
```
### 查询特定数据集字段
```bash
python3 scripts/inspect_data.py --dataset "<数据集名称或ID>"
```
**返回示例**
```json
[
{"name": "产品", "type": 0, "id": "..."},
{"name": "销售额", "type": 2, "id": "..."}
]
```
字段类型:`0`=文本, `1`=日期, `2`=指标, `3`=数值
---
## 📈 图表部署
### 单图表部署(自动截图)
```bash
python3 scripts/deploy.py <type> <title> <dataset> <x_fields> <y_fields>
```
**参数说明**
- `type`: `bar`(柱状图), `line`(折线图), `pie`(饼图), `table_info`(明细表)
- `title`: 图表标题
- `dataset`: 数据集名称或ID
- `x_fields`: 维度字段(逗号分隔)
- `y_fields`: 指标字段(逗号分隔)
**示例**
```bash
python3 scripts/deploy.py bar '各产品销售额' '销售数据' '产品' '实际销售'
python3 scripts/deploy.py bar '销售对比' '销售数据' '公司' '期望销售,实际销售'
python3 scripts/deploy.py pie '产品占比' '销售数据' '产品类别' '实际销售'
```
**可选参数**
- `--no-screenshot`: 跳过截图,仅返回链接
**输出**
- 自动截图并返回 JSON 结果(包含 `screenshot` 路径)
- Agent 应使用 `MEDIA:<screenshot_path>` 展示截图
### 多图表看板部署
```bash
python3 scripts/multi_deploy.py <dashboard_title> '<charts_json>'
```
**JSON 结构**
```json
[
{
"type": "bar",
"title": "图表标题",
"dataset_name": "数据集名",
"x_axis": ["维度字段"],
"y_axis": ["指标1", "指标2"]
}
]
```
**示例**
```bash
python3 scripts/multi_deploy.py '销售分析看板' '[
{"type":"bar","title":"各产品销售","dataset_name":"销售数据","x_axis":["产品"],"y_axis":["实际销售"]},
{"type":"pie","title":"类别占比","dataset_name":"销售数据","x_axis":["产品类别"],"y_axis":["实际销售"]}
]'
```
---
## 🏢 组织管理
### 查询组织列表
```bash
python3 scripts/capture_dashboard.py list-orgs
```
**返回**:所有可用组织及其 ID
### 切换组织
```bash
python3 scripts/capture_dashboard.py switch-org --org-id <组织ID>
```
切换后,后续操作将在新组织上下文中执行。
---
## 📑 资源列表
### 列出仪表板
```bash
python3 scripts/capture_dashboard.py list-resources --busi-type dashboard
```
### 列出数据大屏
```bash
python3 scripts/capture_dashboard.py list-resources --busi-type dataV
```
### 指定组织查询
```bash
python3 scripts/capture_dashboard.py list-resources --org-id <组织ID> --busi-type dashboard
```
---
## 📸 截图导出
### 导出仪表板截图
```bash
python3 scripts/capture_dashboard.py capture \
--resource-id <仪表板ID> \
--busi-type dashboard \
--output-dir <输出目录>
```
### 导出数据大屏
```bash
python3 scripts/capture_dashboard.py capture \
--resource-id <大屏ID> \
--busi-type dataV \
--output-dir <输出目录>
```
### 导出 PDF
```bash
python3 scripts/capture_dashboard.py capture \
--resource-id <ID> \
--busi-type dashboard \
--result-format 1 \
--output-dir <输出目录>
```
### 按名称匹配导出
```bash
python3 scripts/capture_dashboard.py capture \
--resource-name "销售总览" \
--busi-type dashboard
```
### 高级参数
- `--pixel`: 分辨率,默认 `1920*1080`,可用 `2560*1440`
- `--ext-wait-time`: 额外等待时间(秒),用于复杂图表
- `--org-id`: 指定组织 ID
### ⚠️ 导出结果处理
**重要:导出的图片和 PDF 必须直接发送到对话中展示!**
截图或 PDF 导出成功后,返回的 JSON 中包含 `saved_file` 字段:
```json
{
"ok": true,
"saved_file": "/path/to/file.jpg" // 或 .pdf
}
```
**Agent 必须执行:**
1. 读取 `saved_file` 路径
2. 使用 `MEDIA:<saved_file>` 语法直接发送到对话中
**示例:**
```
MEDIA:/path/to/output/仪表板名_123456.jpg
MEDIA:/path/to/output/仪表板名_123456.pdf
```
**不要只返回文件路径,必须使用 MEDIA: 发送文件!**
---
## 🛠 参数规范
### 图表类型
| 类型 | 说明 | 适用场景 |
|-----|------|---------|
| `bar` | 柱状图 | 分类对比、排名 |
| `line` | 折线图 | 趋势分析、时序数据 |
| `pie` | 饼图 | 占比分析、构成分布 |
| `table_info` | 明细表 | 数据明细展示 |
### 维度与指标
- **维度 (x_axis)**: 类别、日期等分组字段
- **指标 (y_axis)**: 数值、金额等聚合字段
- 明细表的 `y_axis` 传空,`x_axis` 列出所有展示字段
### 业务类型
- `dashboard`: 仪表板
- `dataV`: 数据大屏
---
## 📸 结果处理
### 图表部署结果
部署成功后输出 JSON
```json
{
"ok": true,
"dashboard_id": "1243929230048366592",
"url": "https://...",
"screenshot": "/path/to/screenshot.jpg",
"title": "图表标题"
}
```
**Agent 处理流程**
1. 读取 `screenshot` 路径图片
2. 使用 `MEDIA:<screenshot_path>` 展示
3. 同时提供访问链接
### 截图导出结果
```json
{
"ok": true,
"resource_id": "...",
"resource_name": "...",
"saved_file": "/path/to/file.jpg",
"pixel": "1920*1080",
"capture_engine": "local_playwright"
}
```
---
## ⚠️ 常见问题
### SSL 证书错误
macOS 上 Python 可能缺少系统证书,运行:
```bash
/Applications/Python\ 3.13/Install\ Certificates.command
```
### Playwright 浏览器未安装
```bash
npx playwright install chromium
```
### 截图超时
使用 `--ext-wait-time` 增加等待时间:
```bash
python3 scripts/capture_dashboard.py capture --resource-id <ID> --ext-wait-time 5 --busi-type dashboard
```
### 资源名称匹配失败
使用 `--resource-id` 精确指定,或查看 `candidates` 字段获取候选列表。
---
## 📁 文件结构
```
skills/dataease/
├── SKILL.md # 本文档
├── .env # 环境配置
├── .env.example # 配置模板
├── package.json # Node 依赖
├── scripts/
│ ├── inspect_data.py # 数据探索
│ ├── deploy.py # 单图表部署
│ ├── multi_deploy.py # 多图表部署
│ ├── engine.py # 图表引擎
│ ├── multi_engine.py # 多图表引擎
│ ├── client.py # API 客户端
│ ├── capture_dashboard.py # 截图/资源管理
│ └── browser_capture.mjs # 浏览器截图
├── templates/ # 图表模板
│ ├── chart_bar/
│ ├── chart_line/
│ ├── chart_pie/
│ └── chart_table_info/
├── references/ # 参考文档
│ ├── api.md
│ └── resource_aliases.json
├── agents/
│ └── openai.yaml
└── output/ # 截图输出目录
```
+6
View File
@@ -0,0 +1,6 @@
interface:
display_name: "DataEase 全能技能"
short_description: "创建图表看板、查询数据、管理组织、导出截图/PDF"
icon: "chart-bar"
color: "blue"
default_prompt: "Use dataease skill to explore datasets, create charts and dashboards, manage organizations, list resources, and export screenshots or PDFs. Charts are automatically captured and displayed."
+102
View File
@@ -0,0 +1,102 @@
{
"name": "dataease-skills",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dataease-skills",
"version": "1.0.0",
"dependencies": {
"pdf-lib": "^1.17.1",
"playwright": "^1.59.1"
}
},
"node_modules/@pdf-lib/standard-fonts": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
"integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
"license": "MIT",
"dependencies": {
"pako": "^1.0.6"
}
},
"node_modules/@pdf-lib/upng": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
"integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
"license": "MIT",
"dependencies": {
"pako": "^1.0.10"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/pdf-lib": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
"integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
"license": "MIT",
"dependencies": {
"@pdf-lib/standard-fonts": "^1.0.0",
"@pdf-lib/upng": "^1.0.1",
"pako": "^1.0.11",
"tslib": "^1.11.1"
}
},
"node_modules/playwright": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
"integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.59.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.59.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
"integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
"license": "0BSD"
}
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"name": "dataease-skills",
"version": "1.0.0",
"description": "DataEase v2 Chart Skill with auto-screenshot",
"dependencies": {
"pdf-lib": "^1.17.1",
"playwright": "^1.59.1"
}
}
+145
View File
@@ -0,0 +1,145 @@
# DataEase 接口说明
## 服务地址
- 技能执行时建议从环境变量 `DATAEASE_BASE_URL` 读取,避免把环境地址写死到脚本参数中。
## 1. 查询组织树
### 接口地址
`POST /de2api/org/page/tree`
### 请求参数
- `keyword`
- 组织名称关键字
- `desc`
- 是否倒序
### 本技能的用途
用于查看当前凭证可访问的组织树,并辅助确认目标资源是否位于其他组织。
## 2. 切换组织
### 接口地址
`POST /de2api/user/switch/{orgId}`
### 本技能的用途
在用户显式提供 `orgId` 时切换组织上下文,并使用响应中的 `data.token` 作为后续资源树查询和本地预览截图的 `x-de-token`
## 3. 查询可视化资源树
### 接口地址
`POST /de2api/dataVisualization/tree`
### 请求参数
- `busiFlag`
- `dashboard`:仪表板
- `dataV`:数据大屏
- `resourceTable`
- 默认值:`core`
### 响应字段
- `code`
- `msg`
- `data`
- `id`
- `name`
- `leaf`
- `type`
- `children`
### 本技能的用途
通过该接口获取 dashboard 或 dataV 的资源树,并展开为资源列表,供查询和导出使用。
## 4. 本地预览截图
### 预览页地址
- `/#/preview?dvId={resourceId}`
-`busiType=dashboard` 时追加 `&report=true`
### 本技能的用途
`capture` 命令不再调用 `/de2api/report/export`,而是:
1. 通过 `/de2api/dataVisualization/tree` 定位目标资源
2. 获取可用于前端预览页的 `x-de-token`
3. 打开预览页并将 token 注入 `localStorage.user.token`
4. 等待 `.canvas-container` 渲染完成后本地导出
### 导出参数
- `pixel`
- 浏览器视口大小,格式为 `宽*高`
- `extWaitTime`
- 预览画布可见后额外等待的秒数
- `resultFormat`
- `0`JPEG
- `1`PDF
### 默认值
- `busiType=dashboard`
- `pixel=1920*1080`
- `extWaitTime=0`
- `resultFormat=0`
## 5. 鉴权说明
当前按以下配置项接入:
- `DATAEASE_BASE_URL`
- `DATAEASE_ACCESS_KEY`
- `DATAEASE_SECRET_KEY`
- `DATAEASE_USERNAME`
- `DATAEASE_PASSWORD`
- `DATAEASE_LOGIN_ORIGIN`
### 用户名密码登录方式
- 先通过 `GET /de2api/dekey` 获取前端加密所需的 dekey
- 使用 dekey 解出 RSA 公钥后,对用户名和密码做 RSA 加密
- 再调用 `POST /de2api/login/localLogin`
- 响应中的 `data.token` 可直接作为业务接口和预览页使用的 `x-de-token`
- `origin=0` 表示本地账号,`origin=1` 表示 LDAP
### ask-token 生成方式
- 原始签名串格式:`<accessKey>|<uuid>|<timestamp_ms>`
- 使用 `secretKey` 作为 AES key`accessKey` 作为 IV
- 加密算法:`AES/CBC/PKCS5Padding`
- 将加密结果做 Base64,得到 `signature`
- 使用 `secretKey` 对 JWT 进行 `HS256` 签名
- JWT claim 包含:
- `accessKey`
- `signature`
### 请求头
默认请求头:
- `accessKey`
- `signature`
- `x-de-ask-token`
切换组织后查询资源或导出时:
- `x-de-token`
## 6. 当前脚本对应动作
- `list-orgs`
- `switch-org`
- `list-resources`
- `capture`
如果实际网关存在额外签名规则,请按部署环境调整 `scripts/capture_dashboard.py`
+5
View File
@@ -0,0 +1,5 @@
{
"销售总览": "销售经营总览看板",
"华东分析": "华东区域经营分析",
"门店大屏": "门店运营监控"
}
+503
View File
@@ -0,0 +1,503 @@
#!/usr/bin/env node
import fs from 'node:fs/promises';
const MAX_DEBUG_LOGS = 20;
const RENDER_SETTLE_POLL_MS = 500;
const RENDER_SETTLE_IDLE_MS = 2000;
const MAX_CAPTURE_VIEWPORT_HEIGHT = 12000;
function printHelp() {
console.log(`Usage: node scripts/browser_capture.mjs --url <url> --token <x-de-token> --width 1920 --height 1080 --wait-seconds 0 --result-format 0 --output /abs/path/file.jpg
Options:
--url DataEase preview URL
--token X-DE-TOKEN to inject into localStorage as user.token
--width Browser viewport width in pixels
--height Browser viewport height in pixels
--wait-seconds Extra wait time after canvas is visible
--result-format 0=jpeg, 1=pdf
--output Absolute or relative output path
-h, --help Show this help message
`);
}
function parseArgs(argv) {
const args = {};
for (let index = 0; index < argv.length; index += 1) {
const current = argv[index];
if (current === '-h' || current === '--help') {
args.help = true;
continue;
}
if (!current.startsWith('--')) {
throw new Error(`Unexpected argument: ${current}`);
}
const key = current.slice(2);
const value = argv[index + 1];
if (value == null || value.startsWith('--')) {
throw new Error(`Missing value for --${key}`);
}
args[key] = value;
index += 1;
}
return args;
}
function parsePositiveInteger(value, name) {
const number = Number.parseInt(value, 10);
if (!Number.isFinite(number) || number <= 0) {
throw new Error(`${name} must be a positive integer`);
}
return number;
}
function parseWaitSeconds(value) {
const number = Number.parseInt(value ?? '0', 10);
if (!Number.isFinite(number) || number < 0) {
throw new Error('wait-seconds must be a non-negative integer');
}
return number;
}
async function waitForCanvasReady(page, selector, timeout) {
const locator = page.locator(selector).first();
await locator.waitFor({ state: 'visible', timeout });
await page.waitForFunction(
targetSelector => {
const element = document.querySelector(targetSelector);
if (!element) {
return false;
}
const rect = element.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
},
selector,
{ timeout }
);
await page.waitForTimeout(500);
return locator;
}
function buildWsCacheItem(value, now = Date.now(), expiresAt = 253402300799000) {
return JSON.stringify({
c: now,
e: expiresAt,
v: JSON.stringify(value)
});
}
async function collectDiagnostics(page, debugLogs) {
const state = await page.evaluate(() => {
const app = document.querySelector('#app');
const visible = element => {
if (!element) {
return false;
}
const style = window.getComputedStyle(element);
const rect = element.getBoundingClientRect();
return style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || '1') > 0 && rect.width > 0 && rect.height > 0;
};
return {
href: location.href,
title: document.title,
hasCanvas: !!document.querySelector('.canvas-container'),
hasContent: !!document.querySelector('.content'),
hasEmpty: !!document.querySelector('.empty-background, .el-empty'),
visibleLoadingMasks: Array.from(document.querySelectorAll('.el-loading-mask,.ed-loading-mask,.v-loading-mask')).filter(visible).length,
unfinishedReportLoads: document.querySelectorAll('.report-load:not(.report-load-finish)').length,
bodyText: (document.body?.innerText || '').slice(0, 400),
appHtml: (app?.innerHTML || '').slice(0, 1200)
};
});
return {
...state,
logs: debugLogs.slice(-MAX_DEBUG_LOGS)
};
}
async function getRenderState(page, selector) {
return page.evaluate(targetSelector => {
const visible = element => {
if (!element) {
return false;
}
const style = window.getComputedStyle(element);
const rect = element.getBoundingClientRect();
return style.display !== 'none' && style.visibility !== 'hidden' && Number(style.opacity || '1') > 0 && rect.width > 0 && rect.height > 0;
};
const canvas = document.querySelector(targetSelector);
const loadingSelectors = '.el-loading-mask,.ed-loading-mask,.v-loading-mask,[class*="loading-mask"]';
const visibleLoadingMasks = Array.from(document.querySelectorAll(loadingSelectors)).filter(visible).length;
const unfinishedReportLoads = document.querySelectorAll('.report-load:not(.report-load-finish)').length;
const box = canvas?.getBoundingClientRect();
return {
hasCanvas: !!canvas,
visibleLoadingMasks,
unfinishedReportLoads,
width: Math.round(box?.width || 0),
height: Math.round(box?.height || 0)
};
}, selector);
}
async function waitForRenderSettled(page, selector, timeout, debugLogs, getPendingRequests) {
const startedAt = Date.now();
let stableSince = 0;
let lastSnapshot = '';
while (Date.now() - startedAt < timeout) {
const state = await getRenderState(page, selector);
const pendingRequests = getPendingRequests();
const snapshot = JSON.stringify({ ...state, pendingRequests });
if (snapshot !== lastSnapshot) {
debugLogs.push(`render-state ${snapshot}`);
lastSnapshot = snapshot;
}
const settled =
state.hasCanvas &&
state.width > 0 &&
state.height > 0 &&
state.visibleLoadingMasks === 0 &&
state.unfinishedReportLoads === 0 &&
pendingRequests === 0;
if (settled) {
if (!stableSince) {
stableSince = Date.now();
}
if (Date.now() - stableSince >= RENDER_SETTLE_IDLE_MS) {
return state;
}
} else {
stableSince = 0;
}
await page.waitForTimeout(RENDER_SETTLE_POLL_MS);
}
const state = await getRenderState(page, selector);
throw new Error(
`render settle timeout: ${JSON.stringify({
...state,
pendingRequests: getPendingRequests()
})}`
);
}
async function expandScrollableCapture(page, selector, viewportWidth, viewportHeight) {
return page.evaluate(
({ targetSelector, maxViewportHeight, minViewportHeight, viewportWidth }) => {
const canvas = document.querySelector(targetSelector);
if (!canvas) {
return null;
}
const isScrollable = element => {
if (!element) {
return false;
}
const style = window.getComputedStyle(element);
const overflowY = style.overflowY;
return ['auto', 'scroll', 'overlay'].includes(overflowY) && element.scrollHeight - element.clientHeight > 2;
};
const applyStyle = (element, key, value) => {
const styleKey = key;
element.style[styleKey] = value;
};
let scrollableAncestor = null;
let current = canvas.parentElement;
while (current && current !== document.body) {
if (isScrollable(current)) {
scrollableAncestor = current;
break;
}
current = current.parentElement;
}
const scrollingElement = document.scrollingElement || document.documentElement;
const initialRect = canvas.getBoundingClientRect();
if (scrollableAncestor) {
scrollableAncestor.scrollTop = 0;
applyStyle(scrollableAncestor, 'overflowY', 'visible');
applyStyle(scrollableAncestor, 'height', `${scrollableAncestor.scrollHeight}px`);
applyStyle(scrollableAncestor, 'maxHeight', 'none');
}
if (scrollingElement) {
scrollingElement.scrollTop = 0;
}
window.scrollTo(0, 0);
const rect = canvas.getBoundingClientRect();
const fullHeight = Math.max(
Math.ceil(rect.height),
Math.ceil(canvas.scrollHeight || 0),
Math.ceil(scrollableAncestor?.scrollHeight || 0),
Math.ceil(document.documentElement.scrollHeight || 0)
);
return {
hadScrollableAncestor: Boolean(scrollableAncestor),
initialHeight: Math.ceil(initialRect.height),
finalHeight: Math.ceil(rect.height),
captureHeight: fullHeight,
viewportHeight: Math.min(Math.max(fullHeight, minViewportHeight), maxViewportHeight),
viewportWidth: viewportWidth
};
},
{
targetSelector: selector,
maxViewportHeight: MAX_CAPTURE_VIEWPORT_HEIGHT,
minViewportHeight: viewportHeight,
viewportWidth: viewportWidth
}
);
}
async function toPdfBuffer(pngBytes, PDFDocument) {
const pdfDoc = await PDFDocument.create();
const image = await pdfDoc.embedPng(pngBytes);
const page = pdfDoc.addPage([image.width, image.height]);
page.drawImage(image, {
x: 0,
y: 0,
width: image.width,
height: image.height
});
const pdfBytes = await pdfDoc.save();
return Buffer.from(pdfBytes);
}
async function main() {
let browser;
let page;
const debugLogs = [];
try {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
printHelp();
return;
}
const url = args.url;
const token = args.token;
const output = args.output;
if (!url || !token || !output) {
throw new Error('url, token and output are required');
}
const width = parsePositiveInteger(args.width ?? '1920', 'width');
const height = parsePositiveInteger(args.height ?? '1080', 'height');
const waitSeconds = parseWaitSeconds(args['wait-seconds']);
const resultFormat = Number.parseInt(args['result-format'] ?? '0', 10);
if (![0, 1].includes(resultFormat)) {
throw new Error('result-format must be 0 or 1');
}
const [{ chromium }, { PDFDocument }] = await Promise.all([
import('playwright'),
import('pdf-lib')
]);
browser = await chromium.launch({
headless: true,
executablePath: process.env.CHROMIUM_PATH || undefined
});
const context = await browser.newContext({
viewport: { width, height },
deviceScaleFactor: 1,
ignoreHTTPSErrors: true,
extraHTTPHeaders: {
'X-DE-TOKEN': token
}
});
await context.route('**/de2api/**', async route => {
const request = route.request();
const headers = {
...request.headers(),
'X-DE-TOKEN': token
};
const url = request.url();
if (url.includes('/de2api/outerParams/getOuterParamsInfo/')) {
try {
const response = await route.fetch({ headers });
if (response.status() < 500) {
await route.fulfill({ response });
return;
}
debugLogs.push(`outerParams fallback: ${response.status()} ${url}`);
} catch (error) {
debugLogs.push(`outerParams fallback error: ${error?.message || String(error)}`);
}
await route.fulfill({
status: 200,
contentType: 'application/json;charset=UTF-8',
body: JSON.stringify({
code: 0,
msg: 'success',
data: {
outerParamsInfoMap: {},
outerParamsInfoBaseMap: {}
}
})
});
return;
}
await route.continue({ headers });
});
page = await context.newPage();
const selector = '.canvas-container';
const timeout = 120000;
const pendingRequests = new Set();
const trackRequest = request => {
const resourceType = request.resourceType();
if (!['fetch', 'xhr'].includes(resourceType)) {
return false;
}
return request.url().includes('/de2api/');
};
page.on('request', request => {
if (trackRequest(request)) {
pendingRequests.add(request);
}
});
page.on('requestfinished', request => {
pendingRequests.delete(request);
});
page.on('requestfailed', request => {
pendingRequests.delete(request);
});
page.on('response', response => {
if (response.status() >= 400 && response.url().includes('/de2api/')) {
debugLogs.push(`response ${response.status()} ${response.url()}`);
}
});
page.on('pageerror', error => {
debugLogs.push(`pageerror ${error.message}`);
});
page.on('console', message => {
if (message.type() === 'error') {
debugLogs.push(`console ${message.text()}`);
}
});
const now = Date.now();
await page.addInitScript(
({ injectedToken, cacheToken, cacheExp, cacheTime }) => {
localStorage.setItem('user.token', cacheToken);
localStorage.setItem('user.exp', cacheExp);
localStorage.setItem('user.time', cacheTime);
localStorage.setItem('__de_raw_token__', injectedToken);
},
{
injectedToken: token,
cacheToken: buildWsCacheItem(token, now),
cacheExp: buildWsCacheItem(now + 3600 * 1000, now),
cacheTime: buildWsCacheItem(now, now)
}
);
await page.goto(url, { waitUntil: 'domcontentloaded', timeout });
try {
await page.waitForLoadState('networkidle', { timeout: 10000 });
} catch {
// Preview pages may keep network connections alive; selector checks below are stricter.
}
const locator = await waitForCanvasReady(page, selector, timeout);
const renderState = await waitForRenderSettled(
page,
selector,
timeout,
debugLogs,
() => pendingRequests.size
);
const expandedCapture = await expandScrollableCapture(page, selector, width, height);
if (expandedCapture?.captureHeight > height) {
await page.setViewportSize({
width,
height: expandedCapture.viewportHeight
});
await page.waitForTimeout(500);
await waitForRenderSettled(
page,
selector,
timeout,
debugLogs,
() => pendingRequests.size
);
}
if (waitSeconds > 0) {
await page.waitForTimeout(waitSeconds * 1000);
}
const box = await locator.boundingBox();
if (!box || box.width < 1 || box.height < 1) {
throw new Error('canvas container is empty');
}
if (resultFormat === 1) {
const pngBytes = await locator.screenshot({
type: 'png',
animations: 'disabled'
});
const pdfBytes = await toPdfBuffer(pngBytes, PDFDocument);
await fs.writeFile(output, pdfBytes);
console.log(JSON.stringify({
ok: true,
format: 'pdf',
width: Math.round(box.width),
height: Math.round(box.height),
renderState,
expandedCapture,
output
}));
return;
}
await locator.screenshot({
path: output,
type: 'jpeg',
quality: 90,
animations: 'disabled'
});
console.log(JSON.stringify({
ok: true,
format: 'jpeg',
width: Math.round(box.width),
height: Math.round(box.height),
renderState,
expandedCapture,
output
}));
} catch (error) {
if (error && error.code === 'ERR_MODULE_NOT_FOUND') {
console.error('Missing runtime dependency. Please run `npm install` and `npx playwright install chromium` first.');
process.exitCode = 1;
return;
}
try {
const diagnostics = page
? await collectDiagnostics(page, debugLogs)
: null;
if (diagnostics) {
console.error(`${error && error.stack ? error.stack : String(error)}\nDiagnostics: ${JSON.stringify(diagnostics)}`);
} else {
console.error(error && error.stack ? error.stack : String(error));
}
} catch {
console.error(error && error.stack ? error.stack : String(error));
}
process.exitCode = 1;
} finally {
if (browser) {
await browser.close();
}
}
}
main();
File diff suppressed because it is too large Load Diff
+82
View File
@@ -0,0 +1,82 @@
import json
import time
import uuid
import base64
import os
import requests
import jwt
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.backends import default_backend
import urllib3
urllib3.disable_warnings()
class DataEaseClient:
def __init__(self, base_url, access_key, secret_key):
# Separate base URL and API prefix
self.base_url = base_url.rstrip('/')
self.api_prefix = os.environ.get('DATAEASE_API_PREFIX', '/de2api')
self.access_key = access_key
self.secret_key = secret_key
def _get_signature(self, uid, timestamp):
src_str = f"{self.access_key}|{uid}|{timestamp}"
padder = padding.PKCS7(128).padder()
padded_data = padder.update(src_str.encode('utf-8')) + padder.finalize()
cipher = Cipher(
algorithms.AES(self.secret_key.encode('utf-8')),
modes.CBC(self.access_key.encode('utf-8')),
backend=default_backend()
)
encryptor = cipher.encryptor()
ciphertext = encryptor.update(padded_data) + encryptor.finalize()
return base64.b64encode(ciphertext).decode('utf-8')
def _get_headers(self):
timestamp = str(int(time.time() * 1000))
uid = str(uuid.uuid4())
signature = self._get_signature(uid, timestamp)
claims = {
"accessKey": self.access_key,
"signature": signature
}
token = jwt.encode(claims, self.secret_key, algorithm="HS256")
if isinstance(token, bytes):
token = token.decode('utf-8')
return {
"accessKey": self.access_key,
"signature": signature,
"x-de-ask-token": token,
"timestamp": timestamp,
"nonce": uid,
"Content-Type": "application/json"
}
def get(self, path, params=None):
url = f"{self.base_url}{self.api_prefix}{path}"
response = requests.get(url, headers=self._get_headers(), params=params, verify=False)
return response
def post(self, path, payload=None):
url = f"{self.base_url}{self.api_prefix}{path}"
response = requests.post(url, headers=self._get_headers(), json=payload, verify=False)
return response
def get_dataset_fields(self, dataset_id):
return self.post(f"/datasetField/listByDatasetGroup/{dataset_id}").json()
def update_publish_status(self, dashboard_id, name, status=1, type='dashboard', mobile_layout=False, active_view_ids=None):
url = f"{self.base_url}{self.api_prefix}/dataVisualization/updatePublishStatus"
payload = {
"id": dashboard_id,
"name": name,
"mobileLayout": mobile_layout,
"activeViewIds": active_view_ids or [],
"status": status,
"type": type
}
response = requests.post(url, headers=self._get_headers(), json=payload, verify=False)
response.raise_for_status()
return response.json()
+158
View File
@@ -0,0 +1,158 @@
import sys
import os
import json
import subprocess
# Add local path for engine import
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from engine import DataEaseChartEngine
# --- Configuration from Environment ---
def load_dotenv():
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
if os.path.exists(env_path):
with open(env_path) as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
os.environ[key.strip()] = value.strip().strip('"').strip("'")
load_dotenv()
ACCESS_KEY = os.environ.get("DATAEASE_ACCESS_KEY")
SECRET_KEY = os.environ.get("DATAEASE_SECRET_KEY")
BASE_URL = os.environ.get("DATAEASE_BASE_URL")
USERNAME = os.environ.get("DATAEASE_USERNAME")
PASSWORD = os.environ.get("DATAEASE_PASSWORD")
LOGIN_ORIGIN = os.environ.get("DATAEASE_LOGIN_ORIGIN", "0")
# Screenshot configuration - auto-detect OpenClaw workspace for MEDIA: display
def _default_output_dir():
"""优先使用 OpenClaw workspace 目录,否则用 skill 本地 output 目录"""
# 1. 环境变量显式指定
env_dir = os.environ.get("DATAEASE_OUTPUT_DIR")
if env_dir:
return env_dir
# 2. 自动检测 OpenClaw workspace
home = os.path.expanduser("~")
workspace_dir = os.path.join(home, ".openclaw", "workspace", "dataease-output")
if os.path.isdir(os.path.join(home, ".openclaw", "workspace")):
os.makedirs(workspace_dir, exist_ok=True)
return workspace_dir
# 3. 回退到 skill 本地 output
return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "output")
OUTPUT_DIR = _default_output_dir()
DEFAULT_PIXEL = "2560*1440" # 更高分辨率,图片更清晰
def capture_dashboard(dashboard_id, output_format="jpeg"):
"""调用 capture_dashboard.py 进行截图"""
scripts_dir = os.path.dirname(os.path.abspath(__file__))
capture_script = os.path.join(scripts_dir, "capture_dashboard.py")
if not os.path.exists(capture_script):
return None, "Screenshot script not found"
# 确保输出目录存在
os.makedirs(OUTPUT_DIR, exist_ok=True)
# capture_dashboard.py 使用相同的 BASE_URL(不含 /de2api 后缀)
capture_base_url = BASE_URL.rstrip("/")
# 调用截图脚本
result_format = "0" if output_format == "jpeg" else "1" # 0=jpeg, 1=pdf
cmd = [
"python3", capture_script,
"capture",
"--resource-id", str(dashboard_id),
"--busi-type", "dashboard",
"--output-dir", OUTPUT_DIR,
"--result-format", result_format,
"--base-url", capture_base_url,
"--pixel", DEFAULT_PIXEL
]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
if result.returncode == 0:
output = json.loads(result.stdout)
if output.get("ok"):
return output.get("saved_file"), None
else:
return None, output.get("error", "Unknown capture error")
else:
return None, result.stderr or "Capture failed"
except subprocess.TimeoutExpired:
return None, "Capture timeout"
except Exception as e:
return None, str(e)
def main():
if not BASE_URL:
print("Error: Missing required environment variable DATAEASE_BASE_URL.")
sys.exit(1)
if not ACCESS_KEY and not USERNAME:
print("Error: Missing authentication. Please set either:")
print(" DATAEASE_ACCESS_KEY + DATAEASE_SECRET_KEY")
print(" or DATAEASE_USERNAME + DATAEASE_PASSWORD")
sys.exit(1)
if len(sys.argv) < 5:
print("Usage: python3 deploy.py <type> <title> <dataset_name_or_id> <x_fields> <y_fields> [--no-screenshot]")
print("Example: python3 deploy.py line 'Skill Test' '电商用户购买行为' '访问平台' '访问次数'")
sys.exit(1)
chart_type = sys.argv[1]
title = sys.argv[2]
dataset_id = sys.argv[3]
x_axis = [f.strip() for f in sys.argv[4].split(',') if f.strip()]
y_axis = [f.strip() for f in sys.argv[5].split(',') if f.strip()]
no_screenshot = "--no-screenshot" in sys.argv
if ACCESS_KEY and SECRET_KEY:
engine = DataEaseChartEngine(BASE_URL, ACCESS_KEY, SECRET_KEY)
else:
engine = DataEaseChartEngine(BASE_URL, USERNAME, PASSWORD, auth_mode="password")
try:
did, url = engine.deploy(chart_type, title, dataset_id, x_axis, y_axis)
print(f"\n✅ Successfully deployed!")
print(f"Dashboard ID: {did}")
print(f"URL: {url}")
# 自动截图
if not no_screenshot:
print("\n📸 Capturing screenshot...")
screenshot_path, error = capture_dashboard(did)
if screenshot_path:
print(f"Screenshot saved: {screenshot_path}")
# 输出 JSON 结果供 Agent 解析
result = {
"ok": True,
"dashboard_id": str(did),
"url": url,
"screenshot": screenshot_path,
"title": title
}
print(f"\n__RESULT_JSON__\n{json.dumps(result, ensure_ascii=False)}\n__END_JSON__")
else:
print(f"⚠️ Screenshot failed: {error}")
result = {
"ok": True,
"dashboard_id": str(did),
"url": url,
"screenshot": None,
"screenshot_error": error,
"title": title
}
print(f"\n__RESULT_JSON__\n{json.dumps(result, ensure_ascii=False)}\n__END_JSON__")
except Exception as e:
print(f"\n❌ Deployment failed: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()
+326
View File
@@ -0,0 +1,326 @@
import json, re, time, random, os, sys, requests
import urllib3
urllib3.disable_warnings()
# Local SDK client
from client import DataEaseClient
class DataEaseChartEngine:
def __init__(self, base_url, ak_or_user, sk_or_password, auth_mode=None):
self.base_url = base_url.rstrip('/')
self.api_prefix = os.environ.get('DATAEASE_API_PREFIX', '/de2api')
self.auth_mode = auth_mode or 'aksk'
if self.auth_mode == 'password':
import importlib.util
spec = importlib.util.spec_from_file_location("inspect_data", os.path.join(os.path.dirname(__file__), "inspect_data.py"))
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
x_de_token = mod.login_with_password(base_url, self.api_prefix, ak_or_user, sk_or_password)
self.headers = mod.get_headers_token(x_de_token)
self.client = None
else:
self.client = DataEaseClient(base_url, ak_or_user, sk_or_password)
self.headers = None
def _get_headers(self):
if self.headers:
return self.headers
return self._get_headers()
def _post(self, path, payload=None):
url = f"{self.base_url}{self.api_prefix}{path}"
return requests.post(url, headers=self._get_headers(), json=payload, verify=False, timeout=30)
def _get(self, path, params=None):
url = f"{self.base_url}{self.api_prefix}{path}"
return requests.get(url, headers=self._get_headers(), params=params, verify=False, timeout=30)
def resolve_dataset_id(self, name_or_id):
"""Resolve dataset name to ID using /datasetTree/tree if needed"""
if str(name_or_id).isdigit() and len(str(name_or_id)) > 10:
return name_or_id
url = f"{self.base_url}{self.api_prefix}/datasetTree/tree"
headers = self._get_headers()
resp = requests.post(url, headers=headers, json={"busiFlag": "dataset"}, verify=False, timeout=30)
if resp.status_code != 200:
raise Exception(f"Failed to fetch dataset tree: {resp.text}")
nodes = resp.json().get('data', [])
def find_in_tree(items, target_name):
for item in items:
if item.get('name') == target_name:
return item.get('id')
children = item.get('children', [])
if children:
found = find_in_tree(children, target_name)
if found: return found
return None
dataset_id = find_in_tree(nodes, name_or_id)
if not dataset_id:
raise ValueError(f"Dataset '{name_or_id}' not found in DataEase")
return dataset_id
def get_dataset_ctx(self, dataset_name_or_id, x_names, y_names):
"""Fetch dataset metadata and build rendering context"""
dataset_id = self.resolve_dataset_id(dataset_name_or_id)
# DataEase v2 often uses datasetGroup for the tree structure
url = f"{self.base_url}{self.api_prefix}/datasetTree/details/{dataset_id}"
headers = self._get_headers()
resp = requests.get(url, headers=headers, verify=False, timeout=30)
if resp.status_code != 200:
# Fallback to the original method if the specific tree detail endpoint fails
resp = self._post(f"/datasetField/listByDatasetGroup/{dataset_id}")
fields = resp.json().get('data', [])
else:
data = resp.json().get('data', {})
fields = data.get('allFields', [])
if not fields:
# Try to get from datasetField/listByDatasetGroup if tree details is empty
resp = self._post(f"/datasetField/listByDatasetGroup/{dataset_id}")
fields = resp.json().get('data', [])
if not fields:
raise ValueError(f"No fields found for dataset {dataset_id}")
f_map = {f['name']: f for f in fields}
# Get table/datasource IDs from the first field (they should be common)
datasource_id = fields[0].get('datasourceId', "")
table_id = fields[0].get('datasetTableId', "")
ctx = {
"DATASET_GROUP_ID": dataset_id,
"DATASOURCE_ID": datasource_id,
"DATASET_TABLE_ID": table_id,
}
# Map axis fields (supports multi-measure if needed)
for i, name in enumerate(x_names):
suffix = "" if i == 0 else str(i+1)
f = f_map.get(name)
if not f: raise ValueError(f"X-Axis Field '{name}' not found")
ctx[f"XAXIS{suffix}_FIELD_ID"] = f['id']
ctx[f"XAXIS{suffix}_DE_NAME"] = f['dataeaseName']
for i, name in enumerate(y_names):
suffix = "" if i == 0 else str(i+1)
f = f_map.get(name)
if not f: raise ValueError(f"Y-Axis Field '{name}' not found")
ctx[f"YAXIS{suffix}_FIELD_ID"] = f['id']
ctx[f"YAXIS{suffix}_DE_NAME"] = f['dataeaseName']
ctx[f"YAXIS{suffix}_SERIES_ID"] = f"{f['id']}-yAxis"
# Additional keys for deep parameterization
if i == 0:
ctx["YAXIS_FIELD_ID"] = f['id']
ctx["YAXIS_DE_NAME"] = f['dataeaseName']
ctx["YAXIS_SERIES_ID"] = f"{f['id']}-yAxis"
elif i == 1:
ctx["YAXIS2_FIELD_ID"] = f['id']
ctx["YAXIS2_DE_NAME"] = f['dataeaseName']
ctx["YAXIS2_SERIES_ID"] = f"{f['id']}-yAxis"
return ctx
def deploy(self, chart_type, title, dataset_id, x_names, y_names, layout=None):
# 1. Directory standardization
# Use relative path from this script (scripts/engine.py) to templates/
base_dir = os.path.dirname(os.path.abspath(__file__))
tpl_dir = os.path.join(base_dir, "..", "templates", f"chart_{chart_type}")
if not os.path.exists(tpl_dir):
raise FileNotFoundError(f"Template directory {tpl_dir} not found")
with open(os.path.join(tpl_dir, "template.j2")) as f: template_str = f.read()
with open(os.path.join(tpl_dir, "params.json")) as f:
raw_params = json.load(f)
# 2. Build Context (Flatten params + dynamic IDs)
ctx = {}
# Flatten nested params.json
def flatten_dict(d):
items = {}
for k, v in d.items():
if isinstance(v, dict):
items.update(flatten_dict(v))
elif not k.startswith("_"):
items[k] = v
return items
ctx.update(flatten_dict(raw_params))
ctx.update(self.get_dataset_ctx(dataset_id, x_names, y_names))
# 3. Runtime Randomization
view_id, content_id = self.rand_id(), self.rand_id()
ctx.update({"VIEW_ID": view_id, "CONTENT_ID": content_id, "SCENE_ID": "0"})
# 4. Thorough Parameterization (Global substitution)
def rep(m):
k = m.group(1).strip()
if k in ctx:
return str(ctx[k])
return m.group(0) # Keep placeholder if not found
rendered = re.sub(r"\{\{\s*(\w+)\s*\}\}", rep, template_str)
payload = json.loads(rendered)
# 5. Type Closure & Payload Finalization
board_name = f"{title}_{int(time.time())}"
# Override title in canvasViewInfo
if "canvasViewInfo" in payload and view_id in payload["canvasViewInfo"]:
view_info = payload["canvasViewInfo"][view_id]
view_info["title"] = title
# Deep update all field names (xAxis, yAxis, labels, tooltips, etc.)
def deep_update_field_names(obj, target_id, new_name):
if isinstance(obj, dict):
if str(obj.get("id")) == str(target_id):
obj["name"] = new_name
if "description" in obj: obj["description"] = new_name
if "originName" in obj: obj["originName"] = new_name
# 核心修复:DataEase V2 在某些场景下会回退到 dbFieldName,强制设为 null 或同步
if "dbFieldName" in obj: obj["dbFieldName"] = None
# 特殊修复:处理 DataEase 特有的标签和提示框拼接字段 (如 "访问平台(求和)")
# 我们无法预知原始模板里的名字,所以这里尝试匹配常见的拼接模式
if "optionLabel" in obj:
obj["optionLabel"] = new_name + (obj["optionLabel"].partition("(")[1] + obj["optionLabel"].partition("(")[2] if "(" in obj["optionLabel"] else "")
if "optionShowName" in obj:
obj["optionShowName"] = new_name + (obj["optionShowName"].partition("(")[1] + obj["optionShowName"].partition("(")[2] if "(" in obj["optionShowName"] else "")
for v in obj.values():
deep_update_field_names(v, target_id, new_name)
elif isinstance(obj, list):
for item in obj:
deep_update_field_names(item, target_id, new_name)
# Apply updates for X and Y axis fields
# We use the field IDs obtained from dataset metadata to find and replace
dataset_ctx = self.get_dataset_ctx(dataset_id, x_names, y_names)
# Pre-scan templates/params.json for the old names to ensure replacement works
# or use common default names from templates
old_names_to_clear = ["访问次数", "访问平台", "浏览量"]
for i, x_name in enumerate(x_names):
f_id = dataset_ctx.get(f"XAXIS{'' if i==0 else i+1}_FIELD_ID")
if f_id:
deep_update_field_names(view_info, f_id, x_name)
for i, y_name in enumerate(y_names):
f_id = dataset_ctx.get(f"YAXIS{'' if i==0 else i+1}_FIELD_ID")
if f_id:
deep_update_field_names(view_info, f_id, y_name)
# 最终保底:如果还有残留的“访问次数”或“访问平台”,强行全局替换
def brute_force_replace(obj, search_list, replace_list):
if isinstance(obj, dict):
for k, v in obj.items():
if isinstance(v, str):
for s, r in zip(search_list, replace_list):
if s in v:
obj[k] = v.replace(s, r)
else:
brute_force_replace(v, search_list, replace_list)
elif isinstance(obj, list):
for item in obj:
brute_force_replace(item, search_list, replace_list)
# Use primary X/Y names for brute force fallback
if x_names and y_names:
brute_force_replace(view_info, ["访问平台", "访问次数", "浏览量"], [x_names[0], y_names[0], y_names[0]])
# If layout is provided, override it in componentData
if "componentData" in payload:
components = json.loads(payload["componentData"])
if components:
target = components[0]
# Update component name to title
target["name"] = title
target["label"] = title
# 单图表模式:充满整个画布
if layout is None and len(components) == 1:
# DataEase 单图表网格 72x36,让图表充满整个画布
target["x"] = 1
target["y"] = 1
target["sizeX"] = 72
target["sizeY"] = 36
if "style" not in target:
target["style"] = {}
target["style"]["width"] = 1920
target["style"]["height"] = 1080
target["style"]["left"] = 0
target["style"]["top"] = 0
elif layout:
if "x" in layout: target["x"] = layout["x"]
if "y" in layout: target["y"] = layout["y"]
if "sizeX" in layout: target["sizeX"] = layout["sizeX"]
if "sizeY" in layout: target["sizeY"] = layout["sizeY"]
if "style" not in target: target["style"] = {}
if "width" in layout: target["style"]["width"] = layout["width"]
if "height" in layout: target["style"]["height"] = layout["height"]
if "left" in layout: target["style"]["left"] = layout["left"]
if "top" in layout: target["style"]["top"] = layout["top"]
payload["componentData"] = json.dumps(components, separators=(',', ':'), ensure_ascii=False)
# Ensure canvasStyleData is also compact
if "canvasStyleData" in payload:
style = json.loads(payload["canvasStyleData"])
payload["canvasStyleData"] = json.dumps(style, separators=(',', ':'), ensure_ascii=False)
payload.update({
"id": None,
"name": board_name,
"type": "dashboard",
"status": 0,
"dataState": "ready",
"selfWatermarkStatus": True,
"checkVersion": "2.10.20",
"pid": "0",
"mobileLayout": False
})
headers = self._get_headers()
print(f"Deploying chart '{title}' (Type: {chart_type})...")
save_r = requests.post(
f"{self.base_url}{self.api_prefix}/dataVisualization/saveCanvas",
headers=headers, json=payload, verify=False, timeout=30
)
if save_r.status_code != 200:
raise Exception(f"saveCanvas failed: {save_r.text}")
dashboard_id = str(save_r.json()["data"])
# 6. Publish
requests.post(
f"{self.base_url}{self.api_prefix}/dataVisualization/updatePublishStatus",
headers=headers, verify=False, timeout=30,
json={
"id": dashboard_id,
"name": board_name,
"activeViewIds": [view_id],
"status": 1,
"type": "dashboard",
"mobileLayout": False
}
)
# Generate preview URL
return dashboard_id, f"{self.base_url}/#/preview?dvId={dashboard_id}&dvType=dashboard&ignoreParams=true"
@staticmethod
def rand_id():
return str(int(time.time() * 1000) + random.randint(1000, 9999))
+391
View File
@@ -0,0 +1,391 @@
#!/usr/bin/env python3
"""
DataEase 数据集探索脚本
支持 AK/SK 和密码登录两种认证方式
"""
import os
import json
import sys
import argparse
import subprocess
import base64
import hashlib
import hmac
import time
import uuid
import shutil
from pathlib import Path
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
# Add scripts to path for engine import
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
def load_dotenv():
"""加载 .env 文件"""
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
if os.path.exists(env_path):
with open(env_path, encoding="utf8") as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
os.environ[key.strip()] = value.strip().strip('"').strip("'")
def base64url(raw):
"""Base64 URL-safe 编码"""
return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii")
def sign_jwt(payload, secret_key):
"""使用 HMAC-SHA256 签名 JWT"""
header = {"alg": "HS256", "typ": "JWT"}
header_part = base64url(json.dumps(header, separators=(",", ":")).encode("utf-8"))
payload_part = base64url(json.dumps(payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8"))
signing_input = f"{header_part}.{payload_part}".encode("ascii")
signature = hmac.new(secret_key.encode("utf-8"), signing_input, hashlib.sha256).digest()
return f"{header_part}.{payload_part}.{base64url(signature)}"
def aes_cipher_name(secret_key):
"""根据密钥长度返回 AES 加密算法名称"""
length = len(secret_key.encode("utf-8"))
if length == 16:
return "aes-128-cbc"
if length == 24:
return "aes-192-cbc"
if length == 32:
return "aes-256-cbc"
raise ValueError("Secret Key 长度必须是 16、24 或 32 字节")
def aes_encrypt(plain_text, secret_key, iv):
"""AES 加密"""
if shutil.which("openssl") is None:
raise RuntimeError("当前环境缺少 openssl 命令,无法生成鉴权签名")
if len(iv.encode("utf-8")) != 16:
raise ValueError("Access Key 长度必须是 16 字节,才能作为 AES IV")
cmd = [
"openssl", "enc", f"-{aes_cipher_name(secret_key)}",
"-base64", "-A", "-nosalt",
"-K", secret_key.encode("utf-8").hex(),
"-iv", iv.encode("utf-8").hex(),
]
proc = subprocess.run(cmd, input=plain_text.encode("utf-8"), capture_output=True, check=False)
if proc.returncode != 0:
stderr = proc.stderr.decode("utf-8", errors="replace").strip()
raise RuntimeError(stderr or "openssl 加密失败")
return proc.stdout.decode("utf-8").strip()
def build_ask_auth(access_key, secret_key):
"""构建 ASK 认证信息"""
source = f"{access_key}|{uuid.uuid4()}|{int(time.time() * 1000)}"
signature = aes_encrypt(source, secret_key, access_key)
token = sign_jwt({"accessKey": access_key, "signature": signature}, secret_key)
return {
"access_key": access_key,
"signature": signature,
"x_de_ask_token": token,
}
def get_headers_ask(ask_auth):
"""构建 ASK 认证请求头"""
return {
"Accept": "application/json;charset=UTF-8",
"Content-Type": "application/json",
"accessKey": ask_auth["access_key"],
"signature": ask_auth["signature"],
"X-DE-ASK-TOKEN": ask_auth["x_de_ask_token"],
}
def fetch_dekey(base_url, api_prefix):
"""获取 dekey 用于密码登录"""
url = f"{base_url.rstrip('/')}{api_prefix}/dekey"
request = Request(url, headers={"Accept": "application/json;charset=UTF-8"}, method="GET")
try:
with urlopen(request, timeout=30) as response:
payload = json.loads(response.read().decode("utf-8"))
data = payload.get("data")
if not isinstance(data, str) or not data:
raise ValueError("dekey 接口未返回有效字符串")
return data
except HTTPError as err:
raise RuntimeError(f"获取 dekey 失败: {err.code}")
def split_dekey(dekey):
"""拆分 dekey"""
separator = base64.urlsafe_b64encode(b"-pk_separator-").decode("ascii")
if separator and separator in dekey:
parts = dekey.split(separator, 1)
if len(parts) == 2 and parts[0] and parts[1]:
return parts[0], parts[1]
raise ValueError("dekey 格式不符合预期")
def format_public_key(public_key):
"""格式化公钥"""
body = "\n".join(public_key[index:index + 64] for index in range(0, len(public_key), 64))
return f"-----BEGIN PUBLIC KEY-----\n{body}\n-----END PUBLIC KEY-----\n"
# fixed by xuhuanqing
'''
def rsa_encrypt(plain_text, public_key):
"""RSA 加密"""
import tempfile
if shutil.which("openssl") is None:
raise RuntimeError("当前环境缺少 openssl 命令")
with tempfile.TemporaryDirectory(prefix="dataease-pubkey-") as tmpdir:
key_path = Path(tmpdir) / "public.pem"
key_path.write_text(format_public_key(public_key), encoding="utf-8")
proc = subprocess.run(
["openssl", "pkeyutl", "-encrypt", "-pubin", "-inkey", str(key_path)],
input=plain_text.encode("utf-8"),
capture_output=True,
check=False,
)
if proc.returncode != 0:
raise RuntimeError(proc.stderr.decode("utf-8", errors="replace").strip() or "RSA 加密失败")
return base64.b64encode(proc.stdout).decode("ascii")
'''
def rsa_encrypt(plain_text, public_key):
"""RSA加密,兼容OpenSSL pkeyutl参数规范,修复no private key报错"""
import tempfile
import subprocess
import base64
from shutil import which
from pathlib import Path
if which("openssl") is None:
raise RuntimeError("当前环境缺少 openssl 命令")
# 格式化标准PEM公钥
def format_public_key(pub_raw):
body = "\n".join(pub_raw[index:index + 64] for index in range(0, len(pub_raw), 64))
return f"-----BEGIN PUBLIC KEY-----\n{body}\n-----END PUBLIC KEY-----\n"
pub_pem = format_public_key(public_key)
# 创建临时公钥文件
with tempfile.NamedTemporaryFile(mode="w", suffix=".pem", delete=False, encoding="utf-8") as tmp_key:
tmp_key.write(pub_pem)
tmp_key_path = tmp_key.name
try:
# 标准规范参数:-inkey 指定公钥文件、-pubin 标识文件类型为公钥
proc = subprocess.run(
[
"openssl", "pkeyutl",
"-encrypt",
"-pubin",
"-inkey", tmp_key_path
],
input=plain_text.encode("utf-8"),
capture_output=True,
check=False
)
if proc.returncode != 0:
err_msg = proc.stderr.decode("utf-8", errors="replace").strip()
raise RuntimeError(f"RSA加密失败: {err_msg}")
return base64.b64encode(proc.stdout).decode("ascii")
finally:
# 执行完毕强制删除临时密钥文件
Path(tmp_key_path).unlink(missing_ok=True)
def login_with_password(base_url, api_prefix, username, password):
"""使用密码登录获取 x-de-token"""
dekey = fetch_dekey(base_url, api_prefix)
encrypted_pk, aes_key_str = split_dekey(dekey)
# 解密获取公钥
cmd = [
"openssl", "enc", f"-{aes_cipher_name(aes_key_str)}", "-d",
"-base64", "-A", "-nosalt",
"-K", aes_key_str.encode("utf-8").hex(),
"-iv", b"0000000000000000".hex(),
]
proc = subprocess.run(cmd, input=encrypted_pk.encode("utf-8"), capture_output=True, check=False)
if proc.returncode != 0:
raise RuntimeError("解密 dekey 失败")
public_key = proc.stdout.decode("utf-8").strip()
# RSA 加密用户名和密码
# fixed by xuhuanqing
# encrypted_name = rsa_encrypt(username, public_key)
# encrypted_pwd = rsa_encrypt(password, public_key)
encrypted_name = rsa_encrypt(username, public_key)
encrypted_pwd = rsa_encrypt(password, public_key)
login_origin = int(os.environ.get("DATAEASE_LOGIN_ORIGIN", "0"))
payload = {
"name": encrypted_name,
"pwd": encrypted_pwd,
"origin": login_origin,
}
url = f"{base_url.rstrip('/')}{api_prefix}/login/localLogin"
request = Request(
url,
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST"
)
try:
with urlopen(request, timeout=30) as response:
result = json.loads(response.read().decode("utf-8"))
if result.get("code") not in (None, 0):
raise RuntimeError(f"登录失败: {result.get('msg', '未知错误')}")
# fixed by xuhuanqing
# return result.get("data")
return result.get("data")["token"]
except HTTPError as err:
body = err.read().decode("utf-8", errors="replace")
raise RuntimeError(f"登录请求失败: {err.code} - {body}")
def get_headers_token(x_de_token):
"""构建 Token 认证请求头"""
return {
"Accept": "application/json;charset=UTF-8",
"Content-Type": "application/json",
"X-DE-TOKEN": x_de_token,
}
def make_request(url, headers, payload=None, method="POST"):
"""发送 HTTP 请求"""
data = None if payload is None else json.dumps(payload, ensure_ascii=False).encode("utf-8")
request = Request(url, data=data, headers=headers, method=method)
try:
with urlopen(request, timeout=30) as response:
return json.loads(response.read().decode("utf-8"))
except HTTPError as err:
body = err.read().decode("utf-8", errors="replace")
raise RuntimeError(f"请求失败: {err.code} - {body}")
def list_datasets(base_url, api_prefix, headers):
"""列出所有数据集"""
url = f"{base_url.rstrip('/')}{api_prefix}/datasetTree/tree"
payload = {"busiFlag": "dataset"}
result = make_request(url, headers, payload)
nodes = result.get('data', [])
datasets = []
def collect_leaf(items):
for item in items:
if item.get('leaf'):
datasets.append({"name": item.get('name'), "id": item.get('id')})
children = item.get('children', [])
if children:
collect_leaf(children)
collect_leaf(nodes)
return datasets
def get_dataset_fields(base_url, api_prefix, headers, dataset_id):
"""获取数据集字段"""
# 先尝试详情接口
url = f"{base_url.rstrip('/')}{api_prefix}/datasetTree/details/{dataset_id}"
request = Request(url, headers=headers, method="GET")
try:
with urlopen(request, timeout=30) as response:
result = json.loads(response.read().decode("utf-8"))
fields = result.get('data', {}).get('allFields', [])
if fields:
return fields
except Exception:
pass
# 回退到字段列表接口
url = f"{base_url.rstrip('/')}{api_prefix}/datasetField/listByDatasetGroup/{dataset_id}"
result = make_request(url, headers, method="POST")
return result.get('data', [])
def resolve_dataset_id(base_url, api_prefix, headers, name_or_id):
"""解析数据集名称为 ID"""
if str(name_or_id).isdigit() and len(str(name_or_id)) > 10:
return name_or_id
datasets = list_datasets(base_url, api_prefix, headers)
for ds in datasets:
if ds['name'] == name_or_id:
return ds['id']
raise ValueError(f"未找到数据集: {name_or_id}")
def main():
load_dotenv()
base_url = os.environ.get("DATAEASE_BASE_URL", "")
api_prefix = os.environ.get("DATAEASE_API_PREFIX", "/de2api")
access_key = os.environ.get("DATAEASE_ACCESS_KEY", "")
secret_key = os.environ.get("DATAEASE_SECRET_KEY", "")
username = os.environ.get("DATAEASE_USERNAME", "")
password = os.environ.get("DATAEASE_PASSWORD", "")
if not base_url:
print("Error: 请设置 DATAEASE_BASE_URL")
sys.exit(1)
parser = argparse.ArgumentParser(description="探索 DataEase 数据集和字段")
parser.add_argument("--list-datasets", action="store_true", help="列出所有数据集")
parser.add_argument("--dataset", type=str, help="查看指定数据集的字段")
args = parser.parse_args()
# 认证:优先 AK/SK,否则用密码登录
try:
if access_key and secret_key:
ask_auth = build_ask_auth(access_key, secret_key)
headers = get_headers_ask(ask_auth)
auth_mode = "AK/SK"
elif username and password:
x_de_token = login_with_password(base_url, api_prefix, username, password)
headers = get_headers_token(x_de_token)
auth_mode = "密码登录"
else:
print("Error: 请配置 AK/SK (DATAEASE_ACCESS_KEY + DATAEASE_SECRET_KEY) 或用户名密码 (DATAEASE_USERNAME + DATAEASE_PASSWORD)")
sys.exit(1)
except Exception as e:
print(f"认证失败: {e}")
sys.exit(1)
if args.list_datasets:
try:
datasets = list_datasets(base_url, api_prefix, headers)
print(json.dumps(datasets, ensure_ascii=False, indent=2))
except Exception as e:
print(f"获取数据集列表失败: {e}")
sys.exit(1)
elif args.dataset:
try:
dataset_id = resolve_dataset_id(base_url, api_prefix, headers, args.dataset)
fields = get_dataset_fields(base_url, api_prefix, headers, dataset_id)
result = [
{"name": f['name'], "id": f['id'], "type": f.get('deType'), "dataeaseName": f.get('dataeaseName')}
for f in fields
]
print(json.dumps(result, ensure_ascii=False, indent=2))
except Exception as e:
print(f"获取数据集字段失败: {e}")
sys.exit(1)
else:
parser.print_help()
if __name__ == "__main__":
main()
+148
View File
@@ -0,0 +1,148 @@
import sys
import os
import json
import subprocess
# Add local path for engine import
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from multi_engine import MultiDataEaseChartEngine
# --- Configuration from Environment ---
def load_dotenv():
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
if os.path.exists(env_path):
with open(env_path) as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
os.environ[key.strip()] = value.strip().strip('"').strip("'")
load_dotenv()
ACCESS_KEY = os.environ.get("DATAEASE_ACCESS_KEY")
SECRET_KEY = os.environ.get("DATAEASE_SECRET_KEY")
BASE_URL = os.environ.get("DATAEASE_BASE_URL")
# Screenshot configuration - auto-detect OpenClaw workspace for MEDIA: display
def _default_output_dir():
"""优先使用 OpenClaw workspace 目录,否则用 skill 本地 output 目录"""
env_dir = os.environ.get("DATAEASE_OUTPUT_DIR")
if env_dir:
return env_dir
home = os.path.expanduser("~")
workspace_dir = os.path.join(home, ".openclaw", "workspace", "dataease-output")
if os.path.isdir(os.path.join(home, ".openclaw", "workspace")):
os.makedirs(workspace_dir, exist_ok=True)
return workspace_dir
return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "output")
OUTPUT_DIR = _default_output_dir()
DEFAULT_PIXEL = "2560*1440" # 更高分辨率,图片更清晰
def capture_dashboard(dashboard_id, output_format="jpeg"):
"""调用 capture_dashboard.py 进行截图"""
scripts_dir = os.path.dirname(os.path.abspath(__file__))
capture_script = os.path.join(scripts_dir, "capture_dashboard.py")
if not os.path.exists(capture_script):
return None, "Screenshot script not found"
# 确保输出目录存在
os.makedirs(OUTPUT_DIR, exist_ok=True)
# capture_dashboard.py 使用相同的 BASE_URL(不含 /de2api 后缀)
capture_base_url = BASE_URL.rstrip("/")
# 调用截图脚本
result_format = "0" if output_format == "jpeg" else "1" # 0=jpeg, 1=pdf
cmd = [
"python3", capture_script,
"capture",
"--resource-id", str(dashboard_id),
"--busi-type", "dashboard",
"--output-dir", OUTPUT_DIR,
"--result-format", result_format,
"--base-url", capture_base_url,
"--pixel", DEFAULT_PIXEL
]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
if result.returncode == 0:
output = json.loads(result.stdout)
if output.get("ok"):
return output.get("saved_file"), None
else:
return None, output.get("error", "Unknown capture error")
else:
return None, result.stderr or "Capture failed"
except subprocess.TimeoutExpired:
return None, "Capture timeout"
except Exception as e:
return None, str(e)
def main():
if not all([ACCESS_KEY, SECRET_KEY, BASE_URL]):
print("Error: Missing required environment variables.")
sys.exit(1)
if len(sys.argv) < 3:
print("Usage: python3 multi_deploy.py <dashboard_title> <charts_json_config> [--no-screenshot]")
print("Example: python3 multi_deploy.py '综合分析' '[{\"type\":\"bar\",\"dataset_name\":\"ds1\",\"x_axis\":[\"f1\"],\"y_axis\":[\"f2\"]}]'")
sys.exit(1)
title = sys.argv[1]
try:
charts_config = json.loads(sys.argv[2])
except Exception as e:
print(f"Error parsing JSON config: {e}")
sys.exit(1)
# 检查是否禁用截图
no_screenshot = "--no-screenshot" in sys.argv
engine = MultiDataEaseChartEngine(BASE_URL, ACCESS_KEY, SECRET_KEY)
try:
did, url = engine.deploy_multi(title, charts_config)
print(f"\n✅ Successfully deployed multi-chart dashboard!")
print(f"Dashboard ID: {did}")
print(f"URL: {url}")
# 自动截图
if not no_screenshot:
print("\n📸 Capturing screenshot...")
screenshot_path, error = capture_dashboard(did)
if screenshot_path:
print(f"Screenshot saved: {screenshot_path}")
# 输出 JSON 结果供 Agent 解析
result = {
"ok": True,
"dashboard_id": str(did),
"url": url,
"screenshot": screenshot_path,
"title": title,
"charts_count": len(charts_config)
}
print(f"\n__RESULT_JSON__\n{json.dumps(result, ensure_ascii=False)}\n__END_JSON__")
else:
print(f"⚠️ Screenshot failed: {error}")
result = {
"ok": True,
"dashboard_id": str(did),
"url": url,
"screenshot": None,
"screenshot_error": error,
"title": title,
"charts_count": len(charts_config)
}
print(f"\n__RESULT_JSON__\n{json.dumps(result, ensure_ascii=False)}\n__END_JSON__")
except Exception as e:
print(f"\n❌ Deployment failed: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()
+247
View File
@@ -0,0 +1,247 @@
import json, re, time, random, os, sys, requests
from typing import List, Dict, Any
# Local SDK client
from client import DataEaseClient
from engine import DataEaseChartEngine
class MultiDataEaseChartEngine(DataEaseChartEngine):
def __init__(self, base_url, ak, sk):
super().__init__(base_url, ak, sk)
def extract_chart_payload(self, chart_type: str, dataset_id: str, x_names: list, y_names: list, view_id: str, layout: dict, title: str = None):
"""
Reads the single chart template, applies parameters, and extracts the componentData & canvasViewInfo.
"""
base_dir = os.path.dirname(os.path.abspath(__file__))
tpl_dir = os.path.join(base_dir, "..", "templates", f"chart_{chart_type}")
if not os.path.exists(tpl_dir):
raise FileNotFoundError(f"Template directory {tpl_dir} not found")
with open(os.path.join(tpl_dir, "template.j2")) as f:
template_str = f.read()
with open(os.path.join(tpl_dir, "params.json")) as f:
raw_params = json.load(f)
# Build Context (Flatten params + dynamic IDs)
ctx = {}
def flatten_dict(d):
items = {}
for k, v in d.items():
if isinstance(v, dict):
items.update(flatten_dict(v))
elif not k.startswith("_"):
items[k] = v
return items
ctx.update(flatten_dict(raw_params))
ctx.update(self.get_dataset_ctx(dataset_id, x_names, y_names))
# Runtime Randomization
# SCENE_ID is usually "0" for base
ctx.update({"VIEW_ID": view_id, "SCENE_ID": "0"})
# Sub placeholder
def rep(m):
k = m.group(1).strip()
if k in ctx:
return str(ctx[k])
return m.group(0)
rendered = re.sub(r"\{\{\s*(\w+)\s*\}\}", rep, template_str)
payload = json.loads(rendered)
# Extract canvasViewInfo
canvas_view_info = payload.get("canvasViewInfo", {})
target_view_info = canvas_view_info.get(view_id)
if not target_view_info:
raise ValueError(f"View ID {view_id} not found in the rendered template!")
# Override Chart Title in view_info if provided
if title:
target_view_info["title"] = title
# --- Fix: Robust field name replacement ---
def deep_update_field_names(obj, target_id, new_name):
if isinstance(obj, dict):
if str(obj.get("id")) == str(target_id):
obj["name"] = new_name
if "description" in obj: obj["description"] = new_name
if "originName" in obj: obj["originName"] = new_name
if "dbFieldName" in obj: obj["dbFieldName"] = None
if "optionLabel" in obj:
obj["optionLabel"] = new_name + (obj["optionLabel"].partition("(")[1] + obj["optionLabel"].partition("(")[2] if "(" in obj["optionLabel"] else "")
if "optionShowName" in obj:
obj["optionShowName"] = new_name + (obj["optionShowName"].partition("(")[1] + obj["optionShowName"].partition("(")[2] if "(" in obj["optionShowName"] else "")
for v in obj.values():
deep_update_field_names(v, target_id, new_name)
elif isinstance(obj, list):
for item in obj:
deep_update_field_names(item, target_id, new_name)
dataset_ctx = self.get_dataset_ctx(dataset_id, x_names, y_names)
for i, x_name in enumerate(x_names):
f_id = dataset_ctx.get(f"XAXIS{'' if i==0 else i+1}_FIELD_ID")
if f_id: deep_update_field_names(target_view_info, f_id, x_name)
for i, y_name in enumerate(y_names):
f_id = dataset_ctx.get(f"YAXIS{'' if i==0 else i+1}_FIELD_ID")
if f_id: deep_update_field_names(target_view_info, f_id, y_name)
def brute_force_replace(obj, search_list, replace_list):
if isinstance(obj, dict):
for k, v in obj.items():
if isinstance(v, str):
for s, r in zip(search_list, replace_list):
if s in v: obj[k] = v.replace(s, r)
else: brute_force_replace(v, search_list, replace_list)
elif isinstance(obj, list):
for item in obj: brute_force_replace(item, search_list, replace_list)
if x_names and y_names:
brute_force_replace(target_view_info, ["访问平台", "访问次数", "浏览量"], [x_names[0], y_names[0], y_names[0]])
# --- End Fix ---
# Extract componentData
component_data_str = payload.get("componentData", "[]")
components = json.loads(component_data_str)
if not components:
raise ValueError(f"No component data found in the rendered template")
# We only take the first component as it was a single chart template
target_component = components[0]
# Override component name/label if title provided
if title:
target_component["name"] = title
target_component["label"] = title
# Override layout
if "layout" in layout:
layout = layout["layout"] # in case it's nested
if "style" not in target_component:
target_component["style"] = {}
if "x" in layout: target_component["x"] = layout["x"]
if "y" in layout: target_component["y"] = layout["y"]
if "sizeX" in layout: target_component["sizeX"] = layout["sizeX"]
if "sizeY" in layout: target_component["sizeY"] = layout["sizeY"]
if "width" in layout: target_component["style"]["width"] = layout["width"]
if "height" in layout: target_component["style"]["height"] = layout["height"]
if "left" in layout: target_component["style"]["left"] = layout["left"]
if "top" in layout: target_component["style"]["top"] = layout["top"]
return target_component, target_view_info
def deploy_multi(self, title: str, charts_config: List[Dict[str, Any]]):
"""
Deploy multiple charts into a single dashboard.
charts_config supports optional 'layout'. If missing, it will auto-layout.
"""
content_id = self.rand_id()
board_name = f"{title}_{int(time.time())}"
# Load base dashboard style from template
base_dir = os.path.dirname(os.path.abspath(__file__))
dashboard_tpl_path = os.path.join(base_dir, "..", "templates", "dashboard", "base.json")
if os.path.exists(dashboard_tpl_path):
with open(dashboard_tpl_path) as f:
base_tpl = json.load(f)
canvas_style_data = json.loads(base_tpl.get("canvasStyleData", "{}"))
else:
canvas_style_data = {
"width": 1920, "height": 1080, "screenAdaptor": "widthFirst",
"dashboard": {"gap": "yes", "gapSize": 5, "matrixBase": 4}
}
final_component_data = []
final_canvas_view_info = {}
active_view_ids = []
# Auto-layout calculation constants
GRID_COLS = 72
CHART_WIDTH = 36
CHART_HEIGHT = 14
# Process each chart sequentially
for idx, c_conf in enumerate(charts_config):
view_id = self.rand_id()
active_view_ids.append(view_id)
# Calculate auto-layout if not provided
layout = c_conf.get("layout", {})
if not layout:
row = idx // 2
col = idx % 2
layout = {
"x": col * CHART_WIDTH + 1,
"y": row * CHART_HEIGHT + 1,
"sizeX": CHART_WIDTH,
"sizeY": CHART_HEIGHT,
"width": 519, "height": 65, # Standard relative sizes
"left": col * 519, "top": row * 65
}
comp_data, view_info = self.extract_chart_payload(
chart_type=c_conf["type"],
dataset_id=c_conf["dataset_name"],
x_names=c_conf.get("x_axis", []),
y_names=c_conf.get("y_axis", []),
view_id=view_id,
layout=layout,
title=c_conf.get("title")
)
comp_data["_dragId"] = idx
final_component_data.append(comp_data)
final_canvas_view_info[view_id] = view_info
dashboard_payload = {
"id": None,
"name": board_name,
"type": "dashboard",
"status": 0,
"dataState": "ready",
"selfWatermarkStatus": True,
"checkVersion": "2.10.20",
"pid": "0",
"mobileLayout": False,
"canvasStyleData": json.dumps(canvas_style_data, separators=(',', ':'), ensure_ascii=False),
"componentData": json.dumps(final_component_data, separators=(',', ':'), ensure_ascii=False),
"canvasViewInfo": final_canvas_view_info,
"contentId": content_id
}
headers = self.client._get_headers()
print(f"Deploying multi-chart dashboard '{title}' with {len(charts_config)} charts...")
save_r = requests.post(
f"{self.base_url}{self.api_prefix}/dataVisualization/saveCanvas",
headers=headers, json=dashboard_payload, verify=False, timeout=30
)
if save_r.status_code != 200:
raise Exception(f"saveCanvas failed: {save_r.text}")
dashboard_id = str(save_r.json()["data"])
# Publish
requests.post(
f"{self.base_url}{self.api_prefix}/dataVisualization/updatePublishStatus",
headers=headers, verify=False, timeout=30,
json={
"id": dashboard_id,
"name": board_name,
"activeViewIds": active_view_ids,
"status": 1,
"type": "dashboard",
"mobileLayout": False
}
)
return dashboard_id, f"{self.base_url}/#/preview?dvId={dashboard_id}&dvType=dashboard&ignoreParams=true"
return dashboard_id, f"{self.base_url}/#/preview?dvId={dashboard_id}&dvType=dashboard&ignoreParams=true"
+28
View File
@@ -0,0 +1,28 @@
{
"_comment": "chart_bar 模板参数(来源:DataEase 抓包 + 参数化)",
"view": {
"VIEW_ID": "7447105738385657856",
"CONTENT_ID": "7447106138476122112",
"SCENE_ID": "0"
},
"dataset": {
"DATASOURCE_ID": "1166370337189924864",
"DATASET_TABLE_ID": "7387069976563159040",
"DATASET_GROUP_ID": "1178801986976485376"
},
"fields": {
"xAxis": {
"XAXIS_FIELD_ID": "1761214724097",
"XAXIS_DE_NAME": "f_3319f6ba3e484e93"
},
"yAxis": {
"YAXIS_FIELD_ID": "1761214724096",
"YAXIS_DE_NAME": "f_97666027e11d7dc5",
"YAXIS_SERIES_ID": "1761214724096-yAxis"
},
"yAxis2": {
"YAXIS2_FIELD_ID": "1761214724100",
"YAXIS2_DE_NAME": "f_c4c3c6a1a3d6688b"
}
}
}
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
{
"_comment": "chart_line 模板参数(来源:DataEase 抓包 + 参数化)",
"view": {
"VIEW_ID": "7447105738385657856",
"CONTENT_ID": "7447106138476122112",
"SCENE_ID": "0"
},
"dataset": {
"DATASOURCE_ID": "1166370337189924864",
"DATASET_TABLE_ID": "7387069976563159040",
"DATASET_GROUP_ID": "1178801986976485376"
},
"fields": {
"xAxis": {
"XAXIS_FIELD_ID": "1761214724097",
"XAXIS_DE_NAME": "f_3319f6ba3e484e93"
},
"yAxis": {
"YAXIS_FIELD_ID": "1761214724096",
"YAXIS_DE_NAME": "f_97666027e11d7dc5",
"YAXIS_SERIES_ID": "1761214724096-yAxis"
},
"yAxis2": {
"YAXIS2_FIELD_ID": "1761214724100",
"YAXIS2_DE_NAME": "f_c4c3c6a1a3d6688b"
}
}
}
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
{
"_comment": "chart_pie 模板参数(来源:DataEase 抓包 + 参数化)",
"view": {
"VIEW_ID": "7446557568647827456",
"CONTENT_ID": "7446557957866655744",
"SCENE_ID": "0"
},
"dataset": {
"DATASOURCE_ID": "1166370337189924864",
"DATASET_TABLE_ID": "7387069976563159040",
"DATASET_GROUP_ID": "1178801986976485376"
},
"fields": {
"xAxis": {
"XAXIS_FIELD_ID": "1761214724097",
"XAXIS_DE_NAME": "f_3319f6ba3e484e93"
},
"yAxis": {
"YAXIS_FIELD_ID": "1761214724096",
"YAXIS_DE_NAME": "f_97666027e11d7dc5",
"YAXIS_SERIES_ID": "1761214724096-yAxis"
},
"yAxis2": {
"YAXIS2_FIELD_ID": "1761214724100",
"YAXIS2_DE_NAME": "f_c4c3c6a1a3d6688b"
}
}
}
File diff suppressed because one or more lines are too long
+22
View File
@@ -0,0 +1,22 @@
{
"view": {
"VIEW_ID": "7448293416766541824",
"CONTENT_ID": "7449036212012060672",
"SCENE_ID": "1240025405293989888"
},
"dataset": {
"DATASET_GROUP_ID": "1178801986976485376",
"DATASOURCE_ID": "1166370337189924864",
"DATASET_TABLE_ID": "7387069976563159040"
},
"fields": {
"xAxis": {
"XAXIS_FIELD_ID": "1761214724097",
"XAXIS_DE_NAME": "f_3319f6ba3e484e93"
},
"xAxis2": {
"XAXIS2_FIELD_ID": "1761214724096",
"XAXIS2_DE_NAME": "f_97666027e11d7dc5"
}
}
}
+902
View File
@@ -0,0 +1,902 @@
{
"canvasStyleData": "{\"width\":1920,\"height\":1080,\"refreshBrowserEnable\":false,\"refreshBrowserUnit\":\"minute\",\"refreshBrowserTime\":5,\"refreshViewEnable\":false,\"refreshViewLoading\":true,\"refreshUnit\":\"minute\",\"refreshTime\":5,\"popupAvailable\":true,\"popupButtonAvailable\":true,\"suspensionButtonAvailable\":false,\"screenAdaptor\":\"widthFirst\",\"dashboardAdaptor\":\"keepHeightAndWidth\",\"scale\":33,\"scaleWidth\":24,\"scaleHeight\":24,\"backgroundColorSelect\":true,\"backgroundImageEnable\":false,\"backgroundType\":\"backgroundColor\",\"background\":\"\",\"openCommonStyle\":true,\"opacity\":1,\"fontSize\":14,\"fontFamily\":\"PingFang\",\"themeId\":\"10001\",\"color\":\"#000000\",\"backgroundColor\":\"#f5f6f7\",\"dashboard\":{\"gap\":\"yes\",\"gapSize\":5,\"gapMode\":\"middle\",\"showGrid\":false,\"matrixBase\":4,\"resultMode\":\"all\",\"resultCount\":1000,\"themeColor\":\"light\",\"mobileSetting\":{\"customSetting\":false,\"imageUrl\":null,\"backgroundType\":\"image\",\"color\":\"#000\"}},\"component\":{\"chartTitle\":{\"show\":true,\"fontSize\":16,\"hPosition\":\"left\",\"vPosition\":\"top\",\"isItalic\":false,\"isBolder\":true,\"remarkShow\":false,\"remark\":\"\",\"fontFamily\":\"\",\"letterSpace\":\"0\",\"fontShadow\":false,\"color\":\"#000000\",\"remarkBackgroundColor\":\"#ffffff\"},\"chartColor\":{\"basicStyle\":{\"colorScheme\":\"default\",\"colors\":[\"#1E90FF\",\"#90EE90\",\"#00CED1\",\"#E2BD84\",\"#7A90E0\",\"#3BA272\",\"#2BE7FF\",\"#0A8ADA\",\"#FFD700\"],\"alpha\":100,\"gradient\":false,\"mapStyle\":\"normal\",\"areaBaseColor\":\"#FFFFFF\",\"areaBorderColor\":\"#303133\",\"gaugeStyle\":\"default\",\"tableBorderColor\":\"#E6E7E4\",\"tableScrollBarColor\":\"rgba(0, 0, 0, 0.15)\",\"zoomButtonColor\":\"#aaa\",\"zoomBackground\":\"#fff\"},\"misc\":{\"flowMapConfig\":{\"lineConfig\":{\"mapLineAnimate\":true,\"mapLineGradient\":false,\"mapLineSourceColor\":\"#146C94\",\"mapLineTargetColor\":\"#576CBC\"}},\"nameFontColor\":\"#000000\",\"valueFontColor\":\"#5470c6\"},\"tableHeader\":{\"tableHeaderBgColor\":\"#1E90FF\",\"tableHeaderCornerBgColor\":\"#1E90FF\",\"tableHeaderColBgColor\":\"#1E90FF\",\"tableHeaderFontColor\":\"#000000\",\"tableHeaderCornerFontColor\":\"#000000\",\"tableHeaderColFontColor\":\"#000000\"},\"tableCell\":{\"tableItemBgColor\":\"#FFFFFF\",\"tableFontColor\":\"#000000\",\"tableItemSubBgColor\":\"#1E90FF\"}},\"chartCommonStyle\":{\"backgroundColorSelect\":true,\"backdropFilterEnable\":false,\"backgroundImageEnable\":false,\"backgroundType\":\"innerImage\",\"innerImage\":\"board/board_1.svg\",\"outerImage\":null,\"innerPadding\":{\"mode\":\"uniform\",\"top\":12},\"borderRadius\":{\"mode\":\"uniform\",\"topLeft\":0},\"backdropFilter\":4,\"backgroundColor\":\"rgba(255,255,255,1)\",\"innerImageColor\":\"rgba(16, 148, 229,1)\"},\"filterStyle\":{\"layout\":\"horizontal\",\"titleLayout\":\"left\",\"labelColor\":\"#1f2329\",\"titleColor\":\"#1f2329\",\"color\":\"#1f2329\",\"borderColor\":\"#bbbfc4\",\"text\":\"#1f2329\",\"bgColor\":\"#FFFFFF\"},\"tabStyle\":{\"headPosition\":\"left\",\"headFontColor\":\"#000000\",\"headFontActiveColor\":\"#000000\",\"headBorderColor\":\"#ffffff\",\"headBorderActiveColor\":\"#ffffff\"},\"seniorStyleSetting\":{\"linkageIconColor\":\"#a6a6a6\",\"drillLayerColor\":\"#a6a6a6\",\"pagerColor\":\"#a6a6a6\"},\"formatterItem\":{\"type\":\"auto\",\"unitLanguage\":\"ch\",\"unit\":1,\"suffix\":\"\",\"decimalCount\":2,\"thousandSeparator\":true}},\"dialogBackgroundColor\":\"rgba(255, 255, 255, 1)\",\"dialogButton\":\"#020408\"}",
"componentData": "[{\"animations\":[],\"canvasId\":\"canvas-main\",\"events\":{\"checked\":false,\"showTips\":false,\"type\":\"jump\",\"typeList\":[{\"key\":\"jump\",\"label\":\"jump\"},{\"key\":\"download\",\"label\":\"download\"},{\"key\":\"share\",\"label\":\"share\"},{\"key\":\"fullScreen\",\"label\":\"fullScreen\"},{\"key\":\"showHidden\",\"label\":\"showHidden\"},{\"key\":\"refreshDataV\",\"label\":\"refreshDataV\"},{\"key\":\"refreshView\",\"label\":\"refreshView\"}],\"jump\":{\"value\":\"https://\",\"type\":\"_blank\"},\"download\":{\"value\":true},\"share\":{\"value\":true,\"target\":\"all\"}},\"carousel\":{\"enable\":false,\"time\":10},\"multiDimensional\":{\"enable\":false,\"x\":0,\"y\":0,\"z\":0},\"groupStyle\":{},\"isLock\":false,\"maintainRadio\":false,\"aspectRatio\":1,\"isShow\":true,\"dashboardHidden\":false,\"category\":\"base\",\"dragging\":false,\"resizing\":false,\"collapseName\":[\"position\",\"background\",\"style\",\"picture\",\"frameLinks\",\"videoLinks\",\"streamLinks\",\"carouselInfo\",\"events\",\"decoration_style\"],\"linkage\":{\"duration\":0,\"data\":[{\"id\":\"\",\"label\":\"\",\"event\":\"\",\"style\":[{\"key\":\"\",\"value\":\"\"}]}]},\"component\":\"UserView\",\"name\":\"明细表\",\"label\":\"明细表\",\"propValue\":{\"textValue\":\"\",\"urlList\":[]},\"icon\":\"bar\",\"innerType\":\"table-info\",\"editing\":false,\"canvasActive\":false,\"actionSelection\":{\"linkageActive\":\"custom\"},\"x\":1,\"y\":1,\"sizeX\":36,\"sizeY\":14,\"style\":{\"rotate\":0,\"opacity\":1,\"borderActive\":false,\"borderWidth\":1,\"borderRadius\":5,\"borderStyle\":\"solid\",\"borderColor\":\"rgba(204, 204, 204, 1)\",\"adaptation\":\"adaptation\",\"width\":744,\"height\":139.22222222222223,\"left\":0,\"top\":0},\"matrixStyle\":{},\"commonBackground\":{\"backgroundColorSelect\":true,\"backdropFilterEnable\":false,\"backgroundImageEnable\":false,\"backgroundType\":\"innerImage\",\"innerImage\":\"board/board_1.svg\",\"outerImage\":null,\"innerPadding\":{\"mode\":\"uniform\",\"top\":12},\"borderRadius\":{\"mode\":\"uniform\",\"topLeft\":0},\"backdropFilter\":4,\"backgroundColor\":\"rgba(255,255,255,1)\",\"innerImageColor\":\"rgba(16, 148, 229,1)\"},\"state\":\"ready\",\"render\":\"antv\",\"isPlugin\":false,\"id\":\"{{ VIEW_ID }}\",\"_dragId\":0,\"linkageFilters\":[],\"expand\":false,\"resizeInnerKeep\":false}]",
"canvasViewInfo": {
"{{ VIEW_ID }}": {
"id": "{{ VIEW_ID }}",
"title": "明细表",
"sceneId": "{{ SCENE_ID }}",
"tableId": "{{ DATASET_GROUP_ID }}",
"type": "table-info",
"render": "antv",
"resultCount": 1000,
"resultMode": "custom",
"extStack": [],
"extBubble": [],
"extLabel": [],
"extTooltip": [],
"customAttr": {
"basicStyle": {
"alpha": 100,
"tableBorderColor": "#E6E7E4",
"tableScrollBarColor": "rgba(0, 0, 0, 0.15)",
"tableColumnMode": "adapt",
"tableColumnWidth": 100,
"tableFieldWidth": [],
"tablePageMode": "page",
"tablePageStyle": "simple",
"tablePageSize": 20,
"gaugeStyle": "default",
"colorScheme": "default",
"colors": [
"#1E90FF",
"#90EE90",
"#00CED1",
"#E2BD84",
"#7A90E0",
"#3BA272",
"#2BE7FF",
"#0A8ADA",
"#FFD700"
],
"mapVendor": "amap",
"gradient": false,
"lineWidth": 2,
"lineSymbol": "circle",
"lineSymbolSize": 4,
"lineSmooth": true,
"barDefault": true,
"radiusColumnBar": "rightAngle",
"columnBarRightAngleRadius": 20,
"columnWidthRatio": 60,
"barWidth": 40,
"barGap": 0.4,
"lineType": "solid",
"scatterSymbol": "circle",
"scatterSymbolSize": 8,
"radarShape": "polygon",
"mapStyle": "normal",
"heatMapType": "heatmap",
"heatMapIntensity": 2,
"heatMapRadius": 20,
"areaBorderColor": "#303133",
"areaBaseColor": "#FFFFFF",
"mapSymbolOpacity": 0.7,
"mapSymbolStrokeWidth": 2,
"mapSymbol": "circle",
"mapSymbolSize": 6,
"radius": 80,
"innerRadius": 60,
"showZoom": true,
"zoomButtonColor": "#aaa",
"zoomBackground": "#fff",
"tableLayoutMode": "grid",
"defaultExpandLevel": 1,
"calcTopN": false,
"topN": 5,
"topNLabel": "其他",
"gaugeAxisLine": true,
"gaugePercentLabel": true,
"showSummary": false,
"summaryLabel": "总计",
"seriesColor": [],
"layout": "horizontal",
"mapSymbolSizeMin": 4,
"mapSymbolSizeMax": 30,
"showLabel": true,
"mapStyleUrl": "",
"autoFit": true,
"mapCenter": {
"longitude": 117.232,
"latitude": 39.354
},
"zoomLevel": 7,
"customIcon": "",
"showHoverStyle": true,
"autoWrap": false,
"maxLines": 3,
"radarShowPoint": true,
"radarPointSize": 4,
"radarAreaColor": true,
"circleBorderColor": "#fff",
"circleBorderWidth": 0,
"circlePadding": 0,
"quotaPosition": "col",
"quotaColLabel": "数值",
"tableRowHeaderMode": "adapt",
"tableRowHeaderWidth": 120,
"tableRowHeaderWidthPercent": 20
},
"misc": {
"pieInnerRadius": 0,
"pieOuterRadius": 80,
"radarShape": "polygon",
"radarSize": 80,
"gaugeMinType": "fix",
"gaugeMinField": {
"id": "",
"summary": ""
},
"gaugeMin": 0,
"gaugeMaxType": "dynamic",
"gaugeMaxField": {
"id": "",
"summary": ""
},
"gaugeStartAngle": 225,
"gaugeEndAngle": -45,
"nameFontSize": 18,
"valueFontSize": 18,
"nameValueSpace": 10,
"valueFontColor": "#5470c6",
"valueFontFamily": "Microsoft YaHei",
"valueFontIsBolder": false,
"valueFontIsItalic": false,
"valueLetterSpace": 0,
"valueFontShadow": false,
"showName": true,
"nameFontColor": "#000000",
"nameFontFamily": "Microsoft YaHei",
"nameFontIsBolder": false,
"nameFontIsItalic": false,
"nameLetterSpace": "0",
"nameFontShadow": false,
"treemapWidth": 80,
"treemapHeight": 80,
"liquidMaxType": "dynamic",
"liquidMaxField": {
"id": "",
"summary": ""
},
"liquidSize": 80,
"liquidShape": "circle",
"hPosition": "center",
"vPosition": "center",
"mapPitch": 0,
"wordSizeRange": [
8,
32
],
"wordSpacing": 6,
"mapAutoLegend": true,
"mapLegendMax": 0,
"mapLegendMin": 0,
"mapLegendNumber": 9,
"mapLegendRangeType": "quantize",
"mapLegendCustomRange": [],
"flowMapConfig": {
"lineConfig": {
"mapLineAnimate": true,
"mapLineType": "arc",
"mapLineWidth": 1,
"mapLineAnimateDuration": 3,
"mapLineGradient": false,
"mapLineSourceColor": "#146C94",
"mapLineTargetColor": "#576CBC",
"alpha": 100
},
"pointConfig": {
"text": {
"color": "#146C94",
"fontSize": 10
},
"point": {
"color": "#146C94",
"size": 4,
"animate": false,
"speed": 0.01
}
}
},
"wordCloudAxisValueRange": {
"auto": true,
"min": 0,
"max": 0
},
"bullet": {
"bar": {
"ranges": {
"fill": [
"rgba(0,128,255,0.3)"
],
"size": 20,
"showType": "dynamic",
"fixedRangeNumber": 3,
"symbol": "circle",
"symbolSize": 4
},
"measures": {
"fill": [
"rgba(0,128,255,1)"
],
"size": 15,
"symbol": "circle",
"symbolSize": 4
},
"target": {
"fill": "#000000",
"size": 20,
"showType": "dynamic",
"value": 0,
"symbol": "line",
"symbolSize": 4
}
}
},
"liquidShowBorder": false,
"liquidBorderWidth": 4,
"liquidBorderDistance": 8
},
"label": {
"show": false,
"childrenShow": true,
"position": "top",
"color": "#000000",
"fontSize": 12,
"formatter": "",
"labelLine": {
"show": true
},
"labelFormatter": {
"type": "auto",
"unitLanguage": "ch",
"unit": 1,
"suffix": "",
"decimalCount": 2,
"thousandSeparator": true
},
"reserveDecimalCount": 2,
"labelShadow": false,
"labelBgColor": "",
"labelShadowColor": "",
"quotaLabelFormatter": {
"type": "auto",
"unitLanguage": "ch",
"unit": 1,
"suffix": "",
"decimalCount": 2,
"thousandSeparator": true
},
"showDimension": true,
"showQuota": false,
"showProportion": true,
"seriesLabelFormatter": [],
"conversionTag": {
"show": false,
"precision": 2,
"text": "转化率"
},
"showTotal": false,
"totalFontSize": 12,
"totalColor": "#FFF",
"totalFormatter": {
"type": "auto",
"unitLanguage": "ch",
"unit": 1,
"suffix": "",
"decimalCount": 2,
"thousandSeparator": true
},
"showStackQuota": false,
"fullDisplay": false,
"proportionSeriesFormatter": {
"show": false,
"color": "#000000",
"fontSize": 12,
"formatterCfg": {
"decimalCount": 2
}
}
},
"tooltip": {
"show": true,
"trigger": "item",
"confine": true,
"fontSize": 12,
"color": "#000000",
"tooltipFormatter": {
"type": "auto",
"unitLanguage": "ch",
"unit": 1,
"suffix": "",
"decimalCount": 2,
"thousandSeparator": true
},
"backgroundColor": "#FFFFFF",
"seriesTooltipFormatter": [],
"carousel": {
"enable": false,
"stayTime": 3,
"intervalTime": 1
}
},
"tableTotal": {
"row": {
"showGrandTotals": true,
"showSubTotals": true,
"reverseLayout": false,
"reverseSubLayout": false,
"label": "总计",
"subLabel": "小计",
"subTotalsDimensions": [],
"subTotalsDimensionsNew": true,
"calcTotals": {
"aggregation": "SUM",
"cfg": []
},
"calcSubTotals": {
"aggregation": "SUM",
"cfg": []
},
"totalSort": "none",
"totalSortField": ""
},
"col": {
"showGrandTotals": true,
"showSubTotals": true,
"reverseLayout": false,
"reverseSubLayout": false,
"label": "总计",
"subLabel": "小计",
"subTotalsDimensions": [],
"calcTotals": {
"aggregation": "SUM",
"cfg": []
},
"calcSubTotals": {
"aggregation": "SUM",
"cfg": []
},
"totalSort": "none",
"totalSortField": ""
}
},
"tableHeader": {
"indexLabel": "序号",
"showIndex": false,
"tableHeaderAlign": "left",
"tableHeaderCornerAlign": "left",
"tableHeaderColAlign": "left",
"tableHeaderBgColor": "#1E90FF",
"tableHeaderCornerBgColor": "#1E90FF",
"tableHeaderColBgColor": "#1E90FF",
"tableHeaderFontColor": "#000000",
"tableHeaderCornerFontColor": "#000000",
"tableHeaderColFontColor": "#000000",
"tableTitleFontSize": 12,
"tableTitleCornerFontSize": 12,
"tableTitleColFontSize": 12,
"tableTitleHeight": 36,
"tableHeaderSort": false,
"showColTooltip": false,
"showRowTooltip": false,
"showTableHeader": true,
"showHorizonBorder": true,
"showVerticalBorder": true,
"isItalic": false,
"isCornerItalic": false,
"isColItalic": false,
"isBolder": true,
"isCornerBolder": true,
"isColBolder": true,
"headerGroup": false,
"headerGroupConfig": {
"columns": [],
"meta": []
},
"rowHeaderFreeze": true,
"alignConfig": []
},
"tableCell": {
"tableFontColor": "#000000",
"tableItemAlign": "right",
"tableItemBgColor": "#FFFFFF",
"tableItemFontSize": 12,
"tableItemHeight": 36,
"enableTableCrossBG": false,
"tableItemSubBgColor": "#1E90FF",
"showTooltip": false,
"showHorizonBorder": true,
"showVerticalBorder": true,
"isItalic": false,
"isBolder": false,
"tableFreeze": false,
"tableColumnFreezeHead": 0,
"tableRowFreezeHead": 0,
"mergeCells": true,
"alignConfig": []
},
"indicator": {
"show": true,
"fontSize": 20,
"color": "#5470C6ff",
"hPosition": "center",
"vPosition": "center",
"isItalic": false,
"isBolder": true,
"fontFamily": "Microsoft YaHei",
"letterSpace": 0,
"fontShadow": false,
"backgroundColor": "",
"suffixEnable": true,
"suffix": "",
"suffixFontSize": 14,
"suffixColor": "#5470C6ff",
"suffixIsItalic": false,
"suffixIsBolder": true,
"suffixFontFamily": "Microsoft YaHei",
"suffixLetterSpace": 0,
"suffixFontShadow": false
},
"indicatorName": {
"show": true,
"fontSize": 18,
"color": "#ffffffff",
"isItalic": false,
"isBolder": true,
"fontFamily": "Microsoft YaHei",
"letterSpace": 0,
"fontShadow": false,
"nameValueSpacing": 0,
"namePosition": "bottom"
},
"map": {
"id": "",
"level": "world"
}
},
"customAttrMobile": null,
"customStyle": {
"text": {
"show": true,
"fontSize": 16,
"hPosition": "left",
"vPosition": "top",
"isItalic": false,
"isBolder": true,
"remarkShow": false,
"remark": "",
"fontFamily": "PingFang",
"letterSpace": "0",
"fontShadow": false,
"color": "#000000",
"remarkBackgroundColor": "#ffffff"
},
"legend": {
"show": true,
"hPosition": "center",
"vPosition": "bottom",
"orient": "horizontal",
"icon": "circle",
"color": "#000000",
"fontSize": 12,
"size": 4,
"showRange": true,
"sort": "none",
"customSort": []
},
"xAxis": {
"show": true,
"position": "bottom",
"nameShow": false,
"name": "",
"color": "#000000",
"fontSize": 12,
"axisLabel": {
"show": true,
"color": "#000000",
"fontSize": 12,
"rotate": 0,
"formatter": "{value}",
"lengthLimit": 10
},
"axisLine": {
"show": true,
"lineStyle": {
"color": "#cccccc",
"width": 1,
"style": "solid"
}
},
"splitLine": {
"show": false,
"lineStyle": {
"color": "#CCCCCC",
"width": 1,
"style": "solid"
}
},
"axisValue": {
"auto": true,
"min": 10,
"max": 100,
"split": 10,
"splitCount": 10
},
"axisLabelFormatter": {
"type": "auto",
"unitLanguage": "ch",
"unit": 1,
"suffix": "",
"decimalCount": 2,
"thousandSeparator": true
}
},
"yAxis": {
"show": true,
"position": "left",
"nameShow": false,
"name": "",
"color": "#000000",
"fontSize": 12,
"axisLabel": {
"show": true,
"color": "#000000",
"fontSize": 12,
"rotate": 0,
"formatter": "{value}",
"lengthLimit": 10
},
"axisLine": {
"show": false,
"lineStyle": {
"color": "#cccccc",
"width": 1,
"style": "solid"
}
},
"splitLine": {
"show": true,
"lineStyle": {
"color": "#CCCCCC",
"width": 1,
"style": "solid"
}
},
"axisValue": {
"auto": true,
"min": 10,
"max": 100,
"split": 10,
"splitCount": 10
},
"axisLabelFormatter": {
"type": "auto",
"unitLanguage": "ch",
"unit": 1,
"suffix": "",
"decimalCount": 2,
"thousandSeparator": true
}
},
"yAxisExt": {
"show": true,
"position": "right",
"name": "",
"color": "#000000",
"fontSize": 12,
"axisLabel": {
"show": true,
"color": "#000000",
"fontSize": 12,
"rotate": 0,
"formatter": "{value}"
},
"axisLine": {
"show": false,
"lineStyle": {
"color": "#cccccc",
"width": 1,
"style": "solid"
}
},
"splitLine": {
"show": false,
"lineStyle": {
"color": "#CCCCCC",
"width": 1,
"style": "solid"
}
},
"axisValue": {
"auto": true,
"min": 10,
"max": 100,
"split": 10,
"splitCount": 10
},
"axisLabelFormatter": {
"type": "auto",
"unitLanguage": "ch",
"unit": 1,
"suffix": "",
"decimalCount": 2,
"thousandSeparator": true
}
},
"misc": {
"showName": false,
"color": "#000000",
"fontSize": 12,
"axisColor": "#999",
"splitNumber": 5,
"axisLine": {
"show": true,
"lineStyle": {
"color": "#CCCCCC",
"width": 1,
"type": "solid"
}
},
"axisTick": {
"show": false,
"length": 5,
"lineStyle": {
"color": "#000000",
"width": 1,
"type": "solid"
}
},
"axisLabel": {
"show": false,
"rotate": 0,
"margin": 8,
"color": "#000000",
"fontSize": "12",
"formatter": "{value}"
},
"splitLine": {
"show": true,
"lineStyle": {
"color": "#CCCCCC",
"width": 1,
"type": "solid"
}
},
"splitArea": {
"show": true
},
"axisValue": {
"auto": true,
"min": 10,
"max": 100,
"split": 10,
"splitCount": 10
}
}
},
"customStyleMobile": null,
"customFilter": {
"logic": null,
"items": null
},
"drillFields": [],
"senior": {
"functionCfg": {
"sliderShow": false,
"sliderRange": [
0,
10
],
"sliderBg": "#FFFFFF",
"sliderFillBg": "#BCD6F1",
"sliderTextColor": "#999999",
"emptyDataStrategy": "breakLine",
"emptyDataCustomValue": "",
"emptyDataFieldCtrl": []
},
"assistLineCfg": {
"enable": false,
"assistLine": []
},
"threshold": {
"enable": false,
"gaugeThreshold": "",
"liquidThreshold": "",
"labelThreshold": [],
"tableThreshold": [],
"textLabelThreshold": [],
"lineLabelThreshold": []
},
"scrollCfg": {
"open": false,
"row": 1,
"interval": 2000,
"step": 50
},
"areaMapping": {},
"bubbleCfg": {
"enable": false,
"speed": 1,
"rings": 1,
"type": "wave"
}
},
"createBy": null,
"createTime": null,
"updateTime": null,
"snapshot": null,
"stylePriority": "panel",
"chartType": "private",
"isPlugin": false,
"dataFrom": "calc",
"viewFields": [],
"refreshViewEnable": false,
"refreshUnit": "minute",
"refreshTime": 5,
"linkageActive": false,
"jumpActive": false,
"aggregate": null,
"flowMapStartName": [],
"flowMapEndName": [],
"calParams": [],
"extColor": null,
"sortPriority": [],
"data": null,
"privileges": null,
"isLeaf": null,
"pid": null,
"sql": null,
"drill": false,
"drillFilters": null,
"position": null,
"totalPage": 0,
"totalItems": 0,
"datasetMode": 0,
"datasourceType": null,
"chartExtRequest": null,
"isExcelExport": false,
"exportDatasetOriginData": false,
"cache": false,
"sourceTableId": null,
"downloadType": null,
"xAxis": [
{
"id": "{{ XAXIS_FIELD_ID }}",
"datasourceId": "{{ DATASOURCE_ID }}",
"datasetTableId": "{{ DATASET_TABLE_ID }}",
"datasetGroupId": "{{ DATASET_GROUP_ID }}",
"chartId": null,
"originName": "access_platform",
"name": "访问平台",
"dbFieldName": null,
"description": "访问平台",
"dataeaseName": "{{ XAXIS_DE_NAME }}",
"groupType": "d",
"type": "VARCHAR",
"precision": null,
"scale": null,
"deType": 0,
"deExtractType": 0,
"extField": 0,
"checked": true,
"columnIndex": null,
"lastSyncTime": null,
"dateFormat": null,
"dateFormatType": null,
"fieldShortName": "{{ XAXIS_DE_NAME }}",
"groupList": [],
"otherGroup": null,
"desensitized": false,
"orderChecked": false,
"params": [],
"summary": "count",
"sort": "none",
"dateStyle": "y_M_d",
"datePattern": "date_sub",
"dateShowFormat": "y_M_d",
"chartType": "bar",
"compareCalc": {
"type": "none",
"resultData": "percent",
"field": null,
"custom": null
},
"logic": null,
"filterType": null,
"index": null,
"formatterCfg": {
"type": "auto",
"unitLanguage": "ch",
"unit": 1,
"suffix": "",
"decimalCount": 2,
"thousandSeparator": true
},
"chartShowName": null,
"filter": [],
"customSort": null,
"busiType": null,
"hide": false,
"field": null,
"agg": false
},
{
"id": "{{ XAXIS2_FIELD_ID }}",
"datasourceId": "{{ DATASOURCE_ID }}",
"datasetTableId": "{{ DATASET_TABLE_ID }}",
"datasetGroupId": "{{ DATASET_GROUP_ID }}",
"chartId": null,
"originName": "access_count",
"name": "访问次数",
"dbFieldName": null,
"description": "访问次数",
"dataeaseName": "{{ XAXIS2_DE_NAME }}",
"groupType": "q",
"type": "INT",
"precision": null,
"scale": null,
"deType": 2,
"deExtractType": 2,
"extField": 0,
"checked": true,
"columnIndex": null,
"lastSyncTime": null,
"dateFormat": null,
"dateFormatType": null,
"fieldShortName": "{{ XAXIS2_DE_NAME }}",
"groupList": [],
"otherGroup": null,
"desensitized": false,
"orderChecked": false,
"params": [],
"summary": "sum",
"sort": "none",
"dateStyle": "y_M_d",
"datePattern": "date_sub",
"dateShowFormat": "y_M_d",
"chartType": "bar",
"compareCalc": {
"type": "none",
"resultData": "percent",
"field": null,
"custom": null
},
"logic": null,
"filterType": null,
"index": null,
"formatterCfg": {
"type": "auto",
"unitLanguage": "ch",
"unit": 1,
"suffix": "",
"decimalCount": 2,
"thousandSeparator": true
},
"chartShowName": null,
"filter": [],
"customSort": null,
"busiType": null,
"hide": false,
"field": null,
"agg": false
}
],
"xAxisExt": [],
"yAxis": [],
"yAxisExt": [],
"pageInfo": {
"total": 1000,
"pageSize": 20,
"currentPage": 1
}
}
},
"appData": null,
"id": "{{ SCENE_ID }}",
"name": "单明细表",
"pid": "0",
"status": 1,
"selfWatermarkStatus": true,
"type": "dashboard",
"creatorName": "系统管理员",
"updateName": "系统管理员",
"createTime": 1775811560811,
"updateTime": 1775812302437,
"watermarkInfo": null,
"weight": 9,
"ext": 0,
"contentId": "{{ CONTENT_ID }}",
"mobileLayout": false,
"checkVersion": "2.10.20"
}
+3
View File
@@ -0,0 +1,3 @@
{
"canvasStyleData": "{\"width\":1920,\"height\":1080,\"refreshBrowserEnable\":false,\"refreshBrowserUnit\":\"minute\",\"refreshBrowserTime\":5,\"refreshViewEnable\":false,\"refreshViewLoading\":true,\"refreshUnit\":\"minute\",\"refreshTime\":5,\"popupAvailable\":true,\"popupButtonAvailable\":true,\"suspensionButtonAvailable\":false,\"screenAdaptor\":\"widthFirst\",\"dashboardAdaptor\":\"keepHeightAndWidth\",\"scale\":17,\"scaleWidth\":60,\"scaleHeight\":60,\"backgroundColorSelect\":true,\"backgroundImageEnable\":false,\"backgroundType\":\"backgroundColor\",\"background\":\"\",\"openCommonStyle\":true,\"opacity\":1,\"fontSize\":14,\"fontFamily\":\"PingFang\",\"themeId\":\"10001\",\"color\":\"#000000\",\"backgroundColor\":\"rgba(245, 246, 247, 1)\",\"dashboard\":{\"gap\":\"yes\",\"gapSize\":5,\"gapMode\":\"middle\",\"showGrid\":false,\"matrixBase\":4,\"resultMode\":\"all\",\"resultCount\":1000,\"themeColor\":\"light\",\"mobileSetting\":{\"customSetting\":false,\"imageUrl\":null,\"backgroundType\":\"image\",\"color\":\"#000\"}},\"component\":{\"chartTitle\":{\"show\":true,\"fontSize\":16,\"hPosition\":\"left\",\"vPosition\":\"top\",\"isItalic\":false,\"isBolder\":true,\"remarkShow\":false,\"remark\":\"\",\"fontFamily\":\"\",\"letterSpace\":\"0\",\"fontShadow\":false,\"color\":\"#000000\",\"remarkBackgroundColor\":\"#ffffff\"},\"chartColor\":{\"basicStyle\":{\"colorScheme\":\"default\",\"colors\":[\"#1E90FF\",\"#90EE90\",\"#00CED1\",\"#E2BD84\",\"#7A90E0\",\"#3BA272\",\"#2BE7FF\",\"#0A8ADA\",\"#FFD700\"],\"alpha\":100,\"gradient\":false,\"mapStyle\":\"normal\",\"areaBaseColor\":\"#FFFFFF\",\"areaBorderColor\":\"#303133\",\"gaugeStyle\":\"default\",\"tableBorderColor\":\"rgba(230, 231, 228, 1)\",\"tableScrollBarColor\":\"rgba(0, 0, 0, 0.15)\",\"zoomButtonColor\":\"#aaa\",\"zoomBackground\":\"#fff\"},\"misc\":{\"flowMapConfig\":{\"lineConfig\":{\"mapLineAnimate\":true,\"mapLineGradient\":false,\"mapLineSourceColor\":\"#146C94\",\"mapLineTargetColor\":\"#576CBC\"}},\"nameFontColor\":\"#000000\",\"valueFontColor\":\"#5470c6\"},\"tableHeader\":{\"tableHeaderBgColor\":\"#1E90FF\",\"tableHeaderCornerBgColor\":\"#1E90FF\",\"tableHeaderColBgColor\":\"#1E90FF\",\"tableHeaderFontColor\":\"#000000\",\"tableHeaderCornerFontColor\":\"#000000\",\"tableHeaderColFontColor\":\"#000000\"},\"tableCell\":{\"tableItemBgColor\":\"rgba(255, 255, 255, 1)\",\"tableFontColor\":\"#000000\",\"tableItemSubBgColor\":\"#1E90FF\"}},\"chartCommonStyle\":{\"backgroundColorSelect\":true,\"backdropFilterEnable\":false,\"backgroundImageEnable\":false,\"backgroundType\":\"innerImage\",\"innerImage\":\"board/board_1.svg\",\"outerImage\":null,\"innerPadding\":{\"mode\":\"uniform\",\"top\":12},\"borderRadius\":{\"mode\":\"uniform\",\"topLeft\":0},\"backdropFilter\":4,\"backgroundColor\":\"rgba(255,255,255,1)\",\"innerImageColor\":\"rgba(16, 148, 229,1)\"},\"filterStyle\":{\"layout\":\"horizontal\",\"titleLayout\":\"left\",\"labelColor\":\"#1F2329\",\"titleColor\":\"#1F2329\",\"color\":\"#1f2329\",\"borderColor\":\"#BBBFC4\",\"text\":\"#1F2329\",\"bgColor\":\"#FFFFFF\"},\"tabStyle\":{\"headPosition\":\"left\",\"headFontColor\":\"#000000\",\"headFontActiveColor\":\"#000000\",\"headBorderColor\":\"#ffffff\",\"headBorderActiveColor\":\"#ffffff\"},\"seniorStyleSetting\":{\"linkageIconColor\":\"#A6A6A6\",\"drillLayerColor\":\"#A6A6A6\",\"pagerColor\":\"rgba(166, 166, 166, 1)\"},\"formatterItem\":{\"type\":\"auto\",\"unitLanguage\":\"ch\",\"unit\":1,\"suffix\":\"\",\"decimalCount\":2,\"thousandSeparator\":true}},\"dialogBackgroundColor\":\"rgba(255, 255, 255, 1)\",\"dialogButton\":\"#020408\"}"
}