Compare commits
10 Commits
5dc6b63525
...
afd7c5fe85
| Author | SHA1 | Date | |
|---|---|---|---|
| afd7c5fe85 | |||
| 6af5c584f4 | |||
| 3bf7017d05 | |||
| e36964dd23 | |||
| bdc61fe651 | |||
| 02677763cb | |||
| 25714ada1b | |||
| a3a3cc754b | |||
| a7b7903f51 | |||
| a4c2078e6e |
@@ -1,7 +1,38 @@
|
||||
.env
|
||||
.vscode
|
||||
nanobot-0.1.4.post4
|
||||
data
|
||||
_research
|
||||
.trae
|
||||
.env.*
|
||||
|
||||
# OS / editor
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Build / artifacts
|
||||
dist/
|
||||
build/
|
||||
*.log
|
||||
|
||||
# Runtime data (mounted as DATA_ROOT in Docker)
|
||||
data/
|
||||
|
||||
# Intermediate docs/state (do not commit)
|
||||
.claude/
|
||||
.work/
|
||||
|
||||
# Local scratch
|
||||
_research/
|
||||
.trae/
|
||||
nanobot-*/
|
||||
@@ -1,428 +1,149 @@
|
||||
[🇨🇳 简体中文](./README.md) | [🇬🇧 English](./README_en.md)
|
||||
## 项目简介
|
||||
|
||||
# 🦞 龙虾问数 (DataClaw)
|
||||
<video src="../../Videos/Captures/全源灵动_30mb.mp4"></video>
|
||||
|
||||
> **释放你的数据潜能,让分析像养龙虾一样简单爽快!** 🌊📊
|
||||
> 龙虾问数 (DataClaw) 是一个智能的、AI 驱动的数据分析平台。通过自然语言与你的数据对话,瞬间生成可视化图表,轻松搭建仪表盘——从此告别繁琐的 SQL 语句!
|
||||
### 项目概述
|
||||
|
||||
***
|
||||
**全源灵动**是一个面向数据分析与知识检索场景的 AI 平台:用自然语言问数、自动生成图表与报告,并管理分析产物,帮助团队更快得到可验证、可复用的结论。
|
||||
|
||||
## ✨ 为什么选择龙虾问数?
|
||||
### 项目起源
|
||||
|
||||
受够了为了画个简单的柱状图而写半天复杂的 SQL 语句吗?龙虾问数就是你的私人数据科学家。借助强大的大语言模型 (LLM) 和智能 Agent 工作流,它能将你的自然语言提问精准转化为数据库查询,提取数据,并即时渲染出美观的可视化图表。
|
||||
数据分散在不同系统与文件中时,手写 SQL、手工出图、反复同步口径的成本高且难复用。全源灵动将“提问 → 查询 → 可视化 → 产出”串联为一条可复用的分析链路。
|
||||
|
||||
无论你是要查询庞大的 Supabase/PostgreSQL 数据库,还是随手丢进一个 CSV 文件,龙虾问数都能轻松拿捏!🚀
|
||||
### 项目定位
|
||||
|
||||
## 🌟 核心特性
|
||||
- 目标用户:业务/数据分析、运营、产品、研发等需要自助分析与报告产出的角色
|
||||
- 适用场景:NL2SQL 问数、知识库检索增强、自动图表与报告产出、任务委派与跟踪(A2A)
|
||||
|
||||
- **🗣️ 自然语言转 SQL**: 用大白话提问!它能理解你的数据表结构,生成准确的 SQL,甚至在报错时进行自我纠正 (Self-correction)。
|
||||
- **📚 智能知识库检索 (RAG)**: 支持上传 Word、PPT、PDF 等多种格式文档,通过向量检索增强回答,让你的私有文档“开口说话”。
|
||||
- **📈 即时数据可视化**: 拒绝枯燥的生肉表格,根据数据特征自动生成交互式图表。
|
||||
- **🗂️ 动态多数据源**: 无缝连接 PostgreSQL、Supabase,以及本地 CSV/Excel 文件上传解析。
|
||||
- **🧠 灵活的模型接入**: 原生集成 LiteLLM,支持随插随用 OpenAI、DeepSeek、智谱、通义千问 (DashScope)、火山引擎或任何兼容的 LLM 提供商。
|
||||
- **🛠️ 强大的 Agent 技能拓展**: 基于核心 `nanobot`框架(`OpenClaw`的精简版)构建。支持通过斜杠命令 (`/`) 快速调用自定义工具 (Skills),完美贴合特定业务逻辑。
|
||||
- **📊 可定制仪表盘 (Dashboard)**: 一键将对话中生成的图表固定到看板,拖拽布局,随时查看核心指标。
|
||||
- **📦 智能产物管理 (Artifact)**: 自动提取对话中生成的各种文件(网页报告、PDF、PPT、图片等),提供一键内嵌预览与下载功能,让成果触手可及。
|
||||
### 核心价值
|
||||
|
||||
***
|
||||
- 更快:把“写 SQL / 查资料 / 出图”合并为一次对话驱动的分析流程
|
||||
- 更稳:对关键结果提供可回溯的查询与产物(Artifact)
|
||||
- 更灵活:支持多数据源与可配置的模型接入
|
||||
|
||||
## 📸 界面预览
|
||||
## 整体架构与技术栈
|
||||
|
||||
<div align="left">
|
||||
<h3>💬 对话式分析界面</h3>
|
||||
<img src="./docs/index.png" width="80%" />
|
||||
<br />
|
||||
<br />
|
||||
<h3>📊 可定制仪表盘</h3>
|
||||
<img src="./docs/dashboard.png" width="80%" />
|
||||
<br />
|
||||
<br />
|
||||
<h3>📚 智能知识库问答</h3>
|
||||
<img src="./docs/kb.png" width="80%" />
|
||||
<br />
|
||||
<br />
|
||||
<h3>📦 智能产物预览 (Artifact)</h3>
|
||||
<img src="./docs/artifact.png" width="80%" />
|
||||
</div>
|
||||
### 系统架构图
|
||||
|
||||
<br />
|
||||
```mermaid
|
||||
graph TD
|
||||
User([用户/浏览器]) -->|HTTP| WebUI[前端 WebUI]
|
||||
WebUI -->|REST/SSE| API[后端 API]
|
||||
API -->|读写| Data[(DATA_ROOT)]
|
||||
API -->|连接/查询| DB[(数据源)]
|
||||
API -->|调用| LLM([外部 LLM])
|
||||
```
|
||||
|
||||
## 🏗️ 项目架构
|
||||
### 技术栈分类
|
||||
|
||||
DataClaw 的架构主要分为三只“大钳子”:
|
||||
- 后端:FastAPI(Python 3.11+)
|
||||
- 前端:React 19 + Vite + TailwindCSS + Zustand
|
||||
- 数据库:
|
||||
- 业务元数据/会话:SQLite(默认落在 `DATA_ROOT`)
|
||||
- 数据分析数据源:PostgreSQL / ClickHouse / 上传文件(CSV/Excel 等)
|
||||
- 部署:Docker Compose(推荐)
|
||||
|
||||
1. **`frontend/`** 🎨: 闪亮的外壳。基于 **React 19**、**Vite**、**TailwindCSS** 和 **Zustand** 构建。拥有类似微信/ChatGPT的对话界面、支持流式思考过程渲染以及交互式图表展示。
|
||||
2. **`backend/`** ⚙️: 强健的肌肉。一个 **FastAPI** 后端服务,负责管理项目、数据源连接、用户会话持久化以及作为 API 网关。
|
||||
3. **`nanobot/`** 🧠: 智慧的大脑。核心的 AI Agent 框架,负责处理意图路由、NL2SQL 转换、Schema 缓存管理以及与 LLM 的底层交互。
|
||||
4. **`data/`** 🗄️: 运行时数据目录。与代码目录解耦,存放上传文件、会话、技能工作区、报告与配置缓存。
|
||||
## 项目目录概览
|
||||
|
||||
***
|
||||
```text
|
||||
.
|
||||
├── dataclaw-api/ # 后端 FastAPI(API 网关、会话与数据源管理、Agent 能力)
|
||||
├── dataclaw-ui/ # 前端 React + Vite(Nginx 生产静态托管)
|
||||
├── agent-core/ # Agent 核心(nanobot)
|
||||
├── dataclaw-voice/ # Whisper 语音服务(可选)
|
||||
├── data/ # 运行时数据目录(可挂载为 DATA_ROOT)
|
||||
└── docker-compose.yml # 一键启动
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
## 核心业务功能
|
||||
|
||||
准备好大显身手了吗?让我们把龙虾问数在你的本地跑起来!
|
||||
- **自然语言转 SQL**:理解数据表结构,生成/修正 SQL 并执行查询
|
||||
- **知识库检索(RAG)**:上传多种格式文档,向量检索增强问答
|
||||
- **即时可视化**:根据数据特征生成交互式图表
|
||||
- **动态多数据源**:PostgreSQL / Supabase / 本地文件上传解析等
|
||||
- **模型配置**:支持接入 OpenAI 兼容接口的外部模型服务
|
||||
- **产物管理(Artifact)**:自动提取与管理报告/图片/PDF/PPT 等结果文件
|
||||
- **A2A 任务委派与跟踪**:把任务委派到远端 Agent,并可订阅状态与产物
|
||||
|
||||
### 1. 配置环境变量 🔧
|
||||
## 实际应用场景示例
|
||||
|
||||
在项目根目录下,复制并重命名环境配置模板:
|
||||
1. **运营日报/周报**:提问“近 7 天新增用户趋势与转化漏斗”,自动生成图表与报告
|
||||
2. **产品指标分析**:对留存、转化、渠道贡献等指标进行归因分析,并固化仪表盘
|
||||
3. **知识库问答**:上传制度/方案/投标文件等,自动总结与检索引用
|
||||
|
||||
## 帮助解决的核心问题
|
||||
|
||||
- SQL 门槛高、分析链路长、结果难复用
|
||||
- 多数据源/多口径带来的协作成本高
|
||||
- 报告产出与分享效率低
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Docker Desktop(推荐)
|
||||
- (本地开发模式可选)Python 3.11+、Node.js 20+
|
||||
|
||||
### 安装步骤
|
||||
|
||||
在项目根目录复制环境配置模板:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
请记得编辑根目录下的 `.env` 文件,填入你的实际配置(如 QQ 邮箱 SMTP 授权码等)。
|
||||
|
||||
> **QQ 邮箱获取 SMTP 授权码最新教程:**
|
||||
> 1. 登录 QQ 邮箱网页版 (mail.qq.com)
|
||||
> 2. 点击页面顶部的“设置” -> “账号”选项卡
|
||||
> 3. 向下滚动找到“POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务”
|
||||
> 4. 确保“POP3/SMTP服务”已点击“开启”
|
||||
> 5. 点击下方的“生成授权码”,使用手机 QQ 扫码或按提示发送短信
|
||||
> 6. 验证通过后将获得一串 **16位随机字母组合**,将其复制填入 `.env` 文件中的 `SMTP_PASSWORD` 字段
|
||||
|
||||
### 2. 打包 wheel(产物输出到根目录 `dist/`)📦
|
||||
|
||||
如需自行打包发布,请按以下顺序执行:先构建前端,再构建后端 wheel,并将产物输出到项目根目录 `dist/`(与 `backend/` 同级)。
|
||||
### 运行步骤(Docker 全量启动)
|
||||
|
||||
```bash
|
||||
# 1) 构建前端静态资源
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
# 2) 构建后端 wheel,并输出到根目录 dist/
|
||||
cd ../backend
|
||||
uv build --wheel --out-dir ../dist
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
构建完成后,wheel 位于项目根目录 `dist/`,例如 `dist/dataclaw-0.1.0-py3-none-any.whl`。
|
||||
访问地址:
|
||||
- 前端:`http://localhost:10002/`
|
||||
- 后端(直连):`http://localhost:10001/docs`
|
||||
|
||||
### 3. 标准部署(推荐,无需 Node.js)📦
|
||||
运行时数据目录:
|
||||
- 默认挂载 `./data` 到容器内 `/app/data`(对应 `DATA_ROOT=/app/data`),用于存放上传文件、会话与配置等。
|
||||
|
||||
请确保你已安装 Python 3.11 或以上版本。发布包已内置前端静态资源,生产部署无需安装 Node.js。
|
||||
### 默认账号
|
||||
|
||||
系统首次注册的用户将自动成为管理员(示例:用户名 `admin`,密码 `admin`)。
|
||||
|
||||
### 常见问题
|
||||
|
||||
- **后端构建失败(APT 连接失败)**:可使用国内镜像构建:
|
||||
|
||||
```bash
|
||||
# 建议先创建虚拟环境
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
|
||||
# 安装 DataClaw(示例:安装根目录 dist 下的 wheel)
|
||||
pip install ./dist/dataclaw-*.whl
|
||||
|
||||
# 启动服务(默认 http://127.0.0.1:8000)
|
||||
dataclaw start
|
||||
APT_CN_MIRROR=true docker compose build
|
||||
```
|
||||
|
||||
常用服务控制命令:
|
||||
- **接口返回 401**:需要先登录并携带 `Authorization: Bearer <token>`
|
||||
- **接口返回 422**:请求体不符合后端 schema(例如 A2A `message:send` 需要 `message.parts[0].data.project_id`)
|
||||
|
||||
```bash
|
||||
# 查看运行状态
|
||||
dataclaw status
|
||||
## 运行示例截图
|
||||
|
||||
# 自定义监听地址/端口
|
||||
dataclaw start --host 0.0.0.0 --port 8000
|
||||

|
||||
|
||||
# 停止服务
|
||||
dataclaw stop
|
||||
```
|
||||

|
||||
|
||||
可选环境变量:
|
||||

|
||||
|
||||
```bash
|
||||
export DATA_ROOT=/absolute/path/to/data
|
||||
```
|
||||

|
||||
|
||||
若未设置,默认使用仓库根目录下的 `data/`。服务状态文件与日志默认位于 `DATA_ROOT/run/`。
|
||||

|
||||
|
||||
### 4. 开发模式(需要 Node.js)🧪
|
||||

|
||||
|
||||
如果你要调试前端代码或重新构建前端产物,请使用前后端分离开发模式:
|
||||

|
||||
|
||||
```bash
|
||||
cd backend
|
||||
# 创建虚拟环境(可选但强烈建议)
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||

|
||||
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||

|
||||
|
||||
# 启动 FastAPI 服务器
|
||||
uvicorn app.main:app --reload --port 8000
|
||||
```
|
||||

|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
# 安装依赖(仅开发模式需要 Node.js)
|
||||
npm install
|
||||

|
||||
|
||||
# 启动 Vite 开发服务器
|
||||
npm run dev
|
||||
```
|
||||

|
||||
|
||||
*提示:请确保* *`nanobot`* *核心库已根据项目工作区的要求正确链接或以可编辑模式 (editable mode) 安装。*
|
||||
|
||||
### 5. 语音识别服务(可选)🎙️
|
||||
|
||||
若你希望使用聊天输入框中的语音输入能力,请单独启动 `whisper` 服务:
|
||||
|
||||
```bash
|
||||
cd whisper
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
默认服务地址:`http://localhost:8001`
|
||||
健康检查接口:`GET /health`
|
||||
|
||||
前端配置方式:
|
||||
1. 点击左下角用户名,打开菜单;
|
||||
2. 进入「语音输入配置」;
|
||||
3. 填写服务地址(例如 `http://localhost:8001`);
|
||||
4. 点击「测试连接」通过后保存。
|
||||
|
||||
### 6. 初始账号配置 👤
|
||||
系统首次注册的用户将自动成为管理员。您可以在登录页面直接点击“注册”按钮创建您的管理员账号(例如:用户名 `admin`,密码 `admin`),随后即可登录并管理项目、数据源和用户。
|
||||
|
||||
### 7. A2A 模式使用指南 🤖
|
||||
|
||||
A2A(Agent2Agent)用于让 DataClaw 把任务委托给远端 Agent,并保持任务可跟踪(状态流、产物流、取消、重试)。
|
||||
|
||||
#### 7.1 在前端启用 A2A(推荐)
|
||||
|
||||
1. 进入 **Skills** 页面,切到 **A2A** 标签页。
|
||||
2. 点击新增远端 Agent,填写:
|
||||
- `name`: 远端 Agent 名称
|
||||
- `base_url`: 远端 DataClaw/A2A 网关地址(如 `https://agent-b.example.com`)
|
||||
- `auth_scheme`: `none` 或 `bearer`
|
||||
- `auth_token`: 当 `auth_scheme=bearer` 时填写
|
||||
3. 点击健康检查,确认 `healthy=true`。
|
||||
4. 回到聊天页,开启 **A2A Mode**,选择 `route_mode` 与目标 Agent 后发送问题。
|
||||
5. 在消息卡片与 A2A 任务面板中查看 `SUBMITTED/WORKING/COMPLETED/FAILED` 等状态,可执行取消与重试。
|
||||
|
||||
`route_mode` 建议:
|
||||
- `auto`: 按项目灰度配置与策略自动决策
|
||||
- `local`: 强制本地执行
|
||||
- `a2a`: 强制远端 A2A 执行(需选远端 Agent)
|
||||
- `a2a_first`: 先远端,失败按回退链执行
|
||||
- `local_first`: 先本地,按需再回退
|
||||
|
||||
#### 7.2 API 示例(生产常用)
|
||||
|
||||
以下示例假设服务地址为 `http://127.0.0.1:8000`,并已获取登录令牌 `${TOKEN}`。
|
||||
|
||||
**示例 1:查看本机 A2A Agent Card**
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: Bearer ${TOKEN}" \
|
||||
http://127.0.0.1:8000/api/v1/a2a/agent-card
|
||||
```
|
||||
|
||||
**示例 2:注册远端 Agent**
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8000/api/v1/a2a/remote-agents \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"project_id": 1,
|
||||
"name": "Agent-B",
|
||||
"base_url": "https://agent-b.example.com",
|
||||
"auth_scheme": "bearer",
|
||||
"auth_token": "remote-agent-token"
|
||||
}'
|
||||
```
|
||||
|
||||
**示例 3:以 A2A 优先模式发起任务**
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8000/api/v1/a2a/messages/send \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"project_id": 1,
|
||||
"message": "请分析最近30天订单转化趋势并给出建议",
|
||||
"session_id": "chat:demo-a2a",
|
||||
"remote_agent_id": 3,
|
||||
"route_mode": "a2a_first",
|
||||
"fallback_chain": ["a2a", "local", "mcp"],
|
||||
"idempotency_key": "demo-a2a-001"
|
||||
}'
|
||||
```
|
||||
|
||||
返回结果中的 `task.id` 可用于订阅与管理任务。
|
||||
|
||||
**示例 4:订阅任务流(SSE)**
|
||||
|
||||
```bash
|
||||
curl -N -H "Authorization: Bearer ${TOKEN}" \
|
||||
http://127.0.0.1:8000/api/v1/a2a/tasks/<task_id>/subscribe
|
||||
```
|
||||
|
||||
你会收到类似事件:
|
||||
- `TaskStatusUpdateEvent`(状态变更)
|
||||
- `TaskArtifactUpdateEvent`(产物更新)
|
||||
- `done`(流结束)
|
||||
|
||||
**示例 5:取消任务**
|
||||
|
||||
```bash
|
||||
curl -X POST -H "Authorization: Bearer ${TOKEN}" \
|
||||
http://127.0.0.1:8000/api/v1/a2a/tasks/<task_id>/cancel
|
||||
```
|
||||
|
||||
**示例 6:为任务配置 Webhook 回调(离线接收结果)**
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8000/api/v1/a2a/tasks/<task_id>/webhooks \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"target_url": "https://your-system.example.com/a2a/webhook",
|
||||
"secret": "your-webhook-secret",
|
||||
"auth_header": "Bearer your-internal-token"
|
||||
}'
|
||||
```
|
||||
|
||||
#### 7.3 一个完整实战流程
|
||||
|
||||
场景:你有一个“本地数据分析 Agent”,还接入了“外部行业知识 Agent-B”。
|
||||
|
||||
1. 在 Skills -> A2A 注册 `Agent-B` 并完成健康检查。
|
||||
2. 在聊天页开启 A2A,选择 `route_mode=a2a_first`。
|
||||
3. 输入问题:
|
||||
`请结合外部行业知识与本地销量数据,生成本季度增长策略。`
|
||||
4. 系统先尝试委托给 `Agent-B`;若远端异常则按回退链降级到本地/MCP。
|
||||
5. 在任务面板查看状态流与最终产物,必要时可取消或重试。
|
||||
|
||||
生产建议:
|
||||
- 对业务侧请求始终传 `idempotency_key`,避免重复任务。
|
||||
- 为长任务配置 webhook,避免客户端断线丢失进度。
|
||||
- 在项目级 rollout 配置灰度比例,先小流量启用 A2A 再全量放开。
|
||||
|
||||
#### 7.4 本地调试 A2A(双实例联调)
|
||||
|
||||
推荐在本机同时启动两个后端实例:
|
||||
- 实例 A(调用方):`http://127.0.0.1:8000`
|
||||
- 实例 B(被调用远端 Agent):`http://127.0.0.1:8001`
|
||||
|
||||
分别用两个终端启动(建议使用不同 `DATA_ROOT`,避免数据目录冲突):
|
||||
|
||||
```bash
|
||||
# 终端1:实例 A
|
||||
cd backend
|
||||
source .venv/bin/activate
|
||||
DATA_ROOT=/tmp/dataclaw-a uvicorn main:app --reload --port 8000
|
||||
```
|
||||
|
||||
```bash
|
||||
# 终端2:实例 B
|
||||
cd backend
|
||||
source .venv/bin/activate
|
||||
DATA_ROOT=/tmp/dataclaw-b uvicorn main:app --reload --port 8001
|
||||
```
|
||||
|
||||
然后在两个实例分别注册并登录,拿到 token:
|
||||
|
||||
```bash
|
||||
# 分别注册(每个实例首次注册用户会成为管理员)
|
||||
curl -X POST http://127.0.0.1:8000/api/v1/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin_a","email":"a@test.com","password":"admin12345"}'
|
||||
|
||||
curl -X POST http://127.0.0.1:8001/api/v1/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin_b","email":"b@test.com","password":"admin12345"}'
|
||||
|
||||
# 登录并保存 token
|
||||
TOKEN_A=$(curl -s -X POST http://127.0.0.1:8000/api/v1/auth/login \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "username=admin_a&password=admin12345" | jq -r '.access_token')
|
||||
|
||||
TOKEN_B=$(curl -s -X POST http://127.0.0.1:8001/api/v1/auth/login \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "username=admin_b&password=admin12345" | jq -r '.access_token')
|
||||
```
|
||||
|
||||
最后,在实例 A 中把实例 B 注册成远端 Agent(`auth_token` 使用 `TOKEN_B`):
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8000/api/v1/a2a/remote-agents \
|
||||
-H "Authorization: Bearer ${TOKEN_A}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"project_id\": 1,
|
||||
\"name\": \"local-agent-b\",
|
||||
\"base_url\": \"http://127.0.0.1:8001\",
|
||||
\"auth_scheme\": \"bearer\",
|
||||
\"auth_token\": \"${TOKEN_B}\"
|
||||
}"
|
||||
```
|
||||
|
||||
完成后即可在实例 A 发起 A2A 任务、订阅任务流、取消任务,完成本地端到端联调。
|
||||
|
||||
***
|
||||
|
||||
## 🔌 数据源配置说明
|
||||
|
||||
DataClaw 支持连接多种类型的数据源,以满足不同场景的分析需求。你可以在界面的 **Data Sources** 菜单中点击 **+** 新建并配置它们。以下是常见数据源的详细接入指南:
|
||||
|
||||
<details>
|
||||
<summary><b>▶ PostgreSQL (pgsql)</b></summary>
|
||||
|
||||
连接标准的关系型数据库。你既可以通过表单填充分散的参数,也可以直接粘贴完整的 Connection String。
|
||||
|
||||
- **Host**: 数据库的主机地址。如果你是在本地电脑运行了数据库(如使用 pgAdmin),请填入 `127.0.0.1`(不要填 `localhost`,以避免 Unix Socket 解析错误)。
|
||||
- **Port**: 默认一般为 `5432`。
|
||||
- **Database**: 你要连接的具体数据库名称。
|
||||
- **Username / Password**: 数据库的认证凭据(默认用户通常是 `postgres`)。
|
||||
- **Connection String (可选)**: 也可以直接输入类似 `postgresql://postgres:你的密码@127.0.0.1:5432/你的数据库名` 的字符串,它将覆盖上述单独的输入框配置。
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>▶ Supabase</b></summary>
|
||||
|
||||
专门针对 Supabase 云端 PostgreSQL 数据库优化的连接方式,强制开启 SSL 且默认使用连接池以提高稳定性。
|
||||
|
||||
- 推荐直接使用 **Connection String** 配置:
|
||||
进入你的 Supabase 项目控制台 -> `Project Settings` -> `Database` -> `Connection string` -> 选择 `URI` 选项卡。
|
||||
复制那串类似 `postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres?sslmode=require` 的链接并填入。
|
||||
- *注意*: Supabase 默认开启了 Transaction Pooler(端口 6543)。如果想要直连(Direct connection),请将端口改为 `5432`,并确保 URL 中包含 `sslmode=require`。
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>▶ SQLite</b></summary>
|
||||
|
||||
轻量级的本地文件型数据库,非常适合快速测试或分析单机应用数据。
|
||||
|
||||
- **File Upload**: 你可以直接点击按钮,从本地上传 `.db`、`.sqlite` 或 `.sqlite3` 后缀的数据库文件。文件会被安全地保存在服务端的上传目录中供分析使用。
|
||||
- **File Path (进阶)**: 如果服务部署在服务器上,且 SQLite 文件已存在于服务器的某个绝对路径中,你也可以直接在输入框中填入该文件的绝对路径(如 `/data/my_app.db`)。
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>▶ CSV</b></summary>
|
||||
|
||||
最常见的数据交换格式,即插即用,无需复杂的数据库配置。
|
||||
|
||||
- **File Upload**: 与 SQLite 类似,点击按钮选择本地的 `.csv` 文件上传即可。系统会在后台利用 DuckDB 或 Pandas 等引擎将其虚拟化为一个可供 SQL 查询的表。
|
||||
- 上传成功后,在对话界面中,你可以直接把这个 CSV 文件当作一张数据库表来“提问”!
|
||||
</details>
|
||||
|
||||
***
|
||||
|
||||
## 🤝 参与贡献
|
||||
|
||||
有个好点子?发现了一个 Bug?非常欢迎你的加入!随时可以提交 Issue 或 Pull Request。让我们一起让数据分析变得更加有趣!
|
||||
|
||||
***
|
||||
|
||||
## 💖 特别鸣谢
|
||||
|
||||
DataClaw 的开发深受以下优秀开源项目的启发,特此致谢:
|
||||
|
||||
- [WrenAI](https://github.com/Canner/WrenAI): 强大的 Text-to-SQL 解决方案,其架构和思路给了我们很大的启发。
|
||||
- [Aix-DB](https://github.com/apconw/Aix-DB): 在智能数据分析和交互式体验方面提供了极好的参考。
|
||||
|
||||
<br />
|
||||

|
||||
|
||||
@@ -1,379 +0,0 @@
|
||||
[🇨🇳 简体中文](./README.md) | [🇬🇧 English](./README_en.md)
|
||||
|
||||
# 🦞 DataClaw
|
||||
|
||||
> **Unleash the claws on your data, making analysis as easy and refreshing as raising lobsters!** 🌊📊
|
||||
> DataClaw is your intelligent, AI-powered Data Analysis Platform. Chat with your data, visualize insights instantly, and build dashboards—all through natural language. No SQL degree required!
|
||||
|
||||
***
|
||||
|
||||
## ✨ Why DataClaw?
|
||||
|
||||
Tired of writing complex SQL queries just to get a simple bar chart? DataClaw acts as your personal data scientist. Powered by advanced LLMs and an intelligent agentic workflow, it translates your questions into database queries, fetches the data, and renders beautiful visualizations on the fly.
|
||||
|
||||
Whether you're querying a massive Supabase/PostgreSQL database or just tossing in a CSV file, DataClaw's got you covered! 🚀
|
||||
|
||||
## 🌟 Key Features
|
||||
|
||||
- **🗣️ Chat to SQL**: Ask questions in plain English (or Chinese!). DataClaw understands your schema, generates accurate SQL, and self-corrects if things go sideways.
|
||||
- **📚 Smart Knowledge Base (RAG)**: Support uploading Word, PPT, PDF and other document formats. Enhance answers through vector retrieval, making your private documents "speak".
|
||||
- **📈 Instant Visualizations**: Returns not just raw tables, but auto-generated interactive charts tailored to your data's shape.
|
||||
- **🗂️ Multi-Source Ready**: Connects seamlessly to PostgreSQL, Supabase, and local CSV/Excel uploads.
|
||||
- **🧠 Bring Your Own LLM**: Native integration with LiteLLM. Plug in OpenAI, DeepSeek, Zhipu, DashScope, Volcengine, or any compatible provider.
|
||||
- **🛠️ Extensible Agent Skills**: Built on top of the powerful `nanobot` framework (a lightweight version of `OpenClaw`). Add custom tools and slash commands (`/`) to tailor the agent to your specific business logic.
|
||||
- **📊 Customizable Dashboards**: Pin your favorite chat-generated charts to a drag-and-drop dashboard for quick access.
|
||||
- **📦 Intelligent Artifact Management**: Automatically extracts generated files (HTML reports, PDFs, PPTs, images, etc.) from conversations, providing embedded previews and one-click downloads.
|
||||
|
||||
***
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
<div align="left">
|
||||
<h3>💬 Chat Interface</h3>
|
||||
<img src="./docs/index.png" width="80%" />
|
||||
<br />
|
||||
<br />
|
||||
<h3>📊 Customizable Dashboard</h3>
|
||||
<img src="./docs/dashboard.png" width="80%" />
|
||||
<br />
|
||||
<br />
|
||||
<h3>📚 Smart Knowledge Base</h3>
|
||||
<img src="./docs/kb.png" width="80%" />
|
||||
<br />
|
||||
<br />
|
||||
<h3>📦 Artifact Preview</h3>
|
||||
<img src="./docs/artifact.png" width="80%" />
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
DataClaw is divided into three main claws (components):
|
||||
|
||||
1. **`frontend/`** 🎨: The shiny shell. Built with **React 19**, **Vite**, **TailwindCSS**, and **Zustand**. It features a chat-like interface, streaming AI responses, and interactive Vega charts.
|
||||
2. **`backend/`** ⚙️: The muscle. A **FastAPI** application managing projects, data source connections, user sessions, and API gateways.
|
||||
3. **`nanobot/`** 🧠: The brain. The core AI agent framework handling NL2SQL, schema caching, prompt injection, and LLM routing.
|
||||
4. **`data/`** 🗄️: Runtime data root. Decoupled from code directories and used for uploads, sessions, workspace skills, reports, and cached configs.
|
||||
|
||||
***
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
Ready to dive in? Let's get DataClaw running on your local machine!
|
||||
|
||||
### 1. Configure Environment Variables 🔧
|
||||
|
||||
In the root directory of the project, copy and rename the environment template:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Please edit the `.env` file in the root directory and fill in your actual configurations (e.g., QQ Mail SMTP Auth Code).
|
||||
|
||||
> **Guide to getting QQ Mail SMTP Auth Code:**
|
||||
> 1. Log in to QQ Mail web version (mail.qq.com)
|
||||
> 2. Click "Settings" (设置) at the top of the page -> "Account" (账号) tab
|
||||
> 3. Scroll down to find the "POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV Service" section
|
||||
> 4. Ensure "POP3/SMTP Service" is toggled to "On" (开启)
|
||||
> 5. Click "Generate Authorization Code" (生成授权码) below it, scan the QR code with mobile QQ or send an SMS as prompted
|
||||
> 6. After verification, you will get a **16-digit random letter combination**. Copy and paste it into the `SMTP_PASSWORD` field in your `.env` file
|
||||
|
||||
### 2. Standard Deployment (Recommended, No Node.js Required) 📦
|
||||
|
||||
Ensure you have Python 3.11+ installed. The pre-built React frontend is bundled in the Python wheel, so you don't need Node.js for production deployment.
|
||||
|
||||
#### Build the wheel (output to `dist/`)
|
||||
|
||||
```bash
|
||||
# First, build the frontend
|
||||
cd frontend
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
# Then, build the backend wheel
|
||||
cd ../backend
|
||||
uv build --wheel --out-dir ../dist
|
||||
```
|
||||
|
||||
Once built, the wheel is located in the project root `dist/` directory, e.g., `dist/dataclaw-0.1.0-py3-none-any.whl`.
|
||||
|
||||
#### Install and Run
|
||||
|
||||
```bash
|
||||
# We recommend creating a virtual environment first
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
|
||||
# Install DataClaw
|
||||
pip install ./dist/dataclaw-*.whl
|
||||
|
||||
# Start the service (defaults to http://127.0.0.1:8000)
|
||||
dataclaw start
|
||||
```
|
||||
|
||||
Common service control commands:
|
||||
|
||||
```bash
|
||||
# Check running status
|
||||
dataclaw status
|
||||
|
||||
# Custom host/port
|
||||
dataclaw start --host 0.0.0.0 --port 8000
|
||||
|
||||
# Stop the service
|
||||
dataclaw stop
|
||||
```
|
||||
|
||||
Optional environment variable:
|
||||
|
||||
```bash
|
||||
export DATA_ROOT=/absolute/path/to/data
|
||||
```
|
||||
|
||||
If not set, DataClaw uses the repository-level `data/` directory by default. Service state files and logs are located in `DATA_ROOT/run/`.
|
||||
|
||||
### 3. Development Mode (Requires Node.js) 🧪
|
||||
|
||||
If you want to debug the frontend code or rebuild the frontend artifacts, use the separate development mode:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
# Create a virtual environment (optional but recommended)
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Start the FastAPI server
|
||||
uvicorn app.main:app --reload --port 8000
|
||||
```
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start the Vite development server
|
||||
npm run dev
|
||||
```
|
||||
|
||||
*Note: Ensure your* *`nanobot`* *is properly linked or installed in editable mode as per the project workspace.*
|
||||
|
||||
### 4. Optional Voice Service 🎙️
|
||||
|
||||
If you want to use voice input in chat, run the standalone `whisper` service:
|
||||
|
||||
```bash
|
||||
cd whisper
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
python main.py
|
||||
```
|
||||
|
||||
Default service URL: `http://localhost:8001`
|
||||
Health endpoint: `GET /health`
|
||||
|
||||
Frontend setup:
|
||||
1. Click the username in the bottom-left to open the user menu;
|
||||
2. Open `Voice Input Settings`;
|
||||
3. Fill in the service URL (e.g. `http://localhost:8001`);
|
||||
4. Click `Test Connection`, then `Save`.
|
||||
|
||||
### 4. Initial Account Setup 👤
|
||||
The first user to register in the system will automatically be granted admin privileges. You can simply click the "Register" button on the login page to create your admin account (e.g., Username: `admin`, Password: `admin`), and then log in to manage projects, data sources, and users.
|
||||
|
||||
### 5. A2A Mode Guide 🤖
|
||||
|
||||
A2A (Agent2Agent) lets DataClaw delegate tasks to remote agents with full task lifecycle controls (status stream, artifact stream, cancel, retry).
|
||||
|
||||
#### 5.1 Enable A2A in UI (Recommended)
|
||||
|
||||
1. Open **Skills** page and switch to the **A2A** tab.
|
||||
2. Add a remote agent with:
|
||||
- `name`
|
||||
- `base_url` (for example `https://agent-b.example.com`)
|
||||
- `auth_scheme` (`none` or `bearer`)
|
||||
- `auth_token` (required when `auth_scheme=bearer`)
|
||||
3. Run health check and confirm `healthy=true`.
|
||||
4. Go to Chat, enable **A2A Mode**, choose `route_mode` and remote agent, then send your prompt.
|
||||
5. Track task states in Chat (`SUBMITTED/WORKING/COMPLETED/FAILED`) and use cancel/retry when needed.
|
||||
|
||||
`route_mode` quick reference:
|
||||
- `auto`: Use project rollout policy and routing strategy
|
||||
- `local`: Force local execution
|
||||
- `a2a`: Force remote A2A execution
|
||||
- `a2a_first`: Try remote first, then fallback chain
|
||||
- `local_first`: Try local first
|
||||
|
||||
#### 5.2 API Examples
|
||||
|
||||
Assume service URL is `http://127.0.0.1:8000` and your bearer token is `${TOKEN}`.
|
||||
|
||||
```bash
|
||||
# 1) Get local Agent Card
|
||||
curl -H "Authorization: Bearer ${TOKEN}" \
|
||||
http://127.0.0.1:8000/api/v1/a2a/agent-card
|
||||
|
||||
# 2) Register remote agent
|
||||
curl -X POST http://127.0.0.1:8000/api/v1/a2a/remote-agents \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"project_id": 1,
|
||||
"name": "Agent-B",
|
||||
"base_url": "https://agent-b.example.com",
|
||||
"auth_scheme": "bearer",
|
||||
"auth_token": "remote-agent-token"
|
||||
}'
|
||||
|
||||
# 3) Send task with a2a_first route
|
||||
curl -X POST http://127.0.0.1:8000/api/v1/a2a/messages/send \
|
||||
-H "Authorization: Bearer ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"project_id": 1,
|
||||
"message": "Analyze order conversion trend for last 30 days and propose actions",
|
||||
"session_id": "chat:demo-a2a",
|
||||
"remote_agent_id": 3,
|
||||
"route_mode": "a2a_first",
|
||||
"fallback_chain": ["a2a", "local", "mcp"],
|
||||
"idempotency_key": "demo-a2a-001"
|
||||
}'
|
||||
|
||||
# 4) Subscribe task stream
|
||||
curl -N -H "Authorization: Bearer ${TOKEN}" \
|
||||
http://127.0.0.1:8000/api/v1/a2a/tasks/<task_id>/subscribe
|
||||
|
||||
# 5) Cancel task
|
||||
curl -X POST -H "Authorization: Bearer ${TOKEN}" \
|
||||
http://127.0.0.1:8000/api/v1/a2a/tasks/<task_id>/cancel
|
||||
```
|
||||
|
||||
#### 5.3 Local Debugging for A2A (Two-Instance Setup)
|
||||
|
||||
Use two local backend instances:
|
||||
- Instance A (caller): `http://127.0.0.1:8000`
|
||||
- Instance B (remote agent): `http://127.0.0.1:8001`
|
||||
|
||||
Run them in two terminals:
|
||||
|
||||
```bash
|
||||
# Terminal 1 - Instance A
|
||||
cd backend
|
||||
source .venv/bin/activate
|
||||
DATA_ROOT=/tmp/dataclaw-a uvicorn main:app --reload --port 8000
|
||||
```
|
||||
|
||||
```bash
|
||||
# Terminal 2 - Instance B
|
||||
cd backend
|
||||
source .venv/bin/activate
|
||||
DATA_ROOT=/tmp/dataclaw-b uvicorn main:app --reload --port 8001
|
||||
```
|
||||
|
||||
Create/login users and fetch tokens:
|
||||
|
||||
```bash
|
||||
# Register (first user becomes admin) - run once per instance
|
||||
curl -X POST http://127.0.0.1:8000/api/v1/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin_a","email":"a@test.com","password":"admin12345"}'
|
||||
|
||||
curl -X POST http://127.0.0.1:8001/api/v1/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin_b","email":"b@test.com","password":"admin12345"}'
|
||||
|
||||
# Login and keep tokens
|
||||
TOKEN_A=$(curl -s -X POST http://127.0.0.1:8000/api/v1/auth/login \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "username=admin_a&password=admin12345" | jq -r '.access_token')
|
||||
|
||||
TOKEN_B=$(curl -s -X POST http://127.0.0.1:8001/api/v1/auth/login \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "username=admin_b&password=admin12345" | jq -r '.access_token')
|
||||
```
|
||||
|
||||
Then register B as remote agent in A, using `TOKEN_B` as `auth_token`:
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:8000/api/v1/a2a/remote-agents \
|
||||
-H "Authorization: Bearer ${TOKEN_A}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"project_id\": 1,
|
||||
\"name\": \"local-agent-b\",
|
||||
\"base_url\": \"http://127.0.0.1:8001\",
|
||||
\"auth_scheme\": \"bearer\",
|
||||
\"auth_token\": \"${TOKEN_B}\"
|
||||
}"
|
||||
```
|
||||
|
||||
Finally, send/subscribe/cancel tasks from A. This validates the complete local A2A flow.
|
||||
|
||||
***
|
||||
|
||||
## 🔌 Data Source Configuration Guide
|
||||
|
||||
DataClaw supports connecting to various types of data sources to meet different analysis needs. You can click **+** in the **Data Sources** menu to create and configure them. Here are detailed connection guides for common data sources:
|
||||
|
||||
<details>
|
||||
<summary><b>▶ PostgreSQL (pgsql)</b></summary>
|
||||
|
||||
Connects to standard relational databases. You can either fill in the individual parameters through the form or paste a complete Connection String directly.
|
||||
|
||||
- **Host**: The host address of the database. If you are running the database on your local machine (e.g., using pgAdmin), please enter `127.0.0.1` (do not enter `localhost` to avoid Unix Socket resolution errors).
|
||||
- **Port**: Typically defaults to `5432`.
|
||||
- **Database**: The specific name of the database you want to connect to.
|
||||
- **Username / Password**: Database authentication credentials (the default user is usually `postgres`).
|
||||
- **Connection String (Optional)**: You can also directly input a string like `postgresql://postgres:your_password@127.0.0.1:5432/your_database_name`, which will override the individual input fields above.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>▶ Supabase</b></summary>
|
||||
|
||||
A connection method specifically optimized for Supabase cloud PostgreSQL databases, enforcing SSL and using connection pools by default to improve stability.
|
||||
|
||||
- We recommend using the **Connection String** configuration directly:
|
||||
Go to your Supabase project console -> `Project Settings` -> `Database` -> `Connection string` -> Select the `URI` tab.
|
||||
Copy the link that looks like `postgresql://postgres.[project-ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres?sslmode=require` and paste it in.
|
||||
- *Note*: Supabase enables Transaction Pooler by default (Port 6543). If you want a Direct connection, change the port to `5432` and ensure the URL includes `sslmode=require`.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>▶ SQLite</b></summary>
|
||||
|
||||
A lightweight local file-based database, perfect for quick testing or analyzing single-machine application data.
|
||||
|
||||
- **File Upload**: You can directly click the button to upload a `.db`, `.sqlite`, or `.sqlite3` database file from your local machine. The file will be securely saved in the server's upload directory for analysis.
|
||||
- **File Path (Advanced)**: If the service is deployed on a server and the SQLite file already exists at an absolute path on the server, you can also enter the absolute path directly in the input box (e.g., `/data/my_app.db`).
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>▶ CSV</b></summary>
|
||||
|
||||
The most common data exchange format, plug-and-play, no complex database configuration required.
|
||||
|
||||
- **File Upload**: Similar to SQLite, click the button to select and upload a local `.csv` file. The system will use engines like DuckDB or Pandas in the background to virtualize it into an SQL-queryable table.
|
||||
- Once uploaded successfully, you can query this CSV file directly as if it were a database table in the chat interface!
|
||||
</details>
|
||||
|
||||
***
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Got a cool idea? Found a bug? We'd love your help! Feel free to open an issue or submit a pull request. Let's make data analysis fun again!
|
||||
|
||||
***
|
||||
|
||||
## 💖 Acknowledgements
|
||||
|
||||
The development of DataClaw was deeply inspired by the following excellent open-source projects. Special thanks to:
|
||||
|
||||
- [WrenAI](https://github.com/Canner/WrenAI): A powerful Text-to-SQL solution whose architecture and concepts provided great inspiration.
|
||||
- [Aix-DB](https://github.com/apconw/Aix-DB): Provided an excellent reference for intelligent data analysis and interactive user experience.
|
||||
|
||||
<br />
|
||||
|
Before Width: | Height: | Size: 12 MiB After Width: | Height: | Size: 12 MiB |
|
Before Width: | Height: | Size: 5.6 MiB After Width: | Height: | Size: 5.6 MiB |
|
Before Width: | Height: | Size: 6.8 MiB After Width: | Height: | Size: 6.8 MiB |
|
Before Width: | Height: | Size: 6.0 MiB After Width: | Height: | Size: 6.0 MiB |